Commit 16892155e1 for openssl.org
commit 16892155e15340ba9725d01cd9593726376fd995
Author: Nikolas Gauder <nikolas.gauder@tum.de>
Date: Tue Mar 17 20:29:28 2026 +0100
quic: fix NULL txl dereference in qtx_resize_txe
Fixes: 1957148384c7 "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)
diff --git a/ssl/quic/quic_record_tx.c b/ssl/quic/quic_record_tx.c
index 8714437232..0dbdb1491a 100644
--- a/ssl/quic/quic_record_tx.c
+++ b/ssl/quic/quic_record_tx.c
@@ -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;
diff --git a/test/quicapitest.c b/test/quicapitest.c
index 84a5c9566f..0d79ceb7f2 100644
--- a/test/quicapitest.c
+++ b/test/quicapitest.c
@@ -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: