Fixes a code defect reported in https://github.com/envoyproxy/envoy/issues/31710.
In a very specific situation, OgHttp2Session can enter an infinite loop. This requires the following conditions:
* a stream has both response body data and trailers to write;
* the stream becomes blocked by stream-level flow control while writing the body; and
* the connection still has flow control quota available.
In this circumstance, OgHttp2Session::HasReadyStream() returns true because trailers_ready_ is not empty, but WriteForStream() cannot make progress.
The fix is to remove the stream ID from trailers_ready_ in GetNextReadyStream().
Protected by not protected, does not affect the GFE binary.
PiperOrigin-RevId: 597622882
diff --git a/quiche/http2/adapter/oghttp2_adapter_test.cc b/quiche/http2/adapter/oghttp2_adapter_test.cc
index d077051..6b570e2 100644
--- a/quiche/http2/adapter/oghttp2_adapter_test.cc
+++ b/quiche/http2/adapter/oghttp2_adapter_test.cc
@@ -5157,6 +5157,7 @@
send_result = adapter->Send();
EXPECT_EQ(0, send_result);
+ EXPECT_FALSE(adapter->want_write());
} else {
// Even though the data source has not finished sending data, the library
// will write the trailers anyway.
@@ -5171,10 +5172,131 @@
send_result = adapter->Send();
EXPECT_EQ(0, send_result);
EXPECT_THAT(visitor.data(), EqualsFrames({SpdyFrameType::HEADERS}));
+ EXPECT_FALSE(adapter->want_write());
}
}
}
+// Tests the case where the response body and trailers become blocked by flow
+// control while the stream is writing. Regression test for
+// https://github.com/envoyproxy/envoy/issues/31710
+TEST(OgHttp2AdapterTest, ServerSubmitsTrailersWithFlowControlBlockage) {
+ DataSavingVisitor visitor;
+ OgHttp2Adapter::Options options;
+ options.perspective = Perspective::kServer;
+ auto adapter = OgHttp2Adapter::Create(visitor, options);
+
+ const std::string frames = TestFrameSequence()
+ .ClientPreface()
+ .Headers(1,
+ {{":method", "POST"},
+ {":scheme", "https"},
+ {":authority", "example.com"},
+ {":path", "/this/is/request/one"}},
+ /*fin=*/false)
+ .WindowUpdate(0, 2000)
+ .Serialize();
+ testing::InSequence s;
+
+ // Client preface (empty SETTINGS)
+ EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0));
+ EXPECT_CALL(visitor, OnSettingsStart());
+ EXPECT_CALL(visitor, OnSettingsEnd());
+
+ EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 4));
+ EXPECT_CALL(visitor, OnBeginHeadersForStream(1));
+ EXPECT_CALL(visitor, OnHeaderForStream(1, _, _)).Times(4);
+ EXPECT_CALL(visitor, OnEndHeadersForStream(1));
+ EXPECT_CALL(visitor, OnFrameHeader(0, 4, WINDOW_UPDATE, 0));
+ EXPECT_CALL(visitor, OnWindowUpdate(0, 2000));
+
+ const int64_t result = adapter->ProcessBytes(frames);
+ EXPECT_EQ(frames.size(), static_cast<size_t>(result));
+
+ EXPECT_TRUE(adapter->want_write());
+
+ EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0));
+ EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0));
+ EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, 0, ACK_FLAG));
+ EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, 0, ACK_FLAG, 0));
+
+ int send_result = adapter->Send();
+ EXPECT_EQ(0, send_result);
+ visitor.Clear();
+
+ EXPECT_EQ(kInitialFlowControlWindowSize, adapter->GetStreamSendWindowSize(1));
+
+ const std::string kBody(60000, 'a');
+
+ // The body source must indicate that the end of the body is not the end of
+ // the stream.
+ auto body1 = std::make_unique<TestDataFrameSource>(visitor, false);
+ body1->AppendPayload(kBody);
+ auto* body1_ptr = body1.get();
+ int submit_result = adapter->SubmitResponse(
+ 1, ToHeaders({{":status", "200"}, {"x-comment", "Sure, sounds good."}}),
+ std::move(body1));
+ EXPECT_EQ(submit_result, 0);
+ EXPECT_TRUE(adapter->want_write());
+
+ EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, 1, _, 0x4));
+ EXPECT_CALL(visitor, OnFrameSent(HEADERS, 1, _, 0x4, 0));
+ EXPECT_CALL(visitor, OnFrameSent(DATA, 1, _, 0x0, 0)).Times(4);
+
+ send_result = adapter->Send();
+ EXPECT_EQ(0, send_result);
+ EXPECT_THAT(visitor.data(),
+ EqualsFrames({SpdyFrameType::HEADERS, SpdyFrameType::DATA,
+ SpdyFrameType::DATA, SpdyFrameType::DATA,
+ SpdyFrameType::DATA}));
+ visitor.Clear();
+ EXPECT_FALSE(adapter->want_write());
+
+ body1_ptr->AppendPayload(std::string(6000, 'b'));
+ // The next response body data payload is larger than the available stream
+ // flow control window.
+ EXPECT_LT(adapter->GetStreamSendWindowSize(1), 6000);
+ // There is more than enough connection flow control window.
+ EXPECT_GT(adapter->GetSendWindowSize(), 6000);
+
+ adapter->ResumeStream(1);
+ int trailer_result =
+ adapter->SubmitTrailer(1, ToHeaders({{"final-status", "a-ok"}}));
+ ASSERT_EQ(trailer_result, 0);
+
+ EXPECT_TRUE(adapter->want_write());
+ // This will send data but not trailers, because the data source hasn't
+ // finished sending.
+ EXPECT_CALL(visitor, OnFrameSent(DATA, 1, _, 0x0, 0));
+ send_result = adapter->Send();
+ EXPECT_EQ(0, send_result);
+ EXPECT_THAT(visitor.data(), EqualsFrames({SpdyFrameType::DATA}));
+ visitor.Clear();
+
+ // Stream flow control window is exhausted.
+ EXPECT_EQ(adapter->GetStreamSendWindowSize(1), 0);
+ // Connection flow control window is available.
+ EXPECT_GT(adapter->GetSendWindowSize(), 0);
+
+ // After a window update, the adapter will send the last data, followed by
+ // trailers.
+ EXPECT_CALL(visitor, OnFrameHeader(1, 4, WINDOW_UPDATE, 0));
+ EXPECT_CALL(visitor, OnWindowUpdate(1, 2000));
+ adapter->ProcessBytes(TestFrameSequence().WindowUpdate(1, 2000).Serialize());
+
+ EXPECT_CALL(visitor, OnFrameSent(DATA, 1, _, 0x0, 0));
+ EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, 1, _,
+ END_STREAM_FLAG | END_HEADERS_FLAG));
+ EXPECT_CALL(visitor, OnFrameSent(HEADERS, 1, _,
+ END_STREAM_FLAG | END_HEADERS_FLAG, 0));
+
+ send_result = adapter->Send();
+ EXPECT_EQ(0, send_result);
+ EXPECT_THAT(visitor.data(),
+ EqualsFrames({SpdyFrameType::DATA, SpdyFrameType::HEADERS}));
+ EXPECT_FALSE(adapter->want_write());
+}
+
TEST(OgHttp2AdapterTest, ServerSubmitsTrailersWithDataEndStream) {
DataSavingVisitor visitor;
OgHttp2Adapter::Options options;
diff --git a/quiche/http2/adapter/oghttp2_session.cc b/quiche/http2/adapter/oghttp2_session.cc
index 0242f40..ad67a18 100644
--- a/quiche/http2/adapter/oghttp2_session.cc
+++ b/quiche/http2/adapter/oghttp2_session.cc
@@ -632,6 +632,7 @@
const Http2StreamId stream_id = *trailers_ready_.begin();
// WriteForStream() will re-mark the stream as ready, if necessary.
write_scheduler_.MarkStreamNotReady(stream_id);
+ trailers_ready_.erase(trailers_ready_.begin());
return stream_id;
}
return write_scheduler_.PopNextReadyStream();