Skip to content

Commit 66e6e4e

Browse files
committed
quic-limits
DoS possibilities in new quic/h3 code Reported by afldl, 2026-07.
1 parent ebc9425 commit 66e6e4e

3 files changed

Lines changed: 98 additions & 6 deletions

File tree

lib/roles/h3/ops-h3.c

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,6 +1176,27 @@ lws_h3_rx_stream_data(struct lws *wsi, const uint8_t *buf, size_t len)
11761176
lws_quic_enter_closing_state(nwsi, 0x0100 /* LWS_H3_NO_ERROR + 0x0100 (FRAME_ERROR) */, 0, 1);
11771177
return 1;
11781178
}
1179+
1180+
/*
1181+
* F4: HEADERS frames feed the QPACK decoder, which
1182+
* walks the encoded header block roughly byte-by-byte
1183+
* (up to 8 Huffman-table steps per byte). A legitimate
1184+
* QPACK header block is tiny (RFC 9204 implies a few KB
1185+
* at most), so the generic 100 MB frame cap is far too
1186+
* generous here and lets a single HEADERS frame burn
1187+
* ~800M table walks, multiplied across concurrent
1188+
* streams. Apply a dedicated, proportionate bound.
1189+
*/
1190+
if (wsi->h3.rx_frame_type == 0x01 /* HEADERS */ &&
1191+
wsi->h3.rx_frame_len > 65536) {
1192+
struct lws *nwsi = lws_get_quic_network_wsi(wsi);
1193+
lwsl_wsi_notice(wsi, "H3 RX: HEADERS block len "
1194+
"%llu exceeds 64KiB bound",
1195+
(unsigned long long)wsi->h3.rx_frame_len);
1196+
lws_quic_enter_closing_state(nwsi,
1197+
LWS_H3_EXCESSIVE_LOAD, 0, 1);
1198+
return 1;
1199+
}
11791200
wsi->h3.rx_frame_state = 2;
11801201
wsi->h3.rx_frame_payload_read = 0;
11811202
lwsl_wsi_info(wsi, "H3 RX: Frame Type %llu, Len %llu on stream type %d (unidi=%d)",

lib/roles/h3/qpack.c

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -796,10 +796,25 @@ lws_qpack_dynamic_insert(struct lws_qpack_context *ctx, int lws_hdr_idx, const c
796796
struct lws_qpack_dynamic_table_entry *dte;
797797
size_t entry_size;
798798
char *alloc;
799-
799+
800800
if (!ctx || !ctx->dyn_table.entries || !ctx->dyn_table.num_entries)
801801
return 1;
802802

803+
/*
804+
* F6: insert_count is a uint32_t monotonic counter referenced by the
805+
* peer via absolute indices to compute relative_idx arithmetic
806+
* (insert_count - 1 - absolute_idx). It must never wrap. The live
807+
* ring only ever holds num_entries (<=256) entries, but a misbehaving
808+
* peer can stream endless encoder-stream inserts to drive this counter
809+
* toward the 2^32 boundary purely to corrupt the index math. Refuse
810+
* further inserts once we near the wrap point; this also bounds the
811+
* malloc/free churn such a peer can cause. The decoder cross-checks
812+
* (absolute_idx >= insert_count) still hold, so references to the most
813+
* recent inserts remain resolvable.
814+
*/
815+
if (ctx->dyn_table.insert_count >= 0x80000000u)
816+
return 1;
817+
803818
entry_size = name_len + val_len + 32;
804819

805820
alloc = lws_malloc(name_len + val_len + 2, "qpack dyn entry");

lib/roles/quic/parse-quic.c

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -634,7 +634,25 @@ lws_quic_parse_frames(struct lws *nwsi, int level, uint8_t *payload, size_t payl
634634
case LWS_QUIC_FT_ACK:
635635
case LWS_QUIC_FT_ACK_ECN: {
636636
uint64_t largest_ack, ack_delay, ack_range_count, first_ack_range;
637-
uint64_t limit_counter = 0;
637+
638+
/*
639+
* ACK frames describe packet numbers we sent. An ACK can
640+
* legitimately only refer to PNs we actually transmitted, so
641+
* the number of distinct lws_quic_handle_ack() invocations
642+
* that can do useful work is bounded by the number of packets
643+
* currently in flight at this level (plus a small margin for
644+
* races against loss detection draining the list). Any ACK
645+
* claiming more is either corrupt or a deliberate attempt to
646+
* spin the event loop by describing a vast range of PNs.
647+
*
648+
* F1/F2 (GHSA / issue #3651 class): previously a single
649+
* attacker-controlled first_ack_range (up to 2^62) drove an
650+
* unbounded loop. Cap the total work per ACK frame to what
651+
* could conceivably match something in our in-flight list.
652+
*/
653+
uint64_t in_flight_count = qn ? qn->in_flight[level].count : 0;
654+
uint64_t ack_budget = in_flight_count + 16; /* margin */
655+
uint64_t ack_processed = 0;
638656

639657
/* 1. Largest Acknowledged */
640658
consumed = lws_quic_parse_varint(&payload[pos], payload_len - pos, &largest_ack);
@@ -653,6 +671,28 @@ lws_quic_parse_frames(struct lws *nwsi, int level, uint8_t *payload, size_t payl
653671
if (!consumed) return -1;
654672
pos += consumed;
655673

674+
/*
675+
* F2: largest_ack must refer to a packet we actually sent.
676+
* keys[level]->pn_tx is the next PN to be sent, so the highest
677+
* PN ever sent at this level is pn_tx - 1 (when pn_tx == 0 we
678+
* have sent nothing and any ACK is bogus). Reject ACKs for PNs
679+
* we never transmitted rather than walking in_flight for them.
680+
*/
681+
if (qn && qn->keys[level]) {
682+
uint64_t highest_sent = qn->keys[level]->pn_tx ?
683+
qn->keys[level]->pn_tx - 1 : 0;
684+
if (qn->keys[level]->pn_tx == 0 ||
685+
largest_ack > highest_sent) {
686+
lwsl_wsi_notice(nwsi, "QUIC RX: ACK largest %llu "
687+
"exceeds highest sent PN %llu",
688+
(unsigned long long)largest_ack,
689+
(unsigned long long)highest_sent);
690+
lws_quic_enter_closing_state(nwsi,
691+
LWS_QUIC_ERR_FRAME_ENCODING_ERROR, type, 0);
692+
return -1;
693+
}
694+
}
695+
656696
/* 4. First ACK Range */
657697
consumed = lws_quic_parse_varint(&payload[pos], payload_len - pos, &first_ack_range);
658698
if (!consumed) return -1;
@@ -668,13 +708,27 @@ lws_quic_parse_frames(struct lws *nwsi, int level, uint8_t *payload, size_t payl
668708
return -1;
669709
}
670710
for (uint64_t i = 0; i <= first_ack_range; i++) {
711+
if (ack_processed >= ack_budget)
712+
break; /* budget exhausted: stop the costly
713+
* handle_ack walk, but keep parsing the
714+
* remaining range varints below so the
715+
* parser offset stays valid. */
671716
lws_quic_handle_ack(nwsi, level, pn - i, (i == 0) ? 1 : 0, actual_ack_delay_us);
672-
if (++limit_counter > 100000) break;
717+
ack_processed++;
673718
}
674719
pn -= (first_ack_range + 1);
675720

676-
/* 5. Additional ACK Ranges */
677-
if (ack_range_count > 1024 || ack_range_count > (payload_len - pos) / 2) {
721+
/*
722+
* 5. Additional ACK Ranges.
723+
*
724+
* F3: guard pos > payload_len explicitly before the unsigned
725+
* subtraction payload_len - pos, so a future change that
726+
* advanced pos past the end cannot turn this bound into a
727+
* no-op via wraparound.
728+
*/
729+
if (ack_range_count > 1024 ||
730+
pos > payload_len ||
731+
ack_range_count > (payload_len - pos) / 2) {
678732
lws_quic_enter_closing_state(nwsi, LWS_QUIC_ERR_FRAME_ENCODING_ERROR, type, 0);
679733
return -1;
680734
}
@@ -701,8 +755,10 @@ lws_quic_parse_frames(struct lws *nwsi, int level, uint8_t *payload, size_t payl
701755
return -1;
702756
}
703757
for (uint64_t i = 0; i <= ack_range; i++) {
758+
if (ack_processed >= ack_budget)
759+
break; /* budget exhausted: see above */
704760
lws_quic_handle_ack(nwsi, level, pn - i, 0, 0);
705-
if (++limit_counter > 100000) break;
761+
ack_processed++;
706762
}
707763
pn -= (ack_range + 1);
708764
}

0 commit comments

Comments
 (0)