Commit aeeca5a9e0 for openssl.org

commit aeeca5a9e07166183fe323f336c9177a9b524c78
Author: shin4141 <siriusa.paper@gmail.com>
Date:   Sun Sep 6 07:07:20 2026 +0900

    rand: prevent recursive seed source construction

    Strict on-demand seed source construction can re-enter itself when the
    configured RAND requests entropy or a nonce through provider upcalls.
    This recursively constructs the same seed source until the process
    exhausts its stack.

    Track active construction by logical execution so true recursion fails
    with a RAND error while independent ASYNC jobs on the same thread and
    library context can proceed. Keep the guard active during provider
    lookup, context creation, and instantiation.

    Add regression coverage for clean strict failure, independent and
    paused ASYNC jobs, non-strict error propagation, and no-err builds.

    Fixes #32683

    Assisted-by: Codex:gpt-5
    Reviewed-by: Mounir Idrassi <mounir.idrassi@idrix.fr>
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    Merge-date: Wed Sep 16 14:15:40 2026
    Merged-from: https://github.com/openssl/openssl/pull/32685

diff --git a/crypto/rand/rand_lib.c b/crypto/rand/rand_lib.c
index d4a8203851..1306717202 100644
--- a/crypto/rand/rand_lib.c
+++ b/crypto/rand/rand_lib.c
@@ -10,6 +10,9 @@
 /* We need to use some RAND deprecated APIs */
 #define OPENSSL_SUPPRESS_DEPRECATED

+#ifndef FIPS_MODULE
+#include <openssl/async.h>
+#endif
 #include <openssl/err.h>
 #include <openssl/opensslconf.h>
 #include <openssl/core_names.h>
@@ -604,6 +607,69 @@ err:
     return NULL;
 }

+typedef struct rand_seed_construction_st {
+#ifndef FIPS_MODULE
+    ASYNC_JOB *job;
+#endif
+    struct rand_seed_construction_st *next;
+} RAND_SEED_CONSTRUCTION;
+
+static int rand_seed_construction_begin(OSSL_LIB_CTX *ctx,
+    RAND_SEED_CONSTRUCTION *marker)
+{
+    RAND_SEED_CONSTRUCTION *current, *head;
+#ifndef FIPS_MODULE
+    ASYNC_JOB *job = ASYNC_get_current_job();
+#endif
+
+    head = CRYPTO_THREAD_get_local_ex(CRYPTO_THREAD_LOCAL_RAND_SEED_KEY, ctx);
+    for (current = head; current != NULL; current = current->next) {
+#ifndef FIPS_MODULE
+        if (current->job != job)
+            continue;
+#endif
+        ERR_raise(ERR_LIB_RAND, RAND_R_ERROR_INSTANTIATING_DRBG);
+        return 0;
+    }
+
+#ifndef FIPS_MODULE
+    marker->job = job;
+#endif
+    marker->next = head;
+    if (!CRYPTO_THREAD_set_local_ex(CRYPTO_THREAD_LOCAL_RAND_SEED_KEY, ctx,
+            marker)) {
+        ERR_raise(ERR_LIB_RAND, ERR_R_INTERNAL_ERROR);
+        return 0;
+    }
+    return 1;
+}
+
+static int rand_seed_construction_end(OSSL_LIB_CTX *ctx,
+    RAND_SEED_CONSTRUCTION *marker)
+{
+    RAND_SEED_CONSTRUCTION *current, *head;
+
+    head = CRYPTO_THREAD_get_local_ex(CRYPTO_THREAD_LOCAL_RAND_SEED_KEY, ctx);
+    if (head == marker) {
+        if (!CRYPTO_THREAD_set_local_ex(CRYPTO_THREAD_LOCAL_RAND_SEED_KEY,
+                ctx, marker->next)) {
+            ERR_raise(ERR_LIB_RAND, ERR_R_INTERNAL_ERROR);
+            return 0;
+        }
+        return 1;
+    }
+
+    for (current = head; current != NULL; current = current->next) {
+        if (current->next == marker) {
+            current->next = marker->next;
+            return 1;
+        }
+    }
+
+    ERR_raise(ERR_LIB_RAND, ERR_R_INTERNAL_ERROR);
+    return 0;
+}
+
 /*
  * Get the global seed source, creating and storing it if it does not
  * exist yet.  If several threads race here, exactly one instance is
@@ -612,6 +678,7 @@ err:
 static EVP_RAND_CTX *rand_get0_seed(OSSL_LIB_CTX *ctx, RAND_GLOBAL *dgbl)
 {
     EVP_RAND_CTX *ret, *seed;
+    RAND_SEED_CONSTRUCTION marker;

     if (!CRYPTO_THREAD_read_lock(dgbl->lock))
         return NULL;
@@ -620,7 +687,18 @@ static EVP_RAND_CTX *rand_get0_seed(OSSL_LIB_CTX *ctx, RAND_GLOBAL *dgbl)
     if (ret != NULL)
         return ret;

+    /*
+     * Mark before fetching so recursion through provider lookup is covered
+     * as well as recursion during context creation or instantiation.
+     */
+    if (!rand_seed_construction_begin(ctx, &marker))
+        return NULL;
+
     seed = rand_new_seed(ctx);
