Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http: Fix CVE CVE-2023-44487 #30055

Merged
merged 2 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
16 changes: 16 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ behavior_changes:
(UHV) on and off.
The default value is off. This option is currently functional only when the ``ENVOY_ENABLE_UHV`` build flag is enabled.
See https://github.com/envoyproxy/envoy/issues/10646 for more information about UHV.
- area: http
change: |
Add runtime flag ``http.max_requests_per_io_cycle`` for setting the limit on the number of HTTP requests processed
from a single connection in a single I/O cycle. Requests over this limit are processed in subsequent I/O cycles. This
mitigates CPU starvation by connections that simultaneously send high number of requests by allowing requests from other
connections to make progress. This runtime value can be set to 1 in the presence of abusive HTTP/2 or HTTP/3 connections.
By default this limit is disabled.

minor_behavior_changes:
# *Changes that may cause incompatibilities for some users, but should not for most*
Expand Down Expand Up @@ -160,6 +167,15 @@ bug_fixes:
- area: redis
change: |
Fixed a bug where redis key with ``%`` in the key is failing with a validation error.
- area: http
change: |
Close HTTP/2 and HTTP/3 connections that prematurely reset streams. The runtime key
``overload.premature_reset_min_stream_lifetime_seconds`` determines the interval where received stream
reset is considered premature (with 1 second default). The runtime key ``overload.premature_reset_total_stream_count``,
with the default value of 500, determines the number of requests received from a connection before the check for premature
resets is applied. The connection is disconnected if more than 50% of resets are premature.
Setting the runtime key ``envoy.restart_features.send_goaway_for_premature_rst_streams`` to ``false`` completely disables
this check.

removed_config_or_runtime:
# *Normally occurs at the end of the* :ref:`deprecation period <deprecated>`
Expand Down
1 change: 1 addition & 0 deletions source/common/http/conn_manager_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ namespace Http {
COUNTER(downstream_rq_rejected_via_ip_detection) \
COUNTER(downstream_rq_response_before_rq_complete) \
COUNTER(downstream_rq_rx_reset) \
COUNTER(downstream_rq_too_many_premature_resets) \
COUNTER(downstream_rq_timeout) \
COUNTER(downstream_rq_header_timeout) \
COUNTER(downstream_rq_too_large) \
Expand Down
154 changes: 150 additions & 4 deletions source/common/http/conn_manager_impl.cc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "source/common/http/conn_manager_impl.h"

#include <chrono>
#include <cstdint>
#include <functional>
#include <list>
Expand Down Expand Up @@ -55,6 +56,15 @@
namespace Envoy {
namespace Http {

const absl::string_view ConnectionManagerImpl::PrematureResetTotalStreamCountKey =
"overload.premature_reset_total_stream_count";
const absl::string_view ConnectionManagerImpl::PrematureResetMinStreamLifetimeSecondsKey =
"overload.premature_reset_min_stream_lifetime_seconds";
// Runtime key for maximum number of requests that can be processed from a single connection per
// I/O cycle. Requests over this limit are deferred until the next I/O cycle.
const absl::string_view ConnectionManagerImpl::MaxRequestsPerIoCycle =
"http.max_requests_per_io_cycle";

bool requestWasConnect(const RequestHeaderMapSharedPtr& headers, Protocol protocol) {
if (!headers) {
return false;
Expand Down Expand Up @@ -110,6 +120,8 @@ ConnectionManagerImpl::ConnectionManagerImpl(ConnectionManagerConfig& config,
/*node_id=*/local_info_.node().id(),
/*server_name=*/config_.serverName(),
/*proxy_status_config=*/config_.proxyStatusConfig())),
max_requests_during_dispatch_(
runtime_.snapshot().getInteger(ConnectionManagerImpl::MaxRequestsPerIoCycle, UINT32_MAX)),
refresh_rtt_after_request_(
Runtime::runtimeFeatureEnabled("envoy.reloadable_features.refresh_rtt_after_request")) {
ENVOY_LOG_ONCE_IF(
Expand All @@ -127,6 +139,10 @@ const ResponseHeaderMap& ConnectionManagerImpl::continueHeader() {
void ConnectionManagerImpl::initializeReadFilterCallbacks(Network::ReadFilterCallbacks& callbacks) {
read_callbacks_ = &callbacks;
dispatcher_ = &callbacks.connection().dispatcher();
if (max_requests_during_dispatch_ != UINT32_MAX) {
deferred_request_processing_callback_ =
dispatcher_->createSchedulableCallback([this]() -> void { onDeferredRequestProcessing(); });
}

stats_.named_.downstream_cx_total_.inc();
stats_.named_.downstream_cx_active_.inc();
Expand Down Expand Up @@ -273,6 +289,12 @@ void ConnectionManagerImpl::doEndStream(ActiveStream& stream, bool check_for_def
}

void ConnectionManagerImpl::doDeferredStreamDestroy(ActiveStream& stream) {
if (!stream.state_.is_internally_destroyed_) {
++closed_non_internally_destroyed_requests_;
if (isPrematureRstStream(stream)) {
++number_premature_stream_resets_;
}
}
if (stream.max_stream_duration_timer_ != nullptr) {
stream.max_stream_duration_timer_->disableTimer();
stream.max_stream_duration_timer_ = nullptr;
Expand Down Expand Up @@ -349,6 +371,7 @@ void ConnectionManagerImpl::doDeferredStreamDestroy(ActiveStream& stream) {
if (connection_idle_timer_ && streams_.empty()) {
connection_idle_timer_->enableTimer(config_.idleTimeout().value());
}
maybeDrainDueToPrematureResets();
}

RequestDecoderHandlePtr ConnectionManagerImpl::newStreamHandle(ResponseEncoder& response_encoder,
Expand Down Expand Up @@ -453,6 +476,7 @@ void ConnectionManagerImpl::createCodec(Buffer::Instance& data) {
}

Network::FilterStatus ConnectionManagerImpl::onData(Buffer::Instance& data, bool) {
requests_during_dispatch_count_ = 0;
if (!codec_) {
// Http3 codec should have been instantiated by now.
createCodec(data);
Expand Down Expand Up @@ -619,6 +643,58 @@ void ConnectionManagerImpl::doConnectionClose(
}
}

bool ConnectionManagerImpl::isPrematureRstStream(const ActiveStream& stream) const {
// Check if the request was prematurely reset, by comparing its lifetime to the configured
// threshold.
ASSERT(!stream.state_.is_internally_destroyed_);
absl::optional<std::chrono::nanoseconds> duration =
stream.filter_manager_.streamInfo().currentDuration();

// Check if request lifetime is longer than the premature reset threshold.
if (duration) {
const uint64_t lifetime = std::chrono::duration_cast<std::chrono::seconds>(*duration).count();
const uint64_t min_lifetime = runtime_.snapshot().getInteger(
ConnectionManagerImpl::PrematureResetMinStreamLifetimeSecondsKey, 1);
if (lifetime > min_lifetime) {
return false;
}
}

// If request has completed before configured threshold, also check if the Envoy proxied the
// response from the upstream. Requests without the response status were reset.
// TODO(RyanTheOptimist): Possibly support half_closed_local instead.
return !stream.filter_manager_.streamInfo().responseCode();
}

// Sends a GOAWAY if too many streams have been reset prematurely on this
// connection.
void ConnectionManagerImpl::maybeDrainDueToPrematureResets() {
if (!Runtime::runtimeFeatureEnabled(
"envoy.restart_features.send_goaway_for_premature_rst_streams") ||
closed_non_internally_destroyed_requests_ == 0) {
return;
}

const uint64_t limit =
runtime_.snapshot().getInteger(ConnectionManagerImpl::PrematureResetTotalStreamCountKey, 500);

if (closed_non_internally_destroyed_requests_ < limit) {
return;
}

if (static_cast<double>(number_premature_stream_resets_) /
closed_non_internally_destroyed_requests_ <
.5) {
return;
}

if (drain_state_ == DrainState::NotDraining) {
stats_.named_.downstream_rq_too_many_premature_resets_.inc();
doConnectionClose(Network::ConnectionCloseType::Abort, absl::nullopt,
"too_many_premature_resets");
}
}

void ConnectionManagerImpl::onGoAway(GoAwayErrorCode) {
// Currently we do nothing with remote go away frames. In the future we can decide to no longer
// push resources if applicable.
Expand Down Expand Up @@ -1341,7 +1417,12 @@ void ConnectionManagerImpl::ActiveStream::decodeHeaders(RequestHeaderMapSharedPt
traceRequest();
}

filter_manager_.decodeHeaders(*request_headers_, end_stream);
if (!connection_manager_.shouldDeferRequestProxyingToNextIoCycle()) {
filter_manager_.decodeHeaders(*request_headers_, end_stream);
} else {
state_.deferred_to_next_io_iteration_ = true;
state_.deferred_end_stream_ = end_stream;
}

// Reset it here for both global and overridden cases.
resetIdleTimer();
Expand Down Expand Up @@ -1408,8 +1489,15 @@ void ConnectionManagerImpl::ActiveStream::decodeData(Buffer::Instance& data, boo
connection_manager_.read_callbacks_->connection().dispatcher());
maybeEndDecode(end_stream);
filter_manager_.streamInfo().addBytesReceived(data.length());

filter_manager_.decodeData(data, end_stream);
if (!state_.deferred_to_next_io_iteration_) {
filter_manager_.decodeData(data, end_stream);
} else {
if (!deferred_data_) {
deferred_data_ = std::make_unique<Buffer::OwnedImpl>();
}
deferred_data_->move(data);
state_.deferred_end_stream_ = end_stream;
}
}

void ConnectionManagerImpl::ActiveStream::decodeTrailers(RequestTrailerMapPtr&& trailers) {
Expand All @@ -1425,7 +1513,9 @@ void ConnectionManagerImpl::ActiveStream::decodeTrailers(RequestTrailerMapPtr&&
return;
}
maybeEndDecode(true);
filter_manager_.decodeTrailers(*request_trailers_);
if (!state_.deferred_to_next_io_iteration_) {
filter_manager_.decodeTrailers(*request_trailers_);
}
}

void ConnectionManagerImpl::ActiveStream::decodeMetadata(MetadataMapPtr&& metadata_map) {
Expand Down Expand Up @@ -2138,5 +2228,61 @@ void ConnectionManagerImpl::ActiveStream::resetStream(Http::StreamResetReason, a
connection_manager_.doEndStream(*this);
}

bool ConnectionManagerImpl::ActiveStream::onDeferredRequestProcessing() {
// TODO(yanavlasov): Merge this with the filter manager continueIteration() method
if (!state_.deferred_to_next_io_iteration_) {
return false;
}
state_.deferred_to_next_io_iteration_ = false;
bool end_stream =
state_.deferred_end_stream_ && deferred_data_ == nullptr && request_trailers_ == nullptr;
filter_manager_.decodeHeaders(*request_headers_, end_stream);
if (end_stream) {
return true;
}
if (deferred_data_ != nullptr) {
end_stream = state_.deferred_end_stream_ && request_trailers_ == nullptr;
filter_manager_.decodeData(*deferred_data_, end_stream);
}
if (request_trailers_ != nullptr) {
filter_manager_.decodeTrailers(*request_trailers_);
}
return true;
}

bool ConnectionManagerImpl::shouldDeferRequestProxyingToNextIoCycle() {
// Do not defer this stream if stream deferral is disabled
if (deferred_request_processing_callback_ == nullptr) {
return false;
}
// Defer this stream if there are already deferred streams, so they are not
// processed out of order
if (deferred_request_processing_callback_->enabled()) {
return true;
}
++requests_during_dispatch_count_;
bool defer = requests_during_dispatch_count_ > max_requests_during_dispatch_;
if (defer) {
deferred_request_processing_callback_->scheduleCallbackNextIteration();
}
return defer;
}

void ConnectionManagerImpl::onDeferredRequestProcessing() {
requests_during_dispatch_count_ = 1; // 1 stream is always let through
// Streams are inserted at the head of the list. As such process deferred
// streams at the back of the list first.
for (auto reverse_iter = streams_.rbegin(); reverse_iter != streams_.rend();) {
auto& stream_ptr = *reverse_iter;
// Move the iterator to the next item in case the `onDeferredRequestProcessing` call removes the
// stream from the list.
++reverse_iter;
bool was_deferred = stream_ptr->onDeferredRequestProcessing();
if (was_deferred && shouldDeferRequestProxyingToNextIoCycle()) {
break;
}
}
}

} // namespace Http
} // namespace Envoy
46 changes: 45 additions & 1 deletion source/common/http/conn_manager_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,15 @@ class ConnectionManagerImpl : Logger::Loggable<Logger::Id::http>,
void setClearHopByHopResponseHeaders(bool value) { clear_hop_by_hop_response_headers_ = value; }
bool clearHopByHopResponseHeaders() const { return clear_hop_by_hop_response_headers_; }

// This runtime key configures the number of streams which must be closed on a connection before
// envoy will potentially drain a connection due to excessive prematurely reset streams.
static const absl::string_view PrematureResetTotalStreamCountKey;

// The minimum lifetime of a stream, in seconds, in order not to be considered
// prematurely closed.
static const absl::string_view PrematureResetMinStreamLifetimeSecondsKey;
static const absl::string_view MaxRequestsPerIoCycle;

private:
struct ActiveStream;
class MobileConnectionManagerImpl;
Expand Down Expand Up @@ -340,7 +349,7 @@ class ConnectionManagerImpl : Logger::Loggable<Logger::Id::http>,
: codec_saw_local_complete_(false), codec_encode_complete_(false),
on_reset_stream_called_(false), is_zombie_stream_(false), successful_upgrade_(false),
is_internally_destroyed_(false), is_internally_created_(false), is_tunneling_(false),
decorated_propagate_(true) {}
decorated_propagate_(true), deferred_to_next_io_iteration_(false) {}

// It's possibly for the codec to see the completed response but not fully
// encode it.
Expand All @@ -365,6 +374,14 @@ class ConnectionManagerImpl : Logger::Loggable<Logger::Id::http>,
bool is_tunneling_ : 1;

bool decorated_propagate_ : 1;

// Indicates that sending headers to the filter manager is deferred to the
// next I/O cycle. If data or trailers are received when this flag is set
// they are deferred too.
// TODO(yanavlasov): encapsulate the entire state of deferred streams into a separate
// structure, so it can be atomically created and cleared.
bool deferred_to_next_io_iteration_ : 1;
bool deferred_end_stream_ : 1;
};

bool canDestroyStream() const {
Expand Down Expand Up @@ -414,6 +431,11 @@ class ConnectionManagerImpl : Logger::Loggable<Logger::Id::http>,

std::weak_ptr<bool> stillAlive() { return {still_alive_}; }

// Dispatch deferred headers, body and trailers to the filter manager.
// Return true if this stream was deferred and dispatched pending headers, body and trailers (if
// present). Return false if this stream was not deferred.
bool onDeferredRequestProcessing();

ConnectionManagerImpl& connection_manager_;
OptRef<const TracingConnectionManagerConfig> connection_manager_tracing_config_;
// TODO(snowp): It might make sense to move this to the FilterManager to avoid storing it in
Expand Down Expand Up @@ -503,6 +525,7 @@ class ConnectionManagerImpl : Logger::Loggable<Logger::Id::http>,
bool spawnUpstreamSpan() const override;

std::shared_ptr<bool> still_alive_ = std::make_shared<bool>(true);
std::unique_ptr<Buffer::OwnedImpl> deferred_data_;
};

using ActiveStreamPtr = std::unique_ptr<ActiveStream>;
Expand Down Expand Up @@ -570,6 +593,18 @@ class ConnectionManagerImpl : Logger::Loggable<Logger::Id::http>,
void doConnectionClose(absl::optional<Network::ConnectionCloseType> close_type,
absl::optional<StreamInfo::ResponseFlag> response_flag,
absl::string_view details);
// Returns true if a RST_STREAM for the given stream is premature. Premature
// means the RST_STREAM arrived before response headers were sent and than
// the stream was alive for short period of time. This period is specified
// by the optional runtime value PrematureResetMinStreamLifetimeSecondsKey,
// or one second if that is not present.
bool isPrematureRstStream(const ActiveStream& stream) const;
// Sends a GOAWAY if both sufficient streams have been closed on a connection
// and at least half have been prematurely reset?
void maybeDrainDueToPrematureResets();

bool shouldDeferRequestProxyingToNextIoCycle();
void onDeferredRequestProcessing();

enum class DrainState { NotDraining, Draining, Closing };

Expand Down Expand Up @@ -610,7 +645,16 @@ class ConnectionManagerImpl : Logger::Loggable<Logger::Id::http>,
bool clear_hop_by_hop_response_headers_{true};
// The number of requests accumulated on the current connection.
uint64_t accumulated_requests_{};
// The number of requests closed on the current connection which were
// not internally destroyed
uint64_t closed_non_internally_destroyed_requests_{};
// The number of requests that received a premature RST_STREAM, according to
// the definition given in `isPrematureRstStream()`.
uint64_t number_premature_stream_resets_{0};
const std::string proxy_name_; // for Proxy-Status.
uint32_t requests_during_dispatch_count_{0};
const uint32_t max_requests_during_dispatch_{UINT32_MAX};
Event::SchedulableCallbackPtr deferred_request_processing_callback_;

const bool refresh_rtt_after_request_{};
};
Expand Down
1 change: 1 addition & 0 deletions source/common/runtime/runtime_features.cc
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ RUNTIME_GUARD(envoy_reloadable_features_validate_connect);
RUNTIME_GUARD(envoy_reloadable_features_validate_detailed_override_host_statuses);
RUNTIME_GUARD(envoy_reloadable_features_validate_grpc_header_before_log_grpc_status);
RUNTIME_GUARD(envoy_reloadable_features_validate_upstream_headers);
RUNTIME_GUARD(envoy_restart_features_send_goaway_for_premature_rst_streams);
RUNTIME_GUARD(envoy_restart_features_udp_read_normalize_addresses);

// Begin false flags. These should come with a TODO to flip true.
Expand Down
Loading