Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
date: Pending

behavior_changes:
- area: response_decoder
change: |
Updated EnvoyQuicClientStream and ResponseDecoderWrapper to use a handle to access the response decoder
to prevent use-after-free errors by ensuring the decoder instance is still live before calling its methods.
This change is guarded by the runtime flag ``envoy.reloadable_features.use_response_decoder_handle``.
- area: http
change: |
A route refresh will now result in a tracing refresh. The trace sampling decision and decoration
Expand Down
24 changes: 24 additions & 0 deletions envoy/http/codec.h
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,23 @@ class ResponseEncoder : public virtual StreamEncoder {
StreamInfo::StreamInfo& stream_info) PURE;
};

class ResponseDecoder;

/**
* A handle to a ResponseDecoder. This handle can be used to check if the underlying decoder is
* still valid and to get a reference to it.
*/
class ResponseDecoderHandle {
public:
virtual ~ResponseDecoderHandle() = default;

/**
* @return a reference to the underlying decoder if it is still valid.
*/
virtual OptRef<ResponseDecoder> get() PURE;
};
using ResponseDecoderHandlePtr = std::unique_ptr<ResponseDecoderHandle>;

/**
* Decodes an HTTP stream. These are callbacks fired into a sink. This interface contains methods
* common to both the request and response path.
Expand Down Expand Up @@ -304,9 +321,16 @@ class ResponseDecoder : public virtual StreamDecoder {
* @param os the ostream to dump state to
* @param indent_level the depth, for pretty-printing.
*

* This function is called on Envoy fatal errors so should avoid memory allocation.
*/
virtual void dumpState(std::ostream& os, int indent_level = 0) const PURE;

/**
* @return A handle to the response decoder. Caller can check the response decoder's liveness via
* the handle.
*/
virtual ResponseDecoderHandlePtr createResponseDecoderHandle() PURE;
};

/**
Expand Down
13 changes: 12 additions & 1 deletion source/common/http/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,18 @@ envoy_cc_library(
envoy_cc_library(
name = "codec_wrappers_lib",
hdrs = ["codec_wrappers.h"],
deps = ["//envoy/http:codec_interface"],
deps = [
":response_decoder_impl_base",
"//envoy/http:codec_interface",
],
)

envoy_cc_library(
name = "response_decoder_impl_base",
hdrs = ["response_decoder_impl_base.h"],
deps = [
"//envoy/http:codec_interface",
],
)

envoy_cc_library(
Expand Down
10 changes: 9 additions & 1 deletion source/common/http/codec_client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,16 @@ void CodecClient::deleteRequest(ActiveRequest& request) {
}
}

RequestEncoder& CodecClient::newStream(ResponseDecoderHandlePtr response_decoder_handle) {
return enlistAndCreateEncoder(
std::make_unique<ActiveRequest>(*this, std::move(response_decoder_handle)));
}

RequestEncoder& CodecClient::newStream(ResponseDecoder& response_decoder) {
ActiveRequestPtr request(new ActiveRequest(*this, response_decoder));
return enlistAndCreateEncoder(std::make_unique<ActiveRequest>(*this, response_decoder));
}

RequestEncoder& CodecClient::enlistAndCreateEncoder(ActiveRequestPtr request) {
request->setEncoder(codec_->newStream(*request));
LinkedList::moveIntoList(std::move(request), active_requests_);

Expand Down
28 changes: 28 additions & 0 deletions source/common/http/codec_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,16 @@ class CodecClient : protected Logger::Loggable<Logger::Id::client>,
*/
RequestEncoder& newStream(ResponseDecoder& response_decoder);

/**
* Create a new stream. Note: The CodecClient will NOT buffer multiple requests for HTTP1
* connections. Thus, calling newStream() before the previous request has been fully encoded
* is an error. Pipelining is supported however.
* @param response_decoder_handle supplies the decoder to use for response callbacks if it's still
* alive.
* @return StreamEncoder& the encoder to use for encoding the request.
*/
RequestEncoder& newStream(ResponseDecoderHandlePtr response_decoder_handle);

