openssl / openssl/openssl

BN_CTX pool doesn't scrub a BIGNUM's backing memory on release (BN_CTX_end/BN_POOL_release)

Open
#32,018 8 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

help wanted triaged: documentation triaged: feature
Dominant language
C
Stars
30.8k
Forks
11.5k
Avg merge
10m
Merged PRs (30d)
1

Description

Summary

BN_CTX_get() / BN_CTX_end() / BN_POOL_release() in crypto/bn/bn_ctx.c
never scrub a BIGNUM's backing d[] array when a pool slot is released
or recycled. Only BN_CTX_free() (via BN_POOL_finish() -> BN_clear_free())
actually zeroes this memory - and only when the entire BN_CTX is destroyed.

  • Every RSA/DH/ECDSA private-key operation uses BN_CTX pool slots to hold
    intermediate, secret-dependent values during modular exponentiation /
    Montgomery multiplication.

While an individual BN_CTX in the current
RSA/ECDSA code paths (crypto/rsa/rsa_ossl.c, crypto/ec/ecdsa_ossl.c)
is created and freed per-operation (so BN_CTX_free() does eventually
scrub it), any code that follows OpenSSL's own documented best practice
of reusing one BN_CTX across many operations (recommended for
performance) will have this residue persist for the BN_CTX's entire
lifetime.

Root cause

  • BN_CTX_get() (bn_ctx.c:220-243) calls BN_zero(ret) before handing
    back a pool slot. BN_zero() only sets bn->top = 0 - it does not
    touch bn->d.
  • BN_POOL_release() (bn_ctx.c:360-373), called from BN_CTX_end(),
    only adjusts the pool's internal used bookkeeping and a linked-list
    pointer, never zeroes or frees anything.
  • Only place actually scrubs bignums in the pool is
    BN_POOL_finish() (bn_ctx.c:304-317), called from BN_CTX_free(),
    which invokes BN_clear_free() on each bignum with a non-NULL d.

Sample test program

Attached: bn_pool_leak_poc.c

Output:

=== Step 1: secret value stored in a BN_CTX pool slot ===
pool slot address: 0x55b9c2529520, backing size: 32 bytes
raw bytes while in use: babbb8b9bebfbcbdb2b3b0b1b6b7b4b5aaaba8a9aeafacada2a3a0a1a6a7a4a5

=== Step 2: BN_CTX_end() releases it back to the pool ===
raw bytes immediately after release: babbb8b9bebfbcbdb2b3b0b1b6b7b4b5aaaba8a9aeafacada2a3a0a1a6a7a4a5
unchanged from before release: YES

=== Step 3: a later, unrelated computation reuses the same slot ===
fresh->d == old slot address: YES (same slot)
raw bytes of the 'fresh' bignum from BN_CTX_get(): babbb8b9bebfbcbdb2b3b0b1b6b7b4b5aaaba8a9aeafacada2a3a0a1a6a7a4a5
still equals the OLD secret: YES - stale secret handed back as 'fresh' memory

=== Step 4: new computation writes only a 4-byte value into the reused slot ===
raw backing bytes after writing the small value: 0403020100000000b2b3b0b1b6b7b4b5aaaba8a9aeafacada2a3a0a1a6a7a4a5
bytes beyond the touched word still match the OLD secret: YES - old secret bytes still present past 'top'

bn_pool_leak_poc.c

BN_CTX_get() labeled step 3's bignum as "fresh" (it called
BN_zero() internally), but its actual backing memory still held the
previous, unrelated secret byte-for-byte.

Related prior issues/PRs

  • #29591 ("Sensitive key material not consistently zeroized on free")
    raised a similar general concern in Jan 2026 but was closed in March
    2026 for lacking specifics - this issue provides the specific,
    reproducible instance for the BN_CTX pool mechanism.
  • #20626 ("EVP_PKEY_get_bn_param() does not zeroize temporary buffer")
    is a different, already-fixed instance of the same underlying bug
    class (temporary buffer not cleansed).

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in crypto/bn/bn_ctx.c by tracing BN_CTX_get(), BN_CTX_end(), BN_POOL_release(), and BN_POOL_finish(); use the attached bn_pool_leak_poc.c to reproduce the retained bytes. Compare the pool lifecycle with the per-operation paths in crypto/rsa/rsa_ossl.c and crypto/ec/ecdsa_ossl.c. Done means released or recycled pool slots no longer retain the prior backing data.

Written by the indexing model from the issue text.

Assessment

Tech stack
c
Domain
cryptography, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.