++ b/crypto/openssl/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/crypto/openssl/ssl/quic/quic_channel.c b/crypto/openssl/ssl/quic/quic_channel.c index 13692e5bd09e..5f81a8560d5f 100644 --- a/crypto/openssl/ssl/quic/quic_channel.c +++ b/crypto/openssl/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) { diff --git a/crypto/openssl/ssl/quic/quic_channel_local.h b/crypto/openssl/ssl/quic/quic_channel_local.h index ae443fccca1e..e40b4901cbc7 100644 --- a/crypto/openssl/ssl/quic/quic_channel_local.h +++ b/crypto/openssl/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,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 diff --git a/crypto/openssl/ssl/quic/quic_fifd.c b/crypto/openssl/ssl/quic/quic_fifd.c index 03b8cebd3057..e80483b501d7 100644 --- a/crypto/openssl/ssl/quic/quic_fifd.c +++ b/crypto/openssl/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/crypto/openssl/ssl/quic/quic_port.c b/crypto/openssl/ssl/quic/quic_port.c index 1e247e1ec624..dc79485b96a5 100644 --- a/crypto/openssl/ssl/quic/quic_port.c +++ b/crypto/openssl/ssl/quic/quic_port.c @@ -1666,8 +1666,10 @@ static void port_default_packet_handler(QUIC_URXE *e, void *arg, * forget qrx so channel can create a new one * with valid initial encryption level keys. */ - qrx_src = qrx; - qrx = NULL; + if (qrx != NULL) { + qrx_src = qrx; + qrx = NULL; + } } port_bind_channel(port, &e->peer, &scid, &hdr.dst_conn_id, diff --git a/crypto/openssl/ssl/quic/quic_rx_depack.c b/crypto/openssl/ssl/quic/quic_rx_depack.c index 786af9b4c221..1bdb43b7d639 100644 --- a/crypto/openssl/ssl/quic/quic_rx_depack.c +++ b/crypto/openssl/ssl/quic/quic_rx_depack.c @@ -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)); diff --git a/crypto/openssl/ssl/quic/quic_txp.c b/crypto/openssl/ssl/quic/quic_txp.c index 44aaad868d2f..b2565c1a9fee 100644 --- a/crypto/openssl/ssl/quic/quic_txp.c +++ b/crypto/openssl/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/crypto/openssl/test/cmsapitest.c b/crypto/openssl/test/cmsapitest.c index 0752d14df09c..d908bc6fc4c4 100644 --- a/crypto/openssl/test/cmsapitest.c +++ b/crypto/openssl/test/cmsapitest.c @@ -21,6 +21,7 @@ static X509 *cert = NULL; static EVP_PKEY *privkey = NULL; static char *derin = NULL; static char *too_long_iv_cms_in = NULL; +static char *pwri_kek_oob_der_in = NULL; static int test_encrypt_decrypt(const EVP_CIPHER *cipher) { @@ -512,7 +513,48 @@ end: return ret; } -OPT_TEST_DECLARE_USAGE("certfile privkeyfile derfile\n") +/* + * CMS EnvelopedData with a single PasswordRecipientInfo using + * id-alg-PWRI-KEK and an AES-128-CFB key encryption cipher + * (1-byte effective block size). The encryptedKey OCTET STRING is + * only two bytes long, so the wrapped key buffer is shorter than + * the seven octets read by the check-byte test in kek_unwrap_key(). + * Prior to CVE-2026-9076 this triggered an out-of-bounds heap read; + * CMS_decrypt() must now fail cleanly. + */ +static int test_pwri_kek_unwrap_short_encrypted_key(void) +{ + BIO *in = NULL; + CMS_ContentInfo *cms = NULL; + unsigned long err = 0; + int ret = 0; + + if (!TEST_ptr(in = BIO_new_file(pwri_kek_oob_der_in, "rb")) + || !TEST_ptr(cms = d2i_CMS_bio(in, NULL))) + goto end; + + /* + * The unwrap is attempted eagerly inside CMS_decrypt_set1_password(). + * It must fail cleanly (no OOB read) and report CMS_R_UNWRAP_FAILURE. + */ + if (!TEST_false(CMS_decrypt_set1_password(cms, + (unsigned char *)"password", -1))) + goto end; + + err = ERR_peek_last_error(); + if (!TEST_int_eq(ERR_GET_LIB(err), ERR_LIB_CMS) + || !TEST_int_eq(ERR_GET_REASON(err), CMS_R_UNWRAP_FAILURE)) + goto end; + + ERR_clear_error(); + ret = 1; +end: + CMS_ContentInfo_free(cms); + BIO_free(in); + return ret; +} + +OPT_TEST_DECLARE_USAGE("certfile privkeyfile derfile tooLongIVpem pwriKekOobDer\n") int setup_tests(void) { @@ -527,7 +569,8 @@ int setup_tests(void) if (!TEST_ptr(certin = test_get_argument(0)) || !TEST_ptr(privkeyin = test_get_argument(1)) || !TEST_ptr(derin = test_get_argument(2)) - || !TEST_ptr(too_long_iv_cms_in = test_get_argument(3))) + || !TEST_ptr(too_long_iv_cms_in = test_get_argument(3)) + || !TEST_ptr(pwri_kek_oob_der_in = test_get_argument(4))) *** 235 LINES SKIPPED ***