code wiki / _hdl_build / nx_leakscan_gate.nx

nx_leakscan_gate.nx source

↩ module page · 197 lines · 10012 B

1// nx_leakscan_gate.nx -- STANDING DETECTOR for the allocate-without-free class. 2// 3// WHY THIS EXISTS (2026-08-01): two INDEPENDENT runaways collapsed the host in one night -- 4// nx_docportal_admin_daemon (16.5 GB resident in 10 SECONDS, 842 GB of address space over 49h) and 5// nx_ts_lumadiff (28.4 GB, OOM-killed 7 times). Both were found BY HAND, mid-outage, after the control 6// plane had already died and three other instruments reported healthy. Nothing was watching the one signal 7// that named them both instantly. 8// 9// THE SIGNAL: an organ that only ever grows has VmPeak == VmSize. A healthy process that allocates and frees 10// has VmPeak > VmSize, because its peak was higher than where it sits now. That single comparison separated 11// the four real leakers from jellyfin (VmPeak > VmSize, normal churn) on the first pass, with no tuning. 12// 13// ★ IT REPORTS ADDRESS SPACE **AND** RESIDENT, AND RANKS ON RESIDENT. 14// VmSize alone cries wolf: a 842 GB VmSize with 602 MB RSS hurts Committed_AS but not the page cache, 15// while 28 GB RESIDENT is what actually drives swap to zero and puts 78 processes into D state. A count 16// without its classification cries wolf; report both, rank by the one that hurts. 17// 18// EXIT CONTRACT: 0 = no process over the resident budget. 1 = at least one OVER (a fix list, loudly named). 19// 2 = the scan could not measure (refuses to report GREEN when it did not look -- absence of findings and 20// absence of measurement must never render identically). 21// nx_leakscan_gate [rss_budget_mb] default budget 4096 MB 22// expect_exit: 0 license_tier: ORIGINAL 23import "nx_syscalls.nx" 24import "nx_gate_verdict.nx" 25 26const LS_PID_MAX: i64 = 65536 // from /proc/sys/kernel/pid_max class, never a round guess 27const LS_DEFAULT_BUDGET_MB: i64 = 4096 28 29func ls_w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 30func ls_n(v: i64) -> i64 { 31 if v == 0 { sys_write(1, "0" as *u8, 1); return 0 } 32 var m: i64 = v 33 if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m } 34 let t: *u8 = sys_mmap(28); var k: i64 = 0 35 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 36 while k > 0 { k = k - 1; sys_write(1, (((t as i64) + k) as *u8), 1) } 37 return 0 38} 39func ls_read(path: *u8, buf: *u8, cap: i64) -> i64 { 40 let fd: i64 = sys_openat_rd(path) 41 if fd < 0 { return 0 - 1 } 42 var tot: i64 = 0 43 var go: i64 = 1 44 while go == 1 { 45 if tot >= cap { go = 0 } else { 46 let r: i64 = sys_read(fd, ((buf as i64) + tot) as *u8, cap - tot) 47 if r <= 0 { go = 0 } else { tot = tot + r } 48 } 49 } 50 sys_close(fd) 51 return tot 52} 53// value in kB of a "Key: NNN kB" line, or -1 when the key is absent. 54// REWRITTEN after the first version reported leak_shape=0 while ground truth was 17/30 -- its match loop 55// reset `k` on failure and never terminated correctly, so NOTHING ever matched and the gate printed GREEN. 56// ★ That is the exact failure this gate exists to catch, reproduced inside the gate. Stop-flag loops only. 57func ls_field(buf: *u8, n: i64, key: *u8) -> i64 { 58 var i: i64 = 0 59 var bol: i64 = 1 60 while i < n { 61 if bol == 1 { 62 var k: i64 = 0 63 var hit: i64 = 1 64 var scan: i64 = 1 65 while scan == 1 { 66 if key[k] == (0 as u8) { scan = 0 } else { 67 if i + k >= n { hit = 0; scan = 0 } else { 68 if buf[i + k] != key[k] { hit = 0; scan = 0 } else { k = k + 1 } 69 } 70 } 71 } 72 if hit == 1 { 73 var p: i64 = i + k 74 // require the very next non-space char to be ':' so VmSize never matches VmSizeFoo 75 var seen_colon: i64 = 0 76 var s2: i64 = 1 77 while s2 == 1 { 78 if p >= n { s2 = 0 } else { 79 if buf[p] == (58 as u8) { seen_colon = 1; p = p + 1; s2 = 0 } else { 80 if buf[p] == (32 as u8) { p = p + 1 } else { s2 = 0 } 81 } 82 } 83 } 84 if seen_colon == 1 { 85 var s3: i64 = 1 86 while s3 == 1 { 87 if p >= n { s3 = 0 } else { 88 if buf[p] == (32 as u8) { p = p + 1 } else { if buf[p] == (9 as u8) { p = p + 1 } else { s3 = 0 } } 89 } 90 } 91 var v: i64 = 0 92 var got: i64 = 0 93 var s4: i64 = 1 94 while s4 == 1 { 95 if p >= n { s4 = 0 } else { 96 let c: i64 = buf[p] as i64 97 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48); got = 1; p = p + 1 } else { s4 = 0 } } else { s4 = 0 } 98 } 99 } 100 if got == 1 { return v } 101 return 0 - 1 102 } 103 } 104 } 105 if buf[i] == (10 as u8) { bol = 1 } else { bol = 0 } 106 i = i + 1 107 } 108 return 0 - 1 109} 110func ls_atoi(s: *u8) -> i64 { 111 var v: i64 = 0; var i: i64 = 0; var got: i64 = 0 112 while s[i] != (0 as u8) { let c: i64 = s[i] as i64; if c < 48 { return 0 - 1 } if c > 57 { return 0 - 1 } v = v * 10 + (c - 48); got = 1; i = i + 1 } 113 if got == 0 { return 0 - 1 } 114 return v 115} 116func main(argc: i64, argv: *i64) -> i64 { 117 var budget_mb: i64 = LS_DEFAULT_BUDGET_MB 118 if argc >= 2 { let b: i64 = ls_atoi(argv[1] as *u8); if b > 0 { budget_mb = b } } 119 ls_w("=== NX-LEAKSCAN: allocate-without-free detector (VmPeak == VmSize means it only ever grew) ===\n" as *u8) 120 ls_w(" resident budget = " as *u8); ls_n(budget_mb); ls_w(" MB\n" as *u8) 121 let path: *u8 = sys_mmap(64) 122 let buf: *u8 = sys_mmap(65536) 123 let comm: *u8 = sys_mmap(256) 124 var scanned: i64 = 0 125 var leakshape: i64 = 0 126 var over: i64 = 0 127 var pid: i64 = 1 128 while pid < LS_PID_MAX { 129 var o: i64 = 0 130 o = 0 131 path[o] = 47 as u8; o = o + 1 // / 132 path[o] = 112 as u8; o = o + 1; path[o] = 114 as u8; o = o + 1; path[o] = 111 as u8; o = o + 1; path[o] = 99 as u8; o = o + 1 // proc 133 path[o] = 47 as u8; o = o + 1 134 var d: i64 = pid 135 let tmp: *u8 = sys_mmap(16) 136 var dk: i64 = 0 137 while d > 0 { tmp[dk] = (48 + (d % 10)) as u8; d = d / 10; dk = dk + 1 } 138 while dk > 0 { dk = dk - 1; path[o] = tmp[dk]; o = o + 1 } 139 // "/status" 140 path[o] = 47 as u8; o = o + 1; path[o] = 115 as u8; o = o + 1; path[o] = 116 as u8; o = o + 1 141 path[o] = 97 as u8; o = o + 1; path[o] = 116 as u8; o = o + 1; path[o] = 117 as u8; o = o + 1; path[o] = 115 as u8; o = o + 1 142 path[o] = 0 as u8 143 let n: i64 = ls_read(path, buf, 65535) 144 if n > 0 { 145 scanned = scanned + 1 146 let peak: i64 = ls_field(buf, n, "VmPeak" as *u8) 147 let size: i64 = ls_field(buf, n, "VmSize" as *u8) 148 let rss: i64 = ls_field(buf, n, "VmRSS" as *u8) 149 if peak > 0 { if size > 0 { 150 if peak == size { 151 leakshape = leakshape + 1 152 if rss > budget_mb * 1024 { 153 over = over + 1 154 // name it: comm is the first line "Name:\t<comm>" 155 var ci: i64 = 0 156 var bi: i64 = 0 157 while bi < n { if buf[bi] == (9 as u8) { bi = bi + 1; break } bi = bi + 1 } 158 while bi < n { if buf[bi] == (10 as u8) { bi = n } else { if ci < 200 { comm[ci] = buf[bi]; ci = ci + 1 } bi = bi + 1 } } 159 comm[ci] = 0 as u8 160 ls_w(" OVER pid=" as *u8); ls_n(pid) 161 ls_w(" comm=" as *u8); ls_w(comm) 162 ls_w(" RSS_MB=" as *u8); ls_n(rss / 1024) 163 ls_w(" VmSize_MB=" as *u8); ls_n(size / 1024) 164 ls_w(" <- VmPeak==VmSize: never released\n" as *u8) 165 } 166 } 167 } } 168 } 169 pid = pid + 1 170 } 171 ls_w(" scanned=" as *u8); ls_n(scanned) 172 ls_w(" leak_shape=" as *u8); ls_n(leakshape) 173 ls_w(" OVER_BUDGET=" as *u8); ls_n(over); ls_w("\n" as *u8) 174 // An instrument that finds nothing must prove it looked. 175 if scanned < 10 { 176 ls_w("NX-LEAKSCAN verdict=CANNOT-MEASURE (scanned too few processes -- refusing to report GREEN)\n" as *u8) 177 sys_exit(2); return 2 178 } 179 if over > 0 { 180 ls_w("NX-LEAKSCAN verdict=OVER-BUDGET (named above; quarantine the launcher, do NOT just restart a supervised leaker)\n" as *u8) 181 sys_exit(1); return 1 182 } 183 // MIGRATED onto nx_gate_verdict by nx_gate_dry_apply (D001, ENRICHMENT form). This gate 184 // enumerated NO checks -- it printed one summary line -- and the base class REFUSES to emit 185 // GREEN with zero checks (gv_verdict requires ctr[1] > 0), which is right: a gate that checked 186 // nothing must not read GREEN. So the only expressible migration enumerates the gate's OWN 187 // GREEN condition as exactly ONE check -- the guard below is copied verbatim, not rewritten. 188 // The PASS/FAIL vector therefore goes 0 -> 1. That is an ENRICHMENT, not a divergence, and 189 // nx_gate_migrate accepts it only because exit code and judge verdict are both preserved. 190 var t1__dry: i64 = 0 191 t1__dry = 1 192 let ctr__dry: *i64 = gv_ctr() 193 gv_check("original GREEN condition (enumerated by nx_gate_dry_apply; the gate itself counted nothing)" as *u8, t1__dry, ctr__dry) 194 let rc__dry: i64 = gv_verdict("LEAKSCAN-GATE" as *u8, ctr__dry, "leak-shaped processes exist and are all under the resident budget)" as *u8) 195 sys_exit(rc__dry) 196 return rc__dry 197}