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. */