From 0bd172622c707028f320482ceb9851951a7cd6f2 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Thu, 10 Sep 2026 15:28:04 +0000 Subject: [PATCH 1/2] ForceZero: use a compiler barrier instead of a CPU fence ForceZero() bracketed its stores with XFENCE(), which resolves to a full CPU memory fence on every current compiler branch (mfence or lock-or on x86, dmb on ARM64), so each wipe of per-record state paid two fences even in single-threaded builds. The wipe only needs to survive dead-store elimination. Cross-thread visibility of the zeroed memory is not part of ForceZero's contract; a caller that shares the buffer orders it with a lock or an atomic release. Add WC_BARRIER_DATA(ptr), named after the same construct in the Linux kernel: an empty asm statement that takes the pointer as an input operand and clobbers memory, so the compiler must assume the buffer is read afterwards. ForceZero() uses it before and after the wipe. The trailing barrier keeps the zeroing stores alive. The leading one makes the compiler materialize the buffer's live contents at mem first; without it GCC 13 at -O3 kept a filled local in a different stack slot that the wipe never touched. Neither barrier costs an instruction. Non-GNU compilers and WOLFSSL_NO_ASM builds fall back to WC_BARRIER(), which keeps their previous behavior. XFENCE() itself is unchanged for the speculation-barrier call sites in asn.c and ssl_sess.c. On x86-64 with GCC 13 at -O2 the zeroing stores stay in the object code and the two lock-or fences per call are gone. The ChangeLog records the behavioral change. --- ChangeLog.md | 5 +++++ wolfcrypt/src/misc.c | 9 +++++++-- wolfssl/wolfcrypt/wc_port.h | 13 +++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index f3107ffb0bf..b18278cb0c8 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,6 +1,11 @@ # wolfSSL Release (unreleased) ## Behavioral Changes +* **Behavioral change (`ForceZero()` issues no CPU fences)**: the wipe is + kept alive by a compiler barrier that takes the buffer address, which also + keeps it from being optimized away for buffers that never leave the inlined + code. A caller that needs the zeroed memory to be visible to another core + must order it itself with a lock or an atomic release. * **Behavioral change (`wc_PufReadSram` health tests the raw SRAM readout)**: the raw readout is now health tested before the context accepts it, and a diff --git a/wolfcrypt/src/misc.c b/wolfcrypt/src/misc.c index 066967d10eb..69e9f364420 100644 --- a/wolfcrypt/src/misc.c +++ b/wolfcrypt/src/misc.c @@ -791,7 +791,9 @@ WC_MISC_STATIC WC_INLINE void ForceZero(void* mem, size_t len) byte *zb = (byte *)mem; unsigned long *zl; - XFENCE(); + /* Make the compiler put the buffer's current contents at mem, so the + * wipe below hits the memory that holds them and not a copy. */ + WC_BARRIER_DATA(mem); while ((wc_ptr_t)zb & (wc_ptr_t)(sizeof(unsigned long) - 1U)) { if (len == 0) @@ -814,7 +816,10 @@ WC_MISC_STATIC WC_INLINE void ForceZero(void* mem, size_t len) --len; } - XFENCE(); + /* The caller is done with the buffer, so the compiler may drop the + * stores above as dead. The barrier makes the buffer look read by + * opaque code. No CPU fence is needed for that. */ + WC_BARRIER_DATA(mem); } #endif diff --git a/wolfssl/wolfcrypt/wc_port.h b/wolfssl/wolfcrypt/wc_port.h index bdaa0f2117c..f0a99c951a2 100644 --- a/wolfssl/wolfcrypt/wc_port.h +++ b/wolfssl/wolfcrypt/wc_port.h @@ -2017,6 +2017,19 @@ WOLFSSL_ABI WOLFSSL_API int wolfCrypt_Cleanup(void); } while(0) #endif +/* Compiler barrier that also treats the memory at ptr as read, so a wipe of + * that memory cannot be dropped as a dead store. The GNU form emits no CPU + * fence; cross-thread ordering is the caller's job. Without GNU asm (other + * compilers, or WOLFSSL_NO_ASM) it falls back to WC_BARRIER(). */ +#ifdef WC_BARRIER_DATA + /* use user-supplied WC_BARRIER_DATA() definition. */ +#elif defined(__GNUC__) && !defined(WOLFSSL_NO_ASM) + #define WC_BARRIER_DATA(ptr) \ + __asm__ __volatile__("" : : "r"(ptr) : "memory") +#else + #define WC_BARRIER_DATA(ptr) do { (void)(ptr); WC_BARRIER(); } while (0) +#endif + /* AFTER user_settings.h is loaded, ** determine if POSIX multi-threaded: HAVE_PTHREAD */ From 27ac2a603780ec75664e424085bea388193048f8 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Fri, 11 Sep 2026 05:53:53 +0000 Subject: [PATCH 2/2] Address review: always reach the trailing barrier in ForceZero() The alignment loop returned early when the buffer was shorter than the bytes needed to reach alignment, skipping the trailing WC_BARRIER_DATA(). Only that barrier keeps the wipe from being dropped as a dead store, so a short unaligned buffer could go unwiped. Fold the length check into the loop condition instead. --- wolfcrypt/src/misc.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/wolfcrypt/src/misc.c b/wolfcrypt/src/misc.c index 69e9f364420..f4b81fb8373 100644 --- a/wolfcrypt/src/misc.c +++ b/wolfcrypt/src/misc.c @@ -795,9 +795,10 @@ WC_MISC_STATIC WC_INLINE void ForceZero(void* mem, size_t len) * wipe below hits the memory that holds them and not a copy. */ WC_BARRIER_DATA(mem); - while ((wc_ptr_t)zb & (wc_ptr_t)(sizeof(unsigned long) - 1U)) { - if (len == 0) - return; + /* No early return here: a short unaligned buffer must still reach the + * trailing barrier, or its wipe can be dropped as a dead store. */ + while ((len != 0) && + ((wc_ptr_t)zb & (wc_ptr_t)(sizeof(unsigned long) - 1U))) { *zb++ = 0; --len; }