Fix sending MAX_PUSH_ID frame during 0-RTT.

Assume SetMaxPushId() is called with some initial value.  During 0-RTT,
SendInitialData() is called, it sends a MAX_PUSH_ID and sets
|http3_max_push_id_sent_|.  If SetMaxPushId() is called with a different value
after this but before 1-RTT keys are available, it does not send the MAX_PUSH_ID
frame with the updated value because OneRttKeysAvailable() is false.  Then
SendInitialData() is called when 1-RTT keys are available, but it does not send
the updated MAX_PUSH_ID frame either, because |http3_max_push_id_sent_| is true.

This CL fixes that by gating on IsEncryptionEstablished() instead of
OneRttKeysAvailable() in SetMaxPushId().  However, it introduces a new bug:

Assume there is no initial SetMaxPushId() call.  During 0-RTT, SendInitialData()
is called, but it does not send a MAX_PUSH_ID frame because |max_push_id_| is
not set.  Then SetMaxPushId() is called before 1-RTT keys are available, and it
now sends the MAX_PUSH_ID frame because IsEncryptionEstablished() is true.  Then
SendInitialData() is called when 1-RTT keys are available.  It should not send
another MAX_PUSH_ID frame with the same value.  In order to achieve this,
|http3_max_push_id_sent_| must be set to true when the MAX_PUSH_ID frame is sent
by SetMaxPushId().  This is done by setting |http3_max_push_id_sent_| in
SendMaxPushId() instead of SendInitialData().

Client-only change.

PiperOrigin-RevId: 315734365
Change-Id: Ib4dc2f96624b1c8c9a27916aa4a6ab98fbb71494
diff --git a/quic/core/http/quic_spdy_client_session_test.cc b/quic/core/http/quic_spdy_client_session_test.cc
index ca9b1d5..fc6343e 100644
--- a/quic/core/http/quic_spdy_client_session_test.cc
+++ b/quic/core/http/quic_spdy_client_session_test.cc
@@ -43,6 +43,7 @@
 using ::testing::AtLeast;
 using ::testing::AtMost;
 using ::testing::Invoke;
+using ::testing::StrictMock;
 using ::testing::Truly;
 
 namespace quic {
@@ -1234,6 +1235,119 @@
       connection_, crypto_stream_, AlpnForVersion(connection_->version()));
 }
 
