In QuicSession::OnCanWrite, do not send stream data when PTO fires and handshake is not confirmed.

Protected by FLAGS_quic_reloadable_flag_quic_donot_pto_stream_data_before_handshake_confirmed.

PiperOrigin-RevId: 481377538
diff --git a/quiche/quic/core/quic_connection.cc b/quiche/quic/core/quic_connection.cc
index 5c44172..565419d 100644
--- a/quiche/quic/core/quic_connection.cc
+++ b/quiche/quic/core/quic_connection.cc
@@ -2438,8 +2438,7 @@
 
   if (perspective_ == Perspective::IS_SERVER &&
       version().CanSendCoalescedPackets() && !IsHandshakeConfirmed()) {
-    if (in_on_retransmission_time_out_ &&
-        coalesced_packet_.NumberOfPackets() == 0u) {
+    if (in_probe_time_out_ && coalesced_packet_.NumberOfPackets() == 0u) {
       // PTO fires while handshake is not confirmed. Do not preempt handshake
       // data with stream data.
       QUIC_CODE_COUNT(quic_try_to_send_half_rtt_data_when_pto_fires);
@@ -7088,15 +7087,15 @@
 QuicConnection::ScopedRetransmissionTimeoutIndicator::
     ScopedRetransmissionTimeoutIndicator(QuicConnection* connection)
     : connection_(connection) {
-  QUICHE_DCHECK(!connection_->in_on_retransmission_time_out_)
+  QUICHE_DCHECK(!connection_->in_probe_time_out_)
       << "ScopedRetransmissionTimeoutIndicator is not supposed to be nested";
-  connection_->in_on_retransmission_time_out_ = true;
+  connection_->in_probe_time_out_ = true;
 }
 
 QuicConnection::ScopedRetransmissionTimeoutIndicator::
     ~ScopedRetransmissionTimeoutIndicator() {
-  QUICHE_DCHECK(connection_->in_on_retransmission_time_out_);
-  connection_->in_on_retransmission_time_out_ = false;
+  QUICHE_DCHECK(connection_->in_probe_time_out_);
+  connection_->in_probe_time_out_ = false;
 }
 
 void QuicConnection::RestoreToLastValidatedPath(
diff --git a/quiche/quic/core/quic_connection.h b/quiche/quic/core/quic_connection.h
index 36e9bbf..153c142 100644
--- a/quiche/quic/core/quic_connection.h
+++ b/quiche/quic/core/quic_connection.h
@@ -1263,6 +1263,8 @@
     context_.bug_listener.swap(bug_listener);
   }
 
+  bool in_probe_time_out() const { return in_probe_time_out_; }
+
   // Ensures the network blackhole delay is longer than path degrading delay.
   static QuicTime::Delta CalculateNetworkBlackholeDelay(
       QuicTime::Delta blackhole_delay, QuicTime::Delta path_degrading_delay,
@@ -1537,7 +1539,7 @@
     QuicConnection* connection_;
   };
 
-  // A class which sets and clears in_on_retransmission_time_out_ when entering
+  // A class which sets and clears in_probe_time_out_ when entering
   // and exiting OnRetransmissionTimeout, respectively.
   class QUIC_EXPORT_PRIVATE ScopedRetransmissionTimeoutIndicator {
    public:
@@ -2244,7 +2246,7 @@
   bool have_decrypted_first_one_rtt_packet_ = false;
 
   // True if we are currently processing OnRetransmissionTimeout.
-  bool in_on_retransmission_time_out_ = false;
+  bool in_probe_time_out_ = false;
 
   QuicPathValidator path_validator_;
 
diff --git a/quiche/quic/core/quic_flags_list.h b/quiche/quic/core/quic_flags_list.h
index d54304f..d3079d5 100644
--- a/quiche/quic/core/quic_flags_list.h
+++ b/quiche/quic/core/quic_flags_list.h
@@ -51,6 +51,8 @@
 QUIC_FLAG(quic_reloadable_flag_quic_enable_disable_resumption, true)
 // If true, discard INITIAL packet if the key has been dropped.
 QUIC_FLAG(quic_reloadable_flag_quic_discard_initial_packet_with_key_dropped, true)
+// If true, do not PTO new stream data before handshake confirmed.
+QUIC_FLAG(quic_reloadable_flag_quic_donot_pto_stream_data_before_handshake_confirmed, true)
 // If true, do not issue a new connection ID that has been claimed by another connection.
 QUIC_FLAG(quic_reloadable_flag_quic_check_cid_collision_when_issue_new_cid, true)
 // If true, do not mark stream connection level write blocked if its write side has been closed.
diff --git a/quiche/quic/core/quic_session.cc b/quiche/quic/core/quic_session.cc
index b6b0371..b4abf52 100644
--- a/quiche/quic/core/quic_session.cc
+++ b/quiche/quic/core/quic_session.cc
@@ -639,6 +639,14 @@
   if (control_frame_manager_.WillingToWrite()) {
     control_frame_manager_.OnCanWrite();
   }
+  if (GetQuicReloadableFlag(
+          quic_donot_pto_stream_data_before_handshake_confirmed) &&
+      version().UsesTls() && GetHandshakeState() != HANDSHAKE_CONFIRMED &&
+      connection_->in_probe_time_out()) {
+    QUIC_CODE_COUNT(quic_donot_pto_stream_data_before_handshake_confirmed);
+    // Do not PTO stream data before handshake gets confirmed.
+    return;
+  }
   // TODO(b/147146815): this makes all datagrams go before stream data.  We
   // should have a better priority scheme for this.
   if (!datagram_queue_.empty()) {
diff --git a/quiche/quic/core/quic_session_test.cc b/quiche/quic/core/quic_session_test.cc
index 916ed1b..3e66183 100644
--- a/quiche/quic/core/quic_session_test.cc
+++ b/quiche/quic/core/quic_session_test.cc
@@ -3128,6 +3128,40 @@
   }
 }
 
+TEST_P(QuicSessionTestServer, DonotPtoStreamDataBeforeHandshakeConfirmed) {
+  if (!session_.version().UsesTls()) {
+    return;
+  }
+  EXPECT_NE(HANDSHAKE_CONFIRMED, session_.GetHandshakeState());
+
+  TestCryptoStream* crypto_stream = session_.GetMutableCryptoStream();
+  EXPECT_FALSE(crypto_stream->HasBufferedCryptoFrames());
+  std::string data(1350, 'a');
+  EXPECT_CALL(*connection_, SendCryptoData(ENCRYPTION_INITIAL, 1350, 0))
+      .WillOnce(Return(1000));
+  crypto_stream->WriteCryptoData(ENCRYPTION_INITIAL, data);
+  ASSERT_TRUE(crypto_stream->HasBufferedCryptoFrames());
+
+  TestStream* stream = session_.CreateOutgoingBidirectionalStream();
+
+  session_.MarkConnectionLevelWriteBlocked(stream->id());
+  // Buffered crypto data gets sent.
+  EXPECT_CALL(*connection_, SendCryptoData(ENCRYPTION_INITIAL, _, _))
+      .WillOnce(Return(350));
+  if (GetQuicReloadableFlag(
+          quic_donot_pto_stream_data_before_handshake_confirmed)) {
+    // Verify stream data is not sent on PTO before handshake confirmed.
+    EXPECT_CALL(*stream, OnCanWrite()).Times(0);
+  } else {
+    EXPECT_CALL(*stream, OnCanWrite());
+  }
+
+  // Fire PTO.
+  QuicConnectionPeer::SetInProbeTimeOut(connection_, true);
+  session_.OnCanWrite();
+  EXPECT_FALSE(crypto_stream->HasBufferedCryptoFrames());
+}
+
 // A client test class that can be used when the automatic configuration is not
 // desired.
 class QuicSessionTestClientUnconfigured : public QuicSessionTestBase {
diff --git a/quiche/quic/test_tools/quic_connection_peer.cc b/quiche/quic/test_tools/quic_connection_peer.cc
index eb28e2d..f7c6424 100644
--- a/quiche/quic/test_tools/quic_connection_peer.cc
+++ b/quiche/quic/test_tools/quic_connection_peer.cc
@@ -560,5 +560,11 @@
   connection->FlushCoalescedPacket();
 }
 
+// static
+void QuicConnectionPeer::SetInProbeTimeOut(QuicConnection* connection,
+                                           bool value) {
+  connection->in_probe_time_out_ = value;
+}
+
 }  // namespace test
 }  // namespace quic
diff --git a/quiche/quic/test_tools/quic_connection_peer.h b/quiche/quic/test_tools/quic_connection_peer.h
index 169a33c..932ddba 100644
--- a/quiche/quic/test_tools/quic_connection_peer.h
+++ b/quiche/quic/test_tools/quic_connection_peer.h
@@ -226,6 +226,8 @@
   static void FlushCoalescedPacket(QuicConnection* connection);
 
   static QuicAlarm* GetMultiPortProbingAlarm(QuicConnection* connection);
+
+  static void SetInProbeTimeOut(QuicConnection* connection, bool value);
 };
 
 }  // namespace test