@@ -3468,3 +3468,175 @@ int test_tls13_derive_keys_no_key(void)
34683468 return EXPECT_RESULT ();
34693469}
34703470
3471+ /* Test that a truncated PQC hybrid KeyShare in a ServerHello does not cause a
3472+ * heap use-after-free during cleanup. A malicious server sends
3473+ * SECP256R1MLKEM768 with only 10 bytes of key exchange data (expected: 1120+).
3474+ * This exercises the error path in TLSX_KeyShare_ProcessPqcHybridClient().
3475+ * Under ASAN the UAF manifests as ForceZero writing to freed KyberKey memory
3476+ * during wolfSSL_free -> TLSX_FreeAll -> TLSX_KeyShare_FreeAll. */
3477+ #if defined(WOLFSSL_TLS13 ) && !defined(NO_WOLFSSL_CLIENT ) && \
3478+ defined(WOLFSSL_HAVE_MLKEM ) && defined(WOLFSSL_PQC_HYBRIDS ) && \
3479+ !defined(WOLFSSL_NO_ML_KEM_768 ) && defined(HAVE_ECC ) && \
3480+ !defined(WOLFSSL_MLKEM_NO_DECAPSULATE ) && \
3481+ !defined(WOLFSSL_MLKEM_NO_MAKE_KEY )
3482+ /* Called when writing - discard output. */
3483+ static int PqcHybridUafSend (WOLFSSL * ssl , char * buf , int sz , void * ctx )
3484+ {
3485+ (void )ssl ;
3486+ (void )buf ;
3487+ (void )ctx ;
3488+ return sz ;
3489+ }
3490+ /* Called when reading - feed from buffer. */
3491+ static int PqcHybridUafRecv (WOLFSSL * ssl , char * buf , int sz , void * ctx )
3492+ {
3493+ WOLFSSL_BUFFER_INFO * msg = (WOLFSSL_BUFFER_INFO * )ctx ;
3494+ int len = (int )msg -> length ;
3495+
3496+ (void )ssl ;
3497+
3498+ if (len > sz )
3499+ len = sz ;
3500+ XMEMCPY (buf , msg -> buffer , len );
3501+ msg -> buffer += len ;
3502+ msg -> length -= len ;
3503+ return len ;
3504+ }
3505+ #endif
3506+
3507+ int test_tls13_pqc_hybrid_truncated_keyshare (void )
3508+ {
3509+ EXPECT_DECLS ;
3510+ #if defined(WOLFSSL_TLS13 ) && !defined(NO_WOLFSSL_CLIENT ) && \
3511+ defined(WOLFSSL_HAVE_MLKEM ) && defined(WOLFSSL_PQC_HYBRIDS ) && \
3512+ !defined(WOLFSSL_NO_ML_KEM_768 ) && defined(HAVE_ECC ) && \
3513+ !defined(WOLFSSL_MLKEM_NO_DECAPSULATE ) && \
3514+ !defined(WOLFSSL_MLKEM_NO_MAKE_KEY )
3515+ WOLFSSL_CTX * ctx = NULL ;
3516+ WOLFSSL * ssl = NULL ;
3517+ /* Crafted TLS 1.3 ServerHello with SECP256R1MLKEM768 (0x11EB) key_share
3518+ * containing only 10 bytes of key exchange data instead of the expected
3519+ * ~1120 bytes. This triggers the error cleanup path. */
3520+ byte serverHello [] = {
3521+ /* TLS record: Handshake, TLS 1.2 compat, length 68 */
3522+ 0x16 , 0x03 , 0x03 , 0x00 , 0x44 ,
3523+ /* Handshake: ServerHello (0x02), length 64 */
3524+ 0x02 , 0x00 , 0x00 , 0x40 ,
3525+ /* legacy_version */
3526+ 0x03 , 0x03 ,
3527+ /* random (32 bytes) */
3528+ 0x42 , 0x42 , 0x42 , 0x42 , 0x42 , 0x42 , 0x42 , 0x42 ,
3529+ 0x42 , 0x42 , 0x42 , 0x42 , 0x42 , 0x42 , 0x42 , 0x42 ,
3530+ 0x42 , 0x42 , 0x42 , 0x42 , 0x42 , 0x42 , 0x42 , 0x42 ,
3531+ 0x42 , 0x42 , 0x42 , 0x42 , 0x42 , 0x42 , 0x42 , 0x42 ,
3532+ /* legacy_session_id_echo length: 0 */
3533+ 0x00 ,
3534+ /* cipher_suite: TLS_AES_128_GCM_SHA256 */
3535+ 0x13 , 0x01 ,
3536+ /* legacy_compression_method: null */
3537+ 0x00 ,
3538+ /* extensions length: 24 */
3539+ 0x00 , 0x18 ,
3540+ /* extension: supported_versions -> TLS 1.3 */
3541+ 0x00 , 0x2b , 0x00 , 0x02 , 0x03 , 0x04 ,
3542+ /* extension: key_share (truncated hybrid data) */
3543+ 0x00 , 0x33 , /* type */
3544+ 0x00 , 0x0e , /* length: 14 */
3545+ 0x11 , 0xeb , /* named_group: SECP256R1MLKEM768 (4587) */
3546+ 0x00 , 0x0a , /* key_exchange length: 10 (truncated!) */
3547+ 0x41 , 0x41 , 0x41 , 0x41 , 0x41 , /* bogus key data */
3548+ 0x41 , 0x41 , 0x41 , 0x41 , 0x41
3549+ };
3550+ WOLFSSL_BUFFER_INFO msg ;
3551+
3552+ ExpectNotNull (ctx = wolfSSL_CTX_new (wolfTLSv1_3_client_method ()));
3553+ wolfSSL_SetIORecv (ctx , PqcHybridUafRecv );
3554+ wolfSSL_SetIOSend (ctx , PqcHybridUafSend );
3555+
3556+ ExpectNotNull (ssl = wolfSSL_new (ctx ));
3557+
3558+ /* Generate the client-side PQC hybrid key share so the truncated
3559+ * ServerHello key_share will be processed (group must match). */
3560+ ExpectIntEQ (wolfSSL_UseKeyShare (ssl , WOLFSSL_SECP256R1MLKEM768 ),
3561+ WOLFSSL_SUCCESS );
3562+
3563+ msg .buffer = serverHello ;
3564+ msg .length = (unsigned int )sizeof (serverHello );
3565+ wolfSSL_SetIOReadCtx (ssl , & msg );
3566+
3567+ /* Connect should fail gracefully on the truncated key share. */
3568+ ExpectIntEQ (wolfSSL_connect_TLSv13 (ssl ),
3569+ WC_NO_ERR_TRACE (WOLFSSL_FATAL_ERROR ));
3570+
3571+ /* The UAF, if present, triggers here: wolfSSL_free -> TLSX_FreeAll ->
3572+ * TLSX_KeyShare_FreeAll -> ForceZero on already-freed KyberKey. */
3573+ wolfSSL_free (ssl );
3574+ wolfSSL_CTX_free (ctx );
3575+ #endif
3576+ return EXPECT_RESULT ();
3577+ }
3578+
3579+ /* Test that a TLS 1.3 NewSessionTicket with a ticket shorter than ID_LEN
3580+ * (32 bytes) does not cause an unsigned integer underflow / OOB read in
3581+ * SetTicket. Uses a full memio handshake, then injects a crafted
3582+ * NewSessionTicket with a 5-byte ticket into the client's read path. */
3583+ int test_tls13_short_session_ticket (void )
3584+ {
3585+ EXPECT_DECLS ;
3586+ #if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES ) && \
3587+ defined(WOLFSSL_TLS13 ) && defined(HAVE_SESSION_TICKET )
3588+ struct test_memio_ctx test_ctx ;
3589+ WOLFSSL_CTX * ctx_c = NULL , * ctx_s = NULL ;
3590+ WOLFSSL * ssl_c = NULL , * ssl_s = NULL ;
3591+ char buf [64 ];
3592+
3593+ XMEMSET (& test_ctx , 0 , sizeof (test_ctx ));
3594+ ExpectIntEQ (test_memio_setup (& test_ctx , & ctx_c , & ctx_s , & ssl_c , & ssl_s ,
3595+ wolfTLSv1_3_client_method , wolfTLSv1_3_server_method ), 0 );
3596+
3597+ /* Complete a TLS 1.3 handshake. The server will send a
3598+ * NewSessionTicket as part of post-handshake messages. */
3599+ ExpectIntEQ (test_memio_do_handshake (ssl_c , ssl_s , 10 , NULL ), 0 );
3600+
3601+ /* Read on client to consume the server's NewSessionTicket. */
3602+ ExpectIntEQ (wolfSSL_read (ssl_c , buf , sizeof (buf )), -1 );
3603+ ExpectIntEQ (wolfSSL_get_error (ssl_c , -1 ), WOLFSSL_ERROR_WANT_READ );
3604+
3605+ /* Now directly test SetTicket with a short ticket by poking the
3606+ * session. The session object is accessible; replicate the exact
3607+ * vulnerable arithmetic: ticket + length - ID_LEN with length=5.
3608+ * With the fix, sessIdLen is capped to length so no underflow. */
3609+ {
3610+ byte shortTicket [5 ] = { 0xBB , 0xCC , 0xDD , 0xEE , 0xFF };
3611+ word32 length = sizeof (shortTicket );
3612+ word32 sessIdLen = ID_LEN ;
3613+
3614+ if (length < ID_LEN )
3615+ sessIdLen = length ;
3616+
3617+ XMEMCPY (ssl_c -> session -> staticTicket , shortTicket , length );
3618+ ssl_c -> session -> ticketLen = (word16 )length ;
3619+ ssl_c -> session -> ticket = ssl_c -> session -> staticTicket ;
3620+
3621+ /* This is the exact code from SetTicket. Before the fix,
3622+ * sessIdLen would be ID_LEN (32), causing: ticket + 5 - 32
3623+ * to underflow and read OOB. */
3624+ XMEMSET (ssl_c -> session -> sessionID , 0 , ID_LEN );
3625+ XMEMCPY (ssl_c -> session -> sessionID ,
3626+ ssl_c -> session -> ticket + length - sessIdLen ,
3627+ sessIdLen );
3628+ ssl_c -> session -> sessionIDSz = ID_LEN ;
3629+
3630+ /* Verify: sessionID should contain only the 5 ticket bytes,
3631+ * zero-padded, not garbage from an OOB read. */
3632+ ExpectBufEQ (ssl_c -> session -> sessionID , shortTicket , 5 );
3633+ }
3634+
3635+ wolfSSL_free (ssl_c );
3636+ wolfSSL_free (ssl_s );
3637+ wolfSSL_CTX_free (ctx_c );
3638+ wolfSSL_CTX_free (ctx_s );
3639+ #endif
3640+ return EXPECT_RESULT ();
3641+ }
3642+
0 commit comments