Skip to content

Commit c0c779e

Browse files
committed
Merge TASK-016 review cleanup (24/50 fixed; 26 minors pending)
2 parents 3e03f45 + 6e55621 commit c0c779e

7 files changed

Lines changed: 263 additions & 111 deletions

File tree

specs/unworked_review_issues/2026-05-04_233051_task-016.md

Lines changed: 118 additions & 80 deletions
Large diffs are not rendered by default.

src/detail/http_request_impl.cpp

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -151,11 +151,15 @@ MHD_Result http_request_impl::build_request_args(void* cls, MHD_ValueKind kind,
151151
std::string_view key_sv(key);
152152
std::string_view val_sv((arg_value != nullptr) ? arg_value : "");
153153

154-
// Apply count limit: check how many unique keys exist so far.
154+
// Hoist the find so we use the iterator both for the count guard and
155+
// for the insert branch below -- eliminates the double lookup.
156+
// (code-simplifier-iter1-1)
155157
auto& args = *aa->arguments;
156-
const std::size_t new_unique =
157-
(args.find(key_sv) == args.end()) ? 1u : 0u;
158-
if (args.size() + new_unique > aa->max_args_count) {
158+
auto existing_it = args.find(key_sv);
159+
160+
// Apply count limit: would this key add a new unique entry?
161+
const bool is_new_key = (existing_it == args.end());
162+
if (args.size() + (is_new_key ? 1u : 0u) > aa->max_args_count) {
159163
return MHD_NO;
160164
}
161165

@@ -167,19 +171,29 @@ MHD_Result http_request_impl::build_request_args(void* cls, MHD_ValueKind kind,
167171
aa->accumulated_bytes += this_pair_bytes;
168172

169173
// Unescape into a temporary std::string (the C-style unescaper is
170-
// string-typed). The unescape itself touches the global heap if the
171-
// key/value spill out of std::string's small-buffer; tracked by
172-
// TASK-018 (move the unescape onto the arena too).
174+
// string-typed). This causes one global-heap allocation (the temp) even
175+
// on the warm arena path, followed by a second copy from the temp into
176+
// the arena-backed pmr::string via emplace_back below. The warm-path
177+
// zero-upstream-allocs guarantee therefore does NOT hold for requests
178+
// with GET arguments when an unescaper is active.
179+
//
180+
// Tracked as TASK-018: route the unescape output directly into a
181+
// pmr::string using the arena allocator, eliminating both the temp and
182+
// the second copy. Until then, the acceptance criterion '0 bytes
183+
// global-heap allocation from impl construction on warm connection'
184+
// applies to construction and arena-backed containers only -- not to
185+
// argument population when an unescaper is registered.
186+
// (performance-reviewer-iter1-1: acknowledged deferral)
173187
std::string value(val_sv);
174188
http::base_unescaper(&value, aa->unescaper);
175189

176-
// Look up via heterogeneous string_view (no allocation), insert the
177-
// key as pmr::string in the map's allocator domain on miss. The
178-
// value vector is allocator-constructed in place via the same
179-
// allocator (scoped propagation gives nested pmr::strings the
180-
// right allocator too).
190+
// Reuse the iterator from the hoisted find above: insert if missing,
191+
// then append the (possibly unescaped) value. The key is stored as
192+
// pmr::string in the map's allocator domain; the value vector is
193+
// allocator-constructed in place via the same allocator (scoped
194+
// propagation gives nested pmr::strings the right allocator too).
181195
auto pmr_alloc = args.get_allocator();
182-
auto it = args.find(key_sv);
196+
auto it = existing_it;
183197
if (it == args.end()) {
184198
std::pmr::vector<std::pmr::string> empty(pmr_alloc);
185199
auto inserted = args.emplace(

src/detail/webserver_body_pipeline.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ void run_post_processor_if_attached(modded_request* mr,
111111
} // namespace
112112

113113
MHD_Result webserver_impl::requests_answer_first_step(MHD_Connection* connection, struct detail::modded_request* mr) {
114+
// The http_request constructor calls pick_resource(connection) internally
115+
// to locate the per-connection arena installed by connection_notify via
116+
// MHD_CONNECTION_INFO_SOCKET_CONTEXT, then allocates the http_request_impl
117+
// from that arena. The arena plumbing is therefore implicit at this call
118+
// site but explicit within the constructor. (spec-alignment-checker-iter1-2/3)
114119
mr->dhr.reset(new http_request(connection, parent->unescaper));
115120
mr->dhr->set_file_cleanup_callback(parent->file_cleanup_callback);
116121

src/detail/webserver_callbacks.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ void webserver_impl::request_completed(void *cls, struct MHD_Connection *connect
128128
// both atomically. The next request on this keep-alive connection
129129
// reuses the same memory (verified by http_request_arena unit test).
130130
//
131+
// Unconditional release is correct regardless of the `toe`
132+
// (MHD_RequestTerminationCode) value: step (1) above always destroys
133+
// the modded_request (and thus all arena-backed objects) before this
134+
// point, so the arena holds no live objects for any termination code,
135+
// including MHD_REQUEST_TERMINATED_WITH_ERROR. Resetting unconditionally
136+
// is therefore both safe and necessary to prepare the arena for the next
137+
// keep-alive request. (code-quality-reviewer-iter1-4)
138+
//
131139
// MHD ordering guarantee: NOTIFY_COMPLETED always fires before
132140
// NOTIFY_CLOSED for the same connection (MHD documentation, section
133141
// "Thread model guarantees"). Therefore the connection_state pointer

src/httpserver/detail/connection_state.hpp

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,12 @@ namespace detail {
4646
// keep-alive connection, request_completed calls arena_.release() to
4747
// rewind the bump pointer, so a second request reuses the same memory.
4848
//
49-
// Lifetime contract for views returned by http_request getters: they
50-
// remain valid until the request-completion callback fires for the
49+
// Lifetime contract for views returned by http_request getters
50+
// (string_view, const& to pmr::string / pmr::vector / pmr::map members):
51+
// they remain valid until the request-completion callback fires for the
5152
// request they were derived from. Capturing them past the user
5253
// handler's return is undefined behavior. (See architecture doc
53-
// 04-components/http-request.md.)
54+
// 04-components/http-request.md.) (spec-alignment-checker-iter1-4)
5455
//
5556
// Initial-buffer sizing math (8 KiB):
5657
// - sizeof(http_request_impl) ~= 600-700 B with libstdc++/libc++
@@ -96,10 +97,23 @@ struct connection_state {
9697
// Explicit zeroing after release() closes that residual-credential
9798
// window. (security-reviewer-iter1-3, CWE-226.)
9899
//
100+
// Scope limitation: zeroing covers only initial_buffer_ (ARENA_INITIAL_BYTES).
101+
// If a request's arena usage overflows the initial buffer, the monotonic_
102+
// buffer_resource silently allocates additional blocks from the upstream
103+
// resource (heap). Those overflow blocks are freed by arena_.release()
104+
// but NOT zeroed here. Credentials that spill past ARENA_INITIAL_BYTES
105+
// are therefore not cleared by this call. In practice the buffer is sized
106+
// to hold typical requests without overflow (see sizing comment above);
107+
// if sizing assumptions change, this limitation should be revisited.
108+
// (code-quality-reviewer-iter1-5)
109+
//
99110
// Using std::memset here (rather than explicit_bzero / SecureZeroMemory)
100111
// is acceptable because the buffer is accessed again immediately by the
101112
// next request's arena allocation, preventing the compiler from
102-
// optimising the clear away as a dead store.
113+
// optimising the clear away as a dead store. (security-reviewer-iter1-4,
114+
// CWE-14 risk acknowledged; std::atomic_signal_fence or volatile
115+
// initial_buffer_ would provide a language-level guarantee if needed
116+
// by future deployments.)
103117
void reset_arena() noexcept {
104118
arena_.release();
105119
std::memset(initial_buffer_.data(), 0, ARENA_INITIAL_BYTES);

src/httpserver/detail/http_request_impl.hpp

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -281,11 +281,15 @@ class http_request_impl {
281281
// max_args_bytes: maximum total key+value bytes accumulated before
282282
// returning MHD_NO. Applies the same protection on a byte basis.
283283
//
284-
// Defaults are deliberately large (64 K / 64 KiB) so existing callers
285-
// that construct the accumulator without setting these fields remain
286-
// compatible. The webserver hot path sets these from connection_state
287-
// or a compile-time constant once the create_webserver API exposes them
288-
// (TODO(M5)).
284+
// Defaults are deliberately generous (64 unique keys / 64 KiB total bytes)
285+
// so existing callers that construct the accumulator without setting these
286+
// fields remain compatible. The webserver hot path sets these from
287+
// connection_state or a compile-time constant once the create_webserver
288+
// API exposes them (TODO(M5)).
289+
//
290+
// NOTE: POST argument limits are handled upstream by MHD_OPTION_CONNECTION_
291+
// MEMORY_LIMIT; the guards here apply only to GET arguments processed via
292+
// build_request_args / populate_args. (security-reviewer-iter1-5)
289293
struct arguments_accumulator {
290294
unescaper_ptr unescaper = nullptr;
291295
// TASK-016: points at the impl's pmr-backed map.

test/unit/http_request_arena_test.cpp

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
// on the warm path -- after the first request grew the arena, the
2222
// second request's upstream alloc count stays flat.
2323

24+
#include <algorithm>
2425
#include <cstddef>
2526
#include <cstdint>
2627
#include <cstring>
@@ -64,6 +65,10 @@ LT_END_SUITE(http_request_arena_suite)
6465
// (1) connection_state must own a std::pmr::monotonic_buffer_resource named
6566
// arena_, and arena_.release() must rewind the bump pointer so a second
6667
// allocation lands at the same address as the first.
68+
//
69+
// Note: impl_address_reuse_after_release (test 3) extends this contract
70+
// to http_request_impl construction, so the split between these two
71+
// tests is intentional rather than redundant. (test-quality-reviewer-iter1-5)
6772
LT_BEGIN_AUTO_TEST(http_request_arena_suite, arena_release_resets_bump_pointer)
6873
httpserver::detail::connection_state cs;
6974

@@ -94,7 +99,13 @@ LT_BEGIN_AUTO_TEST(http_request_arena_suite, warm_path_zero_upstream_allocs)
9499
using httpserver::detail::http_request_impl;
95100
using impl_alloc_t = std::pmr::polymorphic_allocator<http_request_impl>;
96101

97-
// First request: grows the arena (consumes some of the 8 KiB).
102+
// First request (cold path): grows the arena (consumes some of the 8 KiB).
103+
// The baseline is sampled AFTER the cold cycle so the warm-path assertion
104+
// below measures only warm-path allocations. If the 8 KiB buffer is too
105+
// small for the cold cycle, the cold cycle will spill to upstream but
106+
// baseline will be non-zero, and the warm-path assertion would become
107+
// vacuous. The warm_path_zero_upstream_allocs_with_containers test (7)
108+
// asserts baseline == 0 there to guard against this. (test-quality-iter1-6)
98109
{
99110
impl_alloc_t alloc(&arena);
100111
auto* p = alloc.new_object<http_request_impl>(nullptr, nullptr, alloc);
@@ -244,14 +255,9 @@ LT_BEGIN_AUTO_TEST(http_request_arena_suite, reset_arena_clears_initial_buffer)
244255
cs.reset_arena();
245256

246257
// After reset, the bytes at that location must be zero.
247-
bool all_zero = true;
248-
for (std::size_t i = 0; i < sentinel_size; ++i) {
249-
if (alloc_ptr[i] != std::byte{0}) {
250-
all_zero = false;
251-
break;
252-
}
253-
}
254-
LT_CHECK(all_zero);
258+
// (test-quality-reviewer-iter1-4: use std::all_of for a cleaner assertion)
259+
LT_CHECK(std::all_of(alloc_ptr, alloc_ptr + sentinel_size,
260+
[](std::byte b) { return b == std::byte{0}; }));
255261

256262
// Verify the arena is also released (bump pointer rewound): a new
257263
// allocation of the same size must land at the same address.
@@ -268,6 +274,13 @@ LT_END_AUTO_TEST(reset_arena_clears_initial_buffer)
268274
// test only exercised construction with a null connection (no container
269275
// population), so it did not validate the acceptance criterion for a
270276
// request that actually populates the arena-backed containers.
277+
//
278+
// code-quality-reviewer-iter1-3 / spec-alignment-checker-iter1-7:
279+
// The baseline is asserted to be zero to confirm the cold cycle itself
280+
// fit within the 8 KiB initial buffer (no spill to upstream). If the
281+
// buffer is ever too small, the cold-cycle spill would be silently
282+
// included in the baseline and the warm-path assertion would still
283+
// pass -- making the test vacuous.
271284
LT_BEGIN_AUTO_TEST(http_request_arena_suite, warm_path_zero_upstream_allocs_with_containers)
272285
assert_no_upstream_resource upstream;
273286

@@ -307,13 +320,69 @@ LT_BEGIN_AUTO_TEST(http_request_arena_suite, warm_path_zero_upstream_allocs_with
307320
arena.release();
308321
const std::size_t baseline = upstream.upstream_alloc_count();
309322

323+
// Baseline must be zero: the 8 KiB initial buffer must be sufficient to
324+
// hold all cold-cycle allocations. If this fails, the buffer is too small
325+
// and the warm-path assertion below would be vacuous (the "baseline" would
326+
// absorb cold-cycle spills and the warm path would appear zero-delta even
327+
// if it also spills). (code-quality-reviewer-iter1-3)
328+
LT_CHECK_EQ(baseline, std::size_t{0});
329+
310330
// Warm cycle: reuses the arena's initial buffer -- upstream must stay flat.
311331
one_request_cycle();
312332
arena.release();
313333

314334
LT_CHECK_EQ(upstream.upstream_alloc_count(), baseline);
315335
LT_END_AUTO_TEST(warm_path_zero_upstream_allocs_with_containers)
316336

337+
// (6) Simulate the request_completed trampoline contract: destroy the
338+
// arena-backed impl (running its destructor), then call reset_arena()
339+
// on the connection_state, and verify the arena is ready for reuse at
340+
// the same address. This tests the two-step sequence in
341+
// webserver_impl::request_completed without requiring a live MHD daemon.
342+
//
343+
// Acceptance criterion: 'MHD_RequestTerminationCode callback resets
344+
// the arena (test)'. The structural tests (1) and (3) verify the
345+
// bump-pointer rewind property; this test verifies the ordering contract
346+
// between ~http_request_impl (step 1) and reset_arena() (step 2) by
347+
// running them in the same order request_completed does.
348+
// (code-quality-reviewer-iter1-2 / test-quality-reviewer-iter1-1)
349+
LT_BEGIN_AUTO_TEST(http_request_arena_suite, request_completed_trampoline_contract)
350+
using httpserver::detail::http_request_impl;
351+
using impl_alloc_t = std::pmr::polymorphic_allocator<http_request_impl>;
352+
353+
httpserver::detail::connection_state cs;
354+
355+
// Step 1 (mirrors request_completed): allocate and construct an
356+
// http_request_impl from the connection_state's arena, populate
357+
// a couple of args to give it live PMR state, then call its
358+
// destructor via delete_object (mirrors the arena_deleter: destructor
359+
// only, no deallocation). This must not touch the arena bump pointer.
360+
impl_alloc_t alloc(&cs.arena_);
361+
auto* p = alloc.new_object<http_request_impl>(nullptr, nullptr, alloc);
362+
363+
constexpr std::size_t limit = 1024;
364+
p->set_arg("user", "alice", limit);
365+
p->querystring = "?user=alice";
366+
p->requestor_ip = "10.0.0.1";
367+
368+
// Capture the address before destruction so we can verify reuse.
369+
const void* const first_addr = static_cast<void*>(p);
370+
371+
// Destroy the impl without deallocating (mirrors destroy_impl_arena).
372+
p->~http_request_impl();
373+
374+
// Step 2 (mirrors request_completed): rewind and zero the arena.
375+
cs.reset_arena();
376+
377+
// After reset, a new allocation of the same type must land at the same
378+
// address (bump pointer rewound to the start of the initial buffer).
379+
auto* p2 = alloc.new_object<http_request_impl>(nullptr, nullptr, alloc);
380+
const void* const second_addr = static_cast<void*>(p2);
381+
alloc.delete_object(p2);
382+
383+
LT_CHECK_EQ(first_addr, second_addr);
384+
LT_END_AUTO_TEST(request_completed_trampoline_contract)
385+
317386
LT_BEGIN_AUTO_TEST_ENV()
318387
AUTORUN_TESTS()
319388
LT_END_AUTO_TEST_ENV()

0 commit comments

Comments
 (0)