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}