openssl/0064-CVE-2026-34183.patch
2026-06-11 13:21:23 -04:00

537 lines
17 KiB
Diff

diff --git a/include/internal/quic_cfq.h b/include/internal/quic_cfq.h
index 0b2a3a4cb2d..96c8d89eb60 100644
--- a/include/internal/quic_cfq.h
+++ b/include/internal/quic_cfq.h
@@ -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
diff --git a/include/internal/quic_channel.h b/include/internal/quic_channel.h
index b08e63e148a..d333bc3e5e5 100644
--- a/include/internal/quic_channel.h
+++ b/include/internal/quic_channel.h
@@ -466,6 +466,9 @@ uint64_t ossl_quic_channel_get_max_idle_timeout_actual(const QUIC_CHANNEL *ch);
const QUIC_CONN_ID *scid, const QUIC_CONN_ID *dcid,
const QUIC_CONN_ID *odcid);
+void ossl_ch_reset_rx_state(QUIC_CHANNEL *ch);
+uint64_t ossl_quic_channel_get_path_challenge_count(const QUIC_CHANNEL *ch);
+uint64_t ossl_quic_channel_get_path_response_count(const QUIC_CHANNEL *ch);
#endif
#endif
diff --git a/include/internal/quic_fifd.h b/include/internal/quic_fifd.h
index 4ea7a2e0d22..afa330cbc4a 100644
--- a/include/internal/quic_fifd.h
+++ b/include/internal/quic_fifd.h
@@ -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
diff --git a/ssl/quic/quic_cfq.c b/ssl/quic/quic_cfq.c
index 3c59234ff0f..16818e55f57 100644
--- a/ssl/quic/quic_cfq.c
+++ b/ssl/quic/quic_cfq.c
@@ -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.
diff --git a/ssl/quic/quic_channel.c b/ssl/quic/quic_channel.c
index 631f289b391..9534cbcaf7f 100644
--- a/ssl/quic/quic_channel.c
+++ b/ssl/quic/quic_channel.c
@@ -2213,6 +2213,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)
{
@@ -4060,3 +4066,13 @@ uint64_t ossl_quic_channel_get_max_idle_timeout_actual(const QUIC_CHANNEL *ch)
{
return ch->max_idle_timeout;
}
+
+uint64_t ossl_quic_channel_get_path_challenge_count(const QUIC_CHANNEL *ch)
+{
+ return ch->path_challenge_rx;
+}
+
+uint64_t ossl_quic_channel_get_path_response_count(const QUIC_CHANNEL *ch)
+{
+ return ch->path_response_tx;
+}
diff --git a/ssl/quic/quic_channel_local.h b/ssl/quic/quic_channel_local.h
index ae443fccca1..eb082d6cea7 100644
--- a/ssl/quic/quic_channel_local.h
+++ b/ssl/quic/quic_channel_local.h
@@ -12,6 +12,28 @@
#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
* ======================
@@ -457,6 +479,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;
@@ -467,6 +501,15 @@ 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;
+ /* number of path challenge frames received */
+ unsigned int path_challenge_rx;
+ /* number of path response frames sent */
+ unsigned int path_response_tx;
};
#endif
diff --git a/ssl/quic/quic_fifd.c b/ssl/quic/quic_fifd.c
index 03b8cebd305..e80483b501d 100644
--- a/ssl/quic/quic_fifd.c
+++ b/ssl/quic/quic_fifd.c
@@ -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;
+ }
+ }
+}
diff --git a/ssl/quic/quic_rx_depack.c b/ssl/quic/quic_rx_depack.c
index 786af9b4c22..7ab59f01a1c 100644
--- a/ssl/quic/quic_rx_depack.c
+++ b/ssl/quic/quic_rx_depack.c
@@ -931,6 +931,22 @@ 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--;
+
+ /*
+ * Assume path response frame is being freed on behalf of
+ * finished TX operation. This is for unit testing purposes
+ * only. The counter is also bumped when channel is being
+ * destroyed and CFQ (control frame queue) is freed.
+ * This currently does not matter for check_pc_flood
+ * in test/radix/quic_tests.c.
+ */
+ ch->path_response_tx++;
+
OPENSSL_free(buf);
}
@@ -951,33 +967,41 @@ 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++;
+ }
+
+ ch->path_challenge_rx++;
return 1;
@@ -1432,7 +1456,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));
diff --git a/ssl/quic/quic_txp.c b/ssl/quic/quic_txp.c
index 44aaad868d2..b2565c1a9fe 100644
--- a/ssl/quic/quic_txp.c
+++ b/ssl/quic/quic_txp.c
@@ -3133,6 +3133,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;
}
diff --git a/test/radix/quic_tests.c b/test/radix/quic_tests.c
index 181ba1226b8..48b1df6c1d4 100644
--- a/test/radix/quic_tests.c
+++ b/test/radix/quic_tests.c
@@ -294,6 +294,188 @@ DEF_SCRIPT(check_cwm, "check stream obeys cwm")
OP_WRITE_FAIL(C);
}
+struct mutcbk_ctx {
+ QUIC_PKT_HDR mutctx_qhdrin;
+ OSSL_QTX_IOVEC mutctx_iov;
+ const unsigned char *mutctx_inject;
+ size_t mutctx_inject_sz;
+ int mutctx_done;
+};
+
+static int mutcbk_inject_frames(const QUIC_PKT_HDR *hdrin,
+ const OSSL_QTX_IOVEC *iovecin, size_t numin, QUIC_PKT_HDR **hdrout,
+ const OSSL_QTX_IOVEC **iovecout, size_t *numout, void *arg)
+{
+ struct mutcbk_ctx *mutctx = (struct mutcbk_ctx *)arg;
+ size_t i;
+ size_t grow_allowance = 1200; /* QUIC_MIN_INITIAL_DGRAM_LEN */
+ size_t bufsz = 0;
+ char *buf;
+
+ /*
+ * make injection callback a one shot event,
+ * callback is invoked for every packet we
+ * want to modify only one packet here.
+ */
+ if (mutctx->mutctx_done)
+ return 0;
+
+ mutctx->mutctx_done = 1;
+
+ for (i = 0; i < numin; i++)
+ bufsz += iovecin[i].buf_len;
+
+ mutctx->mutctx_iov.buf_len = bufsz; /* keeps old size */
+ grow_allowance -= (bufsz < grow_allowance) ? bufsz : grow_allowance;
+ /* AEAD tag (16 bytes) + long header (14 bytes) */
+ grow_allowance -= (30 < grow_allowance) ? 30 : grow_allowance;
+
+ grow_allowance -= (hdrin->dst_conn_id.id_len < grow_allowance) ? hdrin->dst_conn_id.id_len : grow_allowance;
+ grow_allowance -= (hdrin->src_conn_id.id_len < grow_allowance) ? hdrin->src_conn_id.id_len : grow_allowance;
+
+ if (grow_allowance == 0) {
+ TEST_info("mutcbk_inject_frames() not enough space to inject");
+ return 0;
+ }
+ bufsz += grow_allowance;
+
+ /* discard const */
+ OPENSSL_free((char *)mutctx->mutctx_iov.buf);
+ mutctx->mutctx_iov.buf = OPENSSL_malloc(bufsz);
+ /* discard const */
+ buf = (char *)mutctx->mutctx_iov.buf;
+ if (buf == NULL) {
+ TEST_info("mutcbk_inject_frames() OPENSSL_malloc() failed");
+ return 0;
+ }
+
+ for (i = 0; i < numin; i++) {
+ memcpy(buf, iovecin[i].buf, iovecin[i].buf_len);
+ buf += iovecin[i].buf_len;
+ }
+
+ /* discard const */
+ buf = (char *)mutctx->mutctx_iov.buf;
+ if (mutctx->mutctx_inject != NULL) {
+ memmove(buf + mutctx->mutctx_inject_sz, buf,
+ mutctx->mutctx_iov.buf_len);
+ memcpy(buf, mutctx->mutctx_inject, mutctx->mutctx_inject_sz);
+ }
+ /*
+ * perhaps needed to have not looked at yet
+ */
+ mutctx->mutctx_qhdrin = *hdrin;
+ *hdrout = &mutctx->mutctx_qhdrin;
+ mutctx->mutctx_iov.buf_len += mutctx->mutctx_inject_sz;
+ *iovecout = &mutctx->mutctx_iov;
+ *numout = 1;
+
+ return 1;
+}
+
+static void mutcbk_finish_injecct_frames(void *arg)
+{
+ struct mutcbk_ctx *mutctx = (struct mutcbk_ctx *)arg;
+
+ OPENSSL_free((char *)mutctx->mutctx_iov.buf);
+ mutctx->mutctx_iov.buf = NULL;
+}
+
+/* 16 path challenge frames */
+#define PATH_CHALLENGE_FRAMES \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH"
+
+DEF_FUNC(mount_flood)
+{
+ int ok = 0;
+ SSL *ssl;
+ QUIC_CHANNEL *ch;
+ static struct mutcbk_ctx mutctx = { 0 };
+ static const unsigned char *inject_frames = (const unsigned char *)PATH_CHALLENGE_FRAMES;
+
+ mutctx.mutctx_inject = inject_frames;
+ mutctx.mutctx_inject_sz = sizeof(PATH_CHALLENGE_FRAMES) - 1;
+ REQUIRE_SSL(ssl);
+ ch = ossl_quic_conn_get_channel(ssl);
+ if (!TEST_ptr(ch))
+ goto err;
+
+ if (!TEST_true(ossl_quic_channel_set_mutator(ch, mutcbk_inject_frames,
+ mutcbk_finish_injecct_frames, &mutctx)))
+ goto err;
+ ok = 1;
+err:
+ return ok;
+}
+
+DEF_FUNC(check_flood_stats)
+{
+ int ok = 0;
+ SSL *ssl;
+ QUIC_CHANNEL *ch;
+ uint64_t path_response_count;
+ uint64_t path_challenge_count;
+
+ REQUIRE_SSL(ssl);
+ ch = ossl_quic_conn_get_channel(ssl);
+ if (!TEST_ptr(ch))
+ goto err;
+
+ path_challenge_count = ossl_quic_channel_get_path_challenge_count(ch);
+ path_response_count = ossl_quic_channel_get_path_response_count(ch);
+
+ if (TEST_uint64_t_ne(path_challenge_count, 16))
+ goto err;
+ if (TEST_uint64_t_ne(path_response_count, 1))
+ goto err;
+
+ ok = 1;
+err:
+ return ok;
+}
+
+DEF_SCRIPT(check_pc_flood, "check path challenge flood")
+{
+ OP_SIMPLE_PAIR_CONN();
+ OP_SELECT_SSL(0, C);
+ OP_FUNC(mount_flood);
+ OP_ACCEPT_CONN_WAIT(L, S, 0);
+ OP_WRITE_B(C, "attack");
+ OP_SELECT_SSL(0, S);
+ OP_FUNC(check_flood_stats);
+}
+
/*
* List of Test Scripts
* ============================================================================
@@ -302,5 +484,6 @@ static SCRIPT_INFO *const scripts[] = {
USE(simple_conn)
USE(simple_thread)
USE(ssl_poll)
- USE(check_cwm)
+ USE(check_cwm)
+ USE(check_pc_flood)
};