@@ -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