Skip to content

Commit d2e9efb

Browse files
Sashant8m
authored andcommitted
QUIC stack must limit the number of PATH_CHALLENGE frames processed in RX
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: Nikola Pajkovsky <nikolap@openssl.org> Reviewed-by: Neil Horman <nhorman@openssl.org> Reviewed-by: Tomas Mraz <tomas@openssl.foundation> MergeDate: Mon Jun 8 14:43:13 2026
1 parent 887c5cc commit d2e9efb

9 files changed

Lines changed: 145 additions & 24 deletions

File tree

‎include/internal/quic_cfq.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ QUIC_CFQ_ITEM *ossl_quic_cfq_get_priority_head(const QUIC_CFQ *cfq,
149149
QUIC_CFQ_ITEM *ossl_quic_cfq_item_get_priority_next(const QUIC_CFQ_ITEM *item,
150150
uint32_t pn_space);
151151

152+
int ossl_quic_cfq_discard_unreliable(QUIC_CFQ *cfq, QUIC_CFQ_ITEM *item);
152153
#endif
153154

154155
#endif

‎include/internal/quic_channel.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,7 @@ uint64_t ossl_quic_channel_get_max_idle_timeout_peer_request(const QUIC_CHANNEL
444444
/* Get the idle timeout actually negotiated. */
445445
uint64_t ossl_quic_channel_get_max_idle_timeout_actual(const QUIC_CHANNEL *ch);
446446

447+
void ossl_ch_reset_rx_state(QUIC_CHANNEL *ch);
447448
#endif
448449

449450
#endif

‎include/internal/quic_fifd.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ int ossl_quic_fifd_pkt_commit(QUIC_FIFD *fifd, QUIC_TXPIM_PKT *pkt);
8383
void ossl_quic_fifd_set_qlog_cb(QUIC_FIFD *fifd, QLOG *(*get_qlog_cb)(void *arg),
8484
void *arg);
8585

86+
void ossl_quic_fifd_pkt_discard_unreliable(QUIC_FIFD *fifd, QUIC_TXPIM_PKT *tpkt);
8687
#endif
8788

8889
#endif

‎ssl/quic/quic_cfq.c‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* https://www.openssl.org/source/license.html
88
*/
99

10+
#include "internal/quic_channel.h"
1011
#include "internal/quic_cfq.h"
1112
#include "internal/numbers.h"
1213

@@ -307,6 +308,20 @@ void ossl_quic_cfq_mark_lost(QUIC_CFQ *cfq, QUIC_CFQ_ITEM *item,
307308
}
308309
}
309310

311+
int ossl_quic_cfq_discard_unreliable(QUIC_CFQ *cfq, QUIC_CFQ_ITEM *item)
312+
{
313+
int discarded;
314+
315+
if (ossl_quic_cfq_item_is_unreliable(item)) {
316+
ossl_quic_cfq_release(cfq, item);
317+
discarded = 1;
318+
} else {
319+
discarded = 0;
320+
}
321+
322+
return discarded;
323+
}
324+
310325
/*
311326
* Releases a CFQ item. The item may be in either state (NEW or TX) prior to the
312327
* call. The QUIC_CFQ_ITEM pointer must not be used following this call.

‎ssl/quic/quic_channel.c‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2035,6 +2035,12 @@ static void ch_rx_check_forged_pkt_limit(QUIC_CHANNEL *ch)
20352035
"forgery limit");
20362036
}
20372037

2038+
void ossl_ch_reset_rx_state(QUIC_CHANNEL *ch)
2039+
{
2040+
ch->did_crypto_frame = 0;
2041+
ch->seen_path_challenge = 0;
2042+
}
2043+
20382044
/* Process queued incoming packets and handle frames, if any. */
20392045
static int ch_rx(QUIC_CHANNEL *ch, int channel_only)
20402046
{

‎ssl/quic/quic_channel_local.h‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,28 @@
1111
#include "internal/quic_fc.h"
1212
#include "internal/quic_stream_map.h"
1313

14+
/*
15+
* This is a part of PATH_CHALLENGE flood [1] mitigation. This limits the
16+
* number of PATH_CHALLENGE frames QUIC stack is willing to process for
17+
* connection. Local QUIC stack creates PATH_RESPONSE frame for PATH_CHALLENGE
18+
* frame it receives from remote peer. The response frame is put Control Frame
19+
* Queue waiting to be dispatched. The PATH_RESPONSE frame is removed from CFQ
20+
* after it is dispatched. The QUIC_PATH_RESPONSE_QLEN limits the number of
21+
* PATH_RESPONSE frames waiting to be dispatched. No new PATH_RESPONSE frames
22+
* are inserted into CFQ if queue limit is exceeded.
23+
*
24+
* QUIC implementations use different limits for PATH_RESPONSE queue lengths:
25+
* quic-go defines maxPathResponses as 256
26+
* quiche from cloadflare sets DEFAULT_MAX_PATH_CHALLENGE_RX_QUEUE_LEN to 3
27+
* t-quic from tencent chooses MAX_PATH_CHALS_RECV to be 8
28+
*
29+
* OpenSSL here introduces QUIC_PATH_RESPONSE_QLEN as 32.
30+
*
31+
* [1] https://www.ietf.org/archive/id/draft-chen-quic-logical-vuln-mitigations-00.txt
32+
* (section 4.2)
33+
*/
34+
#define QUIC_PATH_RESPONSE_QLEN 32
35+
1436
/*
1537
* QUIC Channel Structure
1638
* ======================
@@ -437,6 +459,19 @@ struct quic_channel_st {
437459
/* Has qlog been requested? */
438460
unsigned int use_qlog : 1;
439461

462+
/*
463+
* RFC 9000 Section 9.2.1 says:
464+
* However, an endpoint SHOULD NOT send multiple
465+
* PATH_CHALLENGE frames in a single packet.
466+
* The counter here allows us to detect multiple presence
467+
* of PATH_CHALLENGE frame in packet. We process only the
468+
* first PATH_CHALLENGE frame found in packet. Remaining PATH_CHALLENGE
469+
* frames are ignored.
470+
* seen_path_challenge flag is always reset before
471+
* ossl_quic_handle_frames() gets called.
472+
*/
473+
unsigned int seen_path_challenge : 1;
474+
440475
/* Saved error stack in case permanent error was encountered */
441476
ERR_STATE *err_state;
442477

@@ -446,6 +481,11 @@ struct quic_channel_st {
446481

447482
/* Title for qlog purposes. We own this copy. */
448483
char *qlog_title;
484+
/*
485+
* number of path responses waiting to be dispatched
486+
* from control frame queue (CFQ)
487+
*/
488+
unsigned int path_response_limit;
449489
};
450490

451491
#endif

‎ssl/quic/quic_fifd.c‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,3 +310,46 @@ void ossl_quic_fifd_set_qlog_cb(QUIC_FIFD *fifd, QLOG *(*get_qlog_cb)(void *arg)
310310
fifd->get_qlog_cb = get_qlog_cb;
311311
fifd->get_qlog_cb_arg = get_qlog_cb_arg;
312312
}
313+
314+
static void txpim_pkt_remove_cfq_item(QUIC_TXPIM_PKT *pkt, QUIC_CFQ_ITEM *cfq_item)
315+
{
316+
QUIC_CFQ_ITEM *prev = cfq_item->pkt_prev;
317+
318+
if (prev != NULL) {
319+
prev->pkt_next = cfq_item->pkt_next;
320+
} else {
321+
pkt->retx_head = cfq_item->pkt_next;
322+
}
323+
324+
if (cfq_item->pkt_next != NULL)
325+
cfq_item->pkt_next->pkt_prev = prev;
326+
327+
cfq_item->pkt_prev = NULL;
328+
cfq_item->pkt_next = NULL;
329+
}
330+
331+
void ossl_quic_fifd_pkt_discard_unreliable(QUIC_FIFD *fifd, QUIC_TXPIM_PKT *pkt)
332+
{
333+
QUIC_CFQ_ITEM *cfq_item, *cfq_next;
334+
335+
/*
336+
* The packet has been written to network. We can discard frames we don't
337+
* retransmit when loss is detected.
338+
*/
339+
cfq_item = pkt->retx_head;
340+
while (cfq_item != NULL) {
341+
/*
342+
* Discarded items are moved to free list. If item
343+
* got moved to free list we must also remove it from
344+
* cfq list kept in pkt, so ACKM does not find it when
345+
* receives an ACK for pkt.
346+
*/
347+
if (ossl_quic_cfq_discard_unreliable(fifd->cfq, cfq_item)) {
348+
cfq_next = cfq_item->pkt_next;
349+
txpim_pkt_remove_cfq_item(pkt, cfq_item);
350+
cfq_item = cfq_next;
351+
} else {
352+
cfq_item = cfq_item->pkt_next;
353+
}
354+
}
355+
}

‎ssl/quic/quic_rx_depack.c‎

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,12 @@ static int depack_do_frame_retire_conn_id(PACKET *pkt,
928928

929929
static void free_path_response(unsigned char *buf, size_t buf_len, void *arg)
930930
{
931+
QUIC_CHANNEL *ch = (QUIC_CHANNEL *)arg;
932+
933+
assert(ch->path_response_limit > 0);
934+
935+
ch->path_response_limit--;
936+
931937
OPENSSL_free(buf);
932938
}
933939

@@ -948,33 +954,39 @@ static int depack_do_frame_path_challenge(PACKET *pkt,
948954
return 0;
949955
}
950956

951-
/*
952-
* RFC 9000 s. 8.2.2: On receiving a PATH_CHALLENGE frame, an endpoint MUST
953-
* respond by echoing the data contained in the PATH_CHALLENGE frame in a
954-
* PATH_RESPONSE frame.
955-
*
956-
* TODO(QUIC FUTURE): We should try to avoid allocation here in the future.
957-
*/
958-
encoded_len = sizeof(uint64_t) + 1;
959-
if ((encoded = OPENSSL_malloc(encoded_len)) == NULL)
960-
goto err;
957+
if (ch->seen_path_challenge == 0
958+
&& ch->path_response_limit < QUIC_PATH_RESPONSE_QLEN) {
959+
/*
960+
* RFC 9000 s. 8.2.2: On receiving a PATH_CHALLENGE frame, an endpoint
961+
* MUST respond by echoing the data contained in the PATH_CHALLENGE
962+
* frame in a PATH_RESPONSE frame.
963+
*
964+
* TODO(QUIC FUTURE): We should try to avoid allocation here in the
965+
* future.
966+
*/
967+
encoded_len = sizeof(uint64_t) + 1;
968+
if ((encoded = OPENSSL_malloc(encoded_len)) == NULL)
969+
goto err;
961970

962-
if (!WPACKET_init_static_len(&wpkt, encoded, encoded_len, 0))
963-
goto err;
971+
if (!WPACKET_init_static_len(&wpkt, encoded, encoded_len, 0))
972+
goto err;
964973

965-
if (!ossl_quic_wire_encode_frame_path_response(&wpkt, frame_data)) {
966-
WPACKET_cleanup(&wpkt);
967-
goto err;
968-
}
974+
if (!ossl_quic_wire_encode_frame_path_response(&wpkt, frame_data)) {
975+
WPACKET_cleanup(&wpkt);
976+
goto err;
977+
}
969978

970-
WPACKET_finish(&wpkt);
979+
WPACKET_finish(&wpkt);
971980

972-
if (!ossl_quic_cfq_add_frame(ch->cfq, 0, QUIC_PN_SPACE_APP,
973-
OSSL_QUIC_FRAME_TYPE_PATH_RESPONSE,
974-
QUIC_CFQ_ITEM_FLAG_UNRELIABLE,
975-
encoded, encoded_len,
976-
free_path_response, NULL))
977-
goto err;
981+
if (!ossl_quic_cfq_add_frame(ch->cfq, 0, QUIC_PN_SPACE_APP,
982+
OSSL_QUIC_FRAME_TYPE_PATH_RESPONSE,
983+
QUIC_CFQ_ITEM_FLAG_UNRELIABLE,
984+
encoded, encoded_len,
985+
free_path_response, ch))
986+
goto err;
987+
ch->seen_path_challenge = 1;
988+
ch->path_response_limit++;
989+
}
978990

979991
return 1;
980992

@@ -1415,7 +1427,7 @@ int ossl_quic_handle_frames(QUIC_CHANNEL *ch, OSSL_QRX_PKT *qpacket)
14151427
if (ch == NULL)
14161428
return 0;
14171429

1418-
ch->did_crypto_frame = 0;
1430+
ossl_ch_reset_rx_state(ch);
14191431

14201432
/* Initialize |ackm_data| (and reinitialize |ok|)*/
14211433
memset(&ackm_data, 0, sizeof(ackm_data));

‎ssl/quic/quic_txp.c‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3033,6 +3033,8 @@ static int txp_pkt_commit(OSSL_QUIC_TX_PACKETISER *txp,
30333033
--probe_info->pto[pn_space];
30343034
}
30353035

3036+
ossl_quic_fifd_pkt_discard_unreliable(&txp->fifd, tpkt);
3037+
30363038
return rc;
30373039
}
30383040

0 commit comments

Comments
 (0)