Close Connection when outgoing ACK frame is empty. This can only happen when the peer is misbehaving, sending an opportunistic ACK for a packet it could not have received. Opportunistic ACKs are potentially an attack. Until quic_least_unacked_plus_1 was verified, the failure to clear all obsolete ACK blocks masked this bug. Protected by FLAGS_quic_reloadable_flag_quic_fail_on_empty_ack. PiperOrigin-RevId: 800613375
diff --git a/quiche/common/quiche_feature_flags_list.h b/quiche/common/quiche_feature_flags_list.h index 5b81922..1f7b2bf 100755 --- a/quiche/common/quiche_feature_flags_list.h +++ b/quiche/common/quiche_feature_flags_list.h
@@ -37,6 +37,7 @@ QUICHE_FLAG(bool, quiche_reloadable_flag_quic_enable_server_on_wire_ping, true, true, "If true, enable server retransmittable on wire PING.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_enable_version_rfcv2, false, false, "When true, support RFC9369.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_enobufs_blocked, true, true, "If true, ENOBUFS socket errors are reported as socket blocked instead of socket failure.") +QUICHE_FLAG(bool, quiche_reloadable_flag_quic_fail_on_empty_ack, false, false, "If true, closes the connection if there are no packets in the ack frame. This is always the result of a protocol violation by the peer.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_fin_before_completed_http_headers, false, true, "If true, close the connection with error if FIN is received before finish receiving the whole HTTP headers.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_fix_timeouts, true, true, "If true, postpone setting handshake timeout to infinite to handshake complete.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_ignore_gquic_probing, true, true, "If true, QUIC server will not respond to gQUIC probing packet(PING + PADDING) but treat it as a regular packet.")
diff --git a/quiche/quic/core/quic_connection.cc b/quiche/quic/core/quic_connection.cc index 2e6e872..06050ee 100644 --- a/quiche/quic/core/quic_connection.cc +++ b/quiche/quic/core/quic_connection.cc
@@ -5843,6 +5843,26 @@ uber_received_packet_manager_.DontWaitForPacketsBefore( last_received_packet_info_.decrypted_level, largest_packet_peer_knows_is_acked); + if (uber_received_packet_manager_.IsAckFrameEmpty( + QuicUtils::GetPacketNumberSpace( + last_received_packet_info_.decrypted_level)) && + GetQuicReloadableFlag(quic_fail_on_empty_ack)) { + QUIC_RELOADABLE_FLAG_COUNT(quic_fail_on_empty_ack); + // A packet N arrived from the peer, and was ACKed. Then a packet M < N + // arrived acknowledging the locally generated ACK. This implies that + // the packet numbers are not increasing. Or, M was sent with an + // "Optimistic ACK" for packets it hadn't received, and M was delayed in + // flight to arrive after N. + // Either case causes DontWaitForPacketsBefore to empty the ACK frame, + // which is a symptom of a protocol violation. + // Avoid sending an ACK in CONNECTION_CLOSE, which would be malformed. + uber_received_packet_manager_.ResetAckStates( + last_received_packet_info_.decrypted_level); + CloseConnection(IETF_QUIC_PROTOCOL_VIOLATION, + "Opportunistic ACK received", + ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET); + return; + } } // Always reset the retransmission alarm when an ack comes in, since we now // have a better estimate of the current rtt than when it was set.
diff --git a/quiche/quic/core/quic_connection_test.cc b/quiche/quic/core/quic_connection_test.cc index 050c249..acfa855 100644 --- a/quiche/quic/core/quic_connection_test.cc +++ b/quiche/quic/core/quic_connection_test.cc
@@ -18117,6 +18117,46 @@ } } +// Regression test for b/440033781 and +// https://g-issues.chromium.org/issues/440833156. +// This test will fail when gfe2_reloadable_flag_quic_least_unacked_plus_1 is +// true. +TEST_P(QuicConnectionTest, AllAckedPacketsCleared) { + if (!version().UsesTls()) { + return; + } + if (!GetQuicReloadableFlag(quic_least_unacked_plus_1)) { + return; + } + SetQuicReloadableFlag(quic_fail_on_empty_ack, true); + // Two packets arrive to trigger an ACK. + QuicPacketNumber largest_packet_sent; + EXPECT_CALL(connection_, OnSerializedPacket) + .WillRepeatedly([&](SerializedPacket packet) { + largest_packet_sent = packet.packet_number; + connection_.QuicConnection::OnSerializedPacket(std::move(packet)); + }); + EXPECT_CALL(*send_algorithm_, OnCongestionEvent); + ProcessPacket(4); + ProcessPacket(5); + EXPECT_TRUE(largest_packet_sent.IsInitialized()); + const QuicAckFrame& local_ack_frame_1 = writer_->ack_frames()[0]; + EXPECT_EQ(local_ack_frame_1.largest_acked, QuicPacketNumber(5)); + EXPECT_EQ(local_ack_frame_1.packets.NumIntervals(), 1); + + // The peer ACKs the locally generated ACK, but with an earlier packet number + // so that all packets in the ACK frame are cleared. + QuicAckFrame peer_ack_frame; + peer_ack_frame.largest_acked = largest_packet_sent; + peer_ack_frame.ack_delay_time = QuicTime::Delta::Zero(); + peer_ack_frame.packets.Add(largest_packet_sent); + QuicFrames peer_frames; + peer_frames.push_back(QuicFrame(&peer_ack_frame)); + EXPECT_CALL(visitor_, OnConnectionClosed); + ProcessFramesPacketAtLevel(3, peer_frames, ENCRYPTION_FORWARD_SECURE); + TestConnectionCloseQuicErrorCode(IETF_QUIC_PROTOCOL_VIOLATION); +} + } // namespace } // namespace test } // namespace quic