Close QUIC connections if a RESET_STREAM frame is received with a too-small final byte offset value.

Sends the IETF error code FINAL_SIZE_ERROR in this case, as per:
https://datatracker.ietf.org/doc/html/rfc9000#name-stream-final-size

Protected by quic_reloadable_flag_quic_close_connection_on_underflow.

PiperOrigin-RevId: 881650756
diff --git a/quiche/common/quiche_feature_flags_list.h b/quiche/common/quiche_feature_flags_list.h
index 7fb61fb..6c9ad32 100755
--- a/quiche/common/quiche_feature_flags_list.h
+++ b/quiche/common/quiche_feature_flags_list.h
@@ -19,6 +19,7 @@
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_block_until_settings_received_copt, true, true, "If enabled and a BSUS connection is received, blocks server connections until SETTINGS frame is received.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_clear_body_manager_along_with_sequencer, false, false, "If true, QuicSpdyStream::StopReading always clears BodyManager along with the SequenceBuffer.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_client_check_blockage_before_on_can_write, false, false, "If true, quic clients will only call OnCanWrite() upon write events if the writer is unblocked.")
+QUICHE_FLAG(bool, quiche_reloadable_flag_quic_close_connection_on_underflow, true, true, "If true, close QUIC connections if a RESET_STREAM frame is received with a too-small final byte offset value.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_close_on_idle_timeout, false, false, "If true, closes the connection if it has exceeded the idle timeout when deciding whether to open a stream.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_conservative_bursts, false, false, "If true, set burst token to 2 in cwnd bootstrapping experiment.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_conservative_cwnd_and_pacing_gains, false, false, "If true, uses conservative cwnd gain and pacing gain when cwnd gets bootstrapped.")
diff --git a/quiche/quic/core/quic_error_codes.cc b/quiche/quic/core/quic_error_codes.cc
index 3353f46..e738773 100644
--- a/quiche/quic/core/quic_error_codes.cc
+++ b/quiche/quic/core/quic_error_codes.cc
@@ -138,6 +138,7 @@
     RETURN_STRING_LITERAL(QUIC_FLOW_CONTROL_RECEIVED_TOO_MUCH_DATA);
     RETURN_STRING_LITERAL(QUIC_FLOW_CONTROL_SENT_TOO_MUCH_DATA);
     RETURN_STRING_LITERAL(QUIC_FLOW_CONTROL_INVALID_WINDOW);
+    RETURN_STRING_LITERAL(QUIC_FLOW_CONTROL_FINAL_SIZE_CHANGED);
     RETURN_STRING_LITERAL(QUIC_CONNECTION_IP_POOLED);
     RETURN_STRING_LITERAL(QUIC_PROOF_INVALID);
     RETURN_STRING_LITERAL(QUIC_CRYPTO_DUPLICATE_TAG);
@@ -453,6 +454,8 @@
       return {true, static_cast<uint64_t>(INTERNAL_ERROR)};
     case QUIC_FLOW_CONTROL_INVALID_WINDOW:
       return {true, static_cast<uint64_t>(FLOW_CONTROL_ERROR)};
+    case QUIC_FLOW_CONTROL_FINAL_SIZE_CHANGED:
+      return {true, static_cast<uint64_t>(FINAL_SIZE_ERROR)};
     case QUIC_CONNECTION_IP_POOLED:
       return {true, static_cast<uint64_t>(INTERNAL_ERROR)};
     case QUIC_TOO_MANY_OUTSTANDING_SENT_PACKETS:
diff --git a/quiche/quic/core/quic_error_codes.h b/quiche/quic/core/quic_error_codes.h
index 415e79f..9409892 100644
--- a/quiche/quic/core/quic_error_codes.h
+++ b/quiche/quic/core/quic_error_codes.h
@@ -229,6 +229,9 @@
   QUIC_FLOW_CONTROL_SENT_TOO_MUCH_DATA = 63,
   // The peer received an invalid flow control window.
   QUIC_FLOW_CONTROL_INVALID_WINDOW = 64,
+  // The peer sent a RESET_STREAM or STREAM frame indicating a change in the
+  // final size for the stream.
+  QUIC_FLOW_CONTROL_FINAL_SIZE_CHANGED = 221,
   // The connection has been IP pooled into an existing connection.
   QUIC_CONNECTION_IP_POOLED = 62,
   // The connection has too many outstanding sent packets.
@@ -644,7 +647,7 @@
   QUIC_CLIENT_LOST_NETWORK_ACCESS = 215,
 
   // No error. Used as bound while iterating.
-  QUIC_LAST_ERROR = 221,
+  QUIC_LAST_ERROR = 222,
 };
 // QuicErrorCodes is encoded as four octets on-the-wire when doing Google QUIC,
 // or a varint62 when doing IETF QUIC. Ensure that its value does not exceed
