From da9540d3207126d4865279fa286b3d00387140e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Mon, 14 Sep 2026 20:05:47 +0200 Subject: [PATCH 1/4] Copy the RSA public key onto the CA Signer in static-memory builds ParseCert() guarded its RSA public-key copy with !WOLFSSL_NO_MALLOC, while StoreKey() guards the equivalent copy for every non-RSA key with !WC_ASN_NO_HEAP. Those are not the same condition: WC_ASN_NO_HEAP is auto-defined only when WOLFSSL_NO_MALLOC and NO_WOLFSSL_MEMORY are set without XMALLOC_USER or WOLFSSL_STATIC_MEMORY, so a static-memory build defines WOLFSSL_NO_MALLOC yet still has a working allocator. In such a build the copy was skipped, cert->pubKeyStored stayed 0, and FillSigner() therefore never populated signer->publicKey/pubKeySize. ParseCertRelative() then passed a NULL key and a zero key size to ConfirmSignature(), which rejects them with BAD_FUNC_ARG before its WOLFSSL_ENTER. The effect was that no certificate issued by an RSA CA could be verified against it - wolfSSL_CertManagerVerifyBuffer() and TLS peer validation alike - while ECC, Ed25519, Ed448 and ML-DSA CAs worked, because those keys travel through StoreKey(). Use WC_ASN_NO_HEAP in all three guards, including the one on the ptr declaration. FreeDecodedCert() and FreeSigner() already key off pubKeyStored, so ownership and freeing are unchanged. Point the MC/DC white-box guard for this block at WC_ASN_NO_HEAP too. It still keyed off WOLFSSL_NO_MALLOC, so in a static-memory build the copy is now compiled and executed while the section covering it fell back to its stub, and the coverage claim was inaccurate for the one configuration this fixes. --- tests/unit-mcdc/test_asn_ext_whitebox.c | 4 ++-- wolfcrypt/src/asn.c | 13 ++++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/unit-mcdc/test_asn_ext_whitebox.c b/tests/unit-mcdc/test_asn_ext_whitebox.c index 39f3413f01c..b770ecd9051 100644 --- a/tests/unit-mcdc/test_asn_ext_whitebox.c +++ b/tests/unit-mcdc/test_asn_ext_whitebox.c @@ -2745,7 +2745,7 @@ static void wb_decode_cert_req_version(void) { WB_NOTE("WOLFSSL_CERT_REQ off; sk * Section 26: ParseCert() RSA public key store [:23263-:23267] * (best-effort -- see file-header RESIDUAL note for operands 2/3). * ------------------------------------------------------------------------- */ -#if (!defined(WOLFSSL_NO_MALLOC) && !defined(NO_WOLFSSL_CM_VERIFY)) || \ +#if (!defined(WC_ASN_NO_HEAP) && !defined(NO_WOLFSSL_CM_VERIFY)) || \ defined(WOLFSSL_DYN_CERT) static void wb_parse_cert_rsa_pubkey(void) { @@ -2769,7 +2769,7 @@ static void wb_parse_cert_rsa_pubkey(void) FreeDecodedCert(&cert); } #else -static void wb_parse_cert_rsa_pubkey(void) { WB_NOTE("WOLFSSL_NO_MALLOC build; ParseCert copy-out skipped"); } +static void wb_parse_cert_rsa_pubkey(void) { WB_NOTE("WC_ASN_NO_HEAP build; ParseCert copy-out skipped"); } #endif /* ------------------------------------------------------------------------- * diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index ba98f4b7871..339e8357a8d 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -24206,7 +24206,7 @@ static int DecodeCertReq(DecodedCert* cert, int* criticalExt) int ParseCert(DecodedCert* cert, int type, int verify, void* cm) { int ret; -#if (!defined(WOLFSSL_NO_MALLOC) && !defined(NO_WOLFSSL_CM_VERIFY)) || \ +#if (!defined(WC_ASN_NO_HEAP) && !defined(NO_WOLFSSL_CM_VERIFY)) || \ defined(WOLFSSL_DYN_CERT) char* ptr; #endif @@ -24215,9 +24215,9 @@ int ParseCert(DecodedCert* cert, int type, int verify, void* cm) if (ret < 0) return ret; -#if (!defined(WOLFSSL_NO_MALLOC) && !defined(NO_WOLFSSL_CM_VERIFY)) || \ +#if (!defined(WC_ASN_NO_HEAP) && !defined(NO_WOLFSSL_CM_VERIFY)) || \ defined(WOLFSSL_DYN_CERT) - /* cert->subjectCN not stored as copy of WOLFSSL_NO_MALLOC defined */ + /* cert->subjectCN not stored as a copy when there is no allocator */ if (cert->subjectCNLen > 0) { ptr = (char*)XMALLOC((size_t)cert->subjectCNLen + 1, cert->heap, DYNAMIC_TYPE_SUBJECT_CN); @@ -24230,9 +24230,12 @@ int ParseCert(DecodedCert* cert, int type, int verify, void* cm) } #endif -#if (!defined(WOLFSSL_NO_MALLOC) && !defined(NO_WOLFSSL_CM_VERIFY)) || \ +/* WC_ASN_NO_HEAP, not WOLFSSL_NO_MALLOC: a static-memory build defines the + * latter but still has an allocator, and StoreKey() copies the non-RSA keys + * on the same condition. Skipping the copy here leaves Signer.publicKey NULL, + * so every chain verify under an RSA CA fails BAD_FUNC_ARG. */ +#if (!defined(WC_ASN_NO_HEAP) && !defined(NO_WOLFSSL_CM_VERIFY)) || \ defined(WOLFSSL_DYN_CERT) - /* cert->publicKey not stored as copy if WOLFSSL_NO_MALLOC defined */ if ((cert->keyOID == RSAk #ifdef WC_RSA_PSS || cert->keyOID == RSAPSSk From 00b8e2e5a986a747f6bc24bdacf28d60bbab53c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Mon, 14 Sep 2026 20:05:56 +0200 Subject: [PATCH 2/4] Size the wolfCrypt test pool for no-malloc OPENSSL_EXTRA builds With WOLFSSL_NO_MALLOC there is no system heap to fall back on, so an allocation the compatibility layer makes with a NULL heap has to come out of the static pool the test loads. The test only nominated that pool as the global heap hint when OPENSSL_EXTRA was off, so in the combination of the two those allocations had no source at all and openssl_test() failed. Set the hint in that combination too, and give it a pool sized for the compatibility layer on top of the algorithm tests. gTestMemory was sized for the wolfCrypt tests alone and those allocations exhausted it - first in wolfSSL_CRYPTO_malloc(), then, as the pool was enlarged, further along in wolfSSL_X509_load_certificate_file(). Every other configuration keeps the size it had. The new arm goes ahead of the FrodoKEM and ML-DSA arms rather than after them, because those fire first for exactly the builds that need the most. With --enable-mldsa the 576 KB arm won and the RSA test died with -125, and 1 MB only moved the failure to openssl_pkey1_test(), where an exhausted pool surfaces as a NULL from wolfSSL_X509_get_pubkey() and no error code at all. FrodoKEM fails the same way on its own 1 MB arm. Both pass at 2 MB, so the arm asks for 2 MB when either is enabled and 1 MB otherwise. Claim the hint only when it is unset, and drop it only while it is still ours. Nothing in tree installs one before wolfcrypt_test() runs, but under OPENSSL_EXTRA the hint is never handed back, so a program embedding wolfcrypt_test() as a smoke test would otherwise lose its own pool for the rest of the process. This is the first-wins rule the examples already use. The matching teardown stays restricted to !OPENSSL_EXTRA on purpose. gTestMemory has static storage duration, so the hint cannot dangle, and testsuite runs the echo server after wolfcrypt_test() returns: the echo server has no pool of its own, and this is the one that outlives it. testsuite/testsuite.test now passes with --enable-staticmemory -DWOLFSSL_NO_MALLOC. scripts/unit.test still fails there, at seven API tests this change does not address. BENCH_EMBEDDED gets a #error rather than the enlarged pool. That combination is self-contradictory - openssl_pkey0_test() is not gated on BENCH_EMBEDDED, so it still asks for about 1 MB, which is not something to hand a target that has declared itself embedded. Refusing at build time with the override named beats either silently reserving a megabyte or failing at run time in RSA_new(), which is what a 14 KB pool does today. WOLFSSL_STATIC_MEMORY_TEST_SZ remains the override and is checked first, so such a target can still pick its own size. The arm is also restricted to !WOLFCRYPT_ONLY. The compatibility layer tests are gated on !WOLFCRYPT_ONLY themselves, so a crypt-only build never runs openssl_pkey0_test() and has no reason to reserve a megabyte for it - and with BENCH_EMBEDDED it met the #error above over a test it does not compile. Crypt-only keeps the size it had. Pin the behaviour the asn.c fix restores while it is here: cert_no_malloc_test() only asserted the in-place layout under WC_ASN_NO_HEAP, so nothing covered the copy-out side. Add the mirror assertion - pubKeyStored set, publicKey outside the source DER. It fails on the pre-fix guard and passes after it, and unlike fill_signer_twice_test() it is not gated on NO_FILESYSTEM, which a real static-memory target turns off. Carry ParseCert()'s whole predicate rather than WC_ASN_NO_HEAP alone: the copy-out is also skipped under NO_WOLFSSL_CM_VERIFY without WOLFSSL_DYN_CERT, and the assertion must not claim otherwise there. The same goes for NO_RSA and NO_SHA. Both openssl_pkey0_test() and openssl_pkey1_test() compile their bodies away without either one, and openssl_test() alone then fits the sizes the old ladder gives it: --disable-rsa with BENCH_EMBEDDED passes the whole suite on the 14000-byte arm, and without BENCH_EMBEDDED it passes on the 160000-byte arm rather than reserving a megabyte. Excluding the arm reaches both of those; narrowing only the #error would have left the plain build at 1 MB and handed the embedded one the same megabyte the #error exists to refuse. --- wolfcrypt/test/test.c | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/wolfcrypt/test/test.c b/wolfcrypt/test/test.c index 4310aefe208..3513d3eee92 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -1517,6 +1517,18 @@ static WC_MAYBE_UNUSED Aes* test_AesGcmNew(void* heap, int declaredDevId, #ifdef WOLFSSL_STATIC_MEMORY #if defined(WOLFSSL_STATIC_MEMORY_TEST_SZ) static byte gTestMemory[WOLFSSL_STATIC_MEMORY_TEST_SZ]; + #elif defined(WOLFSSL_NO_MALLOC) && defined(OPENSSL_EXTRA) && \ + !defined(WOLFCRYPT_ONLY) && !defined(NO_RSA) && !defined(NO_SHA) + /* No malloc, so the compatibility layer's NULL-heap allocations come + * out of this pool on top of what the algorithm tests need. */ + #ifdef BENCH_EMBEDDED + #error "openssl_pkey0_test() needs about 1MB of pool, far more \ +than BENCH_EMBEDDED implies: set WOLFSSL_STATIC_MEMORY_TEST_SZ explicitly" + #elif defined(WOLFSSL_HAVE_FRODOKEM) || defined(WOLFSSL_HAVE_MLDSA) + static byte gTestMemory[2048*1024]; + #else + static byte gTestMemory[1024*1024]; + #endif #elif defined(WOLFSSL_HAVE_FRODOKEM) /* FrodoKEM keys (~44 KB) and decaps matrices (~86 KB) are large. */ static byte gTestMemory[1024*1024]; @@ -2354,8 +2366,9 @@ options: [-s max_relative_stack_bytes] [-m max_relative_heap_memory_bytes]\n\ printf("unable to load static memory.\n"); EXIT_TEST(EXIT_FAILURE); } - #ifndef OPENSSL_EXTRA - wolfSSL_SetGlobalHeapHint(HEAP_HINT); + #if !defined(OPENSSL_EXTRA) || defined(WOLFSSL_NO_MALLOC) + if (wolfSSL_GetGlobalHeapHint() == NULL) + wolfSSL_SetGlobalHeapHint(HEAP_HINT); #endif #endif @@ -3591,7 +3604,8 @@ options: [-s max_relative_stack_bytes] [-m max_relative_heap_memory_bytes]\n\ #endif #if defined(WOLFSSL_STATIC_MEMORY) && !defined(OPENSSL_EXTRA) - wolfSSL_SetGlobalHeapHint(NULL); + if (wolfSSL_GetGlobalHeapHint() == HEAP_HINT) + wolfSSL_SetGlobalHeapHint(NULL); #endif TEST_PASS("Test complete\n"); @@ -29695,6 +29709,15 @@ static wc_test_ret_t cert_no_malloc_test(void) ret = WC_TEST_RET_ENC_NC; } } +#elif !defined(NO_WOLFSSL_CM_VERIFY) || defined(WOLFSSL_DYN_CERT) + /* With an allocator the RSA key is copied out, so a Signer built from this + * cert keeps a public key of its own. */ + if ((ret == 0) && ((cert.pubKeyStored != 1) || + (((wc_ptr_t)cert.publicKey >= (wc_ptr_t)cert.source) && + ((wc_ptr_t)cert.publicKey < + (wc_ptr_t)cert.source + cert.maxIdx)))) { + ret = WC_TEST_RET_ENC_NC; + } #endif FreeDecodedCert(&cert); #endif From cb049aebcbd0403d1f8bf556a2aa9447a54970ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Mon, 14 Sep 2026 20:06:04 +0200 Subject: [PATCH 3/4] Set the global heap hint in the static memory examples With WOLFSSL_NO_MALLOC there is no allocator behind a NULL-heap XMALLOC, so an allocation made outside any CTX or SSL object can only be served from the global heap hint. wolfSSL_Init() makes such an allocation: under OPENSSL_EXTRA it seeds the compatibility-layer RNG, whose _InitRng() call allocates with a NULL heap. That returned NULL, wolfSSL_Init() reported WC_INIT_E, and every wolfSSL_CTX_new_ex() that triggered the lazy init failed, so neither example could establish a connection even though the pool it had loaded was large enough. Have each example nominate its own pool, which is what the hint is for. The server could not do that before: it created its CTX through wolfSSL_CTX_load_static_memory(), which loads the pool and creates the CTX in one call, so wolfSSL_Init() ran before the caller ever saw the hint. Load the pool with wc_LoadStaticMemory() and create the CTX afterwards, as the client already does. Claim the hint only when none is set, and drop it again on the way out. Both pools are local to the example's own function, and testsuite runs the wolfCrypt test, both examples and the echo server in one process, so an example that overwrote the hint would leave it pointing at a pool that dies the moment the example returns. scripts/resume.test, scripts/tls13.test and testsuite/testsuite.test go from failing to passing with --enable-staticmemory -DWOLFSSL_NO_MALLOC. Restrict the claim to the standalone programs (!NO_MAIN_DRIVER). testsuite and unit.test compile both examples with NO_MAIN_DRIVER and run server_test on a spawned thread beside client_test, so an in-harness claim would publish one thread's automatic-storage pool as the process allocator, let the other thread allocate from it, and then revoke it when the owning frame unwound. In those builds the harness's own long-lived pool is the one that belongs in the hint. Check ctx after wolfSSL_CTX_new_ex() rather than letting the following IO-pool load report a CTX allocation failure as "unable to load static memory". --- examples/client/client.c | 13 +++++++++++++ examples/server/server.c | 24 +++++++++++++++++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/examples/client/client.c b/examples/client/client.c index 16c40612290..abcbecb42e9 100644 --- a/examples/client/client.c +++ b/examples/client/client.c @@ -3465,6 +3465,12 @@ THREAD_RETURN WOLFSSL_THREAD client_test(void* args) err_sys("unable to load static memory"); } +#if defined(WOLFSSL_NO_MALLOC) && !defined(NO_MAIN_DRIVER) + /* only the standalone program may publish a pool of its own */ + if (wolfSSL_GetGlobalHeapHint() == NULL) + wolfSSL_SetGlobalHeapHint(heap); +#endif + #if defined(WOLFSSL_STATIC_MEMORY) && \ defined(WOLFSSL_STATIC_MEMORY_DEBUG_CALLBACK) wolfSSL_SetDebugMemoryCb(ExampleDebugMemoryCb); @@ -5156,6 +5162,13 @@ THREAD_RETURN WOLFSSL_THREAD client_test(void* args) (void) useVerifyCb; (void) customVerifyCert; +#if defined(WOLFSSL_STATIC_MEMORY) && defined(WOLFSSL_NO_MALLOC) && \ + !defined(NO_MAIN_DRIVER) + /* the pool backing the hint is on this function's stack */ + if (wolfSSL_GetGlobalHeapHint() == (void*)heap) + wolfSSL_SetGlobalHeapHint(NULL); +#endif + WOLFSSL_RETURN_FROM_THREAD(0); } diff --git a/examples/server/server.c b/examples/server/server.c index 41ada9fc131..617f78214c0 100644 --- a/examples/server/server.c +++ b/examples/server/server.c @@ -1835,6 +1835,7 @@ THREAD_RETURN WOLFSSL_THREAD server_test(void* args) WOLFSSL_MEM_STATS mem_stats; #endif #endif + WOLFSSL_HEAP_HINT *heap = NULL; #endif #if defined(WOLFSSL_TLS13) && defined(HAVE_SUPPORTED_CURVES) int onlyKeyShare = 0; @@ -2826,9 +2827,19 @@ THREAD_RETURN WOLFSSL_THREAD server_test(void* args) WOLFMEM_IO_POOL_FIXED)); #endif /* DEBUG_WOLFSSL */ - if (wolfSSL_CTX_load_static_memory(&ctx, method, memory, sizeof(memory),0,1) - != WOLFSSL_SUCCESS) - err_sys_ex(catastrophic, "unable to load static memory and create ctx"); + if (wc_LoadStaticMemory(&heap, memory, sizeof(memory), 0, 1) != 0) + err_sys_ex(catastrophic, "unable to load static memory"); + +#if defined(WOLFSSL_NO_MALLOC) && !defined(NO_MAIN_DRIVER) + /* only the standalone program may publish a pool of its own */ + if (wolfSSL_GetGlobalHeapHint() == NULL) + wolfSSL_SetGlobalHeapHint(heap); +#endif + + if (method != NULL) + ctx = wolfSSL_CTX_new_ex(method(heap), heap); + if (ctx == NULL) + err_sys_ex(catastrophic, "unable to get ctx"); /* load in a buffer for IO */ if (wolfSSL_CTX_load_static_memory(&ctx, NULL, memoryIO, sizeof(memoryIO), @@ -4249,6 +4260,13 @@ THREAD_RETURN WOLFSSL_THREAD server_test(void* args) #if defined(WOLFSSL_CALLBACKS) && defined(WOLFSSL_EARLY_DATA) (void) earlyData; #endif +#if defined(WOLFSSL_STATIC_MEMORY) && defined(WOLFSSL_NO_MALLOC) && \ + !defined(NO_MAIN_DRIVER) + /* the pool backing the hint is on this function's stack */ + if (wolfSSL_GetGlobalHeapHint() == (void*)heap) + wolfSSL_SetGlobalHeapHint(NULL); +#endif + WOLFSSL_RETURN_FROM_THREAD(0); } From fb7208ab4e13fee9a9c8cd7895c69d76a5bb3cd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 15 Sep 2026 15:27:48 +0200 Subject: [PATCH 4/4] Add no-malloc CI coverage for the compatibility layer The no-malloc workflow never built OPENSSL_EXTRA, so nothing in CI reached the allocations this series fixes: all five configs omit --enable-opensslextra, and the two that do enable static memory stop at the algorithm tests. The pool sizes were verified by hand, which leaves them free to drift as footprints grow, and an exhausted pool surfaces as wolfSSL_X509_get_pubkey() returning NULL with no error code. Add two entries. opensslextra-staticmemory is the configuration the series targets. cryptonly-opensslextra-embedded pins the crypt-only exclusion: without it the BENCH_EMBEDDED #error refuses a build whose compatibility tests are not compiled, so a regression there is a build failure rather than a silent over-reservation. Both run testwolfcrypt rather than make check, like the entries beside them: scripts/unit.test still fails seven API tests in this configuration for reasons this series does not address. opensslextra-staticmemory also carries -DWOLFSSL_TEST_CERT -DUSE_CERT_BUFFERS_2048 -DUSE_CERT_BUFFERS_256. No configure option defines the first two, so without them cert_no_malloc_test() and fill_signer_twice_test() are both compiled out and nothing in CI exercises the copy-out that the asn.c fix restores. With them the entry catches a regression of it: putting the guard back to WOLFSSL_NO_MALLOC fails testwolfcrypt at fill_signer_twice_test(), with cert_no_malloc_test()'s mirror assertion behind it. --- .github/workflows/no-malloc.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/no-malloc.yml b/.github/workflows/no-malloc.yml index ac1f6650ef1..0fc8fa38ab2 100644 --- a/.github/workflows/no-malloc.yml +++ b/.github/workflows/no-malloc.yml @@ -59,6 +59,17 @@ jobs: run: | cat > "$RUNNER_TEMP/no-malloc-configs.json" <<'EOF' [ + {"name": "opensslextra-staticmemory", "minutes": 2.0, + "configure": ["--enable-opensslextra", "--enable-staticmemory", + "CFLAGS=-DWOLFSSL_NO_MALLOC -DWOLFSSL_TEST_CERT -DUSE_CERT_BUFFERS_2048 -DUSE_CERT_BUFFERS_256 -pedantic -Wdeclaration-after-statement -Wnull-dereference -DTEST_LIBWOLFSSL_SOURCES_INCLUSION_SEQUENCE"], + "check": false, + "run": [["./wolfcrypt/test/testwolfcrypt"]]}, + {"name": "cryptonly-opensslextra-embedded", "minutes": 1.2, + "configure": ["--enable-cryptonly", "--enable-opensslextra", + "--enable-staticmemory", + "CFLAGS=-DWOLFSSL_NO_MALLOC -DBENCH_EMBEDDED -pedantic -Wdeclaration-after-statement -Wnull-dereference -DTEST_LIBWOLFSSL_SOURCES_INCLUSION_SEQUENCE"], + "check": false, + "run": [["./wolfcrypt/test/testwolfcrypt"]]}, {"name": "rsa-keygen", "minutes": 0.8, "configure": ["--enable-rsa", "--enable-keygen", "--disable-dh", "CFLAGS=-DWOLFSSL_NO_MALLOC -DRSA_MIN_SIZE=1024 -pedantic -Wdeclaration-after-statement -Wnull-dereference -DTEST_LIBWOLFSSL_SOURCES_INCLUSION_SEQUENCE"],