Commit 828c39a508 for openssl.org

commit 828c39a508fb7f59af38508b242119b714b9c497
Author: Daniel Kubec <kubec@openssl.foundation>
Date:   Thu Oct 22 13:55:32 2026 +0000

    entropy: Free OS entropy correctly in ossl_rand_cleanup_user_entropy()

    Without strict seeding, ossl_rand_get_user_entropy() falls back to the operating
    system sources while the library context has no seed source.

    ossl_rand_cleanup_user_entropy() decided how to free the buffer by looking at
    the seed source present at cleanup time. If another thread created the seed
    source in between, a buffer allocated by ossl_rand_get_entropy() was handed to
    the seed source's clear_seed(), which may use a different allocator or none at
    all.

    When the health tests reject seed material, crng_test_get_seed() gives it back
    to its source and reports failure, but leaves *pout pointing at the released
    buffer. A DRBG whose reseed fails releases whatever *pout holds, so the material
    was freed a second time: through the parent's clear_seed() for a CRNG-TEST with
    a parent, and now that the up-call path releases rejected material itself, also
    through the core's cleanup_user_entropy() for the parentless CRNG-TEST that the
    FIPS provider's primary is. Set *pout to NULL once the material is gone, and do
    not call the parent's clear_seed() for material it never produced.

    Assisted-by: Claude Code:claude-opus-5-5
    Reviewed-by: Paul Dale <paul.dale@oracle.com>
    Reviewed-by: Todd Short <todd.short@me.com>
    Merge-date: Mon Oct 26 15:55:20 2026
    Merged-from: https://github.com/openssl/openssl/pull/33340

diff --git a/crypto/rand/prov_seed.c b/crypto/rand/prov_seed.c
index 5e2f6328ef..25b054f06d 100644
--- a/crypto/rand/prov_seed.c
+++ b/crypto/rand/prov_seed.c
@@ -14,6 +14,7 @@
 #include "internal/core.h"
 #include <openssl/core_dispatch.h>
 #include <openssl/err.h>
+#include <string.h>

 size_t ossl_rand_get_entropy(ossl_unused OSSL_LIB_CTX *ctx,
     unsigned char **pout, int entropy,
@@ -41,6 +42,33 @@ size_t ossl_rand_get_entropy(ossl_unused OSSL_LIB_CTX *ctx,
     return ret;
 }

+/*
+ * Copy the seed into a buffer of our own and hand the original straight back
+ * to the seed source. Otherwise ossl_rand_cleanup_user_entropy() would have
+ * to work out who allocated |buf|, but a seed source may be created between
+ * taking entropy from the operating system sources and cleaning it up, so
+ * the seed source present at cleanup time does not tell.
+ */
+static size_t rand_get_seed_copy(EVP_RAND_CTX *rng, unsigned char **pout,
+    int entropy, size_t min_len, size_t max_len)
+{
+    unsigned char *seed = NULL, *buf;
+    size_t ret;
+
+    ret = evp_rand_get_seed(rng, &seed, entropy, min_len, max_len,
+        0, NULL, 0);
+    if (ret == 0)
+        return 0;
+    buf = OPENSSL_secure_malloc(ret);
+    if (buf != NULL)
+        memcpy(buf, seed, ret);
+    evp_rand_clear_seed(rng, seed, ret);
+    if (buf == NULL)
+        return 0;
+    *pout = buf;
+    return ret;
+}
+
 size_t ossl_rand_get_user_entropy(OSSL_LIB_CTX *ctx,
     unsigned char **pout, int entropy,
     size_t min_len, size_t max_len)
@@ -62,8 +90,7 @@ size_t ossl_rand_get_user_entropy(OSSL_LIB_CTX *ctx,
         if (rng == NULL || !evp_rand_can_seed(rng))
             return ossl_rand_get_entropy(ctx, pout, entropy, min_len, max_len);
     }
-    return evp_rand_get_seed(rng, pout, entropy, min_len, max_len,
-        0, NULL, 0);
+    return rand_get_seed_copy(rng, pout, entropy, min_len, max_len);
 }

 void ossl_rand_cleanup_entropy(ossl_unused OSSL_LIB_CTX *ctx,
@@ -72,15 +99,11 @@ void ossl_rand_cleanup_entropy(ossl_unused OSSL_LIB_CTX *ctx,
     OPENSSL_secure_clear_free(buf, len);
 }

+/* See rand_get_seed_copy() */
 void ossl_rand_cleanup_user_entropy(OSSL_LIB_CTX *ctx,
     unsigned char *buf, size_t len)
 {
-    EVP_RAND_CTX *rng = ossl_rand_get0_seed_noncreating(ctx);
-
-    if (rng != NULL && evp_rand_can_seed(rng))
-        evp_rand_clear_seed(rng, buf, len);
-    else
-        OPENSSL_secure_clear_free(buf, len);
+    ossl_rand_cleanup_entropy(ctx, buf, len);
 }

 size_t ossl_rand_get_nonce(ossl_unused OSSL_LIB_CTX *ctx,
diff --git a/providers/implementations/rands/fips_crng_test.c b/providers/implementations/rands/fips_crng_test.c
index c3de165368..5b00c21705 100644
--- a/providers/implementations/rands/fips_crng_test.c
+++ b/providers/implementations/rands/fips_crng_test.c
@@ -300,6 +300,12 @@ static size_t crng_test_get_seed(void *vcrngt, unsigned char **pout,
     size_t n;
     size_t r = 0;

+    /*
+     * Seed material that fails the health tests goes straight back to its
+     * source.  Do not leave |*pout| pointing at it: a DRBG whose reseed
+     * fails releases whatever |*pout| holds, which would free it again.
+     */
+
     /* Without a parent, we rely on the up calls */
     if (crngt->parent == NULL
         || crngt->parent_get_seed == NULL) {
@@ -307,8 +313,11 @@ static size_t crng_test_get_seed(void *vcrngt, unsigned char **pout,
             min_len, max_len);
         if (n == 0)
             return 0;
-        r = crng_test(crngt, *pout, n);
-        return r > 0 ? n : 0;
+        if (crng_test(crngt, *pout, n) > 0)
+            return n;
+        ossl_prov_cleanup_entropy(crngt->provctx, *pout, n);
+        *pout = NULL;
+        return 0;
     }

     /* Grab seed from our parent */
@@ -318,10 +327,13 @@ static size_t crng_test_get_seed(void *vcrngt, unsigned char **pout,
     n = crngt->parent_get_seed(crngt->parent, pout, entropy,
         min_len, max_len, prediction_resistance,
         adin, adin_len);
-    if (n > 0 && crng_test(crngt, *pout, n) > 0)
+    if (n > 0 && crng_test(crngt, *pout, n) > 0) {
         r = n;
-    else if (crngt->parent_clear_seed != NULL)
-        crngt->parent_clear_seed(crngt->parent, *pout, n);
+    } else {
+        if (n > 0 && crngt->parent_clear_seed != NULL)
+            crngt->parent_clear_seed(crngt->parent, *pout, n);
+        *pout = NULL;
+    }
     unlock_parent(crngt);
     return r;
 }
diff --git a/test/rand_test.c b/test/rand_test.c
index e11dc12f92..022f2d1a14 100644
--- a/test/rand_test.c
+++ b/test/rand_test.c
@@ -21,6 +21,7 @@
 #include <openssl/bio.h>
 #include <openssl/core_names.h>
 #include <openssl/params.h>
+#include <openssl/proverr.h>
 #include "crypto/rand.h"
 #include "testutil.h"

@@ -541,6 +542,8 @@ typedef struct {
     int started;
     int completed;
     int seed_calls;
+    int good_seed_calls;
+    int clear_calls;
     int recursive_error;
 } ASYNC_SEED_PROBE;

@@ -654,20 +657,27 @@ static size_t async_seed_get_seed(void *vseed, unsigned char **out,
     size_t additional_input_len)
 {
     ASYNC_SEED *seed = vseed;
-    size_t len = (entropy + 7) / 8;
+    size_t i, len = (entropy + 7) / 8;

     if (len < min_len)
         len = min_len;
     if (!seed->ready || len > max_len
         || (*out = OPENSSL_malloc(len)) == NULL)
         return 0;
-    seed->probe->seed_calls++;
-    memset(*out, 0x5a, len);
+    /* Distinct bytes pass the health tests, the constant byte fails them */
+    if (++seed->probe->seed_calls <= seed->probe->good_seed_calls)
+        for (i = 0; i < len; i++)
+            (*out)[i] = (unsigned char)(0x9d * i + 0x3b);
+    else
+        memset(*out, 0x5a, len);
     return len;
 }

 static void async_seed_clear_seed(void *vseed, unsigned char *out, size_t len)
 {
+    /* Count the seed material handed back, a NULL hands nothing back */
+    if (out != NULL)
+        ((ASYNC_SEED *)vseed)->probe->clear_calls++;
     OPENSSL_clear_free(out, len);
 }

@@ -839,6 +849,161 @@ err:
     return res;
 }

+/*
+ * Entropy handed out through the core's get_user_entropy() must be freed
+ * correctly by its cleanup_user_entropy(), whether it came from the operating
+ * system sources or from the seed source, and even when the seed source was
+ * created in between. Entropy from the seed source must go back to its
+ * clear_seed() straight away and never escape to the caller.
+ *
+ * Without the secure heap both allocators end up in malloc(), so freeing with
+ * the wrong one goes unnoticed. Count the clear_seed() calls instead.
+ */
+static int test_rand_user_entropy_cleanup(void)
+{
+#ifdef OPENSSL_RAND_SEED_NONE
+    TEST_info("skipped: no operating system entropy sources");
+    return 1;
+#else
+    ASYNC_SEED_PROBE probe = { 0 };
+    OSSL_LIB_CTX *ctx = NULL;
+    OSSL_PROVIDER *custom = NULL;
+    unsigned char *buf = NULL;
+    size_t len = 0;
+    int res = 0;
+
+    async_seed_probe = &probe;
+    if (!TEST_ptr(ctx = OSSL_LIB_CTX_new())
+        || !TEST_true(OSSL_PROVIDER_add_builtin(ctx, "async-seed-probe",
+            async_seed_provider_init))
+        || !TEST_ptr(custom = OSSL_PROVIDER_load(ctx, "async-seed-probe"))
+        || !TEST_true(RAND_set_seed_source_type(ctx, "ASYNC-SEED",
+            "provider=async-seed-probe")))
+        goto err;
+
+    /* No seed source yet, so this comes from the operating system */
+    if (!TEST_size_t_eq(len = probe.entropy(probe.handle, &buf, 128, 16, 16),
+            16)
+        || !TEST_int_eq(probe.seed_calls, 0))
+        goto err;
+
+    /* Someone else creates the seed source before the cleanup */
+    if (!TEST_ptr(ossl_rand_get0_seed(ctx)))
+        goto err;
+    probe.clear_entropy(probe.handle, buf, len);
+    buf = NULL;
+    if (!TEST_int_eq(probe.clear_calls, 0))
+        goto err;
+
+    /* Now the seed source is used, and gets its buffer back at once */
+    if (!TEST_size_t_eq(len = probe.entropy(probe.handle, &buf, 128, 16, 16),
+            16)
+        || !TEST_int_eq(probe.seed_calls, 1)
+        || !TEST_int_eq(probe.clear_calls, 1))
+        goto err;
+    probe.clear_entropy(probe.handle, buf, len);
+    buf = NULL;
+    if (!TEST_int_eq(probe.clear_calls, 1))
+        goto err;
+
+    res = 1;
+err:
+    if (buf != NULL)
+        probe.clear_entropy(probe.handle, buf, len);
+    OSSL_PROVIDER_unload(custom);
+    OSSL_LIB_CTX_free(ctx);
+    async_seed_probe = NULL;
+    return res;
+#endif /* OPENSSL_RAND_SEED_NONE */
+}
+
+/*
+ * A CRNG-TEST hands seed material that fails the health tests back to where
+ * it came from.  It must then not leave its caller a pointer to that memory:
+ * a child DRBG releases whatever the pointer holds when its reseed fails,
+ * which would free the material a second time.  Nor may the material be
+ * left for the caller to release, since the child DRBG releases nothing
+ * when its instantiation fails.  Cover both of the CRNG-TEST's sources, the
+ * core's get_user_entropy() up-call that the FIPS provider's primary relies
+ * on, and an explicit parent.  The seed source produces acceptable bytes
+ * for the requests that must succeed and a constant byte afterwards, which
+ * fails the repetition count test.
+ */
+static int test_rand_crngt_entropy_failure(int idx)
+{
+    ASYNC_SEED_PROBE probe = { 0 };
+    OSSL_LIB_CTX *ctx = NULL;
+    OSSL_PROVIDER *custom = NULL;
+    EVP_RAND *crngt_alg = NULL, *drbg_alg = NULL;
+    EVP_RAND_CTX *seed, *crngt = NULL, *drbg = NULL;
+    OSSL_PARAM params[2];
+    unsigned char out[16];
+    int parent = idx & 1, reseed = idx & 2, res = 0;
+
+    params[0] = OSSL_PARAM_construct_utf8_string(OSSL_DRBG_PARAM_CIPHER,
+        (char *)"AES-256-CTR", 0);
+    params[1] = OSSL_PARAM_construct_end();
+
+    /* The seed material for the instantiation passes, the next does not */
+    probe.good_seed_calls = reseed ? 1 : 0;
+    async_seed_probe = &probe;
+    if (!TEST_ptr(ctx = OSSL_LIB_CTX_new())
+        || !TEST_true(OSSL_LIB_CTX_load_config(ctx, configfile))
+        || !TEST_true(OSSL_PROVIDER_add_builtin(ctx, "async-seed-probe",
+            async_seed_provider_init))
+        || !TEST_ptr(custom = OSSL_PROVIDER_load(ctx, "async-seed-probe"))
+        || !TEST_true(RAND_set_seed_source_type(ctx, "ASYNC-SEED",
+            "provider=async-seed-probe,-fips"))
+        || !TEST_ptr(seed = ossl_rand_get0_seed(ctx))
+        || !TEST_ptr(crngt_alg = EVP_RAND_fetch(ctx, "CRNG-TEST", "-fips"))
+        || !TEST_ptr(drbg_alg = EVP_RAND_fetch(ctx, "CTR-DRBG", NULL))
+        || !TEST_ptr(crngt = EVP_RAND_CTX_new(crngt_alg, parent ? seed : NULL))
+        || !TEST_true(EVP_RAND_instantiate(crngt, 0, 0, NULL, 0, NULL))
+        || !TEST_ptr(drbg = EVP_RAND_CTX_new(drbg_alg, crngt)))
+        goto err;
+
+    if (reseed
+        && (!TEST_true(EVP_RAND_instantiate(drbg, 0, 0, NULL, 0, params))
+            || !TEST_int_eq(probe.seed_calls, 1)
+            || !TEST_int_eq(probe.clear_calls, 1)))
+        goto err;
+
+    ERR_clear_error();
+    if (reseed) {
+        if (!TEST_false(EVP_RAND_reseed(drbg, 0, NULL, 0, NULL, 0)))
+            goto err;
+    } else if (!TEST_false(EVP_RAND_instantiate(drbg, 0, 0, NULL, 0,
+                   params))) {
+        goto err;
+    }
+    /* The rejected seed material went back to its source exactly once */
+    if (!TEST_int_eq(ERR_GET_REASON(ERR_peek_error()),
+            PROV_R_ENTROPY_SOURCE_FAILED_RCT_CONTINUOUS_TEST)
+        || !TEST_int_eq(probe.seed_calls, probe.good_seed_calls + 1)
+        || !TEST_int_eq(probe.clear_calls, probe.good_seed_calls + 1)
+        || !TEST_int_eq(EVP_RAND_get_state(drbg), EVP_RAND_STATE_ERROR))
+        goto err;
+    ERR_clear_error();
+
+    /* Recovering from the error state requests seed material once more */
+    if (!TEST_false(EVP_RAND_generate(drbg, out, sizeof(out), 0, 0, NULL, 0))
+        || !TEST_int_eq(probe.seed_calls, probe.good_seed_calls + 2)
+        || !TEST_int_eq(probe.clear_calls, probe.good_seed_calls + 2))
+        goto err;
+    ERR_clear_error();
+
+    res = 1;
+err:
+    EVP_RAND_CTX_free(drbg);
+    EVP_RAND_CTX_free(crngt);
+    EVP_RAND_free(drbg_alg);
+    EVP_RAND_free(crngt_alg);
+    OSSL_PROVIDER_unload(custom);
+    OSSL_LIB_CTX_free(ctx);
+    async_seed_probe = NULL;
+    return res;
+}
+
 static int test_rand_seed_source_recursive_error(void)
 {
     ASYNC_SEED_PROBE probe = { 0 };
@@ -1052,6 +1217,10 @@ int setup_tests(void)
 #ifdef OPENSSL_NO_FIPS_JITTER
     ADD_ALL_TESTS(test_rand_seed_source_async, 4);
     ADD_TEST(test_rand_seed_source_recursive_error);
+    ADD_TEST(test_rand_user_entropy_cleanup);
+    if (OSSL_PROVIDER_available(NULL, "fips")
+        && fips_provider_version_ge(NULL, 4, 1, 0))
+        ADD_ALL_TESTS(test_rand_crngt_entropy_failure, 4);
 #endif

     ADD_MFAIL_ALL_TESTS(test_rand_bytes_mfail, 2);