From 42a450f3fe723443d10a2e048ae0fbd5b8300c1c Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Wed, 19 Aug 2026 15:32:38 -0600 Subject: [PATCH 1/9] drbg: add thread-safe option, --disable-threadsafe-drbg to opt out --- CMakeLists.txt | 8 +++ cmake/options.h.in | 2 + configure.ac | 18 ++++++ wolfcrypt/src/random.c | 91 ++++++++++++++++++++++++++ wolfcrypt/test/test.c | 128 +++++++++++++++++++++++++++++++++++++ wolfcrypt/test/test.h | 7 ++ wolfssl/wolfcrypt/random.h | 33 ++++++++++ 7 files changed, 287 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 08ae2c8b8a3..9c56f27d609 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3688,6 +3688,14 @@ if(WOLFSSL_RNG_BANK) list(APPEND WOLFSSL_DEFINITIONS "-DWC_RNG_BANK_SUPPORT") endif() +# Thread-safe DRBG (share one WC_RNG between threads) +add_option("WOLFSSL_THREADSAFE_DRBG" + "Enable sharing one WC_RNG between threads (default: enabled)" + "yes" "yes;no") +if(NOT WOLFSSL_THREADSAFE_DRBG) + list(APPEND WOLFSSL_DEFINITIONS "-DWC_NO_DRBG_THREAD_SAFE") +endif() + # Valgrind (for unit tests) add_option("WOLFSSL_VALGRIND" "Enable valgrind for unit tests (default: disabled)" diff --git a/cmake/options.h.in b/cmake/options.h.in index c7ecfd7c4c6..7bed3c04d63 100644 --- a/cmake/options.h.in +++ b/cmake/options.h.in @@ -597,6 +597,8 @@ extern "C" { #cmakedefine FP_ECC #undef WC_RNG_BANK_SUPPORT #cmakedefine WC_RNG_BANK_SUPPORT +#undef WC_NO_DRBG_THREAD_SAFE +#cmakedefine WC_NO_DRBG_THREAD_SAFE #undef HAVE_VALGRIND #cmakedefine HAVE_VALGRIND #undef HAVE_CRL_MONITOR diff --git a/configure.ac b/configure.ac index 48993b57edc..680abb69eea 100644 --- a/configure.ac +++ b/configure.ac @@ -2721,6 +2721,24 @@ then AM_CFLAGS="$AM_CFLAGS -DWC_RNG_BANK_SUPPORT" fi +# Thread-safe DRBG. Serializes the generate/reseed path of a single WC_RNG so +# one instance can be shared between threads. Needs real atomics, so a +# single-threaded build elects it off in random.h regardless of this setting. +AC_ARG_ENABLE([threadsafe-drbg], + [AS_HELP_STRING([--enable-threadsafe-drbg],[Enable sharing one WC_RNG between threads (default: enabled)])], + [ ENABLED_THREADSAFE_DRBG=$enableval ], + [ ENABLED_THREADSAFE_DRBG=yes ] + ) + +if test "$ENABLED_THREADSAFE_DRBG" = "no" +then + AM_CFLAGS="$AM_CFLAGS -DWC_NO_DRBG_THREAD_SAFE" +elif test "x$enable_threadsafe_drbg" = "xyes" && \ + test "$ENABLED_SINGLETHREADED" = "yes" +then + AC_MSG_ERROR([--enable-threadsafe-drbg is incompatible with --enable-singlethreaded.]) +fi + # DTLS-SCTP AC_ARG_ENABLE([sctp], diff --git a/wolfcrypt/src/random.c b/wolfcrypt/src/random.c index ea778c402c0..547ec33a571 100644 --- a/wolfcrypt/src/random.c +++ b/wolfcrypt/src/random.c @@ -687,9 +687,44 @@ static int Hash_DRBG_Reseed(DRBG_internal* drbg, const byte* seed, word32 seedSz * and array_add_one (shared utility) which both must * remain available to SHA-512-only builds */ +#ifndef WC_NO_DRBG_THREAD_SAFE +/* Thread-safe DRBG support. A compare-exchange flag + * rather than a lock, so it is legal in every context the DRBG runs in and + * contention waits with WC_RELAX_LONG_LOOP(). Returns 1 when this call took + * the flag and the caller must release it. */ +static int RngExclEnter(WC_RNG* rng) +{ + WC_ATOMIC_INT_ARG expected = WC_RNG_EXCL_FREE; + + if (WOLFSSL_ATOMIC_LOAD(rng->excl) == WC_RNG_EXCL_OWNER) { + return 0; + } + + while (! wolfSSL_Atomic_Int_CompareExchange(&rng->excl, &expected, + WC_RNG_EXCL_HELD)) + { + expected = WC_RNG_EXCL_FREE; + WC_RELAX_LONG_LOOP(); + } + + return 1; +} + +/* Only ever called by the thread that got 1 from RngExclEnter(). */ +static void RngExclExit(WC_RNG* rng) +{ + WOLFSSL_ATOMIC_STORE(rng->excl, WC_RNG_EXCL_FREE); +} +#endif /* !WC_NO_DRBG_THREAD_SAFE */ + /* Returns: DRBG_SUCCESS and DRBG_FAILURE or BAD_FUNC_ARG on fail */ int wc_RNG_DRBG_Reseed(WC_RNG* rng, const byte* seed, word32 seedSz) { +#ifndef WC_NO_DRBG_THREAD_SAFE + int ret; + int excl; +#endif + if (rng == NULL || seed == NULL) { return BAD_FUNC_ARG; } @@ -705,8 +740,19 @@ int wc_RNG_DRBG_Reseed(WC_RNG* rng, const byte* seed, word32 seedSz) #endif return BAD_FUNC_ARG; } +#ifndef WC_NO_DRBG_THREAD_SAFE + /* Serialize against Generate on the same instance. */ + excl = RngExclEnter(rng); + ret = Hash_DRBG_Reseed((DRBG_internal *)rng->drbg, seed, seedSz, + NULL, 0); + if (excl) { + RngExclExit(rng); + } + return ret; +#else return Hash_DRBG_Reseed((DRBG_internal *)rng->drbg, seed, seedSz, NULL, 0); +#endif } #endif #ifdef WOLFSSL_DRBG_SHA512 @@ -720,8 +766,19 @@ int wc_RNG_DRBG_Reseed(WC_RNG* rng, const byte* seed, word32 seedSz) #endif return BAD_FUNC_ARG; } +#ifndef WC_NO_DRBG_THREAD_SAFE + /* Serialize against Generate on the same instance. */ + excl = RngExclEnter(rng); + ret = Hash512_DRBG_Reseed((DRBG_SHA512_internal *)rng->drbg512, + seed, seedSz, NULL, 0); + if (excl) { + RngExclExit(rng); + } + return ret; +#else return Hash512_DRBG_Reseed((DRBG_SHA512_internal *)rng->drbg512, seed, seedSz, NULL, 0); +#endif } #endif @@ -1914,6 +1971,7 @@ static int _InitRng(WC_RNG* rng, byte* nonce, word32 nonceSz, if (nonce == NULL && nonceSz != 0) return BAD_FUNC_ARG; + /* Also initializes rng->excl where the thread-safe DRBG is enabled. */ XMEMSET(rng, 0, sizeof(*rng)); #ifdef WOLFSSL_HEAP_TEST @@ -2526,6 +2584,9 @@ int wc_RNG_GenerateBlock(WC_RNG* rng, byte* output, word32 sz) #endif { int ret; +#ifndef WC_NO_DRBG_THREAD_SAFE + int excl = 0; +#endif if (rng == NULL || output == NULL) return BAD_FUNC_ARG; @@ -2584,12 +2645,31 @@ int wc_RNG_GenerateBlock(WC_RNG* rng, byte* output, word32 sz) if (rng->status != DRBG_OK) return RNG_FAILURE_E; +#ifndef WC_NO_DRBG_THREAD_SAFE + /* Serialize the DRBG core for callers sharing this instance; the paths + * above touch no DRBG state and stay outside. */ + excl = RngExclEnter(rng); + + /* Re-check: the instance may have changed state while we waited. */ + if (rng->status != DRBG_OK) { + if (excl) { + RngExclExit(rng); + } + return RNG_FAILURE_E; + } +#endif + #if defined(HAVE_GETPID) && !defined(WOLFSSL_NO_GETPID) if (rng->pid != getpid()) { rng->pid = getpid(); ret = PollAndReSeed(rng); if (ret != DRBG_SUCCESS) { rng->status = DRBG_FAILED; + #ifndef WC_NO_DRBG_THREAD_SAFE + if (excl) { + RngExclExit(rng); + } + #endif return RNG_FAILURE_E; } } @@ -2637,6 +2717,12 @@ int wc_RNG_GenerateBlock(WC_RNG* rng, byte* output, word32 sz) ret = RNG_FAILURE_E; rng->status = DRBG_FAILED; } + +#ifndef WC_NO_DRBG_THREAD_SAFE + if (excl) { + RngExclExit(rng); + } +#endif #else /* if we get here then there is an RNG configuration error */ @@ -2792,6 +2878,11 @@ int wc_FreeRng(WC_RNG* rng) } #endif +#ifndef WC_NO_DRBG_THREAD_SAFE + /* Last, so a WC_RNG re-instantiated in place does not start out marked. */ + WOLFSSL_ATOMIC_STORE(rng->excl, WC_RNG_EXCL_FREE); +#endif + return ret; } diff --git a/wolfcrypt/test/test.c b/wolfcrypt/test/test.c index ec68a860936..636f8dd5f27 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -925,6 +925,9 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t srp_test(void); #endif #ifndef WC_NO_RNG WOLFSSL_TEST_SUBROUTINE wc_test_ret_t random_test(void); +#ifndef WC_NO_DRBG_THREAD_SAFE +WOLFSSL_TEST_SUBROUTINE wc_test_ret_t random_thread_test(void); +#endif #ifdef WC_RNG_BANK_SUPPORT WOLFSSL_TEST_SUBROUTINE wc_test_ret_t random_bank_test(void); #endif @@ -2558,6 +2561,12 @@ options: [-s max_relative_stack_bytes] [-m max_relative_heap_memory_bytes]\n\ TEST_FAIL("RANDOM test failed!\n", ret); else TEST_PASS("RANDOM test passed!\n"); +#ifndef WC_NO_DRBG_THREAD_SAFE + if ((ret = random_thread_test()) != 0) + TEST_FAIL("RNGTHRD test failed!\n", ret); + else + TEST_PASS("RNGTHRD test passed!\n"); +#endif #ifdef WC_RNG_BANK_SUPPORT if ((ret = random_bank_test()) != 0) TEST_FAIL("RNGBANK test failed!\n", ret); @@ -26998,6 +27007,125 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t random_test(void) #endif /* !HAVE_HASHDRBG || CUSTOM_RAND_GENERATE_BLOCK || HAVE_INTEL_RDRAND */ +#ifndef WC_NO_DRBG_THREAD_SAFE + +/* Exercises the thread-safe DRBG: concurrent draws from one shared WC_RNG. + * Skipped by a build that elects WC_NO_DRBG_THREAD_SAFE, along with the + * feature itself. */ + +#ifndef WC_RNG_THREAD_TEST_THREADS + #define WC_RNG_THREAD_TEST_THREADS 8 +#endif +#ifndef WC_RNG_THREAD_TEST_DRAWS + #define WC_RNG_THREAD_TEST_DRAWS 128 +#endif +#ifndef WC_RNG_THREAD_TEST_BLKSZ + #define WC_RNG_THREAD_TEST_BLKSZ 32 +#endif + +#define WC_RNG_THREAD_TEST_BLOCKS \ + (WC_RNG_THREAD_TEST_THREADS * WC_RNG_THREAD_TEST_DRAWS) + +struct rng_thread_test_args { + WC_RNG* rng; + byte* out; /* this worker's slice, DRAWS * BLKSZ bytes */ + int ret; +}; + +static THREAD_RETURN WOLFSSL_THREAD rng_thread_test_worker(void* argp) +{ + struct rng_thread_test_args* args = (struct rng_thread_test_args*)argp; + int i; + + for (i = 0; i < WC_RNG_THREAD_TEST_DRAWS; i++) { + int ret = wc_RNG_GenerateBlock(args->rng, + args->out + ((size_t)i * WC_RNG_THREAD_TEST_BLKSZ), + WC_RNG_THREAD_TEST_BLKSZ); + if (ret != 0) { + args->ret = ret; + break; + } + } + + WOLFSSL_RETURN_FROM_THREAD(0); +} + +WOLFSSL_TEST_SUBROUTINE wc_test_ret_t random_thread_test(void) +{ + THREAD_TYPE threads[WC_RNG_THREAD_TEST_THREADS]; + struct rng_thread_test_args args[WC_RNG_THREAD_TEST_THREADS]; + WC_RNG rng; + byte* out = NULL; + int rng_inited = 0; + int started = 0; + int i, j; + wc_test_ret_t ret; + + WOLFSSL_ENTER("random_thread_test"); + + out = (byte*)XMALLOC((size_t)WC_RNG_THREAD_TEST_BLOCKS * + WC_RNG_THREAD_TEST_BLKSZ, HEAP_HINT, + DYNAMIC_TYPE_TMP_BUFFER); + if (out == NULL) + return WC_TEST_RET_ENC_EC(MEMORY_E); + + ret = wc_InitRng_ex(&rng, HEAP_HINT, devId); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out_free); + rng_inited = 1; + + for (i = 0; i < WC_RNG_THREAD_TEST_THREADS; i++) { + args[i].rng = &rng; + args[i].out = out + ((size_t)i * WC_RNG_THREAD_TEST_DRAWS * + WC_RNG_THREAD_TEST_BLKSZ); + args[i].ret = 0; + if (wolfSSL_NewThread(&threads[i], &rng_thread_test_worker, + &args[i]) != 0) { + ERROR_OUT(WC_TEST_RET_ENC_NC, out_join); + } + started++; + } + +out_join: + + for (i = 0; i < started; i++) + (void)wolfSSL_JoinThread(threads[i]); + + if (ret != 0) + goto out_free; + + for (i = 0; i < started; i++) { + if (args[i].ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(args[i].ret), out_free); + } + + /* All-pairs rather than a sort: no XQSORT dependency, and the block count + * makes the quadratic scan negligible. */ + for (i = 1; i < WC_RNG_THREAD_TEST_BLOCKS; i++) { + for (j = 0; j < i; j++) { + if (XMEMCMP(out + ((size_t)i * WC_RNG_THREAD_TEST_BLKSZ), + out + ((size_t)j * WC_RNG_THREAD_TEST_BLKSZ), + WC_RNG_THREAD_TEST_BLKSZ) == 0) { + ERROR_OUT(WC_TEST_RET_ENC_NC, out_free); + } + } + } + +out_free: + + if (rng_inited) { + int free_ret = wc_FreeRng(&rng); + if ((ret == 0) && (free_ret != 0)) + ret = WC_TEST_RET_ENC_EC(free_ret); + } + + XFREE(out, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + + return ret; +} + +#endif /* !WC_NO_DRBG_THREAD_SAFE */ + #ifdef WC_RNG_BANK_SUPPORT static char *rng_bank_affinity_lock_lock; diff --git a/wolfcrypt/test/test.h b/wolfcrypt/test/test.h index ab1f22e85b7..75bff32a773 100644 --- a/wolfcrypt/test/test.h +++ b/wolfcrypt/test/test.h @@ -38,6 +38,10 @@ #include #include +#ifndef WC_NO_RNG + /* for WC_NO_DRBG_THREAD_SAFE, which random.h may elect on. */ + #include +#endif #ifdef HAVE_STACK_SIZE THREAD_RETURN WOLFSSL_THREAD wolfcrypt_test(void* args); @@ -251,6 +255,9 @@ extern WOLFSSL_TEST_SUBROUTINE wc_test_ret_t srp_test(void); #endif #ifndef WC_NO_RNG extern WOLFSSL_TEST_SUBROUTINE wc_test_ret_t random_test(void); +#ifndef WC_NO_DRBG_THREAD_SAFE +extern WOLFSSL_TEST_SUBROUTINE wc_test_ret_t random_thread_test(void); +#endif #ifdef WC_RNG_BANK_SUPPORT extern WOLFSSL_TEST_SUBROUTINE wc_test_ret_t random_bank_test(void); #endif diff --git a/wolfssl/wolfcrypt/random.h b/wolfssl/wolfcrypt/random.h index d9c5a9289b9..27312b27ca0 100644 --- a/wolfssl/wolfcrypt/random.h +++ b/wolfssl/wolfcrypt/random.h @@ -357,6 +357,33 @@ enum wc_RngHealthState { WOLF_ENUM_DUMMY_LAST_ELEMENT(wc_RngHealthState) }; +/* Thread-safe DRBG -- enabled by default. + * + * Serializes the generate and reseed path of a single WC_RNG so that one + * instance can be shared across threads without an external lock of its own. + * + * Define WC_NO_DRBG_THREAD_SAFE to opt out where an instance is only ever used + * by one thread at a time and the per-call atomic is not wanted. This is an + * election, independent of SINGLE_THREADED -- a multi-threaded build that keeps + * its WC_RNGs thread-local can opt out and keep the smaller struct. */ + +/* A build with no DRBG, no atomics, or no threads has nothing to implement + * this with, so elect it off here rather than making every use site restate + * the requirements. */ +#if (!defined(HAVE_HASHDRBG) || defined(CUSTOM_RAND_GENERATE_BLOCK) || \ + defined(SINGLE_THREADED) || defined(WOLFSSL_NO_ATOMICS)) && \ + !defined(WC_NO_DRBG_THREAD_SAFE) + #define WC_NO_DRBG_THREAD_SAFE +#endif + +#ifndef WC_NO_DRBG_THREAD_SAFE + #define WC_RNG_EXCL_FREE 0 + #define WC_RNG_EXCL_HELD 1 + /* Stored once by an owner that already supplies exclusivity for this + * instance, which then takes no flag of its own. Nothing here sets it. */ + #define WC_RNG_EXCL_OWNER 2 +#endif + /* RNG context */ struct WC_RNG { struct OS_Seed seed; @@ -417,6 +444,12 @@ struct WC_RNG { #endif /* WC_RNG_BANK_SUPPORT || HAVE_HASHDRBG */ +#ifndef WC_NO_DRBG_THREAD_SAFE + /* Serializes this instance's DRBG generate/reseed path. Outside the union + * above, and left FREE by the _InitRng() XMEMSET. */ + wolfSSL_Atomic_Int excl; +#endif + #if defined(HAVE_GETPID) && !defined(WOLFSSL_NO_GETPID) pid_t pid; #endif From b638c17b021f862dc6ef4a631297d6d2d302d3b4 Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Wed, 19 Aug 2026 15:36:19 -0600 Subject: [PATCH 2/9] update dox --- doc/dox_comments/header_files/random.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/dox_comments/header_files/random.h b/doc/dox_comments/header_files/random.h index fa870a927e4..12f7fe994cd 100644 --- a/doc/dox_comments/header_files/random.h +++ b/doc/dox_comments/header_files/random.h @@ -113,6 +113,13 @@ int wc_InitRng(WC_RNG* rng); \param output buffer to which the block is copied \param sz size of output in bytes + \note One WC_RNG may be shared between threads: the DRBG generate and + reseed path is serialized per instance. Build with + --disable-threadsafe-drbg (WC_NO_DRBG_THREAD_SAFE) to opt out where each + instance is only used by one thread at a time. This covers generate and + reseed only -- wc_InitRng() and wc_FreeRng() must not run concurrently + with a generate on the same instance. + _Example_ \code RNG rng; From f7d57931372b80644a362abb21389a57a8c7b8c5 Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Wed, 19 Aug 2026 17:13:10 -0600 Subject: [PATCH 3/9] Portability and CI fixes --- wolfcrypt/test/test.c | 36 +++++++++++++++++++++--------------- wolfcrypt/test/test.h | 22 +++++++++++++++++++++- wolfssl/wolfcrypt/random.h | 23 ++++++++++++++--------- wolfssl/wolfcrypt/types.h | 30 +++++++++++++++++++++++++++++- 4 files changed, 85 insertions(+), 26 deletions(-) diff --git a/wolfcrypt/test/test.c b/wolfcrypt/test/test.c index 636f8dd5f27..a6aa4406855 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -925,7 +925,7 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t srp_test(void); #endif #ifndef WC_NO_RNG WOLFSSL_TEST_SUBROUTINE wc_test_ret_t random_test(void); -#ifndef WC_NO_DRBG_THREAD_SAFE +#ifdef WC_TEST_THREADSAFE_DRBG WOLFSSL_TEST_SUBROUTINE wc_test_ret_t random_thread_test(void); #endif #ifdef WC_RNG_BANK_SUPPORT @@ -2561,7 +2561,7 @@ options: [-s max_relative_stack_bytes] [-m max_relative_heap_memory_bytes]\n\ TEST_FAIL("RANDOM test failed!\n", ret); else TEST_PASS("RANDOM test passed!\n"); -#ifndef WC_NO_DRBG_THREAD_SAFE +#ifdef WC_TEST_THREADSAFE_DRBG if ((ret = random_thread_test()) != 0) TEST_FAIL("RNGTHRD test failed!\n", ret); else @@ -27007,17 +27007,17 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t random_test(void) #endif /* !HAVE_HASHDRBG || CUSTOM_RAND_GENERATE_BLOCK || HAVE_INTEL_RDRAND */ -#ifndef WC_NO_DRBG_THREAD_SAFE +#ifdef WC_TEST_THREADSAFE_DRBG /* Exercises the thread-safe DRBG: concurrent draws from one shared WC_RNG. - * Skipped by a build that elects WC_NO_DRBG_THREAD_SAFE, along with the - * feature itself. */ + * Needs both the feature and the portable thread API, so it is skipped + * where either is absent. */ #ifndef WC_RNG_THREAD_TEST_THREADS - #define WC_RNG_THREAD_TEST_THREADS 8 + #define WC_RNG_THREAD_TEST_THREADS 4 #endif #ifndef WC_RNG_THREAD_TEST_DRAWS - #define WC_RNG_THREAD_TEST_DRAWS 128 + #define WC_RNG_THREAD_TEST_DRAWS 96 #endif #ifndef WC_RNG_THREAD_TEST_BLKSZ #define WC_RNG_THREAD_TEST_BLKSZ 32 @@ -27058,6 +27058,7 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t random_thread_test(void) byte* out = NULL; int rng_inited = 0; int started = 0; + int nblocks; int i, j; wc_test_ret_t ret; @@ -27066,8 +27067,11 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t random_thread_test(void) out = (byte*)XMALLOC((size_t)WC_RNG_THREAD_TEST_BLOCKS * WC_RNG_THREAD_TEST_BLKSZ, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); - if (out == NULL) - return WC_TEST_RET_ENC_EC(MEMORY_E); + if (out == NULL) { + /* Opportunistic check: a target too small to hold the buffer is not + * evidence of a DRBG defect, so skip rather than report failure. */ + return 0; + } ret = wc_InitRng_ex(&rng, HEAP_HINT, devId); if (ret != 0) @@ -27081,17 +27085,18 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t random_thread_test(void) args[i].ret = 0; if (wolfSSL_NewThread(&threads[i], &rng_thread_test_worker, &args[i]) != 0) { - ERROR_OUT(WC_TEST_RET_ENC_NC, out_join); + /* Out of thread resources; run with the ones we have. */ + break; } started++; } -out_join: - for (i = 0; i < started; i++) (void)wolfSSL_JoinThread(threads[i]); - if (ret != 0) + /* Fewer than two workers means nothing ran concurrently, so there was + * nothing for this test to observe. Skip rather than report failure. */ + if (started < 2) goto out_free; for (i = 0; i < started; i++) { @@ -27101,7 +27106,8 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t random_thread_test(void) /* All-pairs rather than a sort: no XQSORT dependency, and the block count * makes the quadratic scan negligible. */ - for (i = 1; i < WC_RNG_THREAD_TEST_BLOCKS; i++) { + nblocks = started * WC_RNG_THREAD_TEST_DRAWS; + for (i = 1; i < nblocks; i++) { for (j = 0; j < i; j++) { if (XMEMCMP(out + ((size_t)i * WC_RNG_THREAD_TEST_BLKSZ), out + ((size_t)j * WC_RNG_THREAD_TEST_BLKSZ), @@ -27124,7 +27130,7 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t random_thread_test(void) return ret; } -#endif /* !WC_NO_DRBG_THREAD_SAFE */ +#endif /* WC_TEST_THREADSAFE_DRBG */ #ifdef WC_RNG_BANK_SUPPORT diff --git a/wolfcrypt/test/test.h b/wolfcrypt/test/test.h index 75bff32a773..19b3f4788e5 100644 --- a/wolfcrypt/test/test.h +++ b/wolfcrypt/test/test.h @@ -43,6 +43,26 @@ #include #endif +/* The thread-safe DRBG test drives one instance from several threads, so it + * needs more than the feature itself: + * - wolfSSL_NewThread()/wolfSSL_JoinThread(), which are only implemented for + * a subset of targets (notably not WOLFSSL_LINUXKM, which builds this file + * into the kernel module), so require one that has them rather than + * assuming !SINGLE_THREADED is enough; + * - a general-purpose heap for the comparison buffer, which rules out + * WOLFSSL_NO_MALLOC and WOLFSSL_STATIC_MEMORY; + * - a random.c/random.h pair that actually carries the feature. A FIPS + * build checks out locked copies of both from the module's tag, and those + * predate it, so HAVE_FIPS is excluded outright. Note the locked + * random.h also never defines WC_NO_DRBG_THREAD_SAFE, so the test above + * cannot detect this on its own. */ +#if !defined(WC_NO_DRBG_THREAD_SAFE) && !defined(HAVE_FIPS) && \ + !defined(WOLFSSL_NO_MALLOC) && !defined(WOLFSSL_STATIC_MEMORY) && \ + (defined(WOLFSSL_PTHREADS) || \ + (defined(USE_WINDOWS_API) && !defined(_WIN32_WCE))) + #define WC_TEST_THREADSAFE_DRBG +#endif + #ifdef HAVE_STACK_SIZE THREAD_RETURN WOLFSSL_THREAD wolfcrypt_test(void* args); #else @@ -255,7 +275,7 @@ extern WOLFSSL_TEST_SUBROUTINE wc_test_ret_t srp_test(void); #endif #ifndef WC_NO_RNG extern WOLFSSL_TEST_SUBROUTINE wc_test_ret_t random_test(void); -#ifndef WC_NO_DRBG_THREAD_SAFE +#ifdef WC_TEST_THREADSAFE_DRBG extern WOLFSSL_TEST_SUBROUTINE wc_test_ret_t random_thread_test(void); #endif #ifdef WC_RNG_BANK_SUPPORT diff --git a/wolfssl/wolfcrypt/random.h b/wolfssl/wolfcrypt/random.h index 27312b27ca0..02c0922e72f 100644 --- a/wolfssl/wolfcrypt/random.h +++ b/wolfssl/wolfcrypt/random.h @@ -79,6 +79,20 @@ /* avoid redefinition of structs */ +/* A build with no DRBG, no atomics, or no threads has nothing to implement + * this with, so elect it off here rather than making every use site restate + * the requirements. Kept ahead of the FIPS-version guard below: the use + * sites test !defined(WC_NO_DRBG_THREAD_SAFE), so this must be evaluated on + * every path that reaches them, including the one where the WC_RNG defined + * below is not the struct in use. */ +#if (!defined(HAVE_HASHDRBG) || defined(CUSTOM_RAND_GENERATE_BLOCK) || \ + defined(SINGLE_THREADED) || defined(WOLFSSL_NO_ATOMICS) || \ + (defined(HAVE_FIPS) && \ + !(defined(HAVE_FIPS_VERSION) && (HAVE_FIPS_VERSION >= 2)))) && \ + !defined(WC_NO_DRBG_THREAD_SAFE) + #define WC_NO_DRBG_THREAD_SAFE +#endif + #if !defined(HAVE_FIPS) || \ (defined(HAVE_FIPS_VERSION) && (HAVE_FIPS_VERSION >= 2)) @@ -367,15 +381,6 @@ enum wc_RngHealthState { * election, independent of SINGLE_THREADED -- a multi-threaded build that keeps * its WC_RNGs thread-local can opt out and keep the smaller struct. */ -/* A build with no DRBG, no atomics, or no threads has nothing to implement - * this with, so elect it off here rather than making every use site restate - * the requirements. */ -#if (!defined(HAVE_HASHDRBG) || defined(CUSTOM_RAND_GENERATE_BLOCK) || \ - defined(SINGLE_THREADED) || defined(WOLFSSL_NO_ATOMICS)) && \ - !defined(WC_NO_DRBG_THREAD_SAFE) - #define WC_NO_DRBG_THREAD_SAFE -#endif - #ifndef WC_NO_DRBG_THREAD_SAFE #define WC_RNG_EXCL_FREE 0 #define WC_RNG_EXCL_HELD 1 diff --git a/wolfssl/wolfcrypt/types.h b/wolfssl/wolfcrypt/types.h index 3cce9b4c70b..a37469c8ac2 100644 --- a/wolfssl/wolfcrypt/types.h +++ b/wolfssl/wolfcrypt/types.h @@ -2412,8 +2412,36 @@ WOLFSSL_API word32 CheckRunTimeSettings(void); struct wc_static_assert_dummy_struct #endif +/* Hook run once per iteration of a long wait loop. On a preemptive + * general-purpose OS a bare spin merely wastes cycles, but on a uniprocessor + * RTOS a spinning higher-priority task can starve the lower-priority task it + * is waiting on, so map it to that RTOS's cooperative yield where one is in + * scope. Any port may define WC_RELAX_LONG_LOOP ahead of this. */ #ifndef WC_RELAX_LONG_LOOP - #define WC_RELAX_LONG_LOOP() WC_DO_NOTHING + #if defined(WOLFSSL_ZEPHYR) && !defined(SINGLE_THREADED) + /* is included by wc_port.h whenever + * !SINGLE_THREADED, so k_yield() is declared here. */ + #define WC_RELAX_LONG_LOOP() k_yield() + #elif (defined(FREERTOS) || defined(FREERTOS_TCP) || \ + defined(WOLFSSL_SAFERTOS)) && defined(taskYIELD) + /* Same grouping wc_port.h uses for these three. taskYIELD() is a + * macro from FreeRTOS task.h, which none of these paths include + * themselves, so key off the macro rather than assume it: a build + * without task.h keeps the no-op. */ + #define WC_RELAX_LONG_LOOP() taskYIELD() + #elif defined(THREADX) + /* is included by wc_port.h for every THREADX build. */ + #define WC_RELAX_LONG_LOOP() tx_thread_relinquish() + #elif defined(WOLFSSL_TIRTOS) + /* is included by wc_port.h for every TIRTOS + * translation unit. */ + #define WC_RELAX_LONG_LOOP() Task_yield() + #elif defined(RTTHREAD) && !defined(SINGLE_THREADED) + /* "rtthread.h" is included by wc_port.h on the multi-threaded path. */ + #define WC_RELAX_LONG_LOOP() rt_thread_yield() + #else + #define WC_RELAX_LONG_LOOP() WC_DO_NOTHING + #endif #endif #ifndef WC_CHECK_FOR_INTR_SIGNALS #define WC_CHECK_FOR_INTR_SIGNALS() 0 From 2f441fd0bb6748de561847024ba1dca1336ced33 Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Wed, 19 Aug 2026 18:26:38 -0600 Subject: [PATCH 4/9] CI fixes --- wolfcrypt/test/test.h | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/wolfcrypt/test/test.h b/wolfcrypt/test/test.h index 19b3f4788e5..73882da9bff 100644 --- a/wolfcrypt/test/test.h +++ b/wolfcrypt/test/test.h @@ -51,12 +51,13 @@ * assuming !SINGLE_THREADED is enough; * - a general-purpose heap for the comparison buffer, which rules out * WOLFSSL_NO_MALLOC and WOLFSSL_STATIC_MEMORY; - * - a random.c/random.h pair that actually carries the feature. A FIPS - * build checks out locked copies of both from the module's tag, and those - * predate it, so HAVE_FIPS is excluded outright. Note the locked - * random.h also never defines WC_NO_DRBG_THREAD_SAFE, so the test above - * cannot detect this on its own. */ -#if !defined(WC_NO_DRBG_THREAD_SAFE) && !defined(HAVE_FIPS) && \ + * - a random.c/random.h pair that actually carries the feature. A FIPS or + * selftest build checks out locked copies of both from the module's tag, + * and those predate it, so HAVE_FIPS and HAVE_SELFTEST are excluded + * outright. Note the locked random.h also never defines + * WC_NO_DRBG_THREAD_SAFE, so the test above cannot detect this itself. */ +#if !defined(WC_NO_DRBG_THREAD_SAFE) && \ + !defined(HAVE_FIPS) && !defined(HAVE_SELFTEST) && \ !defined(WOLFSSL_NO_MALLOC) && !defined(WOLFSSL_STATIC_MEMORY) && \ (defined(WOLFSSL_PTHREADS) || \ (defined(USE_WINDOWS_API) && !defined(_WIN32_WCE))) From 4c1df45973b42fa71c10e30df692cd02be3ac5b3 Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Wed, 19 Aug 2026 23:43:13 -0600 Subject: [PATCH 5/9] multi-test item --- .wolfssl_known_macro_extras | 1 + 1 file changed, 1 insertion(+) diff --git a/.wolfssl_known_macro_extras b/.wolfssl_known_macro_extras index 4f88a806c0d..c56d965b9af 100644 --- a/.wolfssl_known_macro_extras +++ b/.wolfssl_known_macro_extras @@ -1361,6 +1361,7 @@ fipsCastStatus_get noinline ssize_t sun +taskYIELD versal wc_Des3_SetKey wc_Tls13_HKDF_Expand_Label From a245cf0ac91fa81c40e89f0ab9677238e98af99c Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Thu, 20 Aug 2026 11:42:00 -0600 Subject: [PATCH 6/9] Update per peer reviewer request --- configure.ac | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/configure.ac b/configure.ac index 680abb69eea..4886c34cc60 100644 --- a/configure.ac +++ b/configure.ac @@ -2724,10 +2724,19 @@ fi # Thread-safe DRBG. Serializes the generate/reseed path of a single WC_RNG so # one instance can be shared between threads. Needs real atomics, so a # single-threaded build elects it off in random.h regardless of this setting. +# +# Default off for --enable-linuxkm: the kernel module reaches the DRBG through +# the RNG bank, which checks out an instance exclusively, so the per-generate +# atomic is redundant there and its wait has no place in an atomic context. +# Enable it explicitly with --enable-threadsafe-drbg. AC_ARG_ENABLE([threadsafe-drbg], - [AS_HELP_STRING([--enable-threadsafe-drbg],[Enable sharing one WC_RNG between threads (default: enabled)])], + [AS_HELP_STRING([--enable-threadsafe-drbg],[Enable sharing one WC_RNG between threads (default: enabled, except linuxkm)])], [ ENABLED_THREADSAFE_DRBG=$enableval ], - [ ENABLED_THREADSAFE_DRBG=yes ] + [ if test "$ENABLED_LINUXKM" = "no"; then + ENABLED_THREADSAFE_DRBG=yes + else + ENABLED_THREADSAFE_DRBG=no + fi ] ) if test "$ENABLED_THREADSAFE_DRBG" = "no" From db5fe55b3cf961ee26598a7f05408b08ca49fa5e Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Thu, 20 Aug 2026 12:12:10 -0600 Subject: [PATCH 7/9] Fenrir items addressed --- wolfcrypt/src/random.c | 54 +++++++++++++++++++++++++++++++++++---- wolfssl/wolfcrypt/types.h | 53 +++++++++++++++++++++----------------- 2 files changed, 79 insertions(+), 28 deletions(-) diff --git a/wolfcrypt/src/random.c b/wolfcrypt/src/random.c index 547ec33a571..a18b836e1eb 100644 --- a/wolfcrypt/src/random.c +++ b/wolfcrypt/src/random.c @@ -688,13 +688,24 @@ static int Hash_DRBG_Reseed(DRBG_internal* drbg, const byte* seed, word32 seedSz * remain available to SHA-512-only builds */ #ifndef WC_NO_DRBG_THREAD_SAFE -/* Thread-safe DRBG support. A compare-exchange flag - * rather than a lock, so it is legal in every context the DRBG runs in and - * contention waits with WC_RELAX_LONG_LOOP(). Returns 1 when this call took - * the flag and the caller must release it. */ +/* Thread-safe DRBG support. Serializes this instance's generate and reseed + * path so one WC_RNG can be shared between threads; it is not a lock discipline + * and makes no claim about atomic-context callers. + * + * The wait is bounded the way the rng_bank spins are: it breaks out on an + * interrupting signal, and on WC_RNG_EXCL_TIMEOUT_SEC when that is defined. + * No timeout is applied by default -- the holder can legitimately be blocked + * in wc_GenerateSeed() for as long as the OS entropy source takes, and failing + * a generate on a slow entropy read would be worse than waiting for it. + * + * Returns 1 when this call took the flag and the caller must release it, 0 when + * exclusivity comes from the owner, or a negative error code. */ static int RngExclEnter(WC_RNG* rng) { WC_ATOMIC_INT_ARG expected = WC_RNG_EXCL_FREE; +#ifdef WC_RNG_EXCL_TIMEOUT_SEC + time_t ts1 = XTIME(0); +#endif if (WOLFSSL_ATOMIC_LOAD(rng->excl) == WC_RNG_EXCL_OWNER) { return 0; @@ -703,8 +714,32 @@ static int RngExclEnter(WC_RNG* rng) while (! wolfSSL_Atomic_Int_CompareExchange(&rng->excl, &expected, WC_RNG_EXCL_HELD)) { + int intr_ret; + + #if defined(HAVE_GETPID) && !defined(WOLFSSL_NO_GETPID) + /* fork() copies excl as ordinary memory, so a flag another thread held + * at fork time is inherited HELD with no owner left to release it, and + * the pid recovery further down is never reached. Checked only after a + * failed acquire, so the uncontended path is unchanged; the child is + * single-threaded here, so clearing it is safe. */ + if (rng->pid != getpid()) { + WOLFSSL_ATOMIC_STORE(rng->excl, WC_RNG_EXCL_FREE); + } + #endif + + intr_ret = WC_CHECK_FOR_INTR_SIGNALS(); + if (intr_ret != 0) { + return intr_ret; + } + + #ifdef WC_RNG_EXCL_TIMEOUT_SEC + if (XTIME(0) - ts1 > (time_t)WC_RNG_EXCL_TIMEOUT_SEC) { + return WC_TIMEOUT_E; + } + #endif + expected = WC_RNG_EXCL_FREE; - WC_RELAX_LONG_LOOP(); + WC_SPIN_RELAX(); } return 1; @@ -743,6 +778,9 @@ int wc_RNG_DRBG_Reseed(WC_RNG* rng, const byte* seed, word32 seedSz) #ifndef WC_NO_DRBG_THREAD_SAFE /* Serialize against Generate on the same instance. */ excl = RngExclEnter(rng); + if (excl < 0) { + return excl; + } ret = Hash_DRBG_Reseed((DRBG_internal *)rng->drbg, seed, seedSz, NULL, 0); if (excl) { @@ -769,6 +807,9 @@ int wc_RNG_DRBG_Reseed(WC_RNG* rng, const byte* seed, word32 seedSz) #ifndef WC_NO_DRBG_THREAD_SAFE /* Serialize against Generate on the same instance. */ excl = RngExclEnter(rng); + if (excl < 0) { + return excl; + } ret = Hash512_DRBG_Reseed((DRBG_SHA512_internal *)rng->drbg512, seed, seedSz, NULL, 0); if (excl) { @@ -2649,6 +2690,9 @@ int wc_RNG_GenerateBlock(WC_RNG* rng, byte* output, word32 sz) /* Serialize the DRBG core for callers sharing this instance; the paths * above touch no DRBG state and stay outside. */ excl = RngExclEnter(rng); + if (excl < 0) { + return excl; + } /* Re-check: the instance may have changed state while we waited. */ if (rng->status != DRBG_OK) { diff --git a/wolfssl/wolfcrypt/types.h b/wolfssl/wolfcrypt/types.h index a37469c8ac2..107cf61ef6e 100644 --- a/wolfssl/wolfcrypt/types.h +++ b/wolfssl/wolfcrypt/types.h @@ -2412,35 +2412,42 @@ WOLFSSL_API word32 CheckRunTimeSettings(void); struct wc_static_assert_dummy_struct #endif -/* Hook run once per iteration of a long wait loop. On a preemptive - * general-purpose OS a bare spin merely wastes cycles, but on a uniprocessor - * RTOS a spinning higher-priority task can starve the lower-priority task it - * is waiting on, so map it to that RTOS's cooperative yield where one is in - * scope. Any port may define WC_RELAX_LONG_LOOP ahead of this. */ #ifndef WC_RELAX_LONG_LOOP - #if defined(WOLFSSL_ZEPHYR) && !defined(SINGLE_THREADED) - /* is included by wc_port.h whenever - * !SINGLE_THREADED, so k_yield() is declared here. */ - #define WC_RELAX_LONG_LOOP() k_yield() + #define WC_RELAX_LONG_LOOP() WC_DO_NOTHING +#endif + +/* Yield hook for the DRBG acquire spin in RngExclEnter(). Deliberately + * separate from WC_RELAX_LONG_LOOP(), which also backs the + * SAVE_/RESTORE_NO_VECTOR_REGISTERS fallbacks below and must stay a no-op + * there -- those expand at hundreds of crypto call sites, and a scheduler + * yield does not belong in them. + * + * On a preemptive general-purpose OS a bare spin only wastes cycles, but on a + * uniprocessor RTOS a spinning higher-priority task can starve the + * lower-priority task it waits on, so use that RTOS's cooperative yield where + * one is in scope. SINGLE_THREADED is excluded first: wc_port.h omits these + * kernel headers in that configuration, so the yields have no declaration. + * Any port may define WC_SPIN_RELAX ahead of this. */ +#ifndef WC_SPIN_RELAX + #if defined(SINGLE_THREADED) + #define WC_SPIN_RELAX() WC_DO_NOTHING + #elif defined(WOLFSSL_ZEPHYR) + #define WC_SPIN_RELAX() k_yield() #elif (defined(FREERTOS) || defined(FREERTOS_TCP) || \ defined(WOLFSSL_SAFERTOS)) && defined(taskYIELD) - /* Same grouping wc_port.h uses for these three. taskYIELD() is a - * macro from FreeRTOS task.h, which none of these paths include - * themselves, so key off the macro rather than assume it: a build - * without task.h keeps the no-op. */ - #define WC_RELAX_LONG_LOOP() taskYIELD() + /* taskYIELD() is a macro from FreeRTOS task.h, which none of these + * paths include themselves, so key off the macro rather than assume + * it: a build without task.h falls through to the default. */ + #define WC_SPIN_RELAX() taskYIELD() #elif defined(THREADX) - /* is included by wc_port.h for every THREADX build. */ - #define WC_RELAX_LONG_LOOP() tx_thread_relinquish() + #define WC_SPIN_RELAX() tx_thread_relinquish() #elif defined(WOLFSSL_TIRTOS) - /* is included by wc_port.h for every TIRTOS - * translation unit. */ - #define WC_RELAX_LONG_LOOP() Task_yield() - #elif defined(RTTHREAD) && !defined(SINGLE_THREADED) - /* "rtthread.h" is included by wc_port.h on the multi-threaded path. */ - #define WC_RELAX_LONG_LOOP() rt_thread_yield() + #define WC_SPIN_RELAX() Task_yield() + #elif defined(RTTHREAD) + #define WC_SPIN_RELAX() rt_thread_yield() #else - #define WC_RELAX_LONG_LOOP() WC_DO_NOTHING + /* Ports that supply a real relax hook (linuxkm) keep it here. */ + #define WC_SPIN_RELAX() WC_RELAX_LONG_LOOP() #endif #endif #ifndef WC_CHECK_FOR_INTR_SIGNALS From ed077e601078bda8f9478b3f4e324dda577ad27a Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Thu, 20 Aug 2026 15:03:03 -0600 Subject: [PATCH 8/9] Fenrir round 2 --- .wolfssl_known_macro_extras | 1 + wolfcrypt/src/random.c | 33 ++++++++++++++++++++++++--------- wolfcrypt/test/test.c | 12 +++++++----- wolfcrypt/test/test.h | 4 +++- wolfssl/wolfcrypt/random.h | 8 ++++++-- wolfssl/wolfcrypt/types.h | 8 ++++++++ 6 files changed, 49 insertions(+), 17 deletions(-) diff --git a/.wolfssl_known_macro_extras b/.wolfssl_known_macro_extras index c56d965b9af..4235e8652ef 100644 --- a/.wolfssl_known_macro_extras +++ b/.wolfssl_known_macro_extras @@ -782,6 +782,7 @@ WC_PUF_HELPER_COMPACT WC_PUF_SHA3 WC_RNG_BANK_NO_DEFAULT_SUPPORT WC_RNG_BLOCKING +WC_RNG_EXCL_TIMEOUT_SEC WC_RSA_NONBLOCK_TIME WC_RSA_NO_FERMAT_CHECK WC_RTL8735B_NO_DERIVE_CACHE diff --git a/wolfcrypt/src/random.c b/wolfcrypt/src/random.c index a18b836e1eb..158d7ebd0f6 100644 --- a/wolfcrypt/src/random.c +++ b/wolfcrypt/src/random.c @@ -702,6 +702,15 @@ static int Hash_DRBG_Reseed(DRBG_internal* drbg, const byte* seed, word32 seedSz * exclusivity comes from the owner, or a negative error code. */ static int RngExclEnter(WC_RNG* rng) { +#if defined(HAVE_GETPID) && !defined(WOLFSSL_NO_GETPID) + /* The lock word carries the owner's pid, so a hold inherited through + * fork() (the parent's pid, which no thread here can match) is told apart + * from a live hold by this process without a second variable to race + * against. */ + WC_ATOMIC_INT_ARG self = (WC_ATOMIC_INT_ARG)getpid(); +#else + WC_ATOMIC_INT_ARG self = WC_RNG_EXCL_HELD; +#endif WC_ATOMIC_INT_ARG expected = WC_RNG_EXCL_FREE; #ifdef WC_RNG_EXCL_TIMEOUT_SEC time_t ts1 = XTIME(0); @@ -711,19 +720,25 @@ static int RngExclEnter(WC_RNG* rng) return 0; } - while (! wolfSSL_Atomic_Int_CompareExchange(&rng->excl, &expected, - WC_RNG_EXCL_HELD)) + while (! wolfSSL_Atomic_Int_CompareExchange(&rng->excl, &expected, self)) { int intr_ret; #if defined(HAVE_GETPID) && !defined(WOLFSSL_NO_GETPID) - /* fork() copies excl as ordinary memory, so a flag another thread held - * at fork time is inherited HELD with no owner left to release it, and - * the pid recovery further down is never reached. Checked only after a - * failed acquire, so the uncontended path is unchanged; the child is - * single-threaded here, so clearing it is safe. */ - if (rng->pid != getpid()) { - WOLFSSL_ATOMIC_STORE(rng->excl, WC_RNG_EXCL_FREE); + /* A failed exchange leaves the observed value in expected. Reclaim + * only a hold stamped with a different pid, and only by exchanging + * from that exact value, so a live holder -- including a sibling + * thread of this process, which stamps this same pid -- is never + * displaced. */ + if ((expected != WC_RNG_EXCL_FREE) && + (expected != WC_RNG_EXCL_OWNER) && + (expected != self)) + { + if (wolfSSL_Atomic_Int_CompareExchange(&rng->excl, &expected, + self)) + { + return 1; + } } #endif diff --git a/wolfcrypt/test/test.c b/wolfcrypt/test/test.c index a6aa4406855..bdd98d6b292 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -27094,16 +27094,18 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t random_thread_test(void) for (i = 0; i < started; i++) (void)wolfSSL_JoinThread(threads[i]); - /* Fewer than two workers means nothing ran concurrently, so there was - * nothing for this test to observe. Skip rather than report failure. */ - if (started < 2) - goto out_free; - + /* A generate failure is a real failure however many workers ran, so it is + * inspected before the concurrency check below can skip out. */ for (i = 0; i < started; i++) { if (args[i].ret != 0) ERROR_OUT(WC_TEST_RET_ENC_EC(args[i].ret), out_free); } + /* Fewer than two workers means nothing ran concurrently, so there was + * nothing for this test to observe. Skip rather than report failure. */ + if (started < 2) + goto out_free; + /* All-pairs rather than a sort: no XQSORT dependency, and the block count * makes the quadratic scan negligible. */ nblocks = started * WC_RNG_THREAD_TEST_DRAWS; diff --git a/wolfcrypt/test/test.h b/wolfcrypt/test/test.h index 73882da9bff..41a01e58f63 100644 --- a/wolfcrypt/test/test.h +++ b/wolfcrypt/test/test.h @@ -49,6 +49,8 @@ * a subset of targets (notably not WOLFSSL_LINUXKM, which builds this file * into the kernel module), so require one that has them rather than * assuming !SINGLE_THREADED is enough; + * - an RNG at all: WC_NO_RNG both removes wc_FreeRng() and stops test.h + * from including random.h, so WC_NO_DRBG_THREAD_SAFE would not be seen; * - a general-purpose heap for the comparison buffer, which rules out * WOLFSSL_NO_MALLOC and WOLFSSL_STATIC_MEMORY; * - a random.c/random.h pair that actually carries the feature. A FIPS or @@ -56,7 +58,7 @@ * and those predate it, so HAVE_FIPS and HAVE_SELFTEST are excluded * outright. Note the locked random.h also never defines * WC_NO_DRBG_THREAD_SAFE, so the test above cannot detect this itself. */ -#if !defined(WC_NO_DRBG_THREAD_SAFE) && \ +#if !defined(WC_NO_RNG) && !defined(WC_NO_DRBG_THREAD_SAFE) && \ !defined(HAVE_FIPS) && !defined(HAVE_SELFTEST) && \ !defined(WOLFSSL_NO_MALLOC) && !defined(WOLFSSL_STATIC_MEMORY) && \ (defined(WOLFSSL_PTHREADS) || \ diff --git a/wolfssl/wolfcrypt/random.h b/wolfssl/wolfcrypt/random.h index 02c0922e72f..2a18470297d 100644 --- a/wolfssl/wolfcrypt/random.h +++ b/wolfssl/wolfcrypt/random.h @@ -383,10 +383,14 @@ enum wc_RngHealthState { #ifndef WC_NO_DRBG_THREAD_SAFE #define WC_RNG_EXCL_FREE 0 + /* Value stored while held. Where getpid() is available the holder stores + * its pid instead, so a hold inherited through fork() carries the parent's + * pid and is distinguishable from a live one by the lock word alone. */ #define WC_RNG_EXCL_HELD 1 /* Stored once by an owner that already supplies exclusivity for this - * instance, which then takes no flag of its own. Nothing here sets it. */ - #define WC_RNG_EXCL_OWNER 2 + * instance, which then takes no flag of its own. Nothing here sets it. + * Negative so it can never collide with a pid. */ + #define WC_RNG_EXCL_OWNER (-1) #endif /* RNG context */ diff --git a/wolfssl/wolfcrypt/types.h b/wolfssl/wolfcrypt/types.h index 107cf61ef6e..1b7970ab527 100644 --- a/wolfssl/wolfcrypt/types.h +++ b/wolfssl/wolfcrypt/types.h @@ -2445,6 +2445,14 @@ WOLFSSL_API word32 CheckRunTimeSettings(void); #define WC_SPIN_RELAX() Task_yield() #elif defined(RTTHREAD) #define WC_SPIN_RELAX() rt_thread_yield() + #elif defined(WOLFSSL_PTHREADS) + /* wc_port.h includes on this path, which carries + * ; wolfentropy.c and async.c already call sched_yield() + * under the same assumption. */ + #define WC_SPIN_RELAX() (void)sched_yield() + #elif defined(USE_WINDOWS_API) && !defined(_WIN32_WCE) + /* is included by wc_port.h on this path. */ + #define WC_SPIN_RELAX() (void)SwitchToThread() #else /* Ports that supply a real relax hook (linuxkm) keep it here. */ #define WC_SPIN_RELAX() WC_RELAX_LONG_LOOP() From 2386d14fcc6b32f1df495b437a10959345efdfae Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Thu, 20 Aug 2026 18:00:53 -0600 Subject: [PATCH 9/9] Fenrir round 3 --- wolfcrypt/test/test.c | 17 ++++++++- wolfssl/wolfcrypt/random.h | 8 ++-- wolfssl/wolfcrypt/types.h | 75 +++++++++++++++++++++++--------------- 3 files changed, 67 insertions(+), 33 deletions(-) diff --git a/wolfcrypt/test/test.c b/wolfcrypt/test/test.c index bdd98d6b292..2ff968b08c1 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -27028,7 +27028,8 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t random_test(void) struct rng_thread_test_args { WC_RNG* rng; - byte* out; /* this worker's slice, DRAWS * BLKSZ bytes */ + byte* out; /* this worker's slice, DRAWS * BLKSZ bytes */ + int reseeder; /* nonzero: also drive the reseed side of the exclusion */ int ret; }; @@ -27045,6 +27046,19 @@ static THREAD_RETURN WOLFSSL_THREAD rng_thread_test_worker(void* argp) args->ret = ret; break; } + + /* One worker also reseeds, so the exclusion in wc_RNG_DRBG_Reseed() + * is covered and runs against the other workers' generates. Output + * must stay unique across the reseed. */ + if (args->reseeder && ((i % 8) == 7)) { + byte seed[16]; + XMEMSET(seed, 0xa5, sizeof(seed)); + ret = wc_RNG_DRBG_Reseed(args->rng, seed, (word32)sizeof(seed)); + if (ret != 0) { + args->ret = ret; + break; + } + } } WOLFSSL_RETURN_FROM_THREAD(0); @@ -27082,6 +27096,7 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t random_thread_test(void) args[i].rng = &rng; args[i].out = out + ((size_t)i * WC_RNG_THREAD_TEST_DRAWS * WC_RNG_THREAD_TEST_BLKSZ); + args[i].reseeder = (i == 0); args[i].ret = 0; if (wolfSSL_NewThread(&threads[i], &rng_thread_test_worker, &args[i]) != 0) { diff --git a/wolfssl/wolfcrypt/random.h b/wolfssl/wolfcrypt/random.h index 2a18470297d..9540387e50e 100644 --- a/wolfssl/wolfcrypt/random.h +++ b/wolfssl/wolfcrypt/random.h @@ -79,14 +79,16 @@ /* avoid redefinition of structs */ -/* A build with no DRBG, no atomics, or no threads has nothing to implement - * this with, so elect it off here rather than making every use site restate - * the requirements. Kept ahead of the FIPS-version guard below: the use +/* A build with no DRBG, no atomics, no threads, or no way for the acquire + * spin to yield (WC_SPIN_RELAX_YIELDS, types.h) has nothing to implement this + * with, so elect it off here rather than making every use site restate the + * requirements. Kept ahead of the FIPS-version guard below: the use * sites test !defined(WC_NO_DRBG_THREAD_SAFE), so this must be evaluated on * every path that reaches them, including the one where the WC_RNG defined * below is not the struct in use. */ #if (!defined(HAVE_HASHDRBG) || defined(CUSTOM_RAND_GENERATE_BLOCK) || \ defined(SINGLE_THREADED) || defined(WOLFSSL_NO_ATOMICS) || \ + !defined(WC_SPIN_RELAX_YIELDS) || \ (defined(HAVE_FIPS) && \ !(defined(HAVE_FIPS_VERSION) && (HAVE_FIPS_VERSION >= 2)))) && \ !defined(WC_NO_DRBG_THREAD_SAFE) diff --git a/wolfssl/wolfcrypt/types.h b/wolfssl/wolfcrypt/types.h index 1b7970ab527..9678fc18dfc 100644 --- a/wolfssl/wolfcrypt/types.h +++ b/wolfssl/wolfcrypt/types.h @@ -2414,6 +2414,9 @@ WOLFSSL_API word32 CheckRunTimeSettings(void); #ifndef WC_RELAX_LONG_LOOP #define WC_RELAX_LONG_LOOP() WC_DO_NOTHING +#else + /* A port supplied a real relax hook of its own (linuxkm does). */ + #define WC_HAVE_PORT_RELAX_LONG_LOOP #endif /* Yield hook for the DRBG acquire spin in RngExclEnter(). Deliberately @@ -2428,35 +2431,49 @@ WOLFSSL_API word32 CheckRunTimeSettings(void); * one is in scope. SINGLE_THREADED is excluded first: wc_port.h omits these * kernel headers in that configuration, so the yields have no declaration. * Any port may define WC_SPIN_RELAX ahead of this. */ -#ifndef WC_SPIN_RELAX - #if defined(SINGLE_THREADED) - #define WC_SPIN_RELAX() WC_DO_NOTHING - #elif defined(WOLFSSL_ZEPHYR) - #define WC_SPIN_RELAX() k_yield() - #elif (defined(FREERTOS) || defined(FREERTOS_TCP) || \ - defined(WOLFSSL_SAFERTOS)) && defined(taskYIELD) - /* taskYIELD() is a macro from FreeRTOS task.h, which none of these - * paths include themselves, so key off the macro rather than assume - * it: a build without task.h falls through to the default. */ - #define WC_SPIN_RELAX() taskYIELD() - #elif defined(THREADX) - #define WC_SPIN_RELAX() tx_thread_relinquish() - #elif defined(WOLFSSL_TIRTOS) - #define WC_SPIN_RELAX() Task_yield() - #elif defined(RTTHREAD) - #define WC_SPIN_RELAX() rt_thread_yield() - #elif defined(WOLFSSL_PTHREADS) - /* wc_port.h includes on this path, which carries - * ; wolfentropy.c and async.c already call sched_yield() - * under the same assumption. */ - #define WC_SPIN_RELAX() (void)sched_yield() - #elif defined(USE_WINDOWS_API) && !defined(_WIN32_WCE) - /* is included by wc_port.h on this path. */ - #define WC_SPIN_RELAX() (void)SwitchToThread() - #else - /* Ports that supply a real relax hook (linuxkm) keep it here. */ - #define WC_SPIN_RELAX() WC_RELAX_LONG_LOOP() - #endif +/* WC_SPIN_RELAX_YIELDS is defined alongside every mapping that really hands + * the CPU over. Where it is absent the spin cannot yield, and random.h elects + * the thread-safe DRBG off rather than ship a wait that a priority-preemptive + * scheduler can turn into a livelock. */ +#ifdef WC_SPIN_RELAX + /* Supplied by the port; taken to be a real yield. */ + #define WC_SPIN_RELAX_YIELDS +#elif defined(SINGLE_THREADED) + #define WC_SPIN_RELAX() WC_DO_NOTHING +#elif defined(WOLFSSL_ZEPHYR) + #define WC_SPIN_RELAX() k_yield() + #define WC_SPIN_RELAX_YIELDS +#elif (defined(FREERTOS) || defined(FREERTOS_TCP) || \ + defined(WOLFSSL_SAFERTOS)) && defined(taskYIELD) + /* taskYIELD() is a macro from FreeRTOS task.h, which none of these paths + * include themselves, so key off the macro rather than assume it: a build + * without task.h falls through to the no-yield default. */ + #define WC_SPIN_RELAX() taskYIELD() + #define WC_SPIN_RELAX_YIELDS +#elif defined(THREADX) + #define WC_SPIN_RELAX() tx_thread_relinquish() + #define WC_SPIN_RELAX_YIELDS +#elif defined(WOLFSSL_TIRTOS) + #define WC_SPIN_RELAX() Task_yield() + #define WC_SPIN_RELAX_YIELDS +#elif defined(RTTHREAD) + #define WC_SPIN_RELAX() rt_thread_yield() + #define WC_SPIN_RELAX_YIELDS +#elif defined(WOLFSSL_PTHREADS) + /* wc_port.h includes on this path, which carries ; + * wolfentropy.c and async.c already call sched_yield() the same way. */ + #define WC_SPIN_RELAX() (void)sched_yield() + #define WC_SPIN_RELAX_YIELDS +#elif defined(USE_WINDOWS_API) && !defined(_WIN32_WCE) + /* is included by wc_port.h on this path. */ + #define WC_SPIN_RELAX() (void)SwitchToThread() + #define WC_SPIN_RELAX_YIELDS +#elif defined(WC_HAVE_PORT_RELAX_LONG_LOOP) + /* linuxkm and anything else that installed its own relax hook. */ + #define WC_SPIN_RELAX() WC_RELAX_LONG_LOOP() + #define WC_SPIN_RELAX_YIELDS +#else + #define WC_SPIN_RELAX() WC_DO_NOTHING #endif #ifndef WC_CHECK_FOR_INTR_SIGNALS #define WC_CHECK_FOR_INTR_SIGNALS() 0