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.
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)inbuiltin_shared_incr(src/ext_http.c:640) — the literal #731 shape. A probe confirms this is a heap allocation, not an arena one:(
g_arena.activeis only ever set by the user-callablearena_markbuiltin atbuiltins.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:
It is unmistakably a leak — same binary, same gate methodology (a separate connection per batch), 20,000-request batches:
~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)+memsetper 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
MEASUREto 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.