Skip to content

fix(random): lock the shared DRBGs for every use - #86

Closed
MarkAtwood wants to merge 3 commits into
wolfSSL:mainfrom
MarkAtwood:fix-rng-lock
Closed

MarkAtwood wants to merge 3 commits into
wolfSSL:mainfrom
MarkAtwood:fix-rng-lock

Conversation

@MarkAtwood

Copy link
Copy Markdown

The wrapper's process-wide DRBGs (priv_rng, pub_rng) are used without a lock. wc_rng_lock is held only while seeding and inside wolfssl_rnd(), but src/pk.c passes &priv_rng to wolfCrypt unlocked in 16 places: RSA/DH/ECC/Ed25519/Ed448/X25519/X448 key generation, ECDSA and RSA signing, RSA encryption, and RSA-decrypt/ECDH/X25519 blinding.

A WC_RNG is safe to share only when wolfSSL locks it itself: 5.9.4 and later, unless built with --disable-rng-autolock. With any earlier wolfSSL, including the 5.9.1 FIPS Ready bundle, concurrent gnutls callers corrupt the Hash_DRBG and draw the same output: duplicate private keys and reused ECDSA nonces (a key that signs twice with the same r is recoverable). No error is returned. CI builds wolfSSL master, which has the per-RNG lock, so CI cannot catch this.

Measured with EC P-256 keygen + ECDSA sign from concurrent threads:

wolfSSL wrapper ops duplicate private keys repeated ECDSA r
5.9.1 FIPS Ready main 16 threads x 2000, 6 runs 158-284 119-216
5.9.1 FIPS Ready this PR 16 threads x 2000, 3 runs 0 0
master --disable-rng-autolock main test_pk_threads, 6 runs 13-55 (one run segfaulted) 7-38
master --disable-rng-autolock this PR test_pk_threads, 6 runs 0 0

ThreadSanitizer, wolfSSL master --disable-rng-autolock instrumented, 4 threads: main reports 596 races (Hash_DRBG_Generate, array_add, wc_rng_lock_once); this PR reports 0. Single-thread controls are clean on both.

The fix adds wolfssl_rng_lock()/wolfssl_rng_unlock() and holds the lock across every wolfCrypt call that uses either DRBG, including calls on a key that keeps a pointer to one (wc_RsaSetRNG(), ecc_key.rng, wc_curve25519_set_rng()). Nothing called under the lock re-enters the wrapper's RNG.

It also fixes:

  • the lazy lock init raced (wc_InitMutex() vs wc_LockMutex() in two threads); now pthread_once();
  • wolfssl_ensure_rng() returned with the lock held when wc_InitRng() failed, so the next RNG call hung;
  • the fork path left rng_ready set after freeing the DRBGs;
  • wolfssl_rnd_refresh() wrote rng_ready unlocked, and the per-keygen wc_SetSeed_Cb() calls raced each other (wolfssl_ensure_rng() has already registered the callback).

New test tests/test_pk_threads.c: 8 threads generate and sign, and no private scalar or r may repeat. With wolfSSL master and --disable-rng-autolock it fails on main and passes here. make test passes 26/26 with and without the provider (debian:trixie, wolfSSL master).

Left as they were (pre-existing):

  • a fork() while another thread holds the lock leaves the child's copy locked. The window is longer now (a whole keygen). wolfSSL 5.9.4+ handles fork in its own RNG lock;
  • after wolfssl_rnd_refresh(), wolfssl_ensure_rng() re-runs wc_InitRng() without wc_FreeRng(), which leaks the old DRBG state.

priv_rng and pub_rng are process-wide, but wc_rng_lock was held only
while seeding and inside wolfssl_rnd(). Key generation, ECDSA and RSA
signing, RSA encryption, and RSA/ECDH/X25519 blinding passed &priv_rng
to wolfCrypt with no lock. A WC_RNG is not thread safe unless wolfSSL
locks it itself (5.9.4 and later, unless built with
--disable-rng-autolock), so concurrent callers corrupted the Hash_DRBG
state and drew the same output: duplicate private keys and reused
ECDSA nonces, with no error returned.

Add wolfssl_rng_lock()/wolfssl_rng_unlock() and hold the lock across
every wolfCrypt call that uses either DRBG, including calls on a key
that keeps a pointer to one. Nothing called under the lock re-enters
the wrapper's RNG.

Also:
- initialize the lock with pthread_once(); the old flag check raced
  with wc_InitMutex() when the first two users arrived together;
- release the lock when wc_InitRng() fails (both paths returned with
  it held, hanging the next RNG call);
- clear rng_ready when the fork path frees the DRBGs, so a failed
  re-init is retried instead of using freed state;
- take the lock in wolfssl_rnd_refresh() for rng_ready;
- drop the per-keygen wc_SetSeed_Cb() calls: wolfssl_ensure_rng() has
  already registered the callback under the lock, and the unlocked
  writes raced with each other.
Eight threads generate P-256 keys and sign with them through the
provider; afterwards every private scalar and ECDSA r value must be
distinct and every operation must have succeeded. A shared DRBG used
without its lock repeats outputs; a working one repeats with negligible
probability. -fast runs 8 x 250 operations, the default 8 x 1000.

Link the tests with -pthread.
Copilot AI balanced review requested due to automatic review settings October 1, 2026 19:02

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.

Copilot review overview

🟡 Changes recommended

The wrapper build must link its newly introduced direct pthread dependency.

Review effort: Balanced
Findings: 1 High severity

Open (1)
What changed in this PR

Serializes access to process-wide wolfSSL DRBGs to prevent concurrent state corruption.

Changes:

  • Adds thread-safe RNG initialization and locking.
  • Protects all cryptographic operations using shared DRBGs.
  • Adds concurrent key-generation/signing regression coverage.
File Description
src/​random.c Implements shared RNG locking and safe initialization.
src/​random.h Exposes RNG lock helpers.
src/​pk.c Locks DRBG-dependent operations.
tests/​Makefile Registers and links the threaded test.
tests/​test_pk_threads.c Detects duplicate keys and ECDSA nonces.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

*/
void wolfssl_rng_lock(void)
{
pthread_once(&wc_rng_lock_once, wc_rng_lock_init);
The RNG lock is initialized with pthread_once(), a direct pthread
dependency. glibc before 2.34 provides it only in libpthread, so build
and link the wrapper with -pthread rather than rely on another loaded
library to supply it.
@MarkAtwood

Copy link
Copy Markdown
Author

Closing in favour of #89, per Reda.

@MarkAtwood MarkAtwood closed this Oct 2, 2026
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.

2 participants