+TEST_P(QuicSpdyClientSessionTest, SetMaxPushIdBeforeEncryptionEstablished) {
+  // 0-RTT is TLS-only, MAX_PUSH_ID frame is HTTP/3-only.
+  if (!session_->version().UsesTls() || !session_->version().UsesHttp3()) {
+    return;
+  }
+
+  CompleteFirstConnection();
+
+  CreateConnection();
+  StrictMock<MockHttp3DebugVisitor> debug_visitor;
+  session_->set_debug_visitor(&debug_visitor);
+
+  // No MAX_PUSH_ID frame is sent before encryption is established.
+  session_->SetMaxPushId(5);
+
+  EXPECT_FALSE(session_->IsEncryptionEstablished());
+  EXPECT_FALSE(session_->OneRttKeysAvailable());
+  EXPECT_EQ(ENCRYPTION_INITIAL, session_->connection()->encryption_level());
+
+  // MAX_PUSH_ID frame is sent upon encryption establishment with the value set
+  // by the earlier SetMaxPushId() call.
+  EXPECT_CALL(debug_visitor, OnSettingsFrameSent(_));
+  EXPECT_CALL(debug_visitor, OnMaxPushIdFrameSent(_))
+      .WillOnce(Invoke(
+          [](const MaxPushIdFrame& frame) { EXPECT_EQ(5u, frame.push_id); }));
+  session_->CryptoConnect();
+  testing::Mock::VerifyAndClearExpectations(&debug_visitor);
+
+  EXPECT_TRUE(session_->IsEncryptionEstablished());
+  EXPECT_FALSE(session_->OneRttKeysAvailable());
+  EXPECT_EQ(ENCRYPTION_ZERO_RTT, session_->connection()->encryption_level());
+
+  // Another SetMaxPushId() call with the same value does not trigger sending
+  // another MAX_PUSH_ID frame.
+  session_->SetMaxPushId(5);
+
+  // Calling SetMaxPushId() with a different value results in sending another
+  // MAX_PUSH_ID frame.
+  EXPECT_CALL(debug_visitor, OnMaxPushIdFrameSent(_))
+      .WillOnce(Invoke(
+          [](const MaxPushIdFrame& frame) { EXPECT_EQ(10u, frame.push_id); }));
+  session_->SetMaxPushId(10);
+  testing::Mock::VerifyAndClearExpectations(&debug_visitor);
+
+  QuicConfig config = DefaultQuicConfig();
+  crypto_test_utils::HandshakeWithFakeServer(
+      &config, server_crypto_config_.get(), &helper_, &alarm_factory_,
+      connection_, crypto_stream_, AlpnForVersion(connection_->version()));
+
+  EXPECT_TRUE(session_->IsEncryptionEstablished());
+  EXPECT_TRUE(session_->OneRttKeysAvailable());
+  EXPECT_EQ(ENCRYPTION_FORWARD_SECURE,
+            session_->connection()->encryption_level());
+  EXPECT_TRUE(session_->GetCryptoStream()->IsResumption());
+}
+
+TEST_P(QuicSpdyClientSessionTest, SetMaxPushIdAfterEncryptionEstablished) {
+  // 0-RTT is TLS-only, MAX_PUSH_ID frame is HTTP/3-only.
+  if (!session_->version().UsesTls() || !session_->version().UsesHttp3()) {
+    return;
+  }
+
+  CompleteFirstConnection();
+
+  CreateConnection();
+  StrictMock<MockHttp3DebugVisitor> debug_visitor;
+  session_->set_debug_visitor(&debug_visitor);
+
+  EXPECT_FALSE(session_->IsEncryptionEstablished());
+  EXPECT_FALSE(session_->OneRttKeysAvailable());
+  EXPECT_EQ(ENCRYPTION_INITIAL, session_->connection()->encryption_level());
+
+  // No MAX_PUSH_ID frame is sent if SetMaxPushId() has not been called.
+  EXPECT_CALL(debug_visitor, OnSettingsFrameSent(_));
+  session_->CryptoConnect();
+  testing::Mock::VerifyAndClearExpectations(&debug_visitor);
+
+  EXPECT_TRUE(session_->IsEncryptionEstablished());
+  EXPECT_FALSE(session_->OneRttKeysAvailable());
+  EXPECT_EQ(ENCRYPTION_ZERO_RTT, session_->connection()->encryption_level());
+
+  // Calling SetMaxPushId() for the first time after encryption is established
+  // results in sending a MAX_PUSH_ID frame.
+  EXPECT_CALL(debug_visitor, OnMaxPushIdFrameSent(_))
+      .WillOnce(Invoke(
+          [](const MaxPushIdFrame& frame) { EXPECT_EQ(5u, frame.push_id); }));
+  session_->SetMaxPushId(5);
+  testing::Mock::VerifyAndClearExpectations(&debug_visitor);
+
+  // Another SetMaxPushId() call with the same value does not trigger sending
+  // another MAX_PUSH_ID frame.
+  session_->SetMaxPushId(5);
+
+  // Calling SetMaxPushId() with a different value results in sending another
+  // MAX_PUSH_ID frame.
+  EXPECT_CALL(debug_visitor, OnMaxPushIdFrameSent(_))
+      .WillOnce(Invoke(
+          [](const MaxPushIdFrame& frame) { EXPECT_EQ(10u, frame.push_id); }));
+  session_->SetMaxPushId(10);
+  testing::Mock::VerifyAndClearExpectations(&debug_visitor);
+
+  QuicConfig config = DefaultQuicConfig();
+  crypto_test_utils::HandshakeWithFakeServer(
+      &config, server_crypto_config_.get(), &helper_, &alarm_factory_,
+      connection_, crypto_stream_, AlpnForVersion(connection_->version()));
+
+  EXPECT_TRUE(session_->IsEncryptionEstablished());
+  EXPECT_TRUE(session_->OneRttKeysAvailable());
+  EXPECT_EQ(ENCRYPTION_FORWARD_SECURE,
+            session_->connection()->encryption_level());
+  EXPECT_TRUE(session_->GetCryptoStream()->IsResumption());
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quic
diff --git a/quic/core/http/quic_spdy_session.cc b/quic/core/http/quic_spdy_session.cc
index cb6a7f6..386fb8a 100644
--- a/quic/core/http/quic_spdy_session.cc
+++ b/quic/core/http/quic_spdy_session.cc
@@ -739,7 +739,6 @@
   if (perspective() == Perspective::IS_CLIENT && max_push_id_.has_value() &&
       !http3_max_push_id_sent_) {
     SendMaxPushId();
-    http3_max_push_id_sent_ = true;
   }
 }
 
@@ -1241,7 +1240,7 @@
   }
   max_push_id_ = max_push_id;
 
-  if (OneRttKeysAvailable()) {
+  if (IsEncryptionEstablished()) {
     SendMaxPushId();
   }
 }
@@ -1284,6 +1283,7 @@
   DCHECK_EQ(Perspective::IS_CLIENT, perspective());
 
   send_control_stream_->SendMaxPushIdFrame(max_push_id_.value());
+  http3_max_push_id_sent_ = true;
 }
 
 void QuicSpdySession::EnableServerPush() {
diff --git a/quic/core/http/quic_spdy_session.h b/quic/core/http/quic_spdy_session.h
index 069cceb..013bfc3 100644
--- a/quic/core/http/quic_spdy_session.h
+++ b/quic/core/http/quic_spdy_session.h
@@ -466,8 +466,11 @@
   void CloseConnectionOnDuplicateHttp3UnidirectionalStreams(
       quiche::QuicheStringPiece type);
 
-  // Sends any data which should be sent at the start of a connection,
-  // including the initial SETTINGS frame, etc.
+  // Sends any data which should be sent at the start of a connection, including
+  // the initial SETTINGS frame, and (when IETF QUIC is used) also a MAX_PUSH_ID
+  // frame if SetMaxPushId() had been called before encryption was established.
+  // When using 0-RTT, this method is called twice: once when encryption is
+  // established, and again when 1-RTT keys are available.
   void SendInitialData();
 
   // Send a MAX_PUSH_ID frame.  Used in IETF QUIC only.
@@ -534,12 +537,17 @@
   // Server push is enabled for a client by calling SetMaxPushId().
   bool ietf_server_push_enabled_;
 
-  // Used in IETF QUIC only.  Unset until a MAX_PUSH_ID frame is received/sent.
-  // For a server, the push ID in the most recently received MAX_PUSH_ID frame.
-  // For a client before 1-RTT keys are available, the push ID to be sent in the
-  // initial MAX_PUSH_ID frame.
-  // For a client after 1-RTT keys are available, the push ID in the most
-  // recently sent MAX_PUSH_ID frame.
+  // Used in IETF QUIC only.
+  // For a server:
+  //   the push ID in the most recently received MAX_PUSH_ID frame,
+  //   or unset if no MAX_PUSH_ID frame has been received.
+  // For a client:
+  //   unset until SetMaxPushId() is called;
+  //   before encryption is established, the push ID to be sent in the initial
+  //   MAX_PUSH_ID frame;
+  //   after encryption is established, the push ID in the most recently sent
+  //   MAX_PUSH_ID frame.
+  // Once set, never goes back to unset.
   quiche::QuicheOptional<PushId> max_push_id_;
 
   // An integer used for live check. The indicator is assigned a value in
@@ -555,9 +563,9 @@
   // If the endpoint has sent HTTP/3 GOAWAY frame.
   bool http3_goaway_sent_;
 
-  // If SendMaxPushId() has been called from SendInitialData().  Note that a
-  // MAX_PUSH_ID frame is only sent if SetMaxPushId() had been called
-  // beforehand.
+  // Only used by a client, only with IETF QUIC.  True if a MAX_PUSH_ID frame
+  // has been sent, in which case |max_push_id_| has the value sent in the most
+  // recent MAX_PUSH_ID frame.  Once true, never goes back to false.
   bool http3_max_push_id_sent_;
 
   // Priority values received in PRIORITY_UPDATE frames for streams that are not