quic: fix NULL txl dereference in qtx_resize_txe

Fixes: 1957148384 "QUIC Record Layer (Refactor and TX Side)"

Reviewed-by: Saša Nedvědický <sashan@openssl.org>
Reviewed-by: Paul Dale <paul.dale@oracle.com>
Reviewed-by: Eugene Syromiatnikov <esyr@openssl.org>
MergeDate: Sat Apr 11 20:55:10 2026
(Merged from https://github.com/openssl/openssl/pull/30474)
This commit is contained in:
Nikolas Gauder 2026-03-17 20:29:28 +01:00 committed by Nikola Pajkovsky
parent 4d487eb1ac
commit 16892155e1
2 changed files with 109 additions and 21 deletions

View file

@ -269,9 +269,9 @@ static TXE *qtx_ensure_free_txe(OSSL_QTX *qtx, size_t alloc_len)
* of the TXE might change; the new address is returned, or NULL on failure, in
* which case the original TXE remains valid.
*/
static TXE *qtx_resize_txe(OSSL_QTX *qtx, TXE_LIST *txl, TXE *txe, size_t n)
static TXE *qtx_resize_txe(OSSL_QTX *qtx, TXE *txe, size_t n)
{
TXE *txe2, *p;
TXE *txe2;
/* Should never happen. */
if (txe == NULL)
@ -280,27 +280,21 @@ static TXE *qtx_resize_txe(OSSL_QTX *qtx, TXE_LIST *txl, TXE *txe, size_t n)
if (n >= SIZE_MAX - sizeof(TXE))
return NULL;
/* Remove the item from the list to avoid accessing freed memory */
p = ossl_list_txe_prev(txe);
ossl_list_txe_remove(txl, txe);
/*
* to resize txe, the caller must detach it from the list first,
* fail if txe is still attached.
*/
if (!ossl_assert(ossl_list_txe_prev(txe) == NULL
&& ossl_list_txe_next(txe) == NULL))
return NULL;
/*
* NOTE: We do not clear old memory, although it does contain decrypted
* data.
*/
txe2 = OPENSSL_realloc(txe, sizeof(TXE) + n);
if (txe2 == NULL) {
if (p == NULL)
ossl_list_txe_insert_head(txl, txe);
else
ossl_list_txe_insert_after(txl, p, txe);
if (txe2 == NULL)
return NULL;
}
if (p == NULL)
ossl_list_txe_insert_head(txl, txe2);
else
ossl_list_txe_insert_after(txl, p, txe2);
if (qtx->cons == txe)
qtx->cons = txe2;
@ -313,13 +307,12 @@ static TXE *qtx_resize_txe(OSSL_QTX *qtx, TXE_LIST *txl, TXE *txe, size_t n)
* Ensure the data buffer attached to an TXE is at least n bytes in size.
* Returns NULL on failure.
*/
static TXE *qtx_reserve_txe(OSSL_QTX *qtx, TXE_LIST *txl,
TXE *txe, size_t n)
static TXE *qtx_reserve_txe(OSSL_QTX *qtx, TXE *txe, size_t n)
{
if (txe->alloc_len >= n)
return txe;
return qtx_resize_txe(qtx, txl, txe, n);
return qtx_resize_txe(qtx, txe, n);
}
/* Move a TXE from pending to free. */
@ -840,6 +833,16 @@ int ossl_qtx_write_pkt(OSSL_QTX *qtx, const OSSL_QTX_PKT *pkt)
* serialize/encrypt the packet. We always encrypt packets as soon as
* our caller gives them to us, which relieves the caller of any need to
* keep the plaintext around.
*
* the txe can have three distinct states:
* - attached to free list
* - attached to tx list
* - detached.
*
* if txe is detached (not member of free/tx list), then it is kept
* in qtx->cons. The qtx_ensure_cons() here either returns the txe
* from free list or existing ->cons txe. The txe we obtain here
* is detached.
*/
txe = qtx_ensure_cons(qtx);
if (txe == NULL)
@ -849,9 +852,20 @@ int ossl_qtx_write_pkt(OSSL_QTX *qtx, const OSSL_QTX_PKT *pkt)
* Ensure TXE has at least MDPL bytes allocated. This should only be
* possible if the MDPL has increased.
*/
txe = qtx_reserve_txe(qtx, NULL, txe, qtx->mdpl);
if (txe == NULL)
txe = qtx_reserve_txe(qtx, txe, qtx->mdpl);
if (txe == NULL) {
/*
* realloc of txe failed. however it is still kept in ->cons,
* no memory leak.
* The question is what we should do here to handle error,
* is doing `return 0` enough? or shall we discard ->cons and
* put it back to free list?
* or just stop coalescing the packet and dispatch it to network
* right now so the next packet tx can start from fresh?
* I think this is the problem for another day.
*/
return 0;
}
if (!was_coalescing) {
/* Set addresses in TXE. */
@ -878,6 +892,11 @@ int ossl_qtx_write_pkt(OSSL_QTX *qtx, const OSSL_QTX_PKT *pkt)
/*
* We failed due to insufficient length, so end the current
* datagram and try again.
*
* the ossl_qtx_finish_dgram() also puts the txe (-.cons) to
* tx list, so ->cons becomes attached again. The function also
* sets ->cons to NULL so the next loop iteration starts with
* fresh txe (which is also safe to resize).
*/
ossl_qtx_finish_dgram(qtx);
was_coalescing = 0;

View file

@ -19,6 +19,7 @@
#include "testutil.h"
#include "testutil/output.h"
#include "../ssl/ssl_local.h"
#include "../ssl/quic/quic_channel_local.h"
#include "internal/quic_error.h"
static OSSL_LIB_CTX *libctx = NULL;
@ -3517,6 +3518,73 @@ err:
#endif
}
static int test_quic_resize_txe(void)
{
SSL_CTX *cctx = NULL;
SSL *clientquic = NULL;
QUIC_TSERVER *qtserv = NULL;
QUIC_CHANNEL *ch = NULL;
unsigned char msg[] = "resize test";
unsigned char buf[sizeof(msg)];
size_t numbytes = 0;
int ret = 0;
if (!TEST_ptr(cctx = SSL_CTX_new_ex(libctx, NULL, OSSL_QUIC_client_method())))
goto end;
if (!TEST_true(qtest_create_quic_objects(libctx, cctx, NULL,
cert, privkey, 0,
&qtserv, &clientquic,
NULL, NULL)))
goto end;
if (!TEST_true(qtest_create_quic_connection(qtserv, clientquic)))
goto end;
/*
* Client writes first to open stream 0 (client-initiated bidirectional).
* The server must see the stream before it can write back on it.
*/
if (!TEST_true(SSL_write_ex(clientquic, msg, sizeof(msg), &numbytes))
|| !TEST_size_t_eq(numbytes, sizeof(msg)))
goto end;
ossl_quic_tserver_tick(qtserv);
if (!TEST_true(ossl_quic_tserver_read(qtserv, 0, buf, sizeof(buf),
&numbytes)))
goto end;
/*
* Increase the server's QTX MDPL above the initial allocation size
* (QUIC_MIN_INITIAL_DGRAM_LEN = 1200). All TXEs in the free list have
* alloc_len = 1200, so the next write will trigger qtx_resize_txe.
*/
ch = ossl_quic_tserver_get_channel(qtserv);
if (!TEST_true(ossl_qtx_set_mdpl(ch->qtx,
QUIC_MIN_INITIAL_DGRAM_LEN + 250)))
goto end;
/* Trigger a server write: exercises qtx_resize_txe via qtx_reserve_txe */
if (!TEST_true(ossl_quic_tserver_write(qtserv, 0,
msg, sizeof(msg), &numbytes))
|| !TEST_size_t_eq(numbytes, sizeof(msg)))
goto end;
ossl_quic_tserver_tick(qtserv);
SSL_handle_events(clientquic);
if (!TEST_true(SSL_read_ex(clientquic, buf, sizeof(buf), &numbytes))
|| !TEST_mem_eq(buf, numbytes, msg, sizeof(msg)))
goto end;
ret = 1;
end:
ossl_quic_tserver_free(qtserv);
SSL_free(clientquic);
SSL_CTX_free(cctx);
return ret;
}
/***********************************************************************************/
OPT_TEST_DECLARE_USAGE("provider config certsdir datadir\n")
@ -3625,6 +3693,7 @@ int setup_tests(void)
ADD_TEST(test_quic_peer_addr_v6);
ADD_TEST(test_quic_peer_addr_v4);
ADD_TEST(test_ech);
ADD_TEST(test_quic_resize_txe);
return 1;
err: