Repository navigation
Speed up wolfCose_ForceZero with XMEMSET and a barrier - #94
Conversation
There was a problem hiding this comment.
🔵 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
|
Hey @deadcaf3, thanks for the contribution this looks great! Some small things:
|
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.
b593c10 to
9e25fee
Compare
|
Thanks for taking a look! @aidangarske
I added |

Summary
wolfCose_ForceZerowipes 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 astatic const volatilefunction 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'sWC_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_twords through a volatile pointer. That breaks the C99 effective-type rules (6.5p7) for byte arrays and key structures, so it was replaced.memsetmay write any object, so no aliasing rule is involved.Results
Apple Silicon,
-Os: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 onmain) along with the Rule 11.4 and 11.3 deviations the first revision added.Testing
test_wolfcose_force_zerochecks that every start offset and length zeroes exactly the requested range and nothing else.make test,make zeroize-test,make cxx-check, house style, and a strict C99-Werrorbuild ofwolfcose_util.cwith and withoutWOLFSSL_NO_ASM.c99-checkfails on newer Apple clang inexamples/scenarios/iot_fleet_config.c. Same failure onmain; the file is not touched here.wolfcose-ciimage (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 andmake testwith gcc-11, gcc-13 and clang-14.-O3, the memset is kept only with the asm barrier. Under-O2 -fltowith PGO the volatile pointer call is not promoted and the wipe is kept. The pointer lands in.data.rel.ro.localwith-fPICand.rodatawithout.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.