Close the connection with error QUIC_HTTP_INVALID_FRAME_SEQUENCE_ON_SPDY_STREAM if FIN is received before finishing receiving the whole HTTP headers. Protected by FLAGS_quic_reloadable_flag_quic_fin_before_completed_http_headers. PiperOrigin-RevId: 698480775
diff --git a/quiche/common/quiche_feature_flags_list.h b/quiche/common/quiche_feature_flags_list.h index 23c44df..f3df34d 100755 --- a/quiche/common/quiche_feature_flags_list.h +++ b/quiche/common/quiche_feature_flags_list.h
@@ -36,6 +36,7 @@ QUICHE_FLAG(bool, quiche_reloadable_flag_quic_enable_new_chaos_protector, false, false, "If true, use new refactored QuicChaosProtector implementation.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_enable_server_on_wire_ping, true, true, "If true, enable server retransmittable on wire PING.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_enable_version_rfcv2, false, false, "When true, support RFC9369.") +QUICHE_FLAG(bool, quiche_reloadable_flag_quic_fin_before_completed_http_headers, false, true, "If true, close the connection with error if FIN is received before finish receiving the whole HTTP headers.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_fix_timeouts, true, true, "If true, postpone setting handshake timeout to infinite to handshake complete.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_gfe_allow_alps_new_codepoint, true, true, "If true, allow quic to use new ALPS codepoint to negotiate during handshake for H3 if client sends new ALPS codepoint.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_ignore_gquic_probing, true, true, "If true, QUIC server will not respond to gQUIC probing packet(PING + PADDING) but treat it as a regular packet.")
diff --git a/quiche/quic/core/http/end_to_end_test.cc b/quiche/quic/core/http/end_to_end_test.cc index 2ee4d33..a2043dd 100644 --- a/quiche/quic/core/http/end_to_end_test.cc +++ b/quiche/quic/core/http/end_to_end_test.cc
@@ -8141,6 +8141,32 @@ EXPECT_EQ("", client_->SendSynchronousRequest("/foo")); EXPECT_THAT(client_->connection_error(), IsError(QUIC_PUBLIC_RESET)); } + +TEST_P(EndToEndTest, EmptyResponseWithFin) { + ASSERT_TRUE(Initialize()); + if (!version_.HasIetfQuicFrames()) { + return; + } + memory_cache_backend_.AddSpecialResponse( + server_hostname_, "/empty_response_with_fin", + QuicBackendResponse::EMPTY_PAYLOAD_WITH_FIN); + + quiche::HttpHeaderBlock headers; + headers[":scheme"] = "https"; + headers[":authority"] = server_hostname_; + headers[":method"] = "GET"; + headers[":path"] = "/empty_response_with_fin"; + client_->SendMessage(headers, "", /*fin=*/true); + client_->WaitForResponseForMs(100); + if (GetQuicReloadableFlag(quic_fin_before_completed_http_headers)) { + EXPECT_THAT(client_->connection_error(), + IsError(QUIC_HTTP_INVALID_FRAME_SEQUENCE_ON_SPDY_STREAM)); + } else { + EXPECT_FALSE(client_->response_headers_complete()); + EXPECT_FALSE(client_->response_complete()); + } +} + } // namespace } // namespace test } // namespace quic
diff --git a/quiche/quic/core/http/quic_spdy_stream.cc b/quiche/quic/core/http/quic_spdy_stream.cc index 2e649ab..ee9be7b 100644 --- a/quiche/quic/core/http/quic_spdy_stream.cc +++ b/quiche/quic/core/http/quic_spdy_stream.cc
@@ -822,9 +822,6 @@ if (!VersionUsesHttp3(transport_version())) { // Sequencer must be blocked until headers are consumed. QUICHE_DCHECK(FinishedReadingHeaders()); - } - - if (!VersionUsesHttp3(transport_version())) { HandleBodyAvailable(); return; } @@ -880,6 +877,17 @@ } } + if (GetQuicReloadableFlag(quic_fin_before_completed_http_headers)) { + if (sequencer()->IsClosed() && !headers_decompressed_) { + QUIC_RELOADABLE_FLAG_COUNT_N(quic_fin_before_completed_http_headers, 1, + 2); + OnUnrecoverableError( + QUIC_HTTP_INVALID_FRAME_SEQUENCE_ON_SPDY_STREAM, + "Received FIN before finishing receiving HTTP headers."); + return; + } + QUIC_RELOADABLE_FLAG_COUNT_N(quic_fin_before_completed_http_headers, 2, 2); + } // Do not call HandleBodyAvailable() until headers are consumed. if (!FinishedReadingHeaders()) { return;
diff --git a/quiche/quic/core/http/quic_spdy_stream_test.cc b/quiche/quic/core/http/quic_spdy_stream_test.cc index 1ca8721..05ca93e 100644 --- a/quiche/quic/core/http/quic_spdy_stream_test.cc +++ b/quiche/quic/core/http/quic_spdy_stream_test.cc
@@ -2142,6 +2142,46 @@ EXPECT_FALSE(stream_->HasBytesToRead()); } +TEST_P(QuicSpdyStreamTest, IncompleteHeadersWithFin) { + SetQuicReloadableFlag(quic_fin_before_completed_http_headers, true); + if (!UsesHttp3()) { + return; + } + + Initialize(!kShouldProcessData); + + std::string headers = HeadersFrame({std::make_pair("foo", "bar")}); + std::string partial_headers = headers.substr(0, headers.length() - 2); + EXPECT_FALSE(partial_headers.empty()); + // Receive the first three bytes of the headers frame with FIN. + QuicStreamFrame frame(stream_->id(), true, 0, partial_headers); + EXPECT_CALL( + *connection_, + CloseConnection( + QUIC_HTTP_INVALID_FRAME_SEQUENCE_ON_SPDY_STREAM, + MatchesRegex("Received FIN before finishing receiving HTTP headers."), + ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET)); + stream_->OnStreamFrame(frame); +} + +TEST_P(QuicSpdyStreamTest, EmptyStreamFrameWithFin) { + SetQuicReloadableFlag(quic_fin_before_completed_http_headers, true); + if (!UsesHttp3()) { + return; + } + Initialize(!kShouldProcessData); + + // Receive the first three bytes of the headers frame with FIN. + QuicStreamFrame frame(stream_->id(), true, 0, 0); + EXPECT_CALL( + *connection_, + CloseConnection( + QUIC_HTTP_INVALID_FRAME_SEQUENCE_ON_SPDY_STREAM, + MatchesRegex("Received FIN before finishing receiving HTTP headers."), + ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET)); + stream_->OnStreamFrame(frame); +} + // The test stream will receive a stream frame containing malformed headers and // normal body. Make sure the http decoder stops processing body after the // connection shuts down.
diff --git a/quiche/quic/tools/quic_backend_response.h b/quiche/quic/tools/quic_backend_response.h index 5564a6d..55de71d 100644 --- a/quiche/quic/tools/quic_backend_response.h +++ b/quiche/quic/tools/quic_backend_response.h
@@ -25,8 +25,9 @@ INCOMPLETE_RESPONSE, // The server will act as if there is a non-empty // trailer but it will not be sent, as a result, FIN // will not be sent too. - GENERATE_BYTES // Sends a response with a length equal to the number + GENERATE_BYTES, // Sends a response with a length equal to the number // of bytes in the URL path. + EMPTY_PAYLOAD_WITH_FIN, // Sends an empty STREAM_FRAME with FIN. }; QuicBackendResponse();
diff --git a/quiche/quic/tools/quic_simple_server_stream.cc b/quiche/quic/tools/quic_simple_server_stream.cc index a805b47..0c53857 100644 --- a/quiche/quic/tools/quic_simple_server_stream.cc +++ b/quiche/quic/tools/quic_simple_server_stream.cc
@@ -303,6 +303,15 @@ return; } + if (response->response_type() == + QuicBackendResponse::EMPTY_PAYLOAD_WITH_FIN) { + // Send an empty payload with FIN without any response headers or body. + absl::InlinedVector<quiche::QuicheMemSlice, 4> quic_slices; + absl::Span<quiche::QuicheMemSlice> span(quic_slices); + WriteMemSlices(std::move(span), true); + return; + } + // Examing response status, if it was not pure integer as typical h2 // response status, send error response. Notice that // QuicHttpResponseCache push urls are strictly authority + path only,
diff --git a/quiche/quic/tools/quic_simple_server_stream_test.cc b/quiche/quic/tools/quic_simple_server_stream_test.cc index dab99dd..e1d5856 100644 --- a/quiche/quic/tools/quic_simple_server_stream_test.cc +++ b/quiche/quic/tools/quic_simple_server_stream_test.cc
@@ -740,6 +740,12 @@ absl::string_view data(arr, ABSL_ARRAYSIZE(arr)); QuicStreamFrame frame(stream_->id(), true, 0, data); // Verify that we don't crash when we get a invalid headers in stream frame. + if (GetQuicReloadableFlag(quic_fin_before_completed_http_headers) && + UsesHttp3()) { + EXPECT_CALL( + *connection_, + CloseConnection(QUIC_HTTP_INVALID_FRAME_SEQUENCE_ON_SPDY_STREAM, _, _)); + } stream_->OnStreamFrame(frame); }