Skip to content

test(http): dual-metric RSS/heap leak gate that catches the leak the window absorbed - #809

Merged
InauguralPhysicist merged 1 commit into
InauguralSystems:mainfrom
Nitjsefnie-OSC:fix/rss-leak-gate-window-770
Aug 2, 2026
Merged

test(http): dual-metric RSS/heap leak gate that catches the leak the window absorbed#809
InauguralPhysicist merged 1 commit into
InauguralSystems:mainfrom
Nitjsefnie-OSC:fix/rss-leak-gate-window-770

Conversation

@Nitjsefnie

Copy link
Copy Markdown
Contributor

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:

  • exact heap accounting (mallinfo2().uordblks) for the arena-class leak the issue is about, and
  • an RSS check (restored, not removed) for the class exact accounting cannot see.

Why both, rather than just switching to exact accounting. They have complementary blind spots: uordblks is blind to non-arena memory (a per-request direct-mmap leak, 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 MEASURE to ~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_http binds with SO_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_nonce before 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.
  • a uordblks accounting builtin (glibc-only, #if defined(__GLIBC__); the gate SKIPs where unavailable — fail-safe, never fail-open), documented in docs/BUILTINS.md and left out of SANDBOX_ALLOW (fail-closed, matching the arena_stats precedent).

Testing

  • The original arena leak (drop val_decref(new_v)): old gate at MEASURE=2000 PASSES (the bug); new gate FAILS (heap: leaked 155 kB … | rss: 56/88/84 kB, exit 1); reverted, PASSES.
  • A 512 kB/req direct-mmap leak (the class exact-accounting-only misses): new gate FAILS on the RSS leg (rss: leaked 1025332 kB, heap leg near zero), where a heap-only gate passed.
  • Clean run: no false-fail from the glibc arena-step class (RSS threshold derived against the observed step max).
  • Nonce self-check: caught a live SO_REUSEPORT split (probes mismatched, gate failed loudly) and passes a normal run.
  • Full suite 3464/3464 with the gate green at [45c]; make asan clean. (Merge-base f59d3f1; 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:

  • The gate has a floor. A sustained arena-class leak below ~32 B/req (64 kB over 2,000 requests) stays under the heap threshold and is RSS-absorbed — it passes both legs. That is the inherent floor of any noise-tolerant, bounded-window gate, not a defect; the header scopes the gate to the smallest fault it must catch (the issue's 72 B/req). A slow-drip leak below the floor would need a larger window or a longer run.
  • The heap leg assumes the keep-alive worker's allocations land on glibc's main arena (uordblks reports 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).
  • One stale cite to fix: a comment in tests/test_http_rss_growth.sh points at ext_http.c:~1170 for the shared-store auth branch; the actual shared_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 test passes locally — suite 3464/3464; make asan clean; the gate's three-step leak proof reproduced
  • New builtins have signature comments and docs in docs/BUILTINS.md — the uordblks accounting builtin is documented and stdlib_index_check.sh passes
  • New library functions follow conventions in docs/STDLIB.md — n/a
  • New examples have a comment header explaining what they demonstrate — n/a
  • CHANGELOG.md updated (if user-facing change) — not done; this is test-infrastructure plus a debug-only accounting builtin, happy to add an entry if you'd like one

Generated by Claude Opus 5 (brief, review), Kimi K3 (implementation, verification)

@InauguralPhysicist InauguralPhysicist left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) in builtin_shared_incr): heap leg FAILS with 158 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) */
#endif

and 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:~1170 cite you flagged — yes, please correct it to the shared_find(srv, "require_auth") site while you're in the file.
  • please rebase onto current main when you do: #740 just landed (per-variant objdirs — make http now builds into build/http/ and hard-links src/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.

@Nitjsefnie
Nitjsefnie force-pushed the fix/rss-leak-gate-window-770 branch from 41d1c6e to 75241c6 Compare August 2, 2026 11:26
…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>
@Nitjsefnie
Nitjsefnie force-pushed the fix/rss-leak-gate-window-770 branch from 75241c6 to d892233 Compare August 2, 2026 11:45
@Nitjsefnie

Copy link
Copy Markdown
Contributor Author

Thanks — that's a generous review, and planting the val_decref fault yourself is the check I'd have wanted most.

All three asks are in. Rebased onto main at cd9f823 (#740): test_http_rss_growth.sh only ever resolves $SRC_DIR/eigenscript, and your alias is a hard link to build/http/eigenscript (same inode, link count 2), so nothing in the gate moved. Cite corrected to ext_http.c:1295, naming the shared_find(srv, "require_auth") lookup so it re-resolves after the next drift; I checked the file's other two cites (:296 SO_REUSEPORT, ~1400 worker setup) and both are still accurate. Suite 3468/3468, all four steps of the freestanding job green locally, and the carve-out head came back 16/16 green on CI.

One correction, on the guard shape. I used !EIGENSCRIPT_FREESTANDING rather than !defined(EIGENSCRIPT_FREESTANDING), because eigenscript.h:48-50 is:

#ifndef EIGENSCRIPT_FREESTANDING
#define EIGENSCRIPT_FREESTANDING 0
#endif

so the macro is always defined and !defined(...) would be false in hosted builds too. I built your form rather than argue it — on a plain make http:

  • nm shows no mallinfo2 import at all (value form: U mallinfo2@GLIBC_2.33)
  • heap_inuse of null returns null instead of a byte count
  • the gate prints SKIP: heap_inuse returned 'null' — mallinfo2 unavailable (non-glibc libc), then HTTP_RSS: 0 passed, 0 failed (skipped), exit 0

Since heap_sample_ok() exits the script at the first unavailable sample, that would take the pre-existing #731/#752 RSS legs down along with the new heap leg — silently, with CI green. The value form also matches the ~20 existing guards in the tree (lint.c:135, hash.c:269, arena.c:80, state.c:157, …). If you'd rather have the defined() shape, the header's unconditional #define would need to become hosted-only — happy to do it that way instead, your call.

Understood on the CHANGELOG.

@InauguralPhysicist InauguralPhysicist left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@InauguralPhysicist
InauguralPhysicist merged commit 14c3a07 into InauguralSystems:main Aug 2, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The RSS leak gate's 2000-request window sits inside the heap-slack absorption window: a real 72 B/req Value leak passes it

2 participants