Skip to content

Speed up wolfCose_ForceZero with XMEMSET and a barrier - #94

Merged
aidangarske merged 3 commits into
wolfSSL:mainfrom
deadcaf3:forcezero-word-stores
Oct 22, 2026
Merged

aidangarske merged 3 commits into
wolfSSL:mainfrom
deadcaf3:forcezero-word-stores

Conversation

@deadcaf3

@deadcaf3 deadcaf3 commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

wolfCose_ForceZero wipes the caller's scratch buffer at the end of every sign, verify, MAC, encrypt and decrypt. It wrote one volatile byte at a time, so with the default 8192 B (ML-DSA) or 11264 B (LMS) scratch, the wipe cost more than the crypto on small messages.

It now zeroes with XMEMSET, then passes the buffer to a no-op function called through a static const volatile function pointer. The compiler cannot tell which function the pointer holds, so it must assume the call reads the buffer and cannot drop the memset as a dead store. Where GNU asm is available (same guard as wolfSSL's WC_BARRIER_DATA, which older releases lack), the no-op also holds an empty asm memory barrier, so the stores survive even if the optimizer resolves and inlines the call, as profile-guided call promotion could. No call-site changes.

An earlier revision stored whole size_t words through a volatile pointer. That breaks the C99 effective-type rules (6.5p7) for byte arrays and key structures, so it was replaced. memset may write any object, so no aliasing rule is involved.

Results

Apple Silicon, -Os:

Wipe Byte loop (before) Word loop (first revision) XMEMSET + barrier
8192 B 2.7 µs 0.34 µs 0.09 µs
11264 B 3.6 µs 0.46 µs 0.12 µs
1 MiB 338 µs 42 µs 8.3 µs

For scale, HMAC-SHA256 of 64 B takes 0.23 to 0.29 µs and SHA-256 of 1 MiB takes 440 µs.

With clang -O3 -flto, a 256 B dead stack buffer keeps its zeroing stores ahead of the barrier call and loses them entirely without it. With the call inlined directly, the stores stay only with the asm barrier.

MISRA

The function no longer casts the caller's pointer, so it needs no deviations. The inline asm sits alone in its own function, as Directive 4.3 requires. This removes D-11.5-001 (present on main) along with the Rule 11.4 and 11.3 deviations the first revision added.

Testing

  • test_wolfcose_force_zero checks that every start offset and length zeroes exactly the requested range and nothing else.
  • Pass locally against wolfSSL master: make test, make zeroize-test, make cxx-check, house style, and a strict C99 -Werror build of wolfcose_util.c with and without WOLFSSL_NO_ASM.
  • c99-check fails on newer Apple clang in examples/scenarios/iot_fleet_config.c. Same failure on main; the file is not touched here.
  • Pass in the pinned wolfcose-ci image (amd64), running the workflow steps verbatim: MISRA C 2012 (cppcheck 2.13.0 and the deviation classifier, no stale or unresolved findings), MISRA C 2023 compiler warnings and clang-tidy, and multi-compiler build and make test with gcc-11, gcc-13 and clang-14.
  • x86_64 gcc-13: with the barrier call inlined at -O3, the memset is kept only with the asm barrier. Under -O2 -flto with PGO the volatile pointer call is not promoted and the wipe is kept. The pointer lands in .data.rel.ro.local with -fPIC and .rodata without.

Not included

Wiping only the bytes each call used. It would save at most 0.12 µs per call, but the structure builders don't report a length when they fail, so all 17 scratch-wipe sites would need error-path tracking. Too easy to leave secret bytes behind.

Copilot AI balanced review requested due to automatic review settings October 22, 2026 00:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The incompatible-type word stores introduce undefined behavior into secure erasure, requiring correction and human validation of aliasing and portability guarantees.

1 open issue
What changed in this PR

This PR speeds up wolfCOSE’s secure buffer erasure by replacing most byte stores with aligned word stores, without changing callers.

Changes:

  • Adds byte-head, word-body, and byte-tail wiping.
  • Tests that varied offsets and lengths erase exactly the requested range.
  • Records and documents the new MISRA deviations.
File Description
tests/​test_cose.c Adds exact-range wipe checks across offsets and lengths.
src/​wolfcose_util.c Implements aligned word stores in wolfCose_ForceZero.
scripts/​misra-deviations.json Adds anchored pointer-conversion deviations.
docs/​MISRA-Compliance.md Documents alignment and word-store rationales.

Review effort: Balanced

Comment thread src/wolfcose_util.c Outdated
@deadcaf3 deadcaf3 changed the title Wipe with word stores in wolfCose_ForceZero Speed up wolfCose_ForceZero with XMEMSET and a barrier Oct 22, 2026
@deadcaf3
deadcaf3 requested a balanced review from Copilot October 22, 2026 00:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Optimizer-resistant secure erasure needs human validation across supported toolchains, particularly assembly-disabled configurations.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

@aidangarske aidangarske self-assigned this Oct 22, 2026
@aidangarske

Copy link
Copy Markdown
Member

Hey @deadcaf3, thanks for the contribution this looks great! Some small things:

  1. The declaration comment in src/wolfcose_internal.h:65 still describes the volatile-pointer loop. It needs updating to match the XMEMSET + barrier approach.

  2. I think the new test can't catch the wipe being optimized out. looks like it reads the buffer back right after wolfCose_ForceZero, and the test links against the library without LTO so the compiler never sees a dead buffer. Could you add a check that compiles a caller in the same translation unit as wolfcose_util.c (or with -flto) at -O3, where the local buffer is dead after the wipe and confirms the zeroing stores are still in the output? Run it with and without WOLFSSL_NO_ASM.

@aidangarske aidangarske assigned deadcaf3 and unassigned aidangarske Oct 22, 2026
Every sign, verify, MAC, encrypt and decrypt wipes the whole caller
scratch buffer, which defaults to 8192 B with ML-DSA and 11264 B with
LMS. The byte-at-a-time volatile loop cannot be merged by the compiler,
so for a small MAC0 the wipe costs several times the HMAC itself.

Store whole size_t words through a volatile pointer once the address is
size_t-aligned, with byte stores for the unaligned head and the tail.
This is the technique wolfSSL's own ForceZero() uses. On an Apple
Silicon Mac at -Os, an 11264 B wipe drops from 3.6 us to 0.46 us and a
1 MiB wipe from 338 us to 42 us.

The alignment test and the word store need MISRA C:2012 deviations for
Rules 11.4 and 11.3; both are anchored in scripts/misra-deviations.json
and documented in docs/MISRA-Compliance.md. A new unit test checks that
exactly [off, off + len) is cleared at every start offset and length
across the head, word and tail loops.
Storing size_t words through a volatile pointer into byte arrays and
key structures breaks the C99 effective-type rules (6.5p7), so the
previous commit made the secure erase depend on undefined behavior.

Zero with XMEMSET instead, then pass the buffer to a no-op function
called through a static const volatile function pointer. The compiler
cannot tell which function the pointer holds, so it must assume the
call reads the buffer and cannot drop the memset as a dead store.
memset may write any object, so no aliasing rule is involved.

Where GNU asm is available, the no-op holds an empty asm memory
barrier, so the stores also survive if the optimizer resolves and
inlines the call, as profile-guided call promotion could. The guard
matches wolfSSL's WC_BARRIER_DATA, which older releases lack.

With clang -O3 -flto, a 256 B dead stack buffer keeps its zeroing
stores ahead of the barrier call and loses them without it. Inlined
directly, the call keeps the stores only with the asm barrier. An
11264 B wipe at -Os on an Apple Silicon Mac takes 0.12 us, against
0.46 us for the word loop and 3.6 us for the original byte loop.

The function no longer casts the caller's pointer, so the Rule 11.5,
11.4 and 11.3 deviations for wolfCose_ForceZero are removed.
The unit test reads the buffer back after the wipe and links against
the library without LTO, so it cannot see the wipe being dropped as a
dead store.

scripts/check_force_zero.sh compiles a caller in the same translation
unit as wolfcose_util.c at -O3, wiping a local buffer that is dead
afterwards, and requires zeroing stores in its assembly. A plain
XMEMSET control in the same file must lose its stores, so the check
fails if the optimizer could not have dropped them anyway. It runs
with and without WOLFSSL_NO_ASM and is a prerequisite of make test.

Removing the barrier call fails both modes on AArch64 and x86-64
clang; making the function pointer non-volatile fails the
WOLFSSL_NO_ASM mode, where no asm barrier remains.

Also update the wolfCose_ForceZero declaration comment, which still
described the volatile-pointer byte loop.
@deadcaf3
deadcaf3 force-pushed the forcezero-word-stores branch from b593c10 to 9e25fee Compare October 22, 2026 18:24
@deadcaf3

Copy link
Copy Markdown
Contributor Author

Thanks for taking a look! @aidangarske

  1. Fixed, the comment in wolfcose_internal.h now describes the XMEMSET + barrier version.

  2. yep you're right about the test since it reads the buffer back, the stores are never dead, and without LTO the compiler can't see into libwolfcose.a anyway. I rechecked by deleting the barrier call entirely and the unit suite still passed, so it really wasn't testing anything there - spot on catch

I added scripts/check_force_zero.sh (9e25fee), which now runs as part of make test
I ran it on gcc 11/12/13 and clang 14/15/17 in the CI image, plus Apple clang on arm64 and x86-64. Everything passes as committed; rebased on current main

@aidangarske aidangarske assigned aidangarske and unassigned deadcaf3 Oct 22, 2026
@aidangarske
aidangarske merged commit 13193ea into wolfSSL:main Oct 22, 2026
107 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants