Deliver STOP_SENDING to zombie streams.

This is to fix a bug that a zombie stream is not closed when receiving STOP_SENDING. Credit to external report in https://github.com/google/quiche/pull/76

Protected by FLAGS_quic_reloadable_flag_quic_deliver_stop_sending_to_zombie_streams.

PiperOrigin-RevId: 677548901
diff --git a/quiche/common/quiche_feature_flags_list.h b/quiche/common/quiche_feature_flags_list.h
index 84a1efb..31c4535 100755
--- a/quiche/common/quiche_feature_flags_list.h
+++ b/quiche/common/quiche_feature_flags_list.h
@@ -24,6 +24,7 @@
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_default_enable_5rto_blackhole_detection2, true, true, "If true, default-enable 5RTO blachole detection.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_default_to_bbr, true, false, "When true, defaults to BBR congestion control instead of Cubic.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_default_to_bbr_v2, false, false, "If true, use BBRv2 as the default congestion controller. Takes precedence over --quic_default_to_bbr.")
+QUICHE_FLAG(bool, quiche_reloadable_flag_quic_deliver_stop_sending_to_zombie_streams, false, false, "If true, deliver STOP_SENDING to zombie streams.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_disable_batch_write, false, false, "If true, round-robin stream writes instead of batching in QuicWriteBlockedList.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_disable_server_blackhole_detection, false, false, "If true, disable blackhole detection on server side.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_disable_version_draft_29, false, false, "If true, disable QUIC version h3-29.")
diff --git a/quiche/quic/core/quic_session.cc b/quiche/quic/core/quic_session.cc
index 2ee8651..ae3e4f9 100644
--- a/quiche/quic/core/quic_session.cc
+++ b/quiche/quic/core/quic_session.cc
@@ -371,7 +371,22 @@
     return;
   }
 
-  QuicStream* stream = GetOrCreateStream(stream_id);
+  QuicStream* stream = nullptr;
+  if (enable_stop_sending_for_zombie_streams_) {
+    stream = GetStream(stream_id);
+    if (stream != nullptr) {
+      if (stream->IsZombie()) {
+        QUIC_RELOADABLE_FLAG_COUNT_N(
+            quic_deliver_stop_sending_to_zombie_streams, 1, 3);
+      } else {
+        QUIC_RELOADABLE_FLAG_COUNT_N(
+            quic_deliver_stop_sending_to_zombie_streams, 2, 3);
+      }
+      stream->OnStopSending(frame.error());
+      return;
+    }
+  }
+  stream = GetOrCreateStream(stream_id);
   if (!stream) {
     // Errors are handled by GetOrCreateStream.
     return;
diff --git a/quiche/quic/core/quic_session.h b/quiche/quic/core/quic_session.h
index ba2013e..1b94b70 100644
--- a/quiche/quic/core/quic_session.h
+++ b/quiche/quic/core/quic_session.h
@@ -690,6 +690,10 @@
   // Returns the priority type used by the streams in the session.
   QuicPriorityType priority_type() const { return priority_type_; }
 
+  bool enable_stop_sending_for_zombie_streams() const {
+    return enable_stop_sending_for_zombie_streams_;
+  }
+
  protected:
   using StreamMap =
       absl::flat_hash_map<QuicStreamId, std::unique_ptr<QuicStream>>;
@@ -1094,6 +1098,9 @@
   std::unique_ptr<QuicAlarm> stream_count_reset_alarm_;
 
   QuicPriorityType priority_type_;
+
+  const bool enable_stop_sending_for_zombie_streams_ =
+      GetQuicReloadableFlag(quic_deliver_stop_sending_to_zombie_streams);
 };
 
 }  // namespace quic
diff --git a/quiche/quic/core/quic_session_test.cc b/quiche/quic/core/quic_session_test.cc
index 5067d9e..03c83e4 100644
--- a/quiche/quic/core/quic_session_test.cc
+++ b/quiche/quic/core/quic_session_test.cc
@@ -3047,6 +3047,44 @@
   session_.OnStopSendingFrame(frame);
 }
 
+// Regression test for b/368421586.
+TEST_P(QuicSessionTestServer, OnStopSendingForZombieStreams) {
+  if (!VersionHasIetfQuicFrames(transport_version())) {
+    return;
+  }
+  CompleteHandshake();
+  session_.set_writev_consumes_all_data(true);
+
+  TestStream* stream = session_.CreateOutgoingBidirectionalStream();
+  std::string body(100, '.');
+  QuicStreamPeer::CloseReadSide(stream);
+  stream->WriteOrBufferData(body, true, nullptr);
+  EXPECT_TRUE(stream->IsWaitingForAcks());
+  // Verify that the stream is a zombie.
+  EXPECT_TRUE(stream->IsZombie());
+  ASSERT_EQ(0u, session_.closed_streams()->size());
+
+  QuicStopSendingFrame frame(1, stream->id(), QUIC_STREAM_CANCELLED);
+  EXPECT_CALL(*connection_, CloseConnection(_, _, _)).Times(0);
+  if (GetQuicReloadableFlag(quic_deliver_stop_sending_to_zombie_streams)) {
+    EXPECT_CALL(*connection_, SendControlFrame(_)).Times(1);
+    EXPECT_CALL(*connection_, OnStreamReset(_, _)).Times(1);
+  } else {
+    EXPECT_CALL(*connection_, SendControlFrame(_)).Times(0);
+    EXPECT_CALL(*connection_, OnStreamReset(_, _)).Times(0);
+  }
+  session_.OnStopSendingFrame(frame);
+  if (GetQuicReloadableFlag(quic_deliver_stop_sending_to_zombie_streams)) {
+    // STOP_SENDING should cause the stream to be closed.
+    EXPECT_FALSE(stream->IsZombie());
+    EXPECT_EQ(1u, session_.closed_streams()->size());
+  } else {
+    // STOP_SENDING is not delivered to zombie streams.
+    EXPECT_TRUE(stream->IsZombie());
+    EXPECT_EQ(0u, session_.closed_streams()->size());
+  }
+}
+
 // If stream is closed, return true and do not close the connection.
 TEST_P(QuicSessionTestServer, OnStopSendingClosedStream) {
   if (!VersionHasIetfQuicFrames(transport_version())) {
diff --git a/quiche/quic/core/quic_stream.cc b/quiche/quic/core/quic_stream.cc
index 9430a36..f42faa1 100644
--- a/quiche/quic/core/quic_stream.cc
+++ b/quiche/quic/core/quic_stream.cc
@@ -532,6 +532,12 @@
 
   stream_error_ = error;
   MaybeSendRstStream(error);
+  if (session()->enable_stop_sending_for_zombie_streams() &&
+      read_side_closed_ && write_side_closed_ && !IsWaitingForAcks()) {
+    QUIC_RELOADABLE_FLAG_COUNT_N(quic_deliver_stop_sending_to_zombie_streams, 3,
+                                 3);
+    session()->MaybeCloseZombieStream(id_);
+  }
   return true;
 }