On-Demand SRDS Accumulates Unbounded Pending Callbacks
Affected commit: c33d01624d
Sink: source/common/router/scoped_rds.cc:210
Summary
On-demand SRDS requests whose route configuration is unavailable accumulate pending callbacks in an unbounded vector. Repeated requests with unknown scope keys retain active streams and memory until the route configuration arrives or the process is killed.
Detail
At scoped_rds.cc:210, each on-demand update callback is pushed into on_demand_update_callbacks_, declared in scoped_rds.h:178 as:
std::vector<std::function<void()>> on_demand_update_callbacks_;
The vector grows without bound. At line 206-210:
if (route_provider_ != nullptr && !routeConfig()->name().empty()) {
callback();
return;
}
on_demand_update_callbacks_.push_back(callback);
When the route configuration is unavailable (name is empty or provider is null), every incoming request with an unknown scope key appends a callback. There is no eviction, size limit, or timeout. Callbacks are only cleared when runOnDemandUpdateCallback() is called after route configuration arrives, but if no route configuration ever arrives, the vector grows until OOM.
Reproduce
#!/bin/bash
set -e
git clone --depth 1 https://github.com/envoyproxy/envoy.git /tmp/envoy-srds
cd /tmp/envoy-srds
echo "HEAD: $(git rev-parse HEAD)"
# Verify unbounded vector type
echo "=== Unbounded callback vector declaration ==="
grep -n 'on_demand_update_callbacks_' source/common/router/scoped_rds.h
# Verify push_back without size check
echo ""
echo "=== Callbacks accumulated without limit (scoped_rds.cc:200-216) ==="
sed -n '200,216p' source/common/router/scoped_rds.cc
# Verify callbacks only cleared on route arrival
echo ""
echo "=== Callbacks cleared only when route config arrives ==="
grep -n -A4 'runOnDemandUpdateCallback' source/common/router/scoped_rds.cc
rm -rf /tmp/envoy-srds
Expected output (line numbers may shift):
HEAD: <resolved-HEAD>
=== Unbounded callback vector declaration ===
178: std::vector<std::function<void()>> on_demand_update_callbacks_;
=== Callbacks accumulated without limit (scoped_rds.cc:200-216) ===
void ScopedRdsConfigSubscription::RdsRouteConfigProviderHelper::addOnDemandUpdateCallback(
std::function<void()> callback) {
...
if (route_provider_ != nullptr && !routeConfig()->name().empty()) {
callback();
return;
}
on_demand_update_callbacks_.push_back(callback);
...
}
=== Callbacks cleared only when route config arrives ===
void ScopedRdsConfigSubscription::RdsRouteConfigProviderHelper::runOnDemandUpdateCallback() {
for (auto& callback : on_demand_update_callbacks_) {
callback();
}
on_demand_update_callbacks_.clear();
}
With on-demand SRDS enabled, each request with an unknown scope key appends a std::function<void()> to on_demand_update_callbacks_. Each callback captures request state, so the retained memory includes the active stream. If the control plane never delivers the route configuration, an attacker sending varied scope keys grows this vector toward OOM.
Credit
Zheng Yu @ Depthfirst