code wiki / (root) / nx_mmapbal.nx

nx_mmapbal.nx source

↩ module page · 342 lines · 13804 B

1// nx_mmapbal.nx -- STATIC mmap/munmap BALANCE SCANNER over the .nx corpus. 2// 3// WHY THIS EXISTS (measured 2026-07-31, debt 1785515861): the ecosystem's ONLY leak instrument, 4// nx_leak_check, is DYNAMIC and daemon-only -- its `fleet` verb certifies organs that are RUNNING and 5// a LEAK verdict requires LK_MIN_AGE_S=120s of process age. One-shot organs (5365 gates + every CLI 6// organ) live for milliseconds and exit, so they can NEVER appear in a fleet scan and can NEVER reach 7// the age floor. That hole is why nx_gate_verdict.nx -- the D001 base class 150+ organs import -- 8// leaked on EVERY call for ~13 days while every instrument reported healthy. Dynamic and static 9// detection are not redundant here; they cover disjoint halves of the fleet. 10// 11// THE RULE, stated once so it cannot drift: within ONE function, every sys_mmap must be matched by a 12// sys_munmap, UNLESS the signature RETURNS A POINTER -- that is ownership TRANSFER to the caller, not 13// a leak (gv_ctr is the correct exemplar: it returns the counter, which must outlive the call). A 14// function that allocates and returns i64 has no way to hand the memory back, so an unmatched mmap 15// there is a leak BY CONSTRUCTION, provable from the source alone with no runtime sampling. 16// 17// main() IS REPORTED SEPARATELY, NOT CONVICTED: it runs once per process and the kernel reclaims at 18// exit, so an unbalanced mmap in main is a style note, not a defect. Convicting it would bury the 19// real signal (helpers called in loops) under noise -- the ranked-wrong-thing failure. 20// 21// COMMENT LINES ARE SKIPPED so a header that merely MENTIONS sys_mmap cannot fabricate a finding. 22// 23// ⚠ KNOWN LIMITATION -- OWNERSHIP VIA STRUCT FIELD IS INVISIBLE (measured 2026-07-31). 24// mb_owns detects ownership transfer ONLY through a `-> *` RETURN TYPE. A function that hands its 25// allocation to the caller by STORING IT IN AN OUT-PARAM STRUCT FIELD is reported as a leak even 26// though it is correct. PROVEN CASE: nx_opaque_login.olg_ctx_setup_ttl mmaps 5 key buffers and 27// passes 3 to nx_auth_context_init, which does `ctx.opaque_skS_32 = ...` / `ctx.server_ed25519_priv_32 28// = ...` -- storing the POINTERS, not copying the bytes. Those live for the whole daemon lifetime; 29// freeing them would be a use-after-free on every login and would corrupt the server signing key. 30// CONSEQUENCE: *_init / *_ctx_setup / *_new functions are the FALSE-POSITIVE class of this scanner. 31// A headline count that does not exclude them OVERSTATES the defect. Treat any finding in an 32// initializer as UNPROVEN until the callee is read. Fixing this properly needs callee-aware 33// escape analysis (does the callee store the pointer?), which is a real rung, not a tweak. 34// 35// FAIL-LOUD ON COVERAGE (law L011): prints files_scanned / files_skipped / coverage_complete. A corpus 36// scanner that hides partial coverage presents partial-as-complete, which is the self-ceiling defect. 37// 38// nx_mmapbal scan [dir] walk dir (default buildroot/runtime) for .nx, report unbalanced funcs 39// 40// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0 41// LAYERING (law): runtime/ CANNOT import _hdl_build/, and nx_gate_verdict_lib.nx (the verdict READER, 42// carrying gv_slurp/gv_len) is misplaced in _hdl_build/ -- so this organ cannot reuse it from here and 43// carries a minimal local slurp instead. That is the layering law winning over rule 15, not a DRY miss. 44// Lifting that lib to runtime/ via nx_liblift is filed separately; it is not safe to move mid-flight 45// while a sibling lane holds nx_gate_rollup, which imports it. 46import "nx_syscalls.nx" 47import "nx_gate_verdict.nx" 48 49const MB_DIRBUF: i64 = 262144 50const MB_FILECAP: i64 = 524288 51const MB_PATHCAP: i64 = 1024 52const MB_DIR_TYPE: i64 = 4 53const MB_NL_C: i64 = 10 54const MB_SLASH_C: i64 = 47 55const MB_SPACE_C: i64 = 32 56const MB_TAB_C: i64 = 9 57const MB_DOT_C: i64 = 46 58const MB_N_C: i64 = 110 59const MB_X_C: i64 = 120 60const MB_WORD: i64 = 8 61const MB_ST_SLOTS: i64 = 8 62const MB_S_FILES: i64 = 0 63const MB_S_FUNCS: i64 = 1 64const MB_S_BAD: i64 = 2 65const MB_S_MAIN: i64 = 3 66const MB_S_SKIP: i64 = 4 67const MB_S_SITES: i64 = 5 68 69func mb_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 70 71func mb_streq(a: *u8, b: *u8) -> i64 { 72 var i: i64 = 0 73 while a[i] != (0 as u8) { 74 if a[i] != b[i] { return 0 } 75 i = i + 1 76 } 77 if b[i] != (0 as u8) { return 0 } 78 return 1 79} 80 81// read a whole file into buf; returns bytes read, 0 if unreadable. FAIL-CLOSED: an unreadable file is 82// counted as SKIPPED and drops coverage_complete to 0 -- it is never silently treated as clean. 83func mb_slurp(path: *u8, buf: *u8, cap: i64) -> i64 { 84 let fd: i64 = sys_openat_rd(path) 85 if fd < 0 { return 0 } 86 var tot: i64 = 0 87 var go: i64 = 1 88 while go == 1 { 89 let r: i64 = sys_read(fd, ((buf as i64) + tot) as *u8, cap - tot) 90 if r <= 0 { go = 0 } else { tot = tot + r } 91 if tot >= cap { go = 0 } 92 } 93 sys_close(fd) 94 return tot 95} 96 97func mb_line_end(buf: *u8, s: i64, e: i64) -> i64 { 98 var i: i64 = s 99 while i < e { 100 if buf[i] == (MB_NL_C as u8) { return i } 101 i = i + 1 102 } 103 return e 104} 105 106func mb_starts(buf: *u8, s: i64, e: i64, needle: *u8) -> i64 { 107 var k: i64 = 0 108 while needle[k] != (0 as u8) { 109 if s + k >= e { return 0 } 110 if buf[s+k] != needle[k] { return 0 } 111 k = k + 1 112 } 113 return 1 114} 115 116// leading whitespace then "//" = a comment line; its mentions of sys_mmap are prose, not code 117func mb_is_comment(buf: *u8, s: i64, e: i64) -> i64 { 118 var i: i64 = s 119 var go: i64 = 1 120 while go == 1 { 121 if i >= e { return 0 } 122 let c: i64 = buf[i] as i64 123 if c == MB_SPACE_C { i = i + 1 } else { 124 if c == MB_TAB_C { i = i + 1 } else { go = 0 } 125 } 126 } 127 return mb_starts(buf, i, e, "//" as *u8) 128} 129 130func mb_count_range(buf: *u8, s: i64, e: i64, needle: *u8) -> i64 { 131 var cnt: i64 = 0 132 var ls: i64 = s 133 while ls < e { 134 let le: i64 = mb_line_end(buf, ls, e) 135 if mb_is_comment(buf, ls, le) == 0 { 136 var i: i64 = ls 137 while i < le { 138 if mb_starts(buf, i, le, needle) == 1 { cnt = cnt + 1 } 139 i = i + 1 140 } 141 } 142 ls = le + 1 143 } 144 return cnt 145} 146 147// signature returns a pointer => ownership TRANSFER to the caller, which is correct, not a leak 148func mb_owns(buf: *u8, s: i64, e: i64) -> i64 { 149 var i: i64 = s 150 while i < e { 151 if mb_starts(buf, i, e, "-> *" as *u8) == 1 { return 1 } 152 i = i + 1 153 } 154 return 0 155} 156 157func mb_put_range(buf: *u8, s: i64, e: i64) -> i64 { 158 sys_write(1, ((buf as i64) + s) as *u8, e - s) 159 return 0 160} 161 162func mb_is_nx(nm: *u8, n: i64) -> i64 { 163 if n < 4 { return 0 } 164 if nm[n-3] != (MB_DOT_C as u8) { return 0 } 165 if nm[n-2] != (MB_N_C as u8) { return 0 } 166 if nm[n-1] != (MB_X_C as u8) { return 0 } 167 return 1 168} 169 170func mb_join(dir: *u8, nm: *u8, out: *u8) -> i64 { 171 var o: i64 = 0 172 var i: i64 = 0 173 while dir[i] != (0 as u8) { out[o] = dir[i]; o = o + 1; i = i + 1 } 174 out[o] = MB_SLASH_C as u8; o = o + 1 175 i = 0 176 while nm[i] != (0 as u8) { out[o] = nm[i]; o = o + 1; i = i + 1 } 177 out[o] = 0 as u8 178 return o 179} 180 181// scan one file buffer for unbalanced functions; accumulates into st 182func mb_scan_buf(path: *u8, buf: *u8, n: i64, st: *i64) -> i64 { 183 var ls: i64 = 0 184 while ls < n { 185 let le: i64 = mb_line_end(buf, ls, n) 186 if mb_starts(buf, ls, le, "func " as *u8) == 1 { 187 var fe: i64 = le + 1 188 var scanning: i64 = 1 189 while scanning == 1 { 190 if fe >= n { scanning = 0 } else { 191 let xe: i64 = mb_line_end(buf, fe, n) 192 if mb_starts(buf, fe, xe, "func " as *u8) == 1 { scanning = 0 } else { fe = xe + 1 } 193 } 194 } 195 st[MB_S_FUNCS] = st[MB_S_FUNCS] + 1 196 let na: i64 = mb_count_range(buf, ls, fe, "sys_mmap(" as *u8) 197 let nf: i64 = mb_count_range(buf, ls, fe, "sys_munmap(" as *u8) 198 if na > nf { 199 if mb_owns(buf, ls, le) == 0 { 200 if mb_starts(buf, ls, le, "func main(" as *u8) == 1 { 201 st[MB_S_MAIN] = st[MB_S_MAIN] + 1 202 } else { 203 st[MB_S_BAD] = st[MB_S_BAD] + 1 204 st[MB_S_SITES] = st[MB_S_SITES] + na - nf 205 gv_puts(" UNBALANCED " as *u8) 206 gv_puts(path) 207 gv_puts("\n " as *u8) 208 mb_put_range(buf, ls, le) 209 gv_puts("\n mmap=" as *u8) 210 gv_num(na) 211 gv_puts(" munmap=" as *u8) 212 gv_num(nf) 213 gv_puts(" leaked_sites=" as *u8) 214 gv_num(na - nf) 215 gv_puts("\n" as *u8) 216 } 217 } 218 } 219 ls = fe 220 } else { 221 ls = le + 1 222 } 223 } 224 return 0 225} 226 227func main(argc: i64, argv: *i64) -> i64 { 228 var dir: *u8 = "buildroot/runtime" as *u8 229 if argc >= 3 { dir = argv[2] as *u8 } 230 231 // SINGLE-FILE MODE: `nx_mmapbal file <path>`. Added 2026-07-31 because a TOTAL-COUNT delta over a 232 // SHARED tree is NOT attributable to one lane. Measured: between two scans of buildroot/runtime, 233 // sibling seats added 7 files / 84 funcs / 13 unbalanced funcs, masking this lane's -3 fix under a 234 // +16 drift -- and directory (getdents) order is not stable across runs either, so absence at a byte 235 // offset proves nothing. Per-file scanning is the ONLY sound way to verify one fix on a live corpus. 236 if argc >= 3 { 237 if mb_streq(argv[1] as *u8, "file" as *u8) == 1 { 238 let one: *u8 = argv[2] as *u8 239 let fb1: *u8 = sys_mmap(MB_FILECAP) 240 let st1: *i64 = sys_mmap(MB_ST_SLOTS * MB_WORD) as *i64 241 var z1: i64 = 0 242 while z1 < MB_ST_SLOTS { st1[z1] = 0; z1 = z1 + 1 } 243 gv_puts("=== NX-MMAPBAL -- single file " as *u8) 244 gv_puts(one) 245 gv_puts(" ===\n" as *u8) 246 let n1: i64 = mb_slurp(one, fb1, MB_FILECAP) 247 if n1 <= 0 { 248 gv_puts("verdict=RED rule=file-unreadable\n" as *u8) 249 sys_exit(1) 250 return 1 251 } 252 st1[MB_S_FILES] = 1 253 mb_scan_buf(one, fb1, n1, st1) 254 gv_puts("\nfiles_scanned=1 funcs_scanned=" as *u8) 255 gv_num(st1[MB_S_FUNCS]) 256 gv_puts("\nunbalanced_funcs=" as *u8) 257 gv_num(st1[MB_S_BAD]) 258 gv_puts(" leaked_sites=" as *u8) 259 gv_num(st1[MB_S_SITES]) 260 gv_puts(" main_scope_informational=" as *u8) 261 gv_num(st1[MB_S_MAIN]) 262 gv_puts("\ncoverage_complete=1\n\n" as *u8) 263 let c1: *i64 = gv_ctr() 264 gv_check("F1 no unbalanced non-owning function in this file", st1[MB_S_BAD] == 0, c1) 265 let r1: i64 = gv_verdict("MMAPBAL-FILE", c1, "every mmap matched by a munmap or handed to the caller") 266 sys_exit(r1) 267 return r1 268 } 269 } 270 271 let fd: i64 = sys_openat_rd(dir) 272 if fd < 0 { 273 gv_puts("NX-MMAPBAL\nverdict=RED rule=scandir-absent dir=" as *u8) 274 gv_puts(dir) 275 gv_puts("\n" as *u8) 276 sys_exit(1) 277 return 1 278 } 279 280 let dbuf: *u8 = sys_mmap(MB_DIRBUF) 281 let fbuf: *u8 = sys_mmap(MB_FILECAP) 282 let path: *u8 = sys_mmap(MB_PATHCAP) 283 let st: *i64 = sys_mmap(MB_ST_SLOTS * MB_WORD) as *i64 284 var z: i64 = 0 285 while z < MB_ST_SLOTS { st[z] = 0; z = z + 1 } 286 287 gv_puts("=== NX-MMAPBAL -- static mmap/munmap balance over " as *u8) 288 gv_puts(dir) 289 gv_puts(" ===\n" as *u8) 290 291 var go: i64 = 1 292 while go == 1 { 293 let nr: i64 = sys_getdents64(fd, dbuf, MB_DIRBUF) 294 if nr <= 0 { go = 0 } else { 295 var off: i64 = 0 296 while off < nr { 297 let rec: *u8 = ((dbuf as i64) + off) as *u8 298 let ty: i64 = dirent_type(rec) 299 let nm: *u8 = dirent_name(rec) 300 let nmn: i64 = mb_len(nm) 301 if ty != MB_DIR_TYPE { 302 if mb_is_nx(nm, nmn) == 1 { 303 mb_join(dir, nm, path) 304 let bn: i64 = mb_slurp(path, fbuf, MB_FILECAP) 305 if bn <= 0 { 306 st[MB_S_SKIP] = st[MB_S_SKIP] + 1 307 } else { 308 st[MB_S_FILES] = st[MB_S_FILES] + 1 309 mb_scan_buf(path, fbuf, bn, st) 310 } 311 } 312 } 313 off = off + dirent_reclen(rec) 314 } 315 } 316 } 317 sys_close(fd) 318 319 gv_puts("\nfiles_scanned=" as *u8) 320 gv_num(st[MB_S_FILES]) 321 gv_puts(" files_skipped=" as *u8) 322 gv_num(st[MB_S_SKIP]) 323 gv_puts(" funcs_scanned=" as *u8) 324 gv_num(st[MB_S_FUNCS]) 325 gv_puts("\nunbalanced_funcs=" as *u8) 326 gv_num(st[MB_S_BAD]) 327 gv_puts(" leaked_sites=" as *u8) 328 gv_num(st[MB_S_SITES]) 329 gv_puts(" main_scope_informational=" as *u8) 330 gv_num(st[MB_S_MAIN]) 331 gv_puts("\ncoverage_complete=" as *u8) 332 if st[MB_S_SKIP] == 0 { gv_puts("1" as *u8) } else { gv_puts("0" as *u8) } 333 gv_puts("\n\n" as *u8) 334 335 let ctr: *i64 = gv_ctr() 336 gv_check("C1 corpus readable (at least one .nx scanned)", st[MB_S_FILES] > 0, ctr) 337 gv_check("C2 no unbalanced non-owning function", st[MB_S_BAD] == 0, ctr) 338 gv_check("C3 coverage complete (zero unreadable files)", st[MB_S_SKIP] == 0, ctr) 339 let rc: i64 = gv_verdict("MMAPBAL", ctr, "every mmap matched by a munmap or handed to the caller") 340 sys_exit(rc) 341 return rc 342}