Skip to content
Draft
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
18 changes: 8 additions & 10 deletions net/net-http_server_middlewares.c++m
Original file line number Diff line number Diff line change
Expand Up @@ -224,21 +224,19 @@ struct rate_limiter
key_requests.end()
);

// Periodic cleanup: remove empty key entries to prevent memory leak
// Check / record before erase_if: std::flat_map erase invalidates all
// references (not just the erased key). Cleaning empty entries first
// left key_requests dangling on every 1000th call that removed anything.
const auto allow = key_requests.size() < max_requests;
if(allow)
key_requests.push_back(now);

if(++request_count % 1000 == 0)
{
std::erase_if(requests, [](const auto& pair) { return pair.second.empty(); });
}

// Check if limit exceeded
if(key_requests.size() >= max_requests)
{
return false; // Rate limit exceeded
}

// Add current request
key_requests.push_back(now);
return true; // Within limit
return allow;
}
};

Expand Down
27 changes: 27 additions & 0 deletions net/net-http_server_middlewares.test.c++
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,33 @@ auto register_middleware_tests()
};
};

// Regression: check_limit used to erase_if empty flat_map entries while still
// holding auto& into requests[key]. flat_map erase invalidates all references,
// so the subsequent size()/push_back was use-after-free on every 1000th call
// that removed at least one empty key.
tester::bdd::scenario("rate_limiter check_limit survives periodic empty-key cleanup, [net]") = [] {
tester::bdd::given("A limiter with one empty orphan key and 998 populated keys") = [] {
auto limiter = ::http::middleware::make_rate_limiter();
const auto window = std::chrono::seconds{60};

// max_requests == 0 denies without push_back → leaves an empty map entry.
check_false(limiter->check_limit("orphan", 0, window));

for(int i = 0; i < 998; ++i)
check_true(limiter->check_limit(std::format("k{}", i), 100, window));

tester::bdd::when("The 1000th check_limit erases the orphan via flat_map cleanup") = [limiter, window] {
// request_count hits 1000 here; erase_if removes "orphan".
check_true(limiter->check_limit("victim", 100, window));

tester::bdd::then("Further checks on the victim key remain well-defined") = [limiter, window] {
check_true(limiter->check_limit("victim", 100, window));
check_eq(limiter->requests.size(), 999uz); // 998 + victim; orphan gone
};
};
};
};

tester::bdd::scenario("metrics_middleware - records OK request and scrape body, [net]") = [] {
tester::bdd::given("A metrics middleware with a successful handler") = [] {
auto registry = ::http::middleware::metrics_registry{};
Expand Down
Loading