From 5ae8ffa71a75db1e5f80dc757d129479a9fbb942 Mon Sep 17 00:00:00 2001 From: Andrew Hutchings Date: Tue, 20 Oct 2026 14:48:30 +0100 Subject: [PATCH 1/3] Free decoded keys before decoding them again at login C_Login decodes every token object, including keys that are already decoded in memory: public keys are not scrubbed at logout, and with an empty user PIN every object is decoded at load and again at login. The RSA, ECC, ML-DSA and ML-KEM decoders re-initialise the wolfCrypt key over the decoded one, which drops what it allocated without freeing it. With a wolfSSL built with HAVE_WOLF_BIGINT (e.g. --enable-pkcs11) each re-decoded RSA public key leaked its n and e bigint buffers; valgrind reported 61,383 bytes in 474 blocks definitely lost over one pkcs11test run. RSA and ECC private keys leaked their bigints the same way in the empty-PIN flow, and ML-DSA and ML-KEM keys leaked their key buffers when wolfSSL uses dynamic key buffers. Free the key before re-initialising it unless it is marked encoded, in which case it has already been freed. A failed decode still frees the key and marks it encoded. --- src/internal.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/internal.c b/src/internal.c index 32c00efb..ecb40eed 100644 --- a/src/internal.c +++ b/src/internal.c @@ -5590,6 +5590,10 @@ static int wp11_Object_Decode_RsaKey(WP11_Object* object) word32 idx = 0; RsaKey* key = object->data.rsaKey; + /* A key decoded before, e.g. at an earlier login, holds allocations that + * re-initialising it would leak. */ + if (!object->encoded) + wc_FreeRsaKey(key); ret = wc_InitRsaKey_ex(key, NULL, object->devId); if (ret != 0) { return ret; @@ -5941,6 +5945,10 @@ static int wp11_Object_Decode_EccKey(WP11_Object* object) word32 idx = 0; ecc_key* key = object->data.ecKey; + /* A key decoded before, e.g. at an earlier login, holds allocations that + * re-initialising it would leak. */ + if (!object->encoded) + wc_ecc_free(key); ret = wc_ecc_init_ex(key, NULL, object->devId); if (ret != 0) { return ret; @@ -6195,6 +6203,11 @@ static int wp11_Object_Decode_MldsaKey(WP11_Object* object) { int ret = 0; + /* A key decoded before, e.g. at an earlier login, holds allocations that + * re-initialising it would leak. */ + if (!object->encoded) + wc_MlDsaKey_Free(object->data.mldsaKey); + if (object->objClass == CKO_PRIVATE_KEY) { unsigned char* der = NULL; int len = object->keyDataLen - AES_BLOCK_SIZE; @@ -6776,6 +6789,11 @@ static int wp11_Object_Decode_MlKemKey(WP11_Object* object) { int ret = 0; + /* A key decoded before, e.g. at an earlier login, holds allocations that + * re-initialising it would leak. */ + if (!object->encoded) + wc_MlKemKey_Free(object->data.mlKemKey); + if (object->objClass == CKO_PRIVATE_KEY) { unsigned char* der; int len; From 1c17401b4a302f7a9ee3ab93318b95218701f036 Mon Sep 17 00:00:00 2001 From: Andrew Hutchings Date: Tue, 20 Oct 2026 14:49:45 +0100 Subject: [PATCH 2/3] Add a bigint wolfSSL build to the ASAN tests No CI job builds wolfSSL with HAVE_WOLF_BIGINT, so a decoded wolfCrypt key there holds no heap memory and re-initialising it without a free goes unnoticed. That is how the C_Login key re-decode leak survived. Add an ASAN-only entry to the sanitizer matrix that builds wolfSSL with --enable-pkcs11 (HAVE_WOLF_BIGINT) and ML-DSA/ML-KEM dynamic key buffers, and runs make check on the PKCS#11 v3.2 PQC build, so LeakSanitizer reports such leaks. Other entries pass empty wolfSSL options and are unchanged. --- .github/workflows/sanitizer-tests.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sanitizer-tests.yml b/.github/workflows/sanitizer-tests.yml index e85db885..094428f4 100644 --- a/.github/workflows/sanitizer-tests.yml +++ b/.github/workflows/sanitizer-tests.yml @@ -27,6 +27,16 @@ jobs: configure_flags: "--enable-nss --enable-tpm" - name: "PKCS#11 V3.2 PQC Build" configure_flags: "--enable-pkcs11v32 --enable-mldsa --enable-mlkem" + include: + # With HAVE_WOLF_BIGINT (--enable-pkcs11) and ML-DSA/ML-KEM dynamic + # key buffers, decoded wolfCrypt keys own heap memory, so a key that + # is re-initialised without being freed shows up as a leak. + - sanitizer: asan + config: + name: "wolfSSL Bigint PQC Build" + configure_flags: "--enable-pkcs11v32 --enable-mldsa --enable-mlkem" + wolfssl_flags: "--enable-pkcs11" + wolfssl_cflags: "-DWOLFSSL_MLKEM_DYNAMIC_KEYS -DWOLFSSL_MLDSA_DYNAMIC_KEYS" steps: #pull wolfPKCS11 @@ -81,7 +91,8 @@ jobs: ;; esac ./configure --enable-cryptocb --enable-aescfb --enable-rsapss --enable-keygen --enable-pwdbased --enable-scrypt --enable-md5 --enable-debug \ - --enable-mldsa C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT -DHAVE_AES_ECB -DHAVE_AES_KEYWRAP" + --enable-mldsa ${{ matrix.config.wolfssl_flags }} \ + C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT -DHAVE_AES_ECB -DHAVE_AES_KEYWRAP ${{ matrix.config.wolfssl_cflags }}" - name: wolfssl make working-directory: ./wolfssl run: | From 2d1185fdd7a3deb715654873395f9793e9cac933 Mon Sep 17 00:00:00 2001 From: Andrew Hutchings Date: Tue, 20 Oct 2026 14:59:23 +0100 Subject: [PATCH 3/3] Keep the bigint ASAN build from failing on a wolfSSL warning wolfSSL master does not build with --enable-pkcs11 --enable-mldsa --enable-debug from a git checkout: wc_pkcs11.c narrows CK_ULONG to word32 in its ML-DSA code, and the -Werror that configure adds in a git checkout turns -Wshorten-64-to-32 into an error. Keep that one diagnostic a warning in the bigint build so the job reaches the tests. --- .github/workflows/sanitizer-tests.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sanitizer-tests.yml b/.github/workflows/sanitizer-tests.yml index 094428f4..3af94fe2 100644 --- a/.github/workflows/sanitizer-tests.yml +++ b/.github/workflows/sanitizer-tests.yml @@ -31,12 +31,15 @@ jobs: # With HAVE_WOLF_BIGINT (--enable-pkcs11) and ML-DSA/ML-KEM dynamic # key buffers, decoded wolfCrypt keys own heap memory, so a key that # is re-initialised without being freed shows up as a leak. + # wolfSSL's wc_pkcs11.c narrows CK_ULONG to word32 in its ML-DSA + # code, which -Werror (on in a git checkout) turns into a build + # failure, so keep that one diagnostic a warning. - sanitizer: asan config: name: "wolfSSL Bigint PQC Build" configure_flags: "--enable-pkcs11v32 --enable-mldsa --enable-mlkem" wolfssl_flags: "--enable-pkcs11" - wolfssl_cflags: "-DWOLFSSL_MLKEM_DYNAMIC_KEYS -DWOLFSSL_MLDSA_DYNAMIC_KEYS" + wolfssl_cflags: "-DWOLFSSL_MLKEM_DYNAMIC_KEYS -DWOLFSSL_MLDSA_DYNAMIC_KEYS -Wno-error=shorten-64-to-32" steps: #pull wolfPKCS11