void setConnectionStats(const Network::Connection::ConnectionStats& stats) {
connection_->setConnectionStats(stats);
}
Expand Down Expand Up @@ -248,6 +258,23 @@ class CodecClient : protected Logger::Loggable<Logger::Id::client>,
}
}

ActiveRequest(CodecClient& parent, ResponseDecoderHandlePtr inner_handle)
: ResponseDecoderWrapper(std::move(inner_handle)), RequestEncoderWrapper(nullptr),
parent_(parent), header_validator_(parent.host_->cluster().makeHeaderValidator(
parent.codec_->protocol())) {
switch (parent.protocol()) {
case Protocol::Http10:
case Protocol::Http11:
// HTTP/1.1 codec does not support half-close on the response completion.
wait_encode_complete_ = false;
break;
case Protocol::Http2:
case Protocol::Http3:
wait_encode_complete_ = true;
break;
}
}

void decodeHeaders(ResponseHeaderMapPtr&& headers, bool end_stream) override;

// StreamCallbacks
Expand Down Expand Up @@ -305,6 +332,7 @@ class CodecClient : protected Logger::Loggable<Logger::Id::client>,
void onBelowWriteBufferLowWatermark() override {
codec_->onUnderlyingConnectionBelowWriteBufferLowWatermark();
}
RequestEncoder& enlistAndCreateEncoder(ActiveRequestPtr request);

std::list<ActiveRequestPtr> active_requests_;
Http::ConnectionCallbacks* codec_callbacks_{};
Expand Down
74 changes: 65 additions & 9 deletions source/common/http/codec_wrappers.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,35 @@

#include "envoy/http/codec.h"

#include "source/common/http/response_decoder_impl_base.h"
#include "source/common/runtime/runtime_features.h"