diff --git a/quiche/quic/core/quic_session.cc b/quiche/quic/core/quic_session.cc
index bfb3e54..15c790f 100644
--- a/quiche/quic/core/quic_session.cc
+++ b/quiche/quic/core/quic_session.cc
@@ -1329,6 +1329,15 @@
 
   QUIC_DVLOG(1) << ENDPOINT << "Received final byte offset "
                 << final_byte_offset << " for stream " << stream_id;
+  if (GetQuicReloadableFlag(quic_close_connection_on_underflow)) {
+    QUIC_RELOADABLE_FLAG_COUNT(quic_close_connection_on_underflow);
+    if (final_byte_offset < it->second) {
+      connection_->CloseConnection(
+          QUIC_FLOW_CONTROL_FINAL_SIZE_CHANGED, "Invalid final byte offset",
+          ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET);
+      return;
+    }
+  }
   QuicByteCount offset_diff = final_byte_offset - it->second;
   if (flow_controller_.UpdateHighestReceivedOffset(
           flow_controller_.highest_received_byte_offset() + offset_diff)) {
diff --git a/quiche/quic/core/quic_session_test.cc b/quiche/quic/core/quic_session_test.cc
index 35e490f..e77ec26 100644
--- a/quiche/quic/core/quic_session_test.cc
+++ b/quiche/quic/core/quic_session_test.cc
@@ -225,6 +225,7 @@
   TestStream(PendingStream* pending, QuicSession* session)
       : QuicStream(pending, session, /*is_static=*/false) {}
 
+  using QuicStream::AddBytesConsumed;
   using QuicStream::CloseWriteSide;
   using QuicStream::WriteMemSlices;
 
@@ -3628,6 +3629,64 @@
   EXPECT_FALSE(session_.IsStreamFlowControlBlocked());
 }
 
+TEST_P(QuicSessionTestServer, FlowControlFinalByteUnderflow) {
+  CompleteHandshake();
+
+  // Server creates an outgoing bidirectional stream.
+  TestStream* stream = session_.CreateOutgoingBidirectionalStream();
+  ASSERT_NE(stream, nullptr);
+  const QuicStreamId stream_id = stream->id();
+  const QuicStreamOffset kDataSize = 8000;
+
+  // Peer sends 8000 bytes of data (no FIN) on this stream.
+  std::string data(kDataSize, 'a');
+  QuicStreamFrame data_frame(stream_id, /*fin=*/false, /*offset=*/0,
+                             absl::string_view(data));
+  session_.OnStreamFrame(data_frame);
+
+  EXPECT_EQ(session_.flow_controller()->highest_received_byte_offset(),
+            kDataSize);
+  EXPECT_EQ(stream->highest_received_byte_offset(), kDataSize);
+
+  // Consume all data at stream level so connection bytes_consumed advances.
+  stream->AddBytesConsumed(kDataSize);
+
+  // Server locally resets the stream. Since no FIN was received,
+  // the stream goes into locally_closed_streams_highest_offset_
+  // with offset = 8000.
+  EXPECT_CALL(*connection_, SendControlFrame(_)).Times(AtLeast(1));
+  EXPECT_CALL(*connection_, OnStreamReset(stream_id, _));
+  stream->Reset(QUIC_STREAM_CANCELLED);
+
+  // Peer sends RESET_STREAM with an invalid final_size = 0, which should
+  // trigger the connection to be closed.
+  QuicRstStreamFrame malicious_rst(
+      /*control_frame_id=*/kInvalidControlFrameId, stream_id,
+      QUIC_STREAM_CANCELLED,
+      /*bytes_written=*/0);
+  if (GetQuicReloadableFlag(quic_close_connection_on_underflow)) {
+    EXPECT_CALL(
+        *connection_,
+        CloseConnection(QUIC_FLOW_CONTROL_FINAL_SIZE_CHANGED,
+                        "Invalid final byte offset",
+                        ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET));
+  } else {
+    EXPECT_CALL(*connection_, CloseConnection(_, _, _)).Times(0);
+  }
+  session_.OnRstStream(malicious_rst);
+
+  EXPECT_EQ(session_.flow_controller()->highest_received_byte_offset(),
+            kDataSize);
+  const QuicByteCount bytes_consumed_after =
+      session_.flow_controller()->bytes_consumed();
+
+  if (GetQuicReloadableFlag(quic_close_connection_on_underflow)) {
+    EXPECT_EQ(bytes_consumed_after, kDataSize);
+  } else {
+    EXPECT_NE(bytes_consumed_after, kDataSize);
+  }
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quic