Skip to content

Commit

Permalink
Fixes MAISTRA-1299: fix ASSERT failure and infinite loop when attempt…
Browse files Browse the repository at this point in the history
…ing to unset readDisable state on a closed connection (maistra#9)
  • Loading branch information
Dmitri Dolguikh authored Mar 25, 2020
1 parent 09d777a commit a939c3c
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 8 deletions.
2 changes: 1 addition & 1 deletion source/common/http/http1/codec_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -894,7 +894,7 @@ void ClientConnectionImpl::onMessageComplete() {
// reused, unwind any outstanding readDisable() calls here. Only do this if there are no
// pipelined responses remaining. Also do this before we dispatch end_stream in case the caller
// immediately reuses the connection.
if (pending_responses_.empty()) {
if (connection_.state() == Network::Connection::State::Open && pending_responses_.empty()) {
while (!connection_.readEnabled()) {
connection_.readDisable(false);
}
Expand Down
22 changes: 20 additions & 2 deletions source/common/network/connection_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -292,12 +292,14 @@ void ConnectionImpl::enableHalfClose(bool enabled) {

void ConnectionImpl::readDisable(bool disable) {
ASSERT(state() == State::Open);
ASSERT(file_event_ != nullptr);
if (state() != State::Open || file_event_ == nullptr) {
// If readDisable is called on a closed connection in error, do not crash.
return;
}

ENVOY_CONN_LOG(trace, "readDisable: enabled={} disable={}", *this, read_enabled_, disable);
ENVOY_CONN_LOG(trace, "readDisable: enabled={} disable={} state={}", *this, read_enabled_,
disable, static_cast<int>(state()));

// When we disable reads, we still allow for early close notifications (the equivalent of
// EPOLLRDHUP for an epoll backend). For backends that support it, this allows us to apply
Expand All @@ -314,6 +316,11 @@ void ConnectionImpl::readDisable(bool disable) {
ASSERT(read_enabled_);
read_enabled_ = false;

if (state() != State::Open || file_event_ == nullptr) {
// If readDisable is called on a closed connection, do not crash.
return;
}

// If half-close semantics are enabled, we never want early close notifications; we
// always want to read all available data, even if the other side has closed.
if (detect_early_close_ && !enable_half_close_) {
Expand All @@ -328,6 +335,12 @@ void ConnectionImpl::readDisable(bool disable) {
}
ASSERT(!read_enabled_);
read_enabled_ = true;

if (state() != State::Open || file_event_ == nullptr) {
// If readDisable is called on a closed connection, do not crash.
return;
}

// We never ask for both early close and read at the same time. If we are reading, we want to
// consume all available data.
file_event_->setEnabled(Event::FileReadyType::Read | Event::FileReadyType::Write);
Expand Down Expand Up @@ -358,7 +371,12 @@ void ConnectionImpl::raiseEvent(ConnectionEvent event) {
}
}

bool ConnectionImpl::readEnabled() const { return read_enabled_; }
bool ConnectionImpl::readEnabled() const {
// Calls to readEnabled on a closed socket are considered to be an error.
ASSERT(state() == State::Open);
ASSERT(file_event_ != nullptr);
return read_enabled_;
}

void ConnectionImpl::addConnectionCallbacks(ConnectionCallbacks& cb) { callbacks_.push_back(&cb); }

Expand Down
33 changes: 28 additions & 5 deletions test/common/network/connection_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -570,15 +570,38 @@ TEST_P(ConnectionImplTest, KickUndone) {
disconnect(true);
}

// Regression test for (at least one failure mode of)
// https://github.com/envoyproxy/envoy/issues/3639 where readDisable on a close
// connection caused a crash.
TEST_P(ConnectionImplTest, ReadDisableAfterClose) {
// Ensure that calls to readDisable on a closed connection are handled gracefully. Known past issues
// include a crash on https://github.com/envoyproxy/envoy/issues/3639, and ASSERT failure followed
// by infinite loop in https://github.com/envoyproxy/envoy/issues/9508
TEST_P(ConnectionImplTest, ReadDisableAfterCloseHandledGracefully) {
setUpBasicConnection();
disconnect(false);

client_connection_->readDisable(true);
client_connection_->readDisable(false);

client_connection_->readDisable(true);
client_connection_->readDisable(true);
client_connection_->readDisable(false);
client_connection_->readDisable(false);

client_connection_->readDisable(true);
client_connection_->readDisable(true);
disconnect(false);
#ifndef NDEBUG
// When running in debug mode, verify that calls to readDisable and readEnabled on a closed socket
// trigger ASSERT failures.
EXPECT_DEBUG_DEATH(client_connection_->readEnabled(), "");
EXPECT_DEBUG_DEATH(client_connection_->readDisable(true), "");
EXPECT_DEBUG_DEATH(client_connection_->readDisable(false), "");
#else
// When running in release mode, verify that calls to readDisable change the readEnabled state.
client_connection_->readDisable(false);
client_connection_->readDisable(true);
client_connection_->readDisable(false);
EXPECT_FALSE(client_connection_->readEnabled());
client_connection_->readDisable(false);
EXPECT_TRUE(client_connection_->readEnabled());
#endif
}

TEST_P(ConnectionImplTest, EarlyCloseOnReadDisabledConnection) {
Expand Down
25 changes: 25 additions & 0 deletions test/integration/integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,31 @@ TEST_P(IntegrationTest, RouterUpstreamDisconnectBeforeResponseComplete) {
testRouterUpstreamDisconnectBeforeResponseComplete();
}

// Regression test for https://github.com/envoyproxy/envoy/issues/9508
TEST_P(IntegrationTest, ResponseFramedByConnectionCloseWithReadLimits) {
// Set a small buffer limit on the downstream in order to trigger a call to trigger readDisable on
// the upstream when proxying the response. Upstream limit needs to be larger so that
// RawBufferSocket::doRead reads the response body and detects the upstream close in the same call
// stack.
config_helper_.setBufferLimits(100000, 1);
initialize();

codec_client_ = makeHttpConnection(makeClientConnection((lookupPort("http"))));
fake_upstreams_[0]->set_allow_unexpected_disconnects(true);

auto response = codec_client_->makeHeaderOnlyRequest(default_request_headers_);
waitForNextUpstreamRequest();
// Disable chunk encoding to trigger framing by connection close.
upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "200"}, {":no-chunks", "1"}},
false);
upstream_request_->encodeData(512, true);
ASSERT_TRUE(fake_upstream_connection_->close());

response->waitForEndStream();

EXPECT_TRUE(response->complete());
}

TEST_P(IntegrationTest, RouterDownstreamDisconnectBeforeRequestComplete) {
testRouterDownstreamDisconnectBeforeRequestComplete();
}
Expand Down

0 comments on commit a939c3c

Please sign in to comment.