namespace Envoy {
namespace Http {

/**
* Wrapper for ResponseDecoder that just forwards to an "inner" decoder.
*/
class ResponseDecoderWrapper : public ResponseDecoder {
class ResponseDecoderWrapper : public ResponseDecoderImplBase {
public:
// ResponseDecoder
void decode1xxHeaders(ResponseHeaderMapPtr&& headers) override {
inner_.decode1xxHeaders(std::move(headers));
if (Http::ResponseDecoder* inner = getInnerDecoder()) {
inner->decode1xxHeaders(std::move(headers));
} else {
onInnerDecoderDead();
}
}

void decodeHeaders(ResponseHeaderMapPtr&& headers, bool end_stream) override {
if (end_stream) {
onPreDecodeComplete();
}
inner_.decodeHeaders(std::move(headers), end_stream);
if (Http::ResponseDecoder* inner = getInnerDecoder()) {
inner->decodeHeaders(std::move(headers), end_stream);
} else {
onInnerDecoderDead();
}
if (end_stream) {
onDecodeComplete();
}
Expand All @@ -29,28 +40,51 @@ class ResponseDecoderWrapper : public ResponseDecoder {
if (end_stream) {
onPreDecodeComplete();
}
inner_.decodeData(data, end_stream);
if (Http::ResponseDecoder* inner = getInnerDecoder()) {
inner->decodeData(data, end_stream);
} else {
onInnerDecoderDead();
}
if (end_stream) {
onDecodeComplete();
}
}

void decodeTrailers(ResponseTrailerMapPtr&& trailers) override {
onPreDecodeComplete();
inner_.decodeTrailers(std::move(trailers));
if (Http::ResponseDecoder* inner = getInnerDecoder()) {
inner->decodeTrailers(std::move(trailers));
} else {
onInnerDecoderDead();
}
onDecodeComplete();
}

void decodeMetadata(MetadataMapPtr&& metadata_map) override {
inner_.decodeMetadata(std::move(metadata_map));
if (Http::ResponseDecoder* inner = getInnerDecoder()) {
inner->decodeMetadata(std::move(metadata_map));
} else {
onInnerDecoderDead();
}
}

void dumpState(std::ostream& os, int indent_level) const override {
inner_.dumpState(os, indent_level);
if (Http::ResponseDecoder* inner = getInnerDecoder()) {
inner->dumpState(os, indent_level);
} else {
onInnerDecoderDead();
}
}

protected:
ResponseDecoderWrapper(ResponseDecoder& inner) : inner_(inner) {}
ResponseDecoderWrapper(ResponseDecoder& inner) : inner_(&inner) {}

/**
* @param inner_handle refers a response decoder which may have already died at
* this point. Following access to the decoder will check its liveliness.
*/
ResponseDecoderWrapper(ResponseDecoderHandlePtr inner_handle)
: inner_handle_(std::move(inner_handle)) {}

/**
* Consumers of the wrapper generally want to know when a decode is complete. This is called
Expand All @@ -59,7 +93,29 @@ class ResponseDecoderWrapper : public ResponseDecoder {
virtual void onPreDecodeComplete() PURE;
virtual void onDecodeComplete() PURE;

ResponseDecoder& inner_;
ResponseDecoderHandlePtr inner_handle_;
Http::ResponseDecoder* inner_ = nullptr;

private:
Http::ResponseDecoder* getInnerDecoder() const {
if (inner_handle_ == nullptr) {
return inner_;
}
if (inner_handle_) {
if (OptRef<ResponseDecoder> inner = inner_handle_->get(); inner.has_value()) {
return &inner.value().get();
}
}
return nullptr;
}

void onInnerDecoderDead() const {
const std::string error_msg = "Wrapped decoder use after free detected.";
IS_ENVOY_BUG(error_msg);
RELEASE_ASSERT(!Runtime::runtimeFeatureEnabled(
"envoy.reloadable_features.abort_when_accessing_dead_decoder"),
error_msg);
}
};

/**
Expand Down
16 changes: 14 additions & 2 deletions source/common/http/conn_pool_base.cc
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,21 @@ void HttpConnPoolImplBase::onPoolReady(Envoy::ConnectionPool::ActiveClient& clie
Envoy::ConnectionPool::AttachContext& context) {
ActiveClient* http_client = static_cast<ActiveClient*>(&client);
auto& http_context = typedContext<HttpAttachContext>(context);
// This decoder might have already died if ConnectivityGrid is in use and TCP
// win over QUIC.
Http::ResponseDecoder& response_decoder = *http_context.decoder_;
Http::ConnectionPool::Callbacks& callbacks = *http_context.callbacks_;

// Track this request on the connection
http_client->request_count_++;

Http::RequestEncoder& new_encoder = http_client->newStreamEncoder(response_decoder);
callbacks.onPoolReady(new_encoder, client.real_host_description_,
Http::RequestEncoder* new_encoder = nullptr;
if (http_context.decoder_handle_ == nullptr) {
new_encoder = &http_client->newStreamEncoder(response_decoder);
} else {
new_encoder = &http_client->newStreamEncoder(std::move(http_context.decoder_handle_));
}
Comment thread
abeyad marked this conversation as resolved.
callbacks.onPoolReady(*new_encoder, client.real_host_description_,
http_client->codec_client_->streamInfo(),
http_client->codec_client_->protocol());
}
Expand Down Expand Up @@ -210,5 +217,10 @@ RequestEncoder& MultiplexedActiveClientBase::newStreamEncoder(ResponseDecoder& r
return codec_client_->newStream(response_decoder);
}

RequestEncoder&
MultiplexedActiveClientBase::newStreamEncoder(ResponseDecoderHandlePtr response_decoder_handle) {
return codec_client_->newStream(std::move(response_decoder_handle));
}

} // namespace Http
} // namespace Envoy
12 changes: 11 additions & 1 deletion source/common/http/conn_pool_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "source/common/conn_pool/conn_pool_base.h"
#include "source/common/http/codec_client.h"
#include "source/common/http/http_server_properties_cache_impl.h"
#include "source/common/http/response_decoder_impl_base.h"
#include "source/common/http/utility.h"

#include "absl/strings/string_view.h"
Expand All @@ -18,9 +19,15 @@ namespace Http {

struct HttpAttachContext : public Envoy::ConnectionPool::AttachContext {
HttpAttachContext(Http::ResponseDecoder* d, Http::ConnectionPool::Callbacks* c)
: decoder_(d), callbacks_(c) {}
: decoder_(d), callbacks_(c) {
if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.use_response_decoder_handle")) {
decoder_handle_ = d->createResponseDecoderHandle();
}
}

Http::ResponseDecoder* decoder_;
Http::ConnectionPool::Callbacks* callbacks_;
ResponseDecoderHandlePtr decoder_handle_;
};

// An implementation of Envoy::ConnectionPool::PendingStream for HTTP/1.1 and HTTP/2
Expand Down Expand Up @@ -139,6 +146,8 @@ class ActiveClient : public Envoy::ConnectionPool::ActiveClient {
absl::optional<Http::Protocol> protocol() const override { return codec_client_->protocol(); }
void close() override { codec_client_->close(); }
virtual Http::RequestEncoder& newStreamEncoder(Http::ResponseDecoder& response_decoder) PURE;
virtual Http::RequestEncoder&
newStreamEncoder(Http::ResponseDecoderHandlePtr response_decoder_handle) PURE;
void onEvent(Network::ConnectionEvent event) override {
// Record request metrics only for successfully connected connections that handled requests
if ((event == Network::ConnectionEvent::LocalClose ||
Expand Down Expand Up @@ -221,6 +230,7 @@ class MultiplexedActiveClientBase : public CodecClientCallbacks,
// ConnPoolImpl::ActiveClient
bool closingWithIncompleteStream() const override;
RequestEncoder& newStreamEncoder(ResponseDecoder& response_decoder) override;
RequestEncoder& newStreamEncoder(ResponseDecoderHandlePtr response_decoder_handle) override;

// CodecClientCallbacks
void onStreamDestroy() override;
Expand Down
20 changes: 19 additions & 1 deletion source/common/http/conn_pool_grid.cc
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ ConnectivityGrid::WrapperCallbacks::WrapperCallbacks(ConnectivityGrid& grid,
next_attempt_timer_(
grid_.dispatcher_.createTimer([this]() -> void { onNextAttemptTimer(); })),
stream_options_(options) {
if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.use_response_decoder_handle")) {
decoder_handle_ = decoder.createResponseDecoderHandle();
}

if (!stream_options_.can_use_http3_) {
// If alternate protocols are explicitly disabled, there must have been a failed request over
// HTTP/3 and the failure must be post-handshake. So disable HTTP/3 for this request.
Expand All @@ -70,7 +74,21 @@ ConnectivityGrid::WrapperCallbacks::ConnectionAttemptCallbacks::~ConnectionAttem
ConnectivityGrid::StreamCreationResult
ConnectivityGrid::WrapperCallbacks::ConnectionAttemptCallbacks::newStream() {
ASSERT(!parent_.grid_.isPoolHttp3(pool()) || parent_.stream_options_.can_use_http3_);
auto* cancellable = pool().newStream(parent_.decoder_, *this, parent_.stream_options_);
Http::ResponseDecoder& decoder = parent_.decoder_;
if (parent_.decoder_handle_ != nullptr) {
if (OptRef<ResponseDecoder> opt_ref = parent_.decoder_handle_->get(); opt_ref.has_value()) {
decoder = opt_ref.value().get();
} else {
const std::string error_msg = "parent_.decoder_ use after free detected.";
IS_ENVOY_BUG(error_msg);
RELEASE_ASSERT(!Runtime::runtimeFeatureEnabled(
"envoy.reloadable_features.abort_when_accessing_dead_decoder"),
error_msg);
return StreamCreationResult::ImmediateResult;
}
}

auto* cancellable = pool().newStream(decoder, *this, parent_.stream_options_);
if (cancellable == nullptr) {
return StreamCreationResult::ImmediateResult;
}
Expand Down
2 changes: 2 additions & 0 deletions source/common/http/conn_pool_grid.h
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ class ConnectivityGrid : public ConnectionPool::Instance,
ConnectivityGrid& grid_;
// The decoder for the original newStream, needed to create streams on subsequent pools.
Http::ResponseDecoder& decoder_;
Http::ResponseDecoderHandlePtr decoder_handle_;

// The callbacks from the original caller, which must get onPoolFailure or
// onPoolReady unless there is call to cancel(). Will be nullptr if the caller
// has been notified while attempts are still pending.
Expand Down
Loading
Loading