+    if (!rand_seed_construction_end(ctx, &marker)) {
+        EVP_RAND_CTX_free(seed);
+        return NULL;
+    }
     if (seed == NULL)
         return NULL;

diff --git a/include/internal/threads_common.h b/include/internal/threads_common.h
index c1da68a5db..a9cfa4c6ac 100644
--- a/include/internal/threads_common.h
+++ b/include/internal/threads_common.h
@@ -40,6 +40,7 @@ typedef enum {
     CRYPTO_THREAD_LOCAL_TEVENT_KEY,
     CRYPTO_THREAD_LOCAL_TANDEM_ID_KEY,
     CRYPTO_THREAD_LOCAL_FIPS_DEFERRED_KEY,
+    CRYPTO_THREAD_LOCAL_RAND_SEED_KEY,
     CRYPTO_THREAD_LOCAL_KEY_MAX
 } CRYPTO_THREAD_LOCAL_KEY_ID;

diff --git a/test/rand_test.c b/test/rand_test.c
index 16316dd4a4..de46c99906 100644
--- a/test/rand_test.c
+++ b/test/rand_test.c
@@ -7,7 +7,16 @@
  * https://www.openssl.org/source/license.html
  */

+#ifdef _WIN32
+#include <windows.h>
+#endif
+
 #include <openssl/evp.h>
+#include <openssl/async.h>
+#include <openssl/core.h>
+#include <openssl/core_dispatch.h>
+#include <openssl/err.h>
+#include <openssl/provider.h>
 #include <openssl/rand.h>
 #include <openssl/bio.h>
 #include <openssl/core_names.h>
@@ -480,6 +489,356 @@ err:
     return res;
 }

