]> git.ipfire.org Git - thirdparty/openssl.git/commitdiff
QUIC stack must limit the number of PATH_CHALLENGE frames processed in RX
authorAlexandr Nedvedicky <sashan@openssl.org>
Thu, 26 Mar 2026 13:24:32 +0000 (14:24 +0100)
committerTomas Mraz <tomas@openssl.foundation>
Thu, 11 Jun 2026 15:08:41 +0000 (17:08 +0200)
Currently local QUIC stack allocates PATH_RESPONSE frame for every
PATH_CHALLENGE frame it receives in single packet from its remote peer.
The memory with PATH_RESPONSE frame is released after local QUIC stack
receives an ACK which confirms reception of PATH_RESPONSE by remote peer.
This gives remote peer too much control over memory resources local
QUIC stack may consume.

Quoting RFC 9000 section 9.2.1:
...an endpoint SHOULD NOT send multiple
PATH_CHALLENGE frames in a single packet.

Limiting the number of PATCH_CHALLENGE frames to 1 per QUIC packet received
helps to reduce heap memory overhead required to process PATH_CHALLENGE
frame.

Currently QUIC ACKM (ACK-manager) keeps all frames in retransmission
buffer until ACK is received. It can be changed such frames which
don't need to be ACKed don't need to be kept in retrans buffer,
those can be released right after transmission.

Fixes CVE-2026-34183

Reviewed-by: Neil Horman <nhorman@openssl.org>
Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
MergeDate: Mon Jun  8 14:35:20 2026

include/internal/quic_cfq.h
include/internal/quic_channel.h
include/internal/quic_fifd.h
ssl/quic/quic_cfq.c
ssl/quic/quic_channel.c
ssl/quic/quic_channel_local.h
ssl/quic/quic_fifd.c
ssl/quic/quic_rx_depack.c
ssl/quic/quic_txp.c

