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