@@ -4579,6 +4579,104 @@ int test_tls13_pqc_hybrid_truncated_keyshare(void)
45794579 return EXPECT_RESULT ();
45804580}
45814581
4582+ /* Test that a malformed ECDH portion in a correctly-sized PQC hybrid
4583+ * KeyShare does not leave a dangling pointer in keyShareEntry->key.
4584+ *
4585+ * The earlier truncated-keyshare test is rejected by the keLen <= ctSz
4586+ * check before TLSX_KeyShare_ProcessPqcHybridClient sets up the
4587+ * ecc_kse->key = keyShareEntry->key alias, so it does not exercise the
4588+ * dangling-pointer path. This test sends a SECP256R1MLKEM768 key_share
4589+ * whose total length is correct (65-byte ECDH point + 1088-byte ML-KEM
4590+ * ciphertext = 1153 bytes) but whose ECDH leading byte (0x05) is not a
4591+ * valid X9.63 marker. ProcessEcc_ex then fails at wc_ecc_import_x963
4592+ * AFTER its unconditional cleanup at the end of the function frees the
4593+ * aliased key. Without the fix, the outer keyShareEntry->key still
4594+ * holds the freed pointer; wolfSSL_free -> TLSX_KeyShare_FreeAll calls
4595+ * wc_ecc_free + XFREE on it, producing a use-after-free and a double
4596+ * free that ASAN flags. */
4597+ int test_tls13_pqc_hybrid_malformed_ecdh (void )
4598+ {
4599+ EXPECT_DECLS ;
4600+ #if defined(WOLFSSL_TLS13 ) && !defined(NO_WOLFSSL_CLIENT ) && \
4601+ defined(WOLFSSL_HAVE_MLKEM ) && defined(WOLFSSL_PQC_HYBRIDS ) && \
4602+ !defined(WOLFSSL_NO_ML_KEM_768 ) && defined(HAVE_ECC ) && \
4603+ !defined(WOLFSSL_MLKEM_NO_DECAPSULATE ) && \
4604+ !defined(WOLFSSL_MLKEM_NO_MAKE_KEY ) && \
4605+ (!defined(NO_ECC256 ) || defined(HAVE_ALL_CURVES )) && \
4606+ !defined(NO_ECC_SECP )
4607+ WOLFSSL_CTX * ctx = NULL ;
4608+ WOLFSSL * ssl = NULL ;
4609+ /* 5 (record) + 4 (HS) + 1207 (ServerHello body) = 1216 bytes. */
4610+ static byte serverHello [1216 ];
4611+ word32 i = 0 ;
4612+ WOLFSSL_BUFFER_INFO msg ;
4613+
4614+ XMEMSET (serverHello , 0 , sizeof (serverHello ));
4615+
4616+ /* Record: handshake, TLS 1.2 compat, length 1211 (0x04bb). */
4617+ serverHello [i ++ ] = 0x16 ; serverHello [i ++ ] = 0x03 ; serverHello [i ++ ] = 0x03 ;
4618+ serverHello [i ++ ] = 0x04 ; serverHello [i ++ ] = 0xbb ;
4619+ /* Handshake: ServerHello (0x02), length 1207 (0x0004b7). */
4620+ serverHello [i ++ ] = 0x02 ;
4621+ serverHello [i ++ ] = 0x00 ; serverHello [i ++ ] = 0x04 ; serverHello [i ++ ] = 0xb7 ;
4622+ /* legacy_version */
4623+ serverHello [i ++ ] = 0x03 ; serverHello [i ++ ] = 0x03 ;
4624+ /* random (32 bytes) */
4625+ XMEMSET (& serverHello [i ], 0x42 , 32 ); i += 32 ;
4626+ /* legacy_session_id_echo length: 0 */
4627+ serverHello [i ++ ] = 0x00 ;
4628+ /* cipher_suite: TLS_AES_128_GCM_SHA256 */
4629+ serverHello [i ++ ] = 0x13 ; serverHello [i ++ ] = 0x01 ;
4630+ /* legacy_compression_method: null */
4631+ serverHello [i ++ ] = 0x00 ;
4632+ /* extensions length: 1167 (0x048f) */
4633+ serverHello [i ++ ] = 0x04 ; serverHello [i ++ ] = 0x8f ;
4634+ /* extension: supported_versions -> TLS 1.3 */
4635+ serverHello [i ++ ] = 0x00 ; serverHello [i ++ ] = 0x2b ;
4636+ serverHello [i ++ ] = 0x00 ; serverHello [i ++ ] = 0x02 ;
4637+ serverHello [i ++ ] = 0x03 ; serverHello [i ++ ] = 0x04 ;
4638+ /* extension: key_share, extension_data length 1157 (0x0485) */
4639+ serverHello [i ++ ] = 0x00 ; serverHello [i ++ ] = 0x33 ;
4640+ serverHello [i ++ ] = 0x04 ; serverHello [i ++ ] = 0x85 ;
4641+ /* server_share.group: SECP256R1MLKEM768 (0x11eb) */
4642+ serverHello [i ++ ] = 0x11 ; serverHello [i ++ ] = 0xeb ;
4643+ /* key_exchange length: 1153 (0x0481) */
4644+ serverHello [i ++ ] = 0x04 ; serverHello [i ++ ] = 0x81 ;
4645+ /* ECDH portion (65 bytes): leading 0x05 is not a valid X9.63 marker
4646+ * (valid markers: 0x04, 0x06, 0x07). The remaining 64 bytes stay zero
4647+ * from the initial XMEMSET. */
4648+ serverHello [i ++ ] = 0x05 ;
4649+ i += 64 ;
4650+ /* PQC portion (1088 bytes): all zero from the initial XMEMSET. */
4651+ i += 1088 ;
4652+ AssertIntEQ ((int )i , (int )sizeof (serverHello ));
4653+
4654+ ExpectNotNull (ctx = wolfSSL_CTX_new (wolfTLSv1_3_client_method ()));
4655+ wolfSSL_SetIORecv (ctx , PqcHybridUafRecv );
4656+ wolfSSL_SetIOSend (ctx , PqcHybridUafSend );
4657+
4658+ ExpectNotNull (ssl = wolfSSL_new (ctx ));
4659+
4660+ /* Match the server's offered group so this key_share is processed. */
4661+ ExpectIntEQ (wolfSSL_UseKeyShare (ssl , WOLFSSL_SECP256R1MLKEM768 ),
4662+ WOLFSSL_SUCCESS );
4663+
4664+ msg .buffer = serverHello ;
4665+ msg .length = (unsigned int )sizeof (serverHello );
4666+ wolfSSL_SetIOReadCtx (ssl , & msg );
4667+
4668+ /* Connect should fail gracefully on the malformed ECDH point. */
4669+ ExpectIntEQ (wolfSSL_connect_TLSv13 (ssl ),
4670+ WC_NO_ERR_TRACE (WOLFSSL_FATAL_ERROR ));
4671+
4672+ /* Without the fix, this triggers UAF + double-free in
4673+ * TLSX_KeyShare_FreeAll. */
4674+ wolfSSL_free (ssl );
4675+ wolfSSL_CTX_free (ctx );
4676+ #endif
4677+ return EXPECT_RESULT ();
4678+ }
4679+
45824680/* Test that a TLS 1.3 NewSessionTicket with a ticket shorter than ID_LEN
45834681 * (32 bytes) does not cause an unsigned integer underflow / OOB read in
45844682 * SetTicket. Uses a full memio handshake, then injects a crafted
0 commit comments