Enable envoy.reloadable_features.http2_reject_frames_after_end_stream and remove the http2_reject_half_closed_remote flag. The fix for b/514675798 exists in two layers: * `OgHttp2Session` (cl/934350045): a `HEADERS` or `DATA` frame on a half-closed (remote) stream is a stream error, so oghttp2 sends `RST_STREAM(STREAM_CLOSED)` (RFC 9113 Section 5.1). This was guarded by `quiche_reloadable_flag_http2_reject_half_closed_remote` and stripped from the QUICHE export. * Envoy `Http::Http2::ConnectionImpl` (upstream PR #47507): the same frames are treated as a connection-level protocol error. This is guarded by `envoy.reloadable_features.http2_reject_frames_after_end_stream`. The upstream Envoy tests assumed the second behavior on both codecs. With the oghttp2 check in place they failed on oghttp2, which is why the runtime guard was disabled in google3. This CL: * Makes the `OgHttp2Session` check unconditional and removes the QUICHE flag, code counts and `quiche:google3` markers so it is exported to OSS QUICHE. oghttp2 users outside Envoy keep the protection, and the `HalfClosedRemote*` tests in `oghttp2_session_test.cc` are kept. * Moves the `gfe2_reloadable_flag_http2_reject_half_closed_remote` config to `deprecated/`. The flag has defaulted to true since 2026-08-31, so GFE behavior does not change. GFE2 code counts were zero (b/514675798#comment16). * Enables `envoy.reloadable_features.http2_reject_frames_after_end_stream` (`RUNTIME_GUARD`) and removes the Copybara transforms that disabled it and its tests. * Replaces `b514675798-reject-half-closed-remote.patch` with `b514675798-codec-aware-end-stream-tests.patch`. This patch makes the `*AfterEndStream` tests in `http2_integration_test.cc` accept either a connection-level protocol error (nghttp2) or `RST_STREAM(STREAM_CLOSED)` (oghttp2), and adds `DownstreamRequestHeadersAfterEndStream`. It covers the same scenarios as the removed `Http2HalfClosedRemoteTest`. The change is upstreamed in https://github.com/envoyproxy/envoy/pull/47816; remove the patch once that is imported. Tested: `http2_integration_test` `*AfterEndStream*` (3 runs each) with the oghttp2 check enabled, and with it disabled to simulate current OSS QUICHE; `oghttp2_session_test`. PiperOrigin-RevId: 990357353
diff --git a/quiche/http2/adapter/oghttp2_session.cc b/quiche/http2/adapter/oghttp2_session.cc index ad2f348..c2d083b 100644 --- a/quiche/http2/adapter/oghttp2_session.cc +++ b/quiche/http2/adapter/oghttp2_session.cc
@@ -1206,6 +1206,15 @@ return; } + if (iter->second.half_closed_remote) { + // RFC 9113 Section 5.1: DATA on a half-closed (remote) stream is a stream + // error of type STREAM_CLOSED. + EnqueueFrame(std::make_unique<spdy::SpdyRstStreamIR>( + stream_id, spdy::ERROR_CODE_STREAM_CLOSED)); + streams_reset_.insert(stream_id); + return; + } + if (static_cast<int64_t>(length) > connection_window_manager_.CurrentWindowSize()) { // Peer exceeded the connection flow control limit. @@ -1335,6 +1344,14 @@ spdy::SpdyStreamId stream_id) { auto it = stream_map_.find(stream_id); if (it != stream_map_.end() && !streams_reset_.contains(stream_id)) { + if (it->second.half_closed_remote) { + // RFC 9113 Section 5.1: HEADERS on a half-closed (remote) stream is a + // stream error of type STREAM_CLOSED. + EnqueueFrame(std::make_unique<spdy::SpdyRstStreamIR>( + stream_id, spdy::ERROR_CODE_STREAM_CLOSED)); + streams_reset_.insert(stream_id); + return &noop_headers_handler_; + } headers_handler_.set_stream_id(stream_id); headers_handler_.set_header_type( NextHeaderType(it->second.received_header_type));
diff --git a/quiche/http2/adapter/oghttp2_session_test.cc b/quiche/http2/adapter/oghttp2_session_test.cc index bd7798f..a0757b3 100644 --- a/quiche/http2/adapter/oghttp2_session_test.cc +++ b/quiche/http2/adapter/oghttp2_session_test.cc
@@ -1690,6 +1690,197 @@ EXPECT_EQ(settings_frame.size(), result); } +TEST(OgHttp2SessionTest, HalfClosedRemoteReceivesHeaders) { + TestVisitor visitor; + OgHttp2Session::Options options; + options.perspective = Perspective::kServer; + OgHttp2Session session(visitor, options); + + // Send server preface (initial settings). + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + EXPECT_EQ(0, session.Send()); + visitor.Clear(); + + const std::string frames = TestFrameSequence() + .ClientPreface() + .Headers(1, + {{":method", "POST"}, + {":scheme", "https"}, + {":authority", "example.com"}, + {":path", "/"}}, + /*fin=*/true) + .Headers(1, {{"header", "value"}}, + /*fin=*/true) + .Serialize(); + + testing::InSequence s; + + // Client preface + EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); + EXPECT_CALL(visitor, OnSettingsStart()); + EXPECT_CALL(visitor, OnSettingsEnd()); + + // First Headers frame + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 5)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":method", "POST")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":scheme", "https")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":authority", "example.com")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":path", "/")); + EXPECT_CALL(visitor, OnEndHeadersForStream(1)); + EXPECT_CALL(visitor, OnEndStream(1)); + + // Second Headers frame + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 5)); + + const int64_t result = session.ProcessBytes(frames); + EXPECT_EQ(frames.size(), static_cast<size_t>(result)); + + EXPECT_TRUE(session.want_write()); + + // Expect SETTINGS ACK. + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, 0, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, 0, 0x1, 0)); + + // Expect RST_STREAM due to the unexpected headers on half-closed stream. + EXPECT_CALL(visitor, OnBeforeFrameSent(RST_STREAM, 1, _, 0x0)); + EXPECT_CALL(visitor, + OnFrameSent(RST_STREAM, 1, _, 0x0, + static_cast<int>(Http2ErrorCode::STREAM_CLOSED))); + EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::HTTP2_NO_ERROR)); + + EXPECT_EQ(0, session.Send()); +} + +TEST(OgHttp2SessionTest, HalfClosedRemoteReceivesData) { + TestVisitor visitor; + OgHttp2Session::Options options; + options.perspective = Perspective::kServer; + OgHttp2Session session(visitor, options); + + // Send server preface (initial settings). + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + EXPECT_EQ(0, session.Send()); + visitor.Clear(); + + const std::string frames = TestFrameSequence() + .ClientPreface() + .Headers(1, + {{":method", "POST"}, + {":scheme", "https"}, + {":authority", "example.com"}, + {":path", "/"}}, + /*fin=*/true) + .Data(1, "unexpected data", /*fin=*/true) + .Serialize(); + + testing::InSequence s; + + // Client preface + EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); + EXPECT_CALL(visitor, OnSettingsStart()); + EXPECT_CALL(visitor, OnSettingsEnd()); + + // First Headers frame + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 5)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":method", "POST")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":scheme", "https")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":authority", "example.com")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":path", "/")); + EXPECT_CALL(visitor, OnEndHeadersForStream(1)); + EXPECT_CALL(visitor, OnEndStream(1)); + + // Data frame + EXPECT_CALL(visitor, OnFrameHeader(1, _, DATA, 1)); + + const int64_t result = session.ProcessBytes(frames); + EXPECT_EQ(frames.size(), static_cast<size_t>(result)); + + EXPECT_TRUE(session.want_write()); + + // Expect SETTINGS ACK. + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, 0, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, 0, 0x1, 0)); + + // Expect RST_STREAM due to the unexpected data on half-closed stream. + EXPECT_CALL(visitor, OnBeforeFrameSent(RST_STREAM, 1, _, 0x0)); + EXPECT_CALL(visitor, + OnFrameSent(RST_STREAM, 1, _, 0x0, + static_cast<int>(Http2ErrorCode::STREAM_CLOSED))); + EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::HTTP2_NO_ERROR)); + + EXPECT_EQ(0, session.Send()); +} + +TEST(OgHttp2SessionTest, HalfClosedRemoteReceivesAllowedFrames) { + TestVisitor visitor; + OgHttp2Session::Options options; + options.perspective = Perspective::kServer; + OgHttp2Session session(visitor, options); + + // Send server preface (initial settings). + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + EXPECT_EQ(0, session.Send()); + visitor.Clear(); + + const std::string frames = TestFrameSequence() + .ClientPreface() + .Headers(1, + {{":method", "POST"}, + {":scheme", "https"}, + {":authority", "example.com"}, + {":path", "/"}}, + /*fin=*/true) + .WindowUpdate(1, 1000) + .Priority(1, 0, 15, false) + .RstStream(1, Http2ErrorCode::CANCEL) + .Serialize(); + + testing::InSequence s; + + // Client preface + EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); + EXPECT_CALL(visitor, OnSettingsStart()); + EXPECT_CALL(visitor, OnSettingsEnd()); + + // First Headers frame + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 5)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":method", "POST")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":scheme", "https")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":authority", "example.com")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":path", "/")); + EXPECT_CALL(visitor, OnEndHeadersForStream(1)); + EXPECT_CALL(visitor, OnEndStream(1)); + + // Window Update frame + EXPECT_CALL(visitor, OnFrameHeader(1, 4, WINDOW_UPDATE, 0)); + EXPECT_CALL(visitor, OnWindowUpdate(1, 1000)); + + // Priority frame + EXPECT_CALL(visitor, OnFrameHeader(1, 5, PRIORITY, 0)); + + // RST_STREAM frame + EXPECT_CALL(visitor, OnFrameHeader(1, 4, RST_STREAM, 0)); + EXPECT_CALL(visitor, OnRstStream(1, Http2ErrorCode::CANCEL)); + EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::CANCEL)); + + const int64_t result = session.ProcessBytes(frames); + EXPECT_EQ(frames.size(), static_cast<size_t>(result)); + + EXPECT_TRUE(session.want_write()); + + // Expect SETTINGS ACK. + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, 0, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, 0, 0x1, 0)); + + EXPECT_EQ(0, session.Send()); +} + TEST(OgHttp2SessionTest, ClientReceivesTrailersWithoutEndStream) { TestVisitor visitor; OgHttp2Session::Options options;