Avoid Use-After-Free in QuicSession::OnFinalByteOffsetReceived.
Fixes crbug.com/519347100 based on patch from vmiura@google.com.

PiperOrigin-RevId: 947292495
diff --git a/quiche/quic/core/quic_session.cc b/quiche/quic/core/quic_session.cc
index 49b7427..1157541 100644
--- a/quiche/quic/core/quic_session.cc
+++ b/quiche/quic/core/quic_session.cc
@@ -1354,8 +1354,8 @@
     }
   }
 
-  flow_controller_.AddBytesConsumed(offset_diff);
   locally_closed_streams_highest_offset_.erase(it);
+  flow_controller_.AddBytesConsumed(offset_diff);
   if (!VersionIsIetfQuic(transport_version())) {
     stream_id_manager_.OnStreamClosed(
         /*is_incoming=*/IsIncomingStream(stream_id));
diff --git a/quiche/quic/core/quic_session_test.cc b/quiche/quic/core/quic_session_test.cc
index 01a1b07..7b1e79c 100644
--- a/quiche/quic/core/quic_session_test.cc
+++ b/quiche/quic/core/quic_session_test.cc
@@ -59,6 +59,16 @@
 
 namespace quic {
 namespace test {
+
+// Peer for QuicControlFrameManager so the iterator-invalidation regression
+// test below can fill control_frames_ to exactly kMaxNumControlFrames.
+class QuicControlFrameManagerPeer {
+ public:
+  static size_t QueueSize(QuicControlFrameManager* manager) {
+    return manager->control_frames_.size();
+  }
+};
+
 namespace {
 
 class TestCryptoStream : public QuicCryptoStream, public QuicCryptoHandshaker {
@@ -439,6 +449,7 @@
   using QuicSession::CanOpenNextOutgoingBidirectionalStream;
   using QuicSession::CanOpenNextOutgoingUnidirectionalStream;
   using QuicSession::closed_streams;
+  using QuicSession::control_frame_manager;
   using QuicSession::GetNextOutgoingBidirectionalStreamId;
   using QuicSession::GetNextOutgoingUnidirectionalStreamId;
 
@@ -3679,6 +3690,115 @@
   EXPECT_EQ(bytes_consumed_after, kDataSize);
 }
 
+// Regression test: QuicSession::OnFinalByteOffsetReceived holds an iterator
+// into locally_closed_streams_highest_offset_ across
+// flow_controller_.AddBytesConsumed(). If that call synchronously closes the
+// connection (kMaxNumControlFrames overflow when buffering the resulting
+// connection-level WINDOW_UPDATE), every still-active stream is inserted into
+// locally_closed_streams_highest_offset_ during teardown, rehashing the
+// flat_hash_map and freeing the backing array referenced by the held iterator.
+// erase(it) then operates on a stale iterator (heap-use-after-free in
+// release; abseil generation-check / ASAN crash in debug & sanitizer builds).
+TEST_P(QuicSessionTestClient,
+       OnFinalByteOffsetReceivedStaleIteratorOnReentrantClose) {
+  if (!VersionIsIetfQuic(transport_version())) {
+    return;
+  }
+  CompleteHandshake();
+
+  // From now on, simulate "server withholds ACKs": every control frame
+  // buffered in QuicControlFrameManager stays there. Returning false makes
+  // WriteBufferedFrames stop trying; frames are only popped on ACK.
+  EXPECT_CALL(*connection_, SendControlFrame(_)).WillRepeatedly(Return(false));
+  EXPECT_CALL(*connection_, OnStreamReset(_, _)).Times(AnyNumber());
+
+  // STEP 1: client opens several request streams and aborts each before any
+  // FIN/RST is received. Each is destroyed and recorded in
+  // locally_closed_streams_highest_offset_ with offset 0. (In the real attack
+  // ~500 such streams exist; we use a handful so the map's backing array is
+  // heap-allocated rather than SOO-inline.) The first of these is the entry
+  // whose iterator OnFinalByteOffsetReceived will hold.
+  constexpr int kAbortedStreams = 8;
+  QuicStreamId victim_id = 0;
+  for (int i = 0; i < kAbortedStreams; ++i) {
+    TestStream* s = session_.CreateOutgoingBidirectionalStream();
+    EXPECT_CALL(*s, OnSoonToBeDestroyed()).Times(AnyNumber());
+    ASSERT_NE(s, nullptr);
+    if (i == 0) {
+      victim_id = s->id();
+    }
+    s->Reset(QUIC_STREAM_CANCELLED);
+  }
+  ASSERT_EQ(
+      static_cast<size_t>(kAbortedStreams),
+      QuicSessionPeer::GetLocallyClosedStreamsHighestOffset(&session_).size());
+  ASSERT_TRUE(
+      QuicSessionPeer::GetLocallyClosedStreamsHighestOffset(&session_).contains(
+          victim_id));
+
+  // STEP 2: client opens many additional request streams which the server
+  // leaves open (no FIN). When the connection is reentrantly closed below,
+  // each of these is inserted into locally_closed_streams_highest_offset_,
+  // forcing a swisstable rehash that invalidates the held iterator.
+  constexpr int kActiveStreams = 80;
+  for (int i = 0; i < kActiveStreams; ++i) {
+    TestStream* s = session_.CreateOutgoingBidirectionalStream();
+    ASSERT_NE(s, nullptr);
+    EXPECT_CALL(*s, OnSoonToBeDestroyed()).Times(AnyNumber());
+  }
+
+  // STEP 3: top up the control-frame queue to exactly kMaxNumControlFrames
+  // (1000). Any further buffered control frame will trip
+  // OnControlFrameManagerError -> CloseConnection.
+  constexpr size_t kMaxNumControlFrames = 1000;
+  size_t queued =
+      QuicControlFrameManagerPeer::QueueSize(&session_.control_frame_manager());
+  ASSERT_LT(queued, kMaxNumControlFrames);
+  for (size_t i = queued; i < kMaxNumControlFrames; ++i) {
+    session_.SendWindowUpdate(victim_id, /*byte_offset=*/i + 1);
+  }
+  ASSERT_EQ(kMaxNumControlFrames, QuicControlFrameManagerPeer::QueueSize(
+                                      &session_.control_frame_manager()));
+
+  // STEP 4: arrange the connection-level flow controller so that the
+  // RESET_STREAM final_size below pushes bytes_consumed past
+  // receive_window_size_/2, causing AddBytesConsumed to enqueue a
+  // connection-level WINDOW_UPDATE (the 1001st control frame).
+  constexpr QuicByteCount kRecvWindow = 100000;
+  constexpr QuicStreamOffset kFinalSize = 60000;  // > kRecvWindow/2
+  QuicFlowControllerPeer::SetReceiveWindowOffset(session_.flow_controller(),
+                                                 kRecvWindow);
+  QuicFlowControllerPeer::SetMaxReceiveWindow(session_.flow_controller(),
+                                              kRecvWindow);
+
+  // STEP 5: when the WINDOW_UPDATE overflows the control-frame queue,
+  // OnControlFrameManagerError calls CloseConnection. Route it to the real
+  // implementation so the synchronous OnConnectionClosed -> OnStreamClosed ->
+  // InsertLocallyClosedStreamsHighestOffset chain runs.
+  EXPECT_CALL(*connection_,
+              CloseConnection(QUIC_TOO_MANY_BUFFERED_CONTROL_FRAMES, _, _))
+      .WillOnce(
+          Invoke(connection_, &MockQuicConnection::ReallyCloseConnection));
+  EXPECT_CALL(*connection_, SendConnectionClosePacket(_, _, _))
+      .Times(AnyNumber());
+
+  // STEP 6: server sends RESET_STREAM for the locally-closed victim stream.
+  // OnRstStream -> HandleRstOnValidNonexistentStream ->
+  // OnFinalByteOffsetReceived captures `it`, calls AddBytesConsumed which
+  // (via the chain above) inserts kActiveStreams entries into the same map,
+  // then erase(it) runs on a stale iterator.
+  QuicRstStreamFrame rst(kInvalidControlFrameId, victim_id,
+                         QUIC_STREAM_CANCELLED, kFinalSize);
+  session_.OnRstStream(rst);
+
+  // If we reach here, the iterator was not invalidated (e.g. after a fix that
+  // re-finds the entry post-reentrancy).
+  EXPECT_FALSE(connection_->connected());
+  EXPECT_EQ(
+      static_cast<size_t>(kAbortedStreams - 1 + kActiveStreams),
+      QuicSessionPeer::GetLocallyClosedStreamsHighestOffset(&session_).size());
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quic