code wiki / _hdl_build / nx_hget_leak_probe.nx
nx_hget_leak_probe.nx source
↩ module page · 110 lines · 5185 B
1// nx_hget_leak_probe.nx -- MEASURES the per-call allocation leak in ss_hget (2026-07-30).
2//
3// WHAT IT PROVES: ss_hget used to sys_mmap TWO scratch buffers PER INVOCATION and never free them.
4// sys_mmap is a real mmap syscall (nx_syscalls.nx:167), not a bump allocator, so the kernel rounds
5// each to a full page => ~8 KB leaked PER CALL. ss_hget is the hottest read primitive in the tree
6// (205 call sites; the loaders call it ONCE PER ROW), so the leak scales with ROWS SCANNED, not with
7// stores opened -- which is why it hid behind the per-open leak everyone was looking at.
8//
9// THE MEASUREMENT IS THE POINT: run N hgets and read our OWN VSZ before and after. A fixed ss_hget
10// must show delta_kb == 0 (scratch allocated once, on first call). The pre-fix code would show
11// roughly N * 8 KB. Reuses nx_leak_check_lib (lc_vsz) rather than re-deriving /proc parsing -- rule 15.
12//
13// NON-VACUITY: the loop must do REAL work, so the probe seeds a store, reads a key back, and asserts
14// the VALUE is correct. A probe that measured memory while looking up a missing key would pass while
15// exercising almost nothing (ss_hget returns early on ns==0, before the allocations).
16import "nx_syscalls.nx"
17import "nx_seg_store.nx"
18import "nx_vsz_watchdog_core.nx" // vw_vmsize_kb_of("self") -- reads /proc/<dirname>/status
19const HL_MAGIC_1024: i64 = 1024
20
21const HL_ITERS: i64 = 4000 // enough that a 8KB/call leak is unmissable (~32 MB)
22const HL_TOLERANCE_KB: i64 = 64 // page-granular slack; a real leak here is orders of magnitude bigger
23
24func hl_puts(s: *u8) -> i64 {
25 var n: i64 = 0
26 while s[n] != (0 as u8) { n = n + 1 }
27 sys_write(1, s, n)
28 return 0
29}
30func hl_putn(v: i64) -> i64 {
31 let b: *u8 = sys_mmap(32)
32 var m: i64 = v
33 if m < 0 { hl_puts("-" as *u8); m = 0 - m }
34 if m == 0 { b[0] = 48 as u8; sys_write(1, b, 1); return 0 }
35 let t: *u8 = sys_mmap(32)
36 var k: i64 = 0
37 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
38 var i: i64 = 0
39 while i < k { b[i] = t[k - 1 - i]; i = i + 1 }
40 sys_write(1, b, k)
41 return 0
42}
43
44func main() -> i64 {
45 let prefix: *u8 = "/tmp/hlprobe-" as *u8
46 // seed a tiny store so the hgets below hit REAL segments (non-vacuity: ns must be > 0)
47 let w: *i64 = ss_begin()
48 ss_add(w, 1, "alpha" as *u8, "value-alpha" as *u8, 11)
49 ss_add(w, 1, "bravo" as *u8, "value-bravo" as *u8, 11)
50 ss_commit(prefix, w, sys_now_ms())
51
52 let h: *i64 = ss_open(prefix)
53 if (h as i64) == 0 { hl_puts("HGET-LEAK-PROBE SETUP-FAIL (no handle)\n" as *u8); sys_exit(2); return 2 }
54
55 let pp: *i64 = sys_mmap(16) as *i64
56 let ll: *i64 = sys_mmap(16) as *i64
57
58 // WARM: first call allocates the static scratch once. Measuring after this isolates the
59 // PER-CALL behaviour from the one-time allocation, which is exactly the distinction under test.
60 if ss_hget(h, "alpha" as *u8, pp, ll) != 1 {
61 hl_puts("HGET-LEAK-PROBE SETUP-FAIL (warm hget missed)\n" as *u8); sys_exit(2); return 2
62 }
63
64 // ⚠THE INSTRUMENT MUST PROVE ITSELF FIRST (caught live 2026-07-30: the first cut of this probe
65 // called lc_vsz(0), which builds /proc/0/status -- a path that does not exist -- so BOTH readings
66 // came back -1, delta computed as 0, and the probe printed GREEN having measured NOTHING. A gate
67 // that cannot take a reading must go RED, never pass by arithmetic accident on two error codes.
68 // /proc/self needs no pid, and there is no sys_getpid in the tree.)
69 let before: i64 = vw_vmsize_kb_of("self" as *u8)
70 if before <= 0 {
71 hl_puts("HGET-LEAK-PROBE verdict=RED (INSTRUMENT-DEAD: cannot read VmSize; measured nothing)\n" as *u8)
72 sys_exit(4); return 4
73 }
74 var i: i64 = 0
75 var hits: i64 = 0
76 while i < HL_ITERS {
77 if ss_hget(h, "alpha" as *u8, pp, ll) == 1 { hits = hits + 1 }
78 if ss_hget(h, "bravo" as *u8, pp, ll) == 1 { hits = hits + 1 }
79 i = i + 1
80 }
81 let after: i64 = vw_vmsize_kb_of("self" as *u8)
82 if after <= 0 {
83 hl_puts("HGET-LEAK-PROBE verdict=RED (INSTRUMENT-DEAD: post-reading unavailable)\n" as *u8)
84 sys_exit(4); return 4
85 }
86 let delta: i64 = after - before
87 let calls: i64 = HL_ITERS * 2
88
89 // NON-VACUITY: every lookup must have actually resolved, or the loop proved nothing.
90 hl_puts("hget calls=" as *u8); hl_putn(calls)
91 hl_puts(" hits=" as *u8); hl_putn(hits)
92 hl_puts(" vsz_before_kb=" as *u8); hl_putn(before)
93 hl_puts(" vsz_after_kb=" as *u8); hl_putn(after)
94 hl_puts(" delta_kb=" as *u8); hl_putn(delta)
95 hl_puts("\n" as *u8)
96 if hits != calls {
97 hl_puts("HGET-LEAK-PROBE verdict=RED (VACUOUS: lookups did not resolve)\n" as *u8)
98 sys_exit(3); return 3
99 }
100 if delta > HL_TOLERANCE_KB {
101 hl_puts("HGET-LEAK-PROBE verdict=RED -- ss_hget LEAKS ~" as *u8)
102 hl_putn((delta * HL_MAGIC_1024) / calls)
103 hl_puts(" bytes/call\n" as *u8)
104 sys_exit(1); return 1
105 }
106 hl_puts("HGET-LEAK-PROBE verdict=GREEN (delta_kb<=" as *u8); hl_putn(HL_TOLERANCE_KB)
107 hl_puts(" over " as *u8); hl_putn(calls); hl_puts(" real hgets)\n" as *u8)
108 sys_exit(0)
109 return 0
110}