From 0764c9348a7712b427c2855c131e1b48720c36b1 Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Mon, 23 Feb 2026 13:56:41 +0100 Subject: [PATCH 1/6] Update framework with mbedtls_test_fork_run_child Signed-off-by: Gilles Peskine --- framework | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework b/framework index 9b92164c47..a2083218c7 160000 --- a/framework +++ b/framework @@ -1 +1 @@ -Subproject commit 9b92164c47fdaecb2600b417733507e2a105c3a5 +Subproject commit a2083218c76b0de70ba040608fcf4727f04eae82 From ce8a71c071314863f79489e04aa24c1f0d861950 Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Fri, 16 Jan 2026 19:05:52 +0100 Subject: [PATCH 2/6] Test the PSA RNG after fork() Assert that two forked children have distinct RNG states, and also that the state is distinct from their parent. Signed-off-by: Gilles Peskine --- .../suites/test_suite_psa_crypto_entropy.data | 12 +++ .../test_suite_psa_crypto_entropy.function | 93 +++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/tests/suites/test_suite_psa_crypto_entropy.data b/tests/suites/test_suite_psa_crypto_entropy.data index 03dd6c8222..ba3797eca2 100644 --- a/tests/suites/test_suite_psa_crypto_entropy.data +++ b/tests/suites/test_suite_psa_crypto_entropy.data @@ -104,6 +104,18 @@ PSA external RNG failure: RSA PKCS#1v1.5 (software implementation) depends_on:PSA_WANT_ALG_RSA_PKCS1V15_SIGN:PSA_WANT_KEY_TYPE_RSA_KEY_PAIR_BASIC:PSA_WANT_KEY_TYPE_RSA_KEY_PAIR_IMPORT:PSA_WANT_KEY_TYPE_RSA_KEY_PAIR_EXPORT:MBEDTLS_PSA_BUILTIN_ALG_RSA_PKCS1V15_SIGN external_rng_failure_sign:PSA_KEY_TYPE_RSA_KEY_PAIR:"3082025e02010002818100af057d396ee84fb75fdbb5c2b13c7fe5a654aa8aa2470b541ee1feb0b12d25c79711531249e1129628042dbbb6c120d1443524ef4c0e6e1d8956eeb2077af12349ddeee54483bc06c2c61948cd02b202e796aebd94d3a7cbf859c2c1819c324cb82b9cd34ede263a2abffe4733f077869e8660f7d6834da53d690ef7985f6bc3020301000102818100874bf0ffc2f2a71d14671ddd0171c954d7fdbf50281e4f6d99ea0e1ebcf82faa58e7b595ffb293d1abe17f110b37c48cc0f36c37e84d876621d327f64bbe08457d3ec4098ba2fa0a319fba411c2841ed7be83196a8cdf9daa5d00694bc335fc4c32217fe0488bce9cb7202e59468b1ead119000477db2ca797fac19eda3f58c1024100e2ab760841bb9d30a81d222de1eb7381d82214407f1b975cbbfe4e1a9467fd98adbd78f607836ca5be1928b9d160d97fd45c12d6b52e2c9871a174c66b488113024100c5ab27602159ae7d6f20c3c2ee851e46dc112e689e28d5fcbbf990a99ef8a90b8bb44fd36467e7fc1789ceb663abda338652c3c73f111774902e840565927091024100b6cdbd354f7df579a63b48b3643e353b84898777b48b15f94e0bfc0567a6ae5911d57ad6409cf7647bf96264e9bd87eb95e263b7110b9a1f9f94acced0fafa4d024071195eec37e8d257decfc672b07ae639f10cbb9b0c739d0c809968d644a94e3fd6ed9287077a14583f379058f76a8aecd43c62dc8c0f41766650d725275ac4a1024100bb32d133edc2e048d463388b7be9cb4be29f4b6250be603e70e3647501c97ddde20a4e71be95fd5e71784e25aca4baf25be5738aae59bbfe1c997781447a2b24":PSA_ALG_RSA_PKCS1V15_SIGN_RAW:32 +PSA RNG after fork: parent gets random never +psa_rng_fork:-1 + +PSA RNG after fork: parent gets random before +psa_rng_fork:0 + +PSA RNG after fork: parent gets random between +psa_rng_fork:1 + +PSA RNG after fork: parent gets random after +psa_rng_fork:2 + PSA validate entropy injection: good, minimum size validate_entropy_seed_injection:MBEDTLS_PSA_INJECT_ENTROPY_MIN_SIZE:PSA_SUCCESS:MBEDTLS_PSA_INJECT_ENTROPY_MIN_SIZE:PSA_ERROR_NOT_PERMITTED diff --git a/tests/suites/test_suite_psa_crypto_entropy.function b/tests/suites/test_suite_psa_crypto_entropy.function index 413e1ea5d8..5b0bba3cb7 100644 --- a/tests/suites/test_suite_psa_crypto_entropy.function +++ b/tests/suites/test_suite_psa_crypto_entropy.function @@ -2,6 +2,10 @@ #include #include +#if defined(MBEDTLS_PLATFORM_IS_UNIXLIKE) +#include +#endif + #include /* Some tests in this module configure entropy sources. */ @@ -149,6 +153,20 @@ exit: } #endif /* !defined(MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG) */ +#if defined(MBEDTLS_PLATFORM_IS_UNIXLIKE) && \ + !defined(MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG) +static void child_psa_get_random(void *param, + uint8_t *output, size_t output_size, + size_t *output_length) +{ + (void) param; + PSA_ASSERT(psa_generate_random(output, output_size)); + *output_length = output_size; +exit: + ; +} +#endif /* MBEDTLS_PLATFORM_IS_UNIXLIKE && !MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG */ + /* Calculating the minimum allowed entropy size in bytes */ #define MBEDTLS_PSA_INJECT_ENTROPY_MIN_SIZE MAX(MBEDTLS_ENTROPY_MIN_PLATFORM, \ MBEDTLS_ENTROPY_BLOCK_SIZE) @@ -662,6 +680,81 @@ exit: } /* END_CASE */ +/* BEGIN_CASE depends_on:!MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG:MBEDTLS_PLATFORM_IS_UNIXLIKE */ +/* Test that if a program calls fork(), the PSA RNG returns different byte + * sequences in each child process, and that they're different from the + * parent process. + * + * The argument parent_when controls when the parent calls + * psa_generate_random(): -1 = never, 0 = before forking, >0 = after forking + * that many children. + * + * Note that passing tests don't mean that everything is fine, they only + * mean that things are not too obviously broken. It's possible to badly + * design the RNG so that, for example, different child processes will + * have the same RNG output sequence but at an offset, or so that a child + * process's RNG is seeded from RNG output of the parent (making it + * predictable if an adversary happens to be able to get the right chunk + * of RNG output from th parent), or different sequencing of forking + * grand^n-children ends up with them having identical RNG output sequences. + * These bad designs are practically impossible to detect through testing, + * and must be excluded by human reasoning on the RNG design. + */ +void psa_rng_fork(int parent_when) +{ + struct { + /* We read 16 bytes from the RNG. This is large enough so that the + * probability of a coincidence is negligible, and small enough that + * the RNG won't spontaneously decide reseed to unless it has + * prediction resistance. */ + unsigned char rng_output[16]; + } child[2], parent; + memset(child, 0, sizeof(child)); + memset(&parent, 0, sizeof(parent)); + + PSA_INIT(); + + /* Create some child processes, have them generate random data + * and report that data back to the original process. */ + for (size_t i = 0; i < ARRAY_LENGTH(child); i++) { + mbedtls_test_set_step(i); + if ((size_t) parent_when == i) { + PSA_ASSERT(psa_generate_random(parent.rng_output, + sizeof(parent.rng_output))); + } + size_t length; + TEST_EQUAL(mbedtls_test_fork_run_child( + child_psa_get_random, NULL, + child[i].rng_output, sizeof(child[i].rng_output), + &length), 0); + TEST_EQUAL(length, sizeof(child[i].rng_output)); + } + + if (parent_when == ARRAY_LENGTH(child)) { + PSA_ASSERT(psa_generate_random(parent.rng_output, + sizeof(parent.rng_output))); + } + + /* Did the children have different RNG states? */ + TEST_ASSERT(memcmp(child[0].rng_output, + child[1].rng_output, + sizeof(parent.rng_output)) != 0); + /* If parent_when >= 0: did the children have different RNG states + * from the parent? + * If parent_when < 0: did the children get nonzero RNG output? + */ + for (size_t i = 0; i < ARRAY_LENGTH(child); i++) { + mbedtls_test_set_step(i); + TEST_ASSERT(memcmp(parent.rng_output, + child[i].rng_output, + sizeof(parent.rng_output)) != 0); + } + +exit: + PSA_DONE(); +} +/* END_CASE */ + /* BEGIN_CASE depends_on:MBEDTLS_PSA_INJECT_ENTROPY */ void validate_entropy_seed_injection(int seed_length_a, int expected_status_a, From 0b93865aed2860c16ee2761543145d0a967759d6 Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Fri, 16 Jan 2026 19:30:34 +0100 Subject: [PATCH 3/6] Protect the PSA RNG from duplication on fork MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If a process forks with the PSA subsystem active, the PSA RNG state is duplicated. This led to the parent process and the child process generating the same sequence of random numbers. Fix this by forcing a reseed if the value of `getpid()` changes. This is the same technique used in OpenSSL ≥1.1.1d. Signed-off-by: Gilles Peskine --- ChangeLog.d/rng-cloning.txt | 4 ++++ library/psa_crypto_random.c | 30 ++++++++++++++++++++++++++++++ library/psa_crypto_random_impl.h | 17 +++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/ChangeLog.d/rng-cloning.txt b/ChangeLog.d/rng-cloning.txt index fc7d4db7f5..6f68d26bb8 100644 --- a/ChangeLog.d/rng-cloning.txt +++ b/ChangeLog.d/rng-cloning.txt @@ -6,6 +6,10 @@ Features prediction resistance in the PSA random generator. Security + * If an application called psa_crypto_init() then fork() and continued to + use cryptography APIs (possibly indirectly, e.g. for TLS), the random + generator states were duplicated. Fix this by forcing a RNG reseed in + the child process. CVE-2026-25835 * Applications running in environments where the application state is cloned (for example due to resuming a frozen system state multiple times, or due to cloning a virtual machine image) should arrange to diff --git a/library/psa_crypto_random.c b/library/psa_crypto_random.c index f5eb658ab6..b0e4c859a7 100644 --- a/library/psa_crypto_random.c +++ b/library/psa_crypto_random.c @@ -13,11 +13,17 @@ #include "psa_crypto_core.h" #include "psa_crypto_random.h" #include "psa_crypto_random_impl.h" +#include "threading_internal.h" #if defined(MBEDTLS_PSA_INJECT_ENTROPY) #include "entropy_poll.h" #endif +#if defined(MBEDTLS_PLATFORM_IS_UNIXLIKE) +/* For getpid(), for fork protection */ +#include +#endif + void psa_random_internal_init(mbedtls_psa_random_context_t *rng) { /* Set default configuration if @@ -53,6 +59,9 @@ psa_status_t psa_random_internal_seed(mbedtls_psa_random_context_t *rng) const unsigned char drbg_seed[] = "PSA"; int ret = mbedtls_psa_drbg_seed(&rng->drbg, &rng->entropy, drbg_seed, sizeof(drbg_seed) - 1); +#if defined(MBEDTLS_PLATFORM_IS_UNIXLIKE) + rng->pid = getpid(); +#endif return mbedtls_to_psa_error(ret); } @@ -60,6 +69,27 @@ psa_status_t psa_random_internal_generate( mbedtls_psa_random_context_t *rng, uint8_t *output, size_t output_size) { +#if defined(MBEDTLS_PLATFORM_IS_UNIXLIKE) + intmax_t pid = getpid(); + if (pid != rng->pid) { + /* This is a (grand...)child of the original process, but + * we inherited the RNG state from our parent. We must reseed! */ +#if defined(MBEDTLS_THREADING_C) + mbedtls_mutex_lock(&mbedtls_threading_psa_rngdata_mutex); +#endif /* defined(MBEDTLS_THREADING_C) */ + int ret = mbedtls_psa_drbg_reseed(&rng->drbg, NULL, 0); + if (ret == 0) { + rng->pid = pid; + } +#if defined(MBEDTLS_THREADING_C) + mbedtls_mutex_unlock(&mbedtls_threading_psa_rngdata_mutex); +#endif /* defined(MBEDTLS_THREADING_C) */ + if (ret != 0) { + return mbedtls_to_psa_error(ret); + } + } +#endif /* MBEDTLS_PLATFORM_IS_UNIXLIKE */ + while (output_size > 0) { size_t request_size = (output_size > MBEDTLS_PSA_RANDOM_MAX_REQUEST ? diff --git a/library/psa_crypto_random_impl.h b/library/psa_crypto_random_impl.h index 3ed85e5c4e..68bb3a39f8 100644 --- a/library/psa_crypto_random_impl.h +++ b/library/psa_crypto_random_impl.h @@ -70,6 +70,23 @@ typedef struct { void (* entropy_free)(mbedtls_entropy_context *ctx); mbedtls_entropy_context entropy; mbedtls_psa_drbg_context_t drbg; +#if defined(MBEDTLS_PLATFORM_IS_UNIXLIKE) + /* Fork protection: normally pid = getpid(). If the value changes, + * we are in a (grand)*child of the original process, so reseed + * the RNG to ensure that the child and the original process have + * distinct RNG states. See psa_random_internal_generate(). + * + * The type is intmax_t, not pid_t, for portability reasons: + * pid_t is defined in `unistd.h`, but on some platforms, it may + * only be defined if a certain compatibility level is requested + * by defining a macro such as _POSIX_C_SOURCE or _XOPEN_SOURCE. + * The macro needs to be defined before any system header, which + * may be hard to do in some C files that include this header + * (e.g. test suites). So we sidestep this complication, at the + * cost of possibly a few more instructions to compare pid values. + */ + intmax_t pid; +#endif } mbedtls_psa_random_context_t; /** Initialize the PSA DRBG. From fd0e168fabb41b703634b13de701a2a4d5bea958 Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Mon, 26 Jan 2026 22:23:56 +0100 Subject: [PATCH 4/6] Extend PSA RNG fork protection to NV-seed-only configurations In builds with only a nonvolatile seed but no actual entropy source, the naive protection against fork() by reseeding in the child doesn't work: every child forked from the same RNG state gets the same RNG state. To make the child's RNG state unique in that case, use a public but unique personalization string. The personalization string includes the time. Use `mbedtls_ms_time()` if available. Fall back to the classic (but obsolescent) `gettimeofday()` otherwise. Signed-off-by: Gilles Peskine --- library/psa_crypto_random.c | 77 +++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 4 deletions(-) diff --git a/library/psa_crypto_random.c b/library/psa_crypto_random.c index b0e4c859a7..af6e5de4b2 100644 --- a/library/psa_crypto_random.c +++ b/library/psa_crypto_random.c @@ -22,6 +22,12 @@ #if defined(MBEDTLS_PLATFORM_IS_UNIXLIKE) /* For getpid(), for fork protection */ #include +#if defined(MBEDTLS_HAVE_TIME) +#include +#else +/* For gettimeofday(), for fork protection without actual entropy */ +#include +#endif #endif void psa_random_internal_init(mbedtls_psa_random_context_t *rng) @@ -65,6 +71,69 @@ psa_status_t psa_random_internal_seed(mbedtls_psa_random_context_t *rng) return mbedtls_to_psa_error(ret); } +#if defined(MBEDTLS_PLATFORM_IS_UNIXLIKE) +static psa_status_t psa_random_internal_reseed_child( + mbedtls_psa_random_context_t *rng, + intmax_t pid) +{ + /* Reseeding from actual entropy gives the child a unique RNG state + * which the parent process cannot predict, and wipes the + * parent's RNG state from the child. + * + * However, in some library configurations, there is no actual + * entropy source, only a nonvolatile seed (MBEDTLS_ENTROPY_NV_SEED + * enabled and no actual entropy source enabled). In such a + * configuration, the reseed operation is deterministic and + * always injects the same content, so with the DRBG reseed + * process alone, for example, two child processes forked in + * close sequence would end up with the same RNG state. + + * To avoid this, we use a personalization string that has a high + * likelihood of being unique. This way, the child has a unique state. + * The parent can predict the child's RNG state until the next time + * it reseeds or generates some random output, but that's + * unavoidable in the absence of actual entropy. + */ + struct { + /* Using the PID mostly guarantees that each child gets a + * unique state. */ + /* Use intmax_t, not pid_t, because some Unix-like platforms + * don't define pid_t, or more likely nowadays they define + * pid_t but only with certain platform macros which might not + * be the exact ones we use. In practice, this only costs + * a couple of instructions to pass and compare two words + * rather than one. + */ + intmax_t pid; + /* In case an old child had died and its PID is reused for + * a new child of the same process, also include the time. */ +#if defined(MBEDTLS_HAVE_TIME) + mbedtls_ms_time_t now; +#else + struct timeval now; +#endif + } perso; + memset(&perso, 0, sizeof(perso)); + perso.pid = pid; +#if defined(MBEDTLS_HAVE_TIME) + perso.now = mbedtls_ms_time(); +#else + /* We don't have mbedtls_ms_time(), but the platform has getpid(). + * Use gettimeofday(), which is a classic Unix function. Modern POSIX + * has stopped requiring gettimeofday() (in favor of clock_gettime()), + * but this is fallback code for restricted configurations, so it's + * more likely to be used on embedded platforms that only have a subset + * of Unix APIs and are more likely to have the classic gettimeofday(). */ + if (gettimeofday(&perso.now, NULL) == -1) { + return PSA_ERROR_INSUFFICIENT_ENTROPY; + } +#endif + int ret = mbedtls_psa_drbg_reseed(&rng->drbg, + (unsigned char *) &perso, sizeof(perso)); + return mbedtls_to_psa_error(ret); +} +#endif /* MBEDTLS_PLATFORM_IS_UNIXLIKE */ + psa_status_t psa_random_internal_generate( mbedtls_psa_random_context_t *rng, uint8_t *output, size_t output_size) @@ -77,15 +146,15 @@ psa_status_t psa_random_internal_generate( #if defined(MBEDTLS_THREADING_C) mbedtls_mutex_lock(&mbedtls_threading_psa_rngdata_mutex); #endif /* defined(MBEDTLS_THREADING_C) */ - int ret = mbedtls_psa_drbg_reseed(&rng->drbg, NULL, 0); - if (ret == 0) { + psa_status_t status = psa_random_internal_reseed_child(rng, pid); + if (status == PSA_SUCCESS) { rng->pid = pid; } #if defined(MBEDTLS_THREADING_C) mbedtls_mutex_unlock(&mbedtls_threading_psa_rngdata_mutex); #endif /* defined(MBEDTLS_THREADING_C) */ - if (ret != 0) { - return mbedtls_to_psa_error(ret); + if (status != PSA_SUCCESS) { + return status; } } #endif /* MBEDTLS_PLATFORM_IS_UNIXLIKE */ From 8cffc5dc9d56b3152fabb4095f204f9d792aaff9 Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Mon, 16 Mar 2026 23:46:56 +0100 Subject: [PATCH 5/6] Update framework with fix for platform requirements Signed-off-by: Gilles Peskine --- framework | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework b/framework index a2083218c7..1a5bf10ca0 160000 --- a/framework +++ b/framework @@ -1 +1 @@ -Subproject commit a2083218c76b0de70ba040608fcf4727f04eae82 +Subproject commit 1a5bf10ca08c06be10857c224c5fd70ef38591f2 From 9dfe49a9806bf287db03f9ed49ef69fa988df30b Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Tue, 17 Mar 2026 21:29:58 +0100 Subject: [PATCH 6/6] Update framework with mbedtls_test_fork_run_child merged Signed-off-by: Gilles Peskine --- framework | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework b/framework index 1a5bf10ca0..3d57272bbc 160000 --- a/framework +++ b/framework @@ -1 +1 @@ -Subproject commit 1a5bf10ca08c06be10857c224c5fd70ef38591f2 +Subproject commit 3d57272bbc67fb576a7737155835d2db700cc469