Skip to content

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

Description

@InauguralPhysicist

Found while validating the median-of-three fix in #768/#769. The gate passes a real, unbounded per-request leak of exactly the class it exists to catch.

The measurement

Planted fault: drop val_decref(new_v) in builtin_shared_incr (src/ext_http.c:640) — the literal #731 shape. A probe confirms this is a heap allocation, not an arena one:

PROBE g_arena.active=0 new_v->arena=0 sizeof(Value)=72

(g_arena.active is only ever set by the user-callable arena_mark builtin at builtins.c:3828; nothing in the HTTP path opens an arena window.) So this leaks 72 heap bytes per request, forever.

The gate passes it clean:

PASS: RSS1 shared_incr steady-state growth 0 kB over 2000 reqs (<= 64 kB; batches 0/0/0 kB)

It is unmistakably a leak — same binary, same gate methodology (a separate connection per batch), 20,000-request batches:

batch requests RSS delta
0 0 3412 kB
1 20,000 8760 kB +5348
2 40,000 9664 kB +904
3 60,000 12564 kB +2900
4 80,000 15476 kB +2912
5 100,000 18192 kB +2716
6 120,000 19068 kB +876

~145 B/req sustained across five consecutive batches, unbounded. Under one long-lived connection it is flat for the first ~30,000 requests and then climbs to +15 MB by 150,000.

Why the gate misses it

The gate's entire decision — 1000 warmup + 3 x 2000 measured — happens within the first 7,000 requests. For this leak rate that is entirely inside the window where the leak is absorbed by resident free heap the process already owns (warmup alone adds ~4.2 MB of it). RSS is a proxy for live heap, and the proxy is blind until the slack is exhausted.

It is not per-connection reclaim: the table above closes and reopens a connection per batch and the growth persists.

What makes this sharp

A raw malloc(72) + memset per request in the same function IS caught at 2000-request batches (FAIL: leaked 444 kB, 227 B/req, batches 2384/444/80). So the gate's sensitivity is not a simple function of bytes-per-request — two leaks of identical size in identical positions land on opposite sides of the verdict. I have not pinned the mechanism for that difference and don't want to guess it into the record; determining it is part of this issue.

The practical consequence: the class this gate is the sole instrument for — a leaked Value on a request path, i.e. #731 and #752 — is the class it is weakest against. No sanitizer covers it either (LSan runs atexit; the server dies by signal), so a miss here is a miss everywhere.

Proposed fix

Replace the RSS proxy with exact accounting at the same checkpoints: mallinfo2().uordblks (bytes currently in use by the allocator) is immune to free-heap slack and would show 144 kB for this leak inside a single 2000-request batch. glibc-only, but the gate is already Linux-only (it reads /proc). Needs a small exposure seam — a debug builtin, an endpoint, or an addition to the existing SIGUSR1 dump ([99e]).

Raising MEASURE to 20,000 also works and needs no new surface, but costs ~3 minutes of CI per run and only moves the blind spot to a lower rate rather than removing it.

Whichever lands, it must be validated the way #769's rule was: red on this exact planted fault, green after reverting it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions