Skip to content

Commit

Permalink
hcm: avoid invoking 100-continue handling on decode filter. (#10929)
Browse files Browse the repository at this point in the history
The 100-continue state tracking variables were checking in
commonContinue() (on both decode/encode paths), conditioning
do100ContinueHeaders(). This makes no sense on the decode path, and can
lead to crashes as per #10923 when the decode pipeline is resumed, so
refactored the logic out to just the encode path.

Risk level: Low
Testing: Unit and integration regression tests added, as well as corpus
  entry.

Fixes oss-fuzz issue
https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=18461

Fixes #10923

Signed-off-by: Harvey Tuch <htuch@google.com>
  • Loading branch information
htuch authored Apr 24, 2020
1 parent 6516c88 commit b9ca1fa
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 1 deletion.
2 changes: 1 addition & 1 deletion source/common/http/conn_manager_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2067,7 +2067,7 @@ void ConnectionManagerImpl::ActiveStreamFilterBase::commonContinue() {
allowIteration();

// Only resume with do100ContinueHeaders() if we've actually seen a 100-Continue.
if (parent_.state_.has_continue_headers_ && !continue_headers_continued_) {
if (has100Continueheaders()) {
continue_headers_continued_ = true;
do100ContinueHeaders();
// If the response headers have not yet come in, don't continue on with
Expand Down
5 changes: 5 additions & 0 deletions source/common/http/conn_manager_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ class ConnectionManagerImpl : Logger::Loggable<Logger::Id::http>,
virtual Buffer::WatermarkBufferPtr createBuffer() PURE;
virtual Buffer::WatermarkBufferPtr& bufferedData() PURE;
virtual bool complete() PURE;
virtual bool has100Continueheaders() PURE;
virtual void do100ContinueHeaders() PURE;
virtual void doHeaders(bool end_stream) PURE;
virtual void doData(bool end_stream) PURE;
Expand Down Expand Up @@ -237,6 +238,7 @@ class ConnectionManagerImpl : Logger::Loggable<Logger::Id::http>,
Buffer::WatermarkBufferPtr createBuffer() override;
Buffer::WatermarkBufferPtr& bufferedData() override { return parent_.buffered_request_data_; }
bool complete() override { return parent_.state_.remote_complete_; }
bool has100Continueheaders() override { return false; }
void do100ContinueHeaders() override { NOT_REACHED_GCOVR_EXCL_LINE; }
void doHeaders(bool end_stream) override {
parent_.decodeHeaders(this, *parent_.request_headers_, end_stream);
Expand Down Expand Up @@ -349,6 +351,9 @@ class ConnectionManagerImpl : Logger::Loggable<Logger::Id::http>,
Buffer::WatermarkBufferPtr createBuffer() override;
Buffer::WatermarkBufferPtr& bufferedData() override { return parent_.buffered_response_data_; }
bool complete() override { return parent_.state_.local_complete_; }
bool has100Continueheaders() override {
return parent_.state_.has_continue_headers_ && !continue_headers_continued_;
}
void do100ContinueHeaders() override {
parent_.encode100ContinueHeaders(this, *parent_.continue_headers_);
}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

65 changes: 65 additions & 0 deletions test/common/http/conn_manager_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,71 @@ TEST_F(HttpConnectionManagerImplTest, PauseResume100Continue) {
decoder_filters_[1]->callbacks_->encodeHeaders(std::move(response_headers), false);
}

// Regression test for https://github.com/envoyproxy/envoy/issues/10923.
TEST_F(HttpConnectionManagerImplTest, 100ContinueResponseWithDecoderPause) {
proxy_100_continue_ = true;
setup(false, "envoy-custom-server", false);

std::shared_ptr<MockStreamDecoderFilter> filter(new NiceMock<MockStreamDecoderFilter>());

// Allow headers to pass.
EXPECT_CALL(*filter, decodeHeaders(_, false))
.WillRepeatedly(
InvokeWithoutArgs([]() -> FilterHeadersStatus { return FilterHeadersStatus::Continue; }));
// Pause and then resume the decode pipeline, this is key to triggering #10923.
EXPECT_CALL(*filter, decodeData(_, false)).WillOnce(InvokeWithoutArgs([]() -> FilterDataStatus {
return FilterDataStatus::StopIterationAndBuffer;
}));
EXPECT_CALL(*filter, decodeData(_, true))
.WillRepeatedly(
InvokeWithoutArgs([]() -> FilterDataStatus { return FilterDataStatus::Continue; }));

EXPECT_CALL(*filter, setDecoderFilterCallbacks(_));

EXPECT_CALL(filter_factory_, createFilterChain(_))
.WillRepeatedly(Invoke([&](FilterChainFactoryCallbacks& callbacks) -> void {
callbacks.addStreamDecoderFilter(filter);
}));

EXPECT_CALL(filter_callbacks_.connection_.dispatcher_, deferredDelete_(_));

RequestDecoder* decoder = nullptr;
NiceMock<MockResponseEncoder> encoder;
EXPECT_CALL(*codec_, dispatch(_)).WillRepeatedly(Invoke([&](Buffer::Instance& data) -> void {
decoder = &conn_manager_->newStream(encoder);

// Test not charging stats on the second call.
RequestHeaderMapPtr headers{
new TestRequestHeaderMapImpl{{":authority", "host"}, {":path", "/"}, {":method", "GET"}}};
decoder->decodeHeaders(std::move(headers), false);
// Allow the decode pipeline to pause.
decoder->decodeData(data, false);

ResponseHeaderMapPtr continue_headers{new TestResponseHeaderMapImpl{{":status", "100"}}};
filter->callbacks_->encode100ContinueHeaders(std::move(continue_headers));

// Resume decode pipeline after encoding 100 continue headers, we're now ready to trigger
// #10923.
decoder->decodeData(data, true);

ResponseHeaderMapPtr response_headers{new TestResponseHeaderMapImpl{{":status", "200"}}};
filter->callbacks_->encodeHeaders(std::move(response_headers), true);

data.drain(4);
}));

// Kick off the incoming data.
Buffer::OwnedImpl fake_input("1234");
conn_manager_->onData(fake_input, false);

EXPECT_EQ(1U, stats_.named_.downstream_rq_1xx_.value());
EXPECT_EQ(1U, listener_stats_.downstream_rq_1xx_.value());
EXPECT_EQ(1U, stats_.named_.downstream_rq_2xx_.value());
EXPECT_EQ(1U, listener_stats_.downstream_rq_2xx_.value());
EXPECT_EQ(2U, stats_.named_.downstream_rq_completed_.value());
EXPECT_EQ(2U, listener_stats_.downstream_rq_completed_.value());
}

// By default, Envoy will set the server header to the server name, here "custom-value"
TEST_F(HttpConnectionManagerImplTest, ServerHeaderOverwritten) {
setup(false, "custom-value", false);
Expand Down
1 change: 1 addition & 0 deletions test/integration/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,7 @@ envoy_cc_test(
"//test/integration/filters:clear_route_cache_filter_lib",
"//test/integration/filters:encoder_decoder_buffer_filter_lib",
"//test/integration/filters:process_context_lib",
"//test/integration/filters:stop_iteration_and_continue",
"//test/mocks/http:http_mocks",
"//test/test_common:utility_lib",
"@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto",
Expand Down
10 changes: 10 additions & 0 deletions test/integration/integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,16 @@ TEST_P(IntegrationTest, EnvoyProxyingLate100ContinueWithEncoderFilter) {
testEnvoyProxying100Continue(false, true);
}

// Regression test for https://github.com/envoyproxy/envoy/issues/10923.
TEST_P(IntegrationTest, EnvoyProxying100ContinueWithDecodeDataPause) {
config_helper_.addFilter(R"EOF(
name: stop-iteration-and-continue-filter
typed_config:
"@type": type.googleapis.com/google.protobuf.Empty
)EOF");
testEnvoyProxying100Continue(true);
}

// This is a regression for https://github.com/envoyproxy/envoy/issues/2715 and validates that a
// pending request is not sent on a connection that has been half-closed.
TEST_P(IntegrationTest, UpstreamDisconnectWithTwoRequests) {
Expand Down

0 comments on commit b9ca1fa

Please sign in to comment.