index 0b2a3a4cb2d6f70dcef8900b334a83de07f8ec90..96c8d89eb6006de44ba000b94edd309cc72632a4 100644 (file)
@@ -149,6 +149,7 @@ QUIC_CFQ_ITEM *ossl_quic_cfq_get_priority_head(const QUIC_CFQ *cfq,
 QUIC_CFQ_ITEM *ossl_quic_cfq_item_get_priority_next(const QUIC_CFQ_ITEM *item,
     uint32_t pn_space);
 
+int ossl_quic_cfq_discard_unreliable(QUIC_CFQ *cfq, QUIC_CFQ_ITEM *item);
 #endif
 
 #endif
index 1cfd6495b0e074d6f5abddb1ce4cef19e7aec74f..9abcee458c8acb7717734b3340ecf24f712a53a4 100644 (file)
@@ -543,6 +543,8 @@ int ossl_quic_bind_channel(QUIC_CHANNEL *ch, const BIO_ADDR *peer,
 
 void ossl_quic_channel_set_tcause(QUIC_CHANNEL *ch, uint64_t app_error_code,
     const char *app_reason);
+
+void ossl_ch_reset_rx_state(QUIC_CHANNEL *ch);
 #endif
 
 #endif
index 4ea7a2e0d22625667ffff63a2f59f23b3bb2a6a5..afa330cbc4a2ea2ed836e02444d334e8f8dbee7a 100644 (file)
@@ -83,6 +83,7 @@ int ossl_quic_fifd_pkt_commit(QUIC_FIFD *fifd, QUIC_TXPIM_PKT *pkt);
 void ossl_quic_fifd_set_qlog_cb(QUIC_FIFD *fifd, QLOG *(*get_qlog_cb)(void *arg),
     void *arg);
 
+void ossl_quic_fifd_pkt_discard_unreliable(QUIC_FIFD *fifd, QUIC_TXPIM_PKT *tpkt);
 #endif
 
 #endif
index a93ba41006eb69fc77ad05fea6a1562fe602bc7a..889b093deaa64df2a74dce927cc2b806805e2514 100644 (file)
@@ -7,6 +7,7 @@
  * https://www.openssl.org/source/license.html
  */
 
+#include "internal/quic_channel.h"
 #include "internal/quic_cfq.h"
 #include "internal/numbers.h"
 
@@ -307,6 +308,20 @@ void ossl_quic_cfq_mark_lost(QUIC_CFQ *cfq, QUIC_CFQ_ITEM *item,
     }
 }
 
+int ossl_quic_cfq_discard_unreliable(QUIC_CFQ *cfq, QUIC_CFQ_ITEM *item)
+{
+    int discarded;
+
+    if (ossl_quic_cfq_item_is_unreliable(item)) {
+        ossl_quic_cfq_release(cfq, item);
+        discarded = 1;
+    } else {
+        discarded = 0;
+    }
+
+    return discarded;
+}
+
 /*
  * Releases a CFQ item. The item may be in either state (NEW or TX) prior to the
  * call. The QUIC_CFQ_ITEM pointer must not be used following this call.
index d00b14f04f707a461cf0259214cdcf625fc2674f..d63b395676c1dc16867b922ff6d2cc5e5d1ce5de 100644 (file)
@@ -2310,6 +2310,12 @@ static void ch_rx_check_forged_pkt_limit(QUIC_CHANNEL *ch)
         "forgery limit");
 }
 
+void ossl_ch_reset_rx_state(QUIC_CHANNEL *ch)
+{
+    ch->did_crypto_frame = 0;
+    ch->seen_path_challenge = 0;
+}
+
 /* Process queued incoming packets and handle frames, if any. */
 static int ch_rx(QUIC_CHANNEL *ch, int channel_only, int *notify_other_threads)
 {
index 6dea6fdf1b24309e82ca2156bb1ff5360ad36988..0d59165811adfa3e0b7ce0520c2ef393827a4154 100644 (file)
 #include "internal/quic_stream_map.h"
 #include "internal/quic_tls.h"
 
+/*
+ * This is a part of PATH_CHALLENGE flood [1] mitigation. This limits the
+ * number of PATH_CHALLENGE frames  QUIC stack is willing to process for
+ * connection. Local QUIC stack creates PATH_RESPONSE frame for PATH_CHALLENGE
+ * frame it receives from remote peer. The response frame is put Control Frame
+ * Queue waiting to be dispatched. The PATH_RESPONSE frame is removed from CFQ
+ * after it is dispatched. The QUIC_PATH_RESPONSE_QLEN limits the number of
+ * PATH_RESPONSE frames waiting to be dispatched. No new PATH_RESPONSE frames
+ * are inserted into CFQ if queue limit is exceeded.
+ *
+ * QUIC implementations use different limits for PATH_RESPONSE queue lengths:
+ *    quic-go defines maxPathResponses as 256
+ *    quiche from cloadflare sets DEFAULT_MAX_PATH_CHALLENGE_RX_QUEUE_LEN to 3
+ *    t-quic from tencent chooses MAX_PATH_CHALS_RECV to be 8
+ *
+ * OpenSSL here introduces QUIC_PATH_RESPONSE_QLEN as 32.
+ *
+ * [1] https://www.ietf.org/archive/id/draft-chen-quic-logical-vuln-mitigations-00.txt
+ *     (section 4.2)
+ */
+#define QUIC_PATH_RESPONSE_QLEN 32
+
 /*
  * QUIC Channel Structure
  * ======================
@@ -481,6 +503,18 @@ struct quic_channel_st {
 
     /* Has qlog been requested? */
     unsigned int is_tserver_ch : 1;
+    /*
+     * RFC 9000 Section 9.2.1 says:
+     *      However, an endpoint SHOULD NOT send multiple
+     *      PATH_CHALLENGE frames in a single packet.
+     * The counter here allows us to detect multiple presence
+     * of PATH_CHALLENGE frame in packet. We process only the
+     * first PATH_CHALLENGE frame found in packet. Remaining PATH_CHALLENGE
+     * frames are ignored.
+     * seen_path_challenge flag is always reset before
+     * ossl_quic_handle_frames() gets called.
+     */
+    unsigned int seen_path_challenge : 1;
 
     /* Saved error stack in case permanent error was encountered */
     ERR_STATE *err_state;
@@ -491,6 +525,11 @@ struct quic_channel_st {
 
     /* Title for qlog purposes. We own this copy. */
     char *qlog_title;
+    /*
+     * number of path responses waiting to be dispatched
+     * from control frame queue (CFQ)
+     */
+    unsigned int path_response_limit;
 };
 
 #endif
index 03b8cebd3057cf7b246c894839f61f514e08071a..e80483b501d72b0e310a0678270ad941e9f808a8 100644 (file)
@@ -310,3 +310,46 @@ void ossl_quic_fifd_set_qlog_cb(QUIC_FIFD *fifd, QLOG *(*get_qlog_cb)(void *arg)
     fifd->get_qlog_cb = get_qlog_cb;
     fifd->get_qlog_cb_arg = get_qlog_cb_arg;
 }
+
+static void txpim_pkt_remove_cfq_item(QUIC_TXPIM_PKT *pkt, QUIC_CFQ_ITEM *cfq_item)
+{
+    QUIC_CFQ_ITEM *prev = cfq_item->pkt_prev;
+
+    if (prev != NULL) {
+        prev->pkt_next = cfq_item->pkt_next;
+    } else {
+        pkt->retx_head = cfq_item->pkt_next;
+    }
+
+    if (cfq_item->pkt_next != NULL)
+        cfq_item->pkt_next->pkt_prev = prev;
+
+    cfq_item->pkt_prev = NULL;
+    cfq_item->pkt_next = NULL;
+}
+
+void ossl_quic_fifd_pkt_discard_unreliable(QUIC_FIFD *fifd, QUIC_TXPIM_PKT *pkt)
+{
+    QUIC_CFQ_ITEM *cfq_item, *cfq_next;
+
+    /*
+     * The packet has been written to network. We can discard frames we don't
+     * retransmit when loss is detected.
+     */
+    cfq_item = pkt->retx_head;
+    while (cfq_item != NULL) {
+        /*
+         * Discarded items are moved to free list. If item
+         * got moved to free list we must also remove it from
+         * cfq list kept in pkt, so ACKM does not find it when
+         * receives an ACK for pkt.
+         */
+        if (ossl_quic_cfq_discard_unreliable(fifd->cfq, cfq_item)) {
+            cfq_next = cfq_item->pkt_next;
+            txpim_pkt_remove_cfq_item(pkt, cfq_item);
+            cfq_item = cfq_next;
+        } else {
+            cfq_item = cfq_item->pkt_next;
+        }
+    }
+}
index 55345bf2cf945e763a9cddc1a9eaeed3684ab983..7961a8bfd701bf402851b84112b96628b1dcf16f 100644 (file)
@@ -931,6 +931,12 @@ static int depack_do_frame_retire_conn_id(PACKET *pkt,
 
 static void free_path_response(unsigned char *buf, size_t buf_len, void *arg)
 {
+    QUIC_CHANNEL *ch = (QUIC_CHANNEL *)arg;
+
+    assert(ch->path_response_limit > 0);
+
+    ch->path_response_limit--;
+
     OPENSSL_free(buf);
 }
 
@@ -951,33 +957,39 @@ static int depack_do_frame_path_challenge(PACKET *pkt,
         return 0;
     }
 
-    /*
-     * RFC 9000 s. 8.2.2: On receiving a PATH_CHALLENGE frame, an endpoint MUST
-     * respond by echoing the data contained in the PATH_CHALLENGE frame in a
-     * PATH_RESPONSE frame.
-     *
-     * TODO(QUIC FUTURE): We should try to avoid allocation here in the future.
-     */
-    encoded_len = sizeof(uint64_t) + 1;
-    if ((encoded = OPENSSL_malloc(encoded_len)) == NULL)
-        goto err;
+    if (ch->seen_path_challenge == 0
+        && ch->path_response_limit < QUIC_PATH_RESPONSE_QLEN) {
+        /*
+         * RFC 9000 s. 8.2.2: On receiving a PATH_CHALLENGE frame, an endpoint
+         * MUST respond by echoing the data contained in the PATH_CHALLENGE
+         * frame in a PATH_RESPONSE frame.
+         *
+         * TODO(QUIC FUTURE): We should try to avoid allocation here in the
+         * future.
+         */
+        encoded_len = sizeof(uint64_t) + 1;
+        if ((encoded = OPENSSL_malloc(encoded_len)) == NULL)
+            goto err;
 
-    if (!WPACKET_init_static_len(&wpkt, encoded, encoded_len, 0))
-        goto err;
+        if (!WPACKET_init_static_len(&wpkt, encoded, encoded_len, 0))
+            goto err;
 
-    if (!ossl_quic_wire_encode_frame_path_response(&wpkt, frame_data)) {
-        WPACKET_cleanup(&wpkt);
-        goto err;
-    }
+        if (!ossl_quic_wire_encode_frame_path_response(&wpkt, frame_data)) {
+            WPACKET_cleanup(&wpkt);
+            goto err;
+        }
 
-    WPACKET_finish(&wpkt);
+        WPACKET_finish(&wpkt);
 
-    if (!ossl_quic_cfq_add_frame(ch->cfq, 0, QUIC_PN_SPACE_APP,
-            OSSL_QUIC_FRAME_TYPE_PATH_RESPONSE,
-            QUIC_CFQ_ITEM_FLAG_UNRELIABLE,
-            encoded, encoded_len,
-            free_path_response, NULL))
-        goto err;
+        if (!ossl_quic_cfq_add_frame(ch->cfq, 0, QUIC_PN_SPACE_APP,
+                OSSL_QUIC_FRAME_TYPE_PATH_RESPONSE,
+                QUIC_CFQ_ITEM_FLAG_UNRELIABLE,
+                encoded, encoded_len,
+                free_path_response, ch))
+            goto err;
+        ch->seen_path_challenge = 1;
+        ch->path_response_limit++;
+    }
 
     return 1;
 
@@ -1432,7 +1444,7 @@ int ossl_quic_handle_frames(QUIC_CHANNEL *ch, OSSL_QRX_PKT *qpacket)
     if (ch == NULL)
         return 0;
 
-    ch->did_crypto_frame = 0;
+    ossl_ch_reset_rx_state(ch);
 
     /* Initialize |ackm_data| (and reinitialize |ok|)*/
     memset(&ackm_data, 0, sizeof(ackm_data));
index 72ac08c95daaf7b34e010d272ff67b99e7895681..425f76a003ca7f62006137c206070f9d339881f9 100644 (file)
@@ -3145,6 +3145,8 @@ static int txp_pkt_commit(OSSL_QUIC_TX_PACKETISER *txp,
             --probe_info->pto[pn_space];
     }
 
+    ossl_quic_fifd_pkt_discard_unreliable(&txp->fifd, tpkt);
+
     return rc;
 }