Commit b6011a9ee8 for openssl.org
commit b6011a9ee87ab25f47d999a716a1567da00e1878
Author: Alexandr Nedvedicky <sashan@openssl.org>
Date: Wed Aug 5 00:56:25 2026 +0200
Avoid double free of qrx in port_default_packet_handler()
port_default_packet_handler() may perform double free of qrx
when channel creation fails. The port_default_packet_handler()
transfers ownership of qrx to channel/connection via call to
port_bind_channel(). The port_bind_channel() however may
release the qrx when channel can not be bound. The error is
then detected in port_default_packet_handler() which then agains
releases qrx for the second time.
The fix is to add a reference counter to QRX object so transfer
of ownership between port_default_packet_handler() and QUIC_CHANNEL
can be handled safely.
Fixes CVE-2026-18798
Reviewed-by: Igor Ustinov <igus@openssl.foundation>
Reviewed-by: Andrew Dinh <andrewd@openssl.org>
Merge-date: Mon Aug 24 15:46:13 2026
diff --git a/include/internal/quic_record_rx.h b/include/internal/quic_record_rx.h
index 287837b2a5..175d3b2f30 100644
--- a/include/internal/quic_record_rx.h
+++ b/include/internal/quic_record_rx.h
@@ -51,8 +51,9 @@ typedef struct ossl_qrx_args_st {
OSSL_QRX *ossl_qrx_new(const OSSL_QRX_ARGS *args);
/*
- * Frees the QRX. All packets obtained using ossl_qrx_read_pkt must already
- * have been released by calling ossl_qrx_release_pkt.
+ * Frees the QRX/reference to QRX. Frees the QRX object, if all references are
+ * gone. All packets obtained using ossl_qrx_read_pkt must already have been
+ * released by calling ossl_qrx_release_pkt.
*
* You do not need to call ossl_qrx_remove_dst_conn_id first; this function will
* unregister the QRX from the demuxer for all registered destination connection
@@ -60,6 +61,12 @@ OSSL_QRX *ossl_qrx_new(const OSSL_QRX_ARGS *args);
*/
void ossl_qrx_free(OSSL_QRX *qrx);
+/*
+ * Obtains a new reference to QRX object. Returns NULL if reference can not
+ * be obtained.
+ */
+OSSL_QRX *ossl_qrx_newref(OSSL_QRX *qrx);
+
/* Setters for the msg_callback and msg_callback_arg */
void ossl_qrx_set_msg_callback(OSSL_QRX *qrx, ossl_msg_cb msg_callback,
SSL *msg_callback_ssl);
diff --git a/ssl/quic/quic_port.c b/ssl/quic/quic_port.c
index afd21fec2a..9a1760e496 100644
--- a/ssl/quic/quic_port.c
+++ b/ssl/quic/quic_port.c
@@ -624,8 +624,10 @@ static QUIC_CHANNEL *port_make_channel(QUIC_PORT *port, SSL *tls, OSSL_QRX *qrx,
* start by allocation and provisioning as much of the channel as we can
*/
ch = ossl_quic_channel_alloc(&args);
- if (ch == NULL)
+ if (ch == NULL) {
+ ossl_qrx_free(qrx);
return NULL;
+ }
if (tls != NULL) {
ch->tls = tls;
@@ -1618,7 +1620,7 @@ static void port_default_packet_handler(QUIC_URXE *e, void *arg,
QUIC_CHANNEL *ch = NULL, *new_ch = NULL;
QUIC_CONN_ID odcid;
uint8_t gen_new_token = 0;
- OSSL_QRX *qrx = NULL;
+ OSSL_QRX *qrx = NULL, *qrx_ref;
OSSL_QRX *qrx_src = NULL;
OSSL_QRX_ARGS qrx_args = { 0 };
uint64_t cause_flags = 0;
@@ -1811,8 +1813,22 @@ static void port_default_packet_handler(QUIC_URXE *e, void *arg,
}
}
+ qrx_ref = NULL;
+ if (qrx != NULL) {
+ /*
+ * if we are here, then client is validated via retry packet
+ * (client sent a valid token). In this case the qrx has valid
+ * secrets set for QUIC initial level encryption. We can pass
+ * reference to qrx to newly created channel.
+ *
+ * Note: port_bind_channel()/channel becomes owner of qrx_ref.
+ */
+ qrx_ref = ossl_qrx_newref(qrx);
+ if (qrx_ref == NULL)
+ goto undesirable;
+ }
port_bind_channel(port, &e->peer, &hdr.dst_conn_id,
- &odcid, qrx, &new_ch);
+ &odcid, qrx_ref, &new_ch);
/*
* if packet validates it gets moved to channel, we've just bound
@@ -1827,19 +1843,19 @@ static void port_default_packet_handler(QUIC_URXE *e, void *arg,
if (gen_new_token == 1)
generate_new_token(new_ch, &e->peer);
- if (qrx != NULL) {
+ if (qrx_src != NULL) {
/*
- * The qrx belongs to channel now, so don't free it.
- */
- qrx = NULL;
- } else {
- /*
- * We still need to salvage packets from almost forgotten qrx
- * and pass them to channel.
+ * Time to reinject packets from qrx to channel before
+ * qrx will be destroyed here.
*/
while (ossl_qrx_read_pkt(qrx_src, &qrx_pkt) == 1)
ossl_quic_channel_inject_pkt(new_ch, qrx_pkt);
ossl_qrx_update_pn_space(qrx_src, new_ch->qrx);
+ /*
+ * transfer ownership back to qrx;
+ */
+ qrx = qrx_src;
+ qrx_src = NULL;
}
/*
@@ -1856,7 +1872,7 @@ static void port_default_packet_handler(QUIC_URXE *e, void *arg,
*/
undesirable:
- ossl_qrx_free(qrx);
+ ossl_qrx_free(qrx); /* releases reference */
ossl_qrx_free(qrx_src);
ossl_quic_demux_release_urxe(port->demux, e);
}
diff --git a/ssl/quic/quic_record_rx.c b/ssl/quic/quic_record_rx.c
index 868650a612..0538aaa463 100644
--- a/ssl/quic/quic_record_rx.c
+++ b/ssl/quic/quic_record_rx.c
@@ -171,6 +171,8 @@ struct ossl_qrx_st {
ossl_msg_cb msg_callback;
void *msg_callback_arg;
SSL *msg_callback_ssl;
+
+ uint32_t refcount;
};
static RXE *qrx_ensure_free_rxe(OSSL_QRX *qrx, size_t alloc_len);
@@ -212,6 +214,7 @@ OSSL_QRX *ossl_qrx_new(const OSSL_QRX_ARGS *args)
qrx->short_conn_id_len = args->short_conn_id_len;
qrx->init_key_phase_bit = args->init_key_phase_bit;
qrx->max_deferred = args->max_deferred;
+ qrx->refcount = 1;
return qrx;
}
@@ -247,13 +250,10 @@ void ossl_qrx_update_pn_space(OSSL_QRX *src, OSSL_QRX *dst)
return;
}
-void ossl_qrx_free(OSSL_QRX *qrx)
+static void qrx_destroy(OSSL_QRX *qrx)
{
uint32_t i;
- if (qrx == NULL)
- return;
-
/* Free RXE queue data. */
qrx_cleanup_rxl(&qrx->rx_free);
qrx_cleanup_rxl(&qrx->rx_pending);
@@ -267,6 +267,30 @@ void ossl_qrx_free(OSSL_QRX *qrx)
OPENSSL_free(qrx);
}
+void ossl_qrx_free(OSSL_QRX *qrx)
+{
+ if (qrx == NULL)
+ return;
+
+ qrx->refcount--;
+ if (qrx->refcount == 0)
+ qrx_destroy(qrx);
+}
+
+OSSL_QRX *ossl_qrx_newref(OSSL_QRX *qrx)
+{
+ OSSL_QRX *rv_qrx;
+
+ if (qrx != NULL && qrx->refcount != (uint32_t)~0) {
+ qrx->refcount++;
+ rv_qrx = qrx;
+ } else {
+ rv_qrx = NULL;
+ }
+
+ return rv_qrx;
+}
+
void ossl_qrx_inject_urxe(OSSL_QRX *qrx, QUIC_URXE *urxe)
{
/* Initialize our own fields inside the URXE and add to the pending list. */