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);