+#ifdef OPENSSL_NO_FIPS_JITTER
+typedef struct {
+    const OSSL_CORE_HANDLE *handle;
+    OSSL_LIB_CTX *libctx;
+    OSSL_FUNC_get_user_entropy_fn *entropy;
+    OSSL_FUNC_cleanup_user_entropy_fn *clear_entropy;
+    int pause_instances;
+    int recurse_instance;
+    int direct_recurse;
+    int started;
+    int completed;
+    int seed_calls;
+    int recursive_error;
+} ASYNC_SEED_PROBE;
+
+typedef struct {
+    ASYNC_SEED_PROBE *probe;
+    int instance;
+    int ready;
+} ASYNC_SEED;
+
+static ASYNC_SEED_PROBE *async_seed_probe;
+
+static size_t async_seed_request_entropy(ASYNC_SEED_PROBE *probe)
+{
+    unsigned char sentinel, *out = &sentinel;
+    size_t len;
+
+    len = probe->entropy(probe->handle, &out, 128, 16, 32);
+    if (len > 0 && out != NULL && out != &sentinel)
+        probe->clear_entropy(probe->handle, out, len);
+    return len;
+}
+
+static void *async_seed_newctx(void *vprobe, void *parent,
+    const OSSL_DISPATCH *dispatch)
+{
+    ASYNC_SEED *seed = OPENSSL_zalloc(sizeof(*seed));
+
+    if (seed != NULL)
+        seed->probe = vprobe;
+    return seed;
+}
+
+static void async_seed_freectx(void *vseed)
+{
+    OPENSSL_free(vseed);
+}
+
+static int async_seed_instantiate(void *vseed, unsigned int strength,
+    int prediction_resistance, const unsigned char *personalisation,
+    size_t personalisation_len, const OSSL_PARAM params[])
+{
+    ASYNC_SEED *seed = vseed;
+    ASYNC_SEED_PROBE *probe = seed->probe;
+    size_t len;
+
+    seed->instance = ++probe->started;
+    if (seed->instance <= probe->pause_instances) {
+        if (!ASYNC_pause_job())
+            return 0;
+    }
+
+    if (seed->instance == probe->recurse_instance) {
+        ERR_clear_error();
+        if (probe->direct_recurse) {
+            probe->recursive_error = ossl_rand_get0_seed(probe->libctx) == NULL
+                && ERR_GET_LIB(ERR_peek_error()) == ERR_LIB_RAND;
+        } else {
+            len = async_seed_request_entropy(probe);
+            probe->recursive_error = len == 0
+                && ERR_GET_LIB(ERR_peek_error()) == ERR_LIB_RAND;
+        }
+        return 0;
+    }
+
+    seed->ready = 1;
+    probe->completed++;
+    return 1;
+}
+
+static int async_seed_uninstantiate(void *vseed)
+{
+    ((ASYNC_SEED *)vseed)->ready = 0;
+    return 1;
+}
+
+static int async_seed_generate(void *vseed, unsigned char *out, size_t len,
+    unsigned int strength, int prediction_resistance,
+    const unsigned char *additional_input, size_t additional_input_len)
+{
+    if (!((ASYNC_SEED *)vseed)->ready)
+        return 0;
+    memset(out, 0x5a, len);
+    return 1;
+}
+
+static int async_seed_get_ctx_params(void *vseed, OSSL_PARAM params[])
+{
+    ASYNC_SEED *seed = vseed;
+    OSSL_PARAM *p;
+    int state;
+
+    p = OSSL_PARAM_locate(params, OSSL_RAND_PARAM_STRENGTH);
+    if (p != NULL && !OSSL_PARAM_set_uint(p, 256))
+        return 0;
+    p = OSSL_PARAM_locate(params, OSSL_RAND_PARAM_STATE);
+    state = seed->ready ? EVP_RAND_STATE_READY : EVP_RAND_STATE_UNINITIALISED;
+    if (p != NULL && !OSSL_PARAM_set_int(p, state))
+        return 0;
+    p = OSSL_PARAM_locate(params, OSSL_RAND_PARAM_MAX_REQUEST);
+    if (p != NULL && !OSSL_PARAM_set_size_t(p, 65536))
+        return 0;
+    p = OSSL_PARAM_locate(params, OSSL_DRBG_PARAM_RESEED_COUNTER);
+    if (p != NULL && !OSSL_PARAM_set_uint(p, 1))
+        return 0;
+    return 1;
+}
+
+static size_t async_seed_get_seed(void *vseed, unsigned char **out,
+    int entropy, size_t min_len, size_t max_len,
+    int prediction_resistance, const unsigned char *additional_input,
+    size_t additional_input_len)
+{
+    ASYNC_SEED *seed = vseed;
+    size_t 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);
+    return len;
+}
+
+static void async_seed_clear_seed(void *vseed, unsigned char *out, size_t len)
+{
+    OPENSSL_clear_free(out, len);
+}
+
+static const OSSL_DISPATCH async_seed_rand_functions[] = {
+    { OSSL_FUNC_RAND_NEWCTX, (void (*)(void))async_seed_newctx },
+    { OSSL_FUNC_RAND_FREECTX, (void (*)(void))async_seed_freectx },
+    { OSSL_FUNC_RAND_INSTANTIATE, (void (*)(void))async_seed_instantiate },
+    { OSSL_FUNC_RAND_UNINSTANTIATE,
+        (void (*)(void))async_seed_uninstantiate },
+    { OSSL_FUNC_RAND_GENERATE, (void (*)(void))async_seed_generate },
+    { OSSL_FUNC_RAND_GET_CTX_PARAMS,
+        (void (*)(void))async_seed_get_ctx_params },
+    { OSSL_FUNC_RAND_GET_SEED, (void (*)(void))async_seed_get_seed },
+    { OSSL_FUNC_RAND_CLEAR_SEED, (void (*)(void))async_seed_clear_seed },
+    OSSL_DISPATCH_END
+};
+
+static const OSSL_ALGORITHM async_seed_algorithms[] = {
+    { "ASYNC-SEED:JITTER", "provider=async-seed-probe",
+        async_seed_rand_functions, "ASYNC seed source test" },
+    { NULL, NULL, NULL, NULL }
+};
+
+static const OSSL_ALGORITHM *async_seed_query(void *vprobe, int operation,
+    int *no_cache)
+{
+    *no_cache = 0;
+    return operation == OSSL_OP_RAND ? async_seed_algorithms : NULL;
+}
+
+static const OSSL_DISPATCH async_seed_provider_functions[] = {
+    { OSSL_FUNC_PROVIDER_QUERY_OPERATION, (void (*)(void))async_seed_query },
+    OSSL_DISPATCH_END
+};
+
+static int async_seed_provider_init(const OSSL_CORE_HANDLE *handle,
+    const OSSL_DISPATCH *in, const OSSL_DISPATCH **out, void **vprobe)
+{
+    ASYNC_SEED_PROBE *probe = async_seed_probe;
+
+    probe->handle = handle;
+    for (; in->function_id != 0; in++) {
+        switch (in->function_id) {
+        case OSSL_FUNC_GET_USER_ENTROPY:
+            probe->entropy = OSSL_FUNC_get_user_entropy(in);
+            break;
+        case OSSL_FUNC_CLEANUP_USER_ENTROPY:
+            probe->clear_entropy = OSSL_FUNC_cleanup_user_entropy(in);
+            break;
+        }
+    }
+    *vprobe = probe;
+    *out = async_seed_provider_functions;
+    return probe->entropy != NULL && probe->clear_entropy != NULL;
+}
+
+static int async_seed_rand_job(void *vctx)
+{
+    OSSL_LIB_CTX *ctx = *(OSSL_LIB_CTX **)vctx;
+
+    return RAND_get0_primary(ctx) != NULL;
+}
+
+/*
+ * A paused ASYNC job must not make an independent job, or code running
+ * outside ASYNC on the same thread, look like recursive seed construction.
+ * If two jobs pause, each construction marker must survive until its own job
+ * resumes; a real recursive request by either job must still be rejected.
+ */
+static int test_rand_seed_source_async(int idx)
+{
+    ASYNC_SEED_PROBE probe = { 0 };
+    OSSL_LIB_CTX *ctx = NULL;
+    OSSL_PROVIDER *custom = NULL, *def = NULL;
+    ASYNC_JOB *a = NULL, *b = NULL;
+    ASYNC_WAIT_CTX *wa = NULL, *wb = NULL;
+    unsigned char out;
+    int ra = -1, rb = -1, sa = ASYNC_ERR, sb = ASYNC_ERR;
+    int async_started = 0, res = 0;
+
+    probe.pause_instances = idx == 2 ? 2 : 1;
+    probe.recurse_instance = idx == 2 ? 1 : 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_ptr(def = OSSL_PROVIDER_load(ctx, "default"))
+        || !TEST_true(RAND_set_seed_source_type(ctx,
+            idx == 1 ? "ASYNC-SEED" : "JITTER",
+            "provider=async-seed-probe")))
+        goto err;
+    if (!ASYNC_is_capable()) {
+        TEST_info("skipped: ASYNC jobs are unavailable");
+        res = 1;
+        goto err;
+    }
+    if (!TEST_true(ASYNC_init_thread(2, 2)))
+        goto err;
+    async_started = 1;
+    if (!TEST_ptr(wa = ASYNC_WAIT_CTX_new())
+        || !TEST_ptr(wb = ASYNC_WAIT_CTX_new()))
+        goto err;
+
+    ERR_clear_error();
+    sa = ASYNC_start_job(&a, wa, &ra, async_seed_rand_job, &ctx, sizeof(ctx));
+    if (!TEST_int_eq(sa, ASYNC_PAUSE)
+        || !TEST_int_eq(probe.started, 1)
+        || !TEST_int_eq(probe.completed, 0))
+        goto err;
+
+    if (idx == 3) {
+        if (!TEST_true(RAND_bytes_ex(ctx, &out, sizeof(out), 0))
+            || !TEST_int_eq(probe.started, 2)
+            || !TEST_int_eq(probe.completed, 1)
+            || !TEST_int_gt(probe.seed_calls, 0))
+            goto err;
+    } else {
+        sb = ASYNC_start_job(&b, wb, &rb, async_seed_rand_job, &ctx,
+            sizeof(ctx));
+        if (!TEST_int_eq(sb, idx == 2 ? ASYNC_PAUSE : ASYNC_FINISH)
+            || !TEST_int_eq(probe.started, 2))
+            goto err;
+        if (idx != 2
+            && (!TEST_int_eq(rb, 1)
+                || !TEST_int_eq(probe.completed, 1)
+                || !TEST_int_gt(probe.seed_calls, 0)))
+            goto err;
+    }
+
+    sa = ASYNC_start_job(&a, wa, &ra, async_seed_rand_job, &ctx, sizeof(ctx));
+    if (!TEST_int_eq(sa, ASYNC_FINISH))
+        goto err;
+    if (idx == 2) {
+        if (!TEST_int_eq(ra, 0)
+            || !TEST_true(probe.recursive_error)
+            || !TEST_int_eq(probe.started, 2)
+            || !TEST_int_eq(probe.completed, 0))
+            goto err;
+        ERR_clear_error();
+        sb = ASYNC_start_job(&b, wb, &rb, async_seed_rand_job, &ctx,
+            sizeof(ctx));
+        if (!TEST_int_eq(sb, ASYNC_FINISH)
+            || !TEST_int_eq(rb, 1)
+            || !TEST_int_eq(probe.started, 2)
+            || !TEST_int_eq(probe.completed, 1)
+            || !TEST_int_gt(probe.seed_calls, 0))
+            goto err;
+    } else if (!TEST_int_eq(ra, 1)
+        || !TEST_int_eq(probe.started, 2)
+        || !TEST_int_eq(probe.completed, 2)) {
+        goto err;
+    }
+
+    res = 1;
+err:
+    if (a != NULL)
+        ASYNC_start_job(&a, wa, &ra, async_seed_rand_job, &ctx, sizeof(ctx));
+    if (b != NULL)
+        ASYNC_start_job(&b, wb, &rb, async_seed_rand_job, &ctx, sizeof(ctx));
+    ASYNC_WAIT_CTX_free(wa);
+    ASYNC_WAIT_CTX_free(wb);
+    if (async_started)
+        ASYNC_cleanup_thread();
+    OSSL_PROVIDER_unload(def);
+    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 };
+    OSSL_LIB_CTX *ctx = NULL;
+    OSSL_PROVIDER *custom = NULL, *def = NULL;
+    unsigned char out;
+    int res = 0;
+
+    probe.recurse_instance = 1;
+    probe.direct_recurse = 1;
+    async_seed_probe = &probe;
+    if (!TEST_ptr(ctx = OSSL_LIB_CTX_new()))
+        goto err;
+    probe.libctx = ctx;
+    if (!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_ptr(def = OSSL_PROVIDER_load(ctx, "default"))
+        || !TEST_true(RAND_set_seed_source_type(ctx, "ASYNC-SEED",
+            "provider=async-seed-probe")))
+        goto err;
+
+    ERR_clear_error();
+    if (!TEST_false(RAND_bytes_ex(ctx, &out, sizeof(out), 0))
+        || !TEST_true(probe.recursive_error)
+        || !TEST_int_eq(probe.started, 1)
+        || !TEST_int_eq(probe.completed, 0)
+        || !TEST_ulong_ne(ERR_peek_error(), 0))
+        goto err;
+
+    res = 1;
+err:
+    OSSL_PROVIDER_unload(def);
+    OSSL_PROVIDER_unload(custom);
+    OSSL_LIB_CTX_free(ctx);
+    async_seed_probe = NULL;
+    return res;
+}
+#endif /* OPENSSL_NO_FIPS_JITTER */
+
 /* Warm up the DRBG cipher fetch caches outside the mfail injection window */
 static int rand_drbg_fetch_warmup(EVP_RAND *drbg_alg)
 {
@@ -647,6 +1006,10 @@ int setup_tests(void)

     ADD_TEST(test_rand_seed_source_strict);
     ADD_TEST(test_rand_seed_source_nonstrict);
+#ifdef OPENSSL_NO_FIPS_JITTER
+    ADD_ALL_TESTS(test_rand_seed_source_async, 4);
+    ADD_TEST(test_rand_seed_source_recursive_error);
+#endif

     ADD_MFAIL_ALL_TESTS(test_rand_bytes_mfail, 2);
     ADD_MFAIL_TEST(test_rand_seed_src_mfail);
diff --git a/test/recipes/20-test_rand_config.t b/test/recipes/20-test_rand_config.t
index 34bcf38782..675df8e9ae 100644
--- a/test/recipes/20-test_rand_config.t
+++ b/test/recipes/20-test_rand_config.t
@@ -10,7 +10,7 @@
 use strict;
 use warnings;

-use OpenSSL::Test qw/:DEFAULT result_dir/;
+use OpenSSL::Test qw/:DEFAULT result_dir with/;
 use OpenSSL::Test::Utils;

 setup("test_rand_config");
@@ -81,9 +81,17 @@ if (disabled("fips-jitter")) {
         { seed => 'NONEXISTENT-SEED-SOURCE',
           expected_ok => 0,
           desc => 'unavailable configured seed source fails, no fallback' };
+    push @seed_tests,
+        { seed => 'HASH-DRBG',
+          strict => 'yes',
+          args => ['-hex', '1'],
+          expected_exit => 1,
+          stderr_re => qr/error:1200006E:/,
+          desc => 'recursive strict seed source fails cleanly' };
 }

-plan tests => scalar @rand_tests * 2 + scalar @seed_tests;
+plan tests => scalar @rand_tests * 2 + scalar @seed_tests
+    + scalar grep { defined $_->{stderr_re} } @seed_tests;

 my $contents =<<'CONFIGEND';
 openssl_conf = openssl_init
@@ -127,8 +135,29 @@ foreach (@seed_tests) {

     $ENV{OPENSSL_CONF} = $tmpfile;

-    my $ok = run(app(["openssl", "rand", "-hex", "16"]));
-    ok(!$ok == !$_->{expected_ok}, $_->{desc});
+    my @args = @{ $_->{args} // ['-hex', '16'] };
+    if (defined $_->{expected_exit}) {
+        my $expected_exit = $_->{expected_exit};
+        my $stderr_file = 'rand_seed_config.err';
+
+        with({ exit_checker => sub { return shift == $expected_exit; } },
+            sub {
+                ok(run(app(["openssl", "rand", @args],
+                           stderr => $stderr_file)), $_->{desc});
+            });
+        if (defined $_->{stderr_re}) {
+            open(my $err, '<', $stderr_file)
+                or die "Could not open error output";
+            my $error = do { local $/; <$err> };
+            close($err);
+            like($error, $_->{stderr_re},
+                 "$_->{desc} reports a RAND error");
+        }
+        unlink($stderr_file);
+    } else {
+        my $ok = run(app(["openssl", "rand", @args]));
+        ok(!$ok == !$_->{expected_ok}, $_->{desc});
+    }
 }

 # Check that the stdout output contains the expected values.