Skip to content

Commit de411b9

Browse files
wbpcodephlax
authored andcommitted
ratelimit: fix a bug where response phase limit may result in crash
[CVE-2026-26330](GHSA-c23c-rp3m-vpg3) Signed-off-by: wbpcode <wbphub@gmail.com> Signed-off-by: Ryan Northey <ryan@synca.io>
1 parent 7c5facd commit de411b9

File tree

3 files changed

+36
-1
lines changed

3 files changed

+36
-1
lines changed

changelogs/current.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,12 @@ bug_fixes:
181181
intentionally not parseable (for example ``goo.gle/debugonly`` prefixes). This change can
182182
be reverted by setting runtime guard
183183
``envoy.reloadable_features.cel_message_serialize_text_format`` to ``false``.
184+
- area: ratelimit
185+
change: |
186+
Fixed a bug in the gRPC rate limit client where the client could get into a bad state if the
187+
callbacks were not properly released after a request completion, leading to potential use-after-free
188+
issues. The fix ensures that callbacks and request references are cleared after completion, and adds
189+
assertions to enforce correct usage patterns.
184190
- area: ext_authz
185191
change: |
186192
Fixed a bug where headers from a denied authorization response (non-200) were not properly propagated

source/extensions/filters/common/ratelimit/ratelimit_impl.cc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ void GrpcClientImpl::cancel() {
3737
}
3838

3939
void GrpcClientImpl::detach() {
40-
ASSERT(callbacks_ != nullptr);
4140
if (request_) {
4241
request_->detach();
4342
request_ = nullptr;
@@ -77,8 +76,11 @@ void GrpcClientImpl::limit(RequestCallbacks& callbacks, const std::string& domai
7776
const std::vector<Envoy::RateLimit::Descriptor>& descriptors,
7877
Tracing::Span& parent_span, const StreamInfo::StreamInfo& stream_info,
7978
uint32_t hits_addend) {
79+
// The client should only be used for one outstanding request at a time,
80+
// so we assert that there is no existing request or callback.
8081
ASSERT(callbacks_ == nullptr);
8182
callbacks_ = &callbacks;
83+
request_ = nullptr;
8284

8385
envoy::service::ratelimit::v3::RateLimitRequest request;
8486
createRequest(request, domain, descriptors, hits_addend);
@@ -130,6 +132,7 @@ void GrpcClientImpl::onSuccess(
130132
// callback, so we release the callback here to make the destructor happy.
131133
auto call_backs = callbacks_;
132134
callbacks_ = nullptr;
135+
request_ = nullptr;
133136
call_backs->complete(status, std::move(descriptor_statuses), std::move(response_headers_to_add),
134137
std::move(request_headers_to_add), response->raw_body(),
135138
std::move(dynamic_metadata));
@@ -144,6 +147,7 @@ void GrpcClientImpl::onFailure(Grpc::Status::GrpcStatus status, const std::strin
144147
// callback, so we release the callback here to make the destructor happy.
145148
auto call_backs = callbacks_;
146149
callbacks_ = nullptr;
150+
request_ = nullptr;
147151
call_backs->complete(LimitStatus::Error, nullptr, nullptr, nullptr, EMPTY_STRING, nullptr);
148152
}
149153

test/extensions/filters/common/ratelimit/ratelimit_impl_test.cc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,31 @@ TEST_F(RateLimitGrpcClientTest, SendRequestAndDetach) {
259259
EXPECT_CALL(request_callbacks_, complete_(LimitStatus::OK, _, _, _, _, _));
260260
client_.onSuccess(std::move(response), span_);
261261
}
262+
263+
// Send request and then fail before detach, and than call the detach should be no-op.
264+
{
265+
envoy::service::ratelimit::v3::RateLimitRequest request;
266+
GrpcClientImpl::createRequest(request, "foo", {{{{"foo", "bar"}}}}, 0);
267+
EXPECT_CALL(*async_client_, sendRaw(_, _, Grpc::ProtoBufferEq(request), Ref(client_), _, _))
268+
.WillOnce(
269+
Invoke([this](absl::string_view service_full_name, absl::string_view method_name,
270+
Buffer::InstancePtr&&, Grpc::RawAsyncRequestCallbacks&, Tracing::Span&,
271+
const Http::AsyncClient::RequestOptions&) -> Grpc::AsyncRequest* {
272+
std::string service_name = "envoy.service.ratelimit.v3.RateLimitService";
273+
EXPECT_EQ(service_name, service_full_name);
274+
EXPECT_EQ("ShouldRateLimit", method_name);
275+
return &async_request_;
276+
}));
277+
278+
client_.limit(request_callbacks_, "foo", {{{{"foo", "bar"}}}}, Tracing::NullSpan::instance(),
279+
stream_info_, 0);
280+
281+
EXPECT_CALL(request_callbacks_, complete_(LimitStatus::Error, _, _, _, _, _));
282+
client_.onFailure(Grpc::Status::Unknown, "", span_);
283+
284+
// Detach should be no-op since the request has already failed.
285+
client_.detach();
286+
}
262287
}
263288

264289
} // namespace

0 commit comments

Comments
 (0)