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