test(http): dual-metric RSS/heap leak gate that catches the leak the window absorbed - #809
Conversation
InauguralPhysicist
left a comment
There was a problem hiding this comment.
This is excellent work — the dual-metric design is the right call and the verification behind it is the kind we want as the house standard. I reproduced your claims independently on this box:
- clean run: both routes PASS, 32.7 s wall (your 28–51 s claim holds), verdict self-test 15/15
- planted the #770 fault myself (dropped
val_decref(new_v)inbuiltin_shared_incr): heap leg FAILS with158 kB over 2000 reqs (80 B/req; batches 158/162/147 — median batch grew)while the RSS leg reads 0 kB batches after a one-off 1324 kB step — your disjoint-blind-spots analysis, demonstrated exactly - reverted: PASS again
The builtin itself is clean (adopting env_set_local_owned, owned return, fail-closed out of SANDBOX_ALLOW, BUILTINS.md entry).
One required change — the failing freestanding profile check is real:
FAIL: symbols imported outside the freestanding allowlist:
mallinfo2
The symbol gate compiles builtins.c with EIGENSCRIPT_FREESTANDING=1 against hosted glibc headers, so __GLIBC__ is still defined there and the guard doesn't carve the call out. Both guards in builtins.c need the freestanding term:
#if defined(__GLIBC__) && !defined(EIGENSCRIPT_FREESTANDING)
#include <malloc.h> /* mallinfo2, for builtin_heap_inuse (#770) */
#endifand the same condition in builtin_heap_inuse (the #else null branch then covers the freestanding profile, which matches your "fail-safe, never fail-open" contract). make freestanding-check locally will confirm — that's the exact gate CI runs.
Two small things you can fold into the same push:
- the stale
ext_http.c:~1170cite you flagged — yes, please correct it to theshared_find(srv, "require_auth")site while you're in the file. - please rebase onto current
mainwhen you do: #740 just landed (per-variant objdirs —make httpnow builds intobuild/http/and hard-linkssrc/eigenscript; your workflow is unchanged, but the rebase avoids a stale-base CI matrix), and the suite totals you cited will drift again, which is expected.
No CHANGELOG entry needed for test infrastructure + a debug builtin — the BUILTINS.md entry you added is the right documentation surface.
The Follow-ups section is appreciated — the ~32 B/req floor and the main-arena assumption are exactly the kind of scoping that keeps this gate trustworthy. Once the freestanding carve-out is in and CI is green, this merges.
41d1c6e to
75241c6
Compare
…n-arena (InauguralSystems#770) The per-request leak gate measured VmRSS over a 1000 + 3x2000-request window. RSS is a proxy for live heap, and the proxy is blind while a leak is absorbed by resident free heap the process already owns: the literal InauguralSystems#731 shape (a dropped val_decref in builtin_shared_incr, 72 heap B/req) passes the gate at 0 kB while leaking unboundedly. Raising MEASURE to 20000 does catch it (verified 2/2: 2416/2696 kB per batch) but costs 6-10x the gate's wall-clock (~30s vs ~4.5-6 min) and only moves the blind spot to a lower leak rate. So the gate now checks TWO metrics at the same checkpoints and fails if EITHER trips — their blind spots are disjoint: HEAP LEG: mallinfo2().uordblks (live arena bytes), sampled from the server itself through a new heap_inuse debug builtin over a /heap_inuse route. Sees the arena class from the first batch (the 72 B/req fault: ~144-165 kB per 2000-req batch, threshold 64 kB), immune to free-heap slack AND to the one-off per-thread-arena mmap step that false-failed CI as an RSS step (a fresh arena is free heap). Blind to non-arena memory: direct mmap at/above glibc's dynamic mmap threshold, and thread stacks — a 512 kB/req direct-mmap leak moves it 0/0/0 kB. RSS LEG: VmRSS, threshold 4096 kB. Sees exactly that non-arena class (the same 512 kB/req leak: ~1,024,000 kB per batch). The threshold stays ~1.4x above the largest observed arena step (2876 kB, run 30439602640) so the step class cannot false-fail even if steps land in two of the three median-of-three batches; any non-arena leak above ~2 B/req still trips. Blind to small arena leaks inside the slack window — that is the heap leg's job. Also: the gate now proves it is talking to its own server. ext_http binds SO_REUSEPORT (ext_http.c:296), so a stale server can share the port and silently split the gate's traffic while batch counts still pass — this corrupted a real measurement of this gate. Both server scripts carry a /gate_nonce route with a per-run nonce, probed 20x before measuring; any mismatch is a loud INSTRUMENT failure (verified against a live split: 10/20 probes mismatched). heap_inuse is glibc-only (returns null elsewhere; the gate then skips, same posture as its existing platform skips) and mallinfo2 reports the main arena only — the gate's sequential traffic is served there, probe-verified; both constraints documented in the builtin comment and the gate header. The gate was already Linux-only (it reads /proc), so CI coverage is unchanged, and so is its cost (~30-50s). Validated per InauguralSystems#769's rule, both classes: old gate at MEASURE=2000 passes the 72 B/req fault (the bug); new gate fails it via the heap leg (163/155/147 kB); reverted, it passes (0-2 kB, arena steps of 1516 kB absorbed). A planted 512 kB/req direct-mmap leak fails via the RSS leg (1025524/1025332/1024000 kB) where a heap-only gate passed. Verdict self-test extended to 15 planted cases, both legs. Full suite 3468/3468; make asan clean. heap_inuse's mallinfo2 call is also carved out under EIGENSCRIPT_FREESTANDING, not just __GLIBC__: the freestanding profile compiles on a glibc host, so the host's mallinfo2 leaked into its import surface and tools/freestanding_check.sh rejected it (no HAL/mini-libc provides it). Under the profile the builtin reports unavailable — the same null the existing non-glibc path returns. Co-Authored-By: Kimi K3 <noreply@kimi.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
75241c6 to
d892233
Compare
|
Thanks — that's a generous review, and planting the All three asks are in. Rebased onto One correction, on the guard shape. I used #ifndef EIGENSCRIPT_FREESTANDING
#define EIGENSCRIPT_FREESTANDING 0
#endifso the macro is always defined and
Since Understood on the CHANGELOG. |
InauguralPhysicist
left a comment
There was a problem hiding this comment.
The carve-out matches the file's value-test gating style exactly, the cite now names the real shared_find site, and the branch is rebased onto post-#740 main — freestanding leg green along with the other 15. My earlier independent verification of the red-green stands (planted 72 B/req fault: heap leg fails, RSS blind — the #770 thesis demonstrated). Merging. Thanks for a genuinely excellent gate, Peter — the disjoint-blind-spots analysis and the verdict self-test are the house standard now.
What does this PR do?
Fixes #770. The RSS leak gate's decision happened entirely within the first ~7,000 requests, small enough that glibc heap slack absorbs a genuine ~72 B/req Value leak — so the gate passed while leaking. This replaces the single blind RSS check with a dual-metric gate that fails if EITHER metric trips:
mallinfo2().uordblks) for the arena-class leak the issue is about, andWhy both, rather than just switching to exact accounting. They have complementary blind spots:
uordblksis blind to non-arena memory (a per-request direct-mmapleak, or thread stacks), while RSS is blind to a small arena leak hiding in the slack window. Dropping RSS entirely would trade the issue's bug for a different one — a 512 kB/req direct-mmap leak passes exact-accounting-only while the old RSS check caught it. Keeping both closes both classes.On your suggested alternative: raising
MEASUREto ~20,000 requests genuinely does catch this leak — I'm not claiming otherwise. I chose exact accounting over the larger window for cost (the gate stays ~28–51 s vs ~4m33s–6m5s for the 20k window, ~6–10×) and because exact accounting eliminates the arena-step false-fails a pure-RSS gate suffers. The RSS leg here runs at a threshold tuned so those steps don't false-fail.A server-identity self-check was necessary and is included.
ext_httpbinds withSO_REUSEPORT, so a stray listener on the same port silently splits traffic with the gate's server and the batch-count check still passes — which corrupts the measurement invisibly. The gate now probes a per-run/gate_noncebefore measuring and fails loudly on a mismatch (verified against a deliberately-planted rogue second server on the same port).Changes
tests/test_http_rss_growth.sh— dual-metric gate (heap + RSS legs), the nonce self-check, and the residual-blind-spot note in the header.uordblksaccounting builtin (glibc-only,#if defined(__GLIBC__); the gate SKIPs where unavailable — fail-safe, never fail-open), documented indocs/BUILTINS.mdand left out ofSANDBOX_ALLOW(fail-closed, matching thearena_statsprecedent).Testing
val_decref(new_v)): old gate atMEASURE=2000PASSES (the bug); new gate FAILS (heap: leaked 155 kB … | rss: 56/88/84 kB, exit 1); reverted, PASSES.rss: leaked 1025332 kB, heap leg near zero), where a heap-only gate passed.SO_REUSEPORTsplit (probes mismatched, gate failed loudly) and passes a normal run.[45c];make asanclean. (Merge-basef59d3f1; suite totals drift here, so that's the base.)Follow-ups / Known Limitations
Stated because they are real and you'd rather know than discover them:
uordblksreports main only). The gate drives a single connection, so this holds today, but it is load-bearing and platform-dependent — a future multi-connection client, or a glibc pinning the worker to a per-thread arena, would blind the heap leg (the RSS leg would still cover it).tests/test_http_rss_growth.shpoints atext_http.c:~1170for the shared-store auth branch; the actualshared_find(srv, "require_auth")is nearer~1295(line 1170 is request-line parsing). Cosmetic, in the diff — I'll correct it, or fold it into your review, your call.Checklist
make testpasses locally — suite3464/3464;make asanclean; the gate's three-step leak proof reproduceddocs/BUILTINS.md— theuordblksaccounting builtin is documented andstdlib_index_check.shpassesdocs/STDLIB.md— n/aGenerated by Claude Opus 5 (brief, review), Kimi K3 (implementation, verification)