nx_mmapbal.nx
buildroot/runtime/nx_mmapbal.nx
about
nx_mmapbal.nx -- STATIC mmap/munmap BALANCE SCANNER over the .nx corpus.
WHY THIS EXISTS (measured 2026-07-31, debt 1785515861): the ecosystem's ONLY leak instrument,
nx_leak_check, is DYNAMIC and daemon-only -- its `fleet` verb certifies organs that are RUNNING and
a LEAK verdict requires LK_MIN_AGE_S=120s of process age. One-shot organs (5365 gates + every CLI
organ) live for milliseconds and exit, so they can NEVER appear in a fleet scan and can NEVER reach
the age floor. That hole is why nx_gate_verdict.nx -- the D001 base class 150+ organs import --
leaked on EVERY call for ~13 days while every instrument reported healthy. Dynamic and static
detection are not redundant here; they cover disjoint halves of the fleet.
THE RULE, stated once so it cannot drift: within ONE function, every sys_mmap must be matched by a
sys_munmap, UNLESS the signature RETURNS A POINTER -- that is ownership TRANSFER to the caller, not
a leak (gv_ctr is the correct exemplar: it returns the counter, which must outlive the call). A
function that allocates and returns i64 has no way to hand the memory back, so an unmatched mmap
there is a leak BY CONSTRUCTION, provable from the source alone with no runtime sampling.
main() IS REPORTED SEPARATELY, NOT CONVICTED: it runs once per process and the kernel reclaims at
exit, so an unbalanced mmap in main is a style note, not a defect. Convicting it would bury the
real signal (helpers called in loops) under noise -- the ranked-wrong-thing failure.
COMMENT LINES ARE SKIPPED so a header that merely MENTIONS sys_mmap cannot fabricate a finding.
⚠ KNOWN LIMITATION -- OWNERSHIP VIA STRUCT FIELD IS INVISIBLE (measured 2026-07-31).
mb_owns detects ownership transfer ONLY through a `-> *` RETURN TYPE. A function that hands its
allocation to the caller by STORING IT IN AN OUT-PARAM STRUCT FIELD is reported as a leak even
though it is correct. PROVEN CASE: nx_opaque_login.olg_ctx_setup_ttl mmaps 5 key buffers and
passes 3 to nx_auth_context_init, which does `ctx.opaque_skS_32 = ...` / `ctx.server_ed25519_priv_32
= ...` -- storing the POINTERS, not copying the bytes. Those live for the whole daemon lifetime;
freeing them would be a use-after-free on every login and would corrupt the server signing key.
CONSEQUENCE: *_init / *_ctx_setup / *_new functions are the FALSE-POSITIVE class of this scanner.
A headline count that does not exclude them OVERSTATES the defect. Treat any finding in an
initializer as UNPROVEN until the callee is read. Fixing this properly needs callee-aware
escape analysis (does the callee store the pointer?), which is a real rung, not a tweak.
FAIL-LOUD ON COVERAGE (law L011): prints files_scanned / files_skipped / coverage_complete. A corpus
scanner that hides partial coverage presents partial-as-complete, which is the self-ceiling defect.
WHY `deep` EXISTS AND `scan` CANNOT SUBSTITUTE FOR IT (measured 2026-08-14): `scan` streams every
finding and its summary block sits BELOW that stream, so BOTH transports truncate before the totals
are reached -- the MCP capture cap at 163,840 B and the async job capture cap at 1 MiB, both hit on
dependencies 2 imports · 0 importers
imports: nx_syscalls.nxnx_gate_verdict.nx
imported by: nobody (leaf or entry point)
structs
| none |
consts
| 64 | const MB_DIRBUF: i64 = 262144 |
| 65 | const MB_FILECAP: i64 = 524288 |
| 66 | const MB_PATHCAP: i64 = 1024 |
| 67 | const MB_DIR_TYPE: i64 = 4 |
| 68 | const MB_NL_C: i64 = 10 |
| 69 | const MB_SLASH_C: i64 = 47 |
| 70 | const MB_SPACE_C: i64 = 32 |
| 71 | const MB_TAB_C: i64 = 9 |
| 72 | const MB_DOT_C: i64 = 46 |
| 73 | const MB_N_C: i64 = 110 |
| 74 | const MB_X_C: i64 = 120 |
| 75 | const MB_WORD: i64 = 8 |
| 76 | const MB_ST_SLOTS: i64 = 32 |
| 77 | const MB_S_FILES: i64 = 0 |
| 78 | const MB_S_FUNCS: i64 = 1 |
| 79 | const MB_S_BAD: i64 = 2 |
| 80 | const MB_S_MAIN: i64 = 3 |
| 81 | const MB_S_SKIP: i64 = 4 |
| 82 | const MB_S_SITES: i64 = 5 |
| 84 | const MB_S_TRUNC: i64 = 6 // files that FILLED the read cap, i.e. were silently cut short |
| 85 | const MB_S_INIT: i64 = 7 // unbalanced funcs whose name marks them the documented FP class |
| 86 | const MB_S_INITSITES: i64 = 8 // their sites, kept OUT of the convictable headline |
| 87 | const MB_S_DIRS: i64 = 9 // directories walked (1 == the old non-recursive behaviour) |
| 88 | const MB_S_QOVF: i64 = 10 // directory-queue overflows; any is a coverage hole |
| 92 | const MB_S_C_SHIM: i64 = 11 // calls ccz_cat_num but hand-rolls the mmap'd fd shim -> nxi_fd drop-in |
| 93 | const MB_S_C_SHIMS: i64 = 12 |
| 94 | const MB_S_C_INLINE: i64 = 13 // open-coded %10 digit loop into an mmap'd scratch -> same output, needs proof |
| 95 | const MB_S_C_INLINES: i64 = 14 |
| 96 | const MB_S_C_OTHER: i64 = 15 // everything else: a real buffer with a real lifetime, no free |
| 97 | const MB_S_C_OTHERS: i64 = 16 |
| 102 | const MB_QCAP: i64 = 512 |
| 103 | const MB_DOT_ENT: *u8 = "." |
| 104 | const MB_DOTDOT_ENT: *u8 = ".." |
| 177 | const MB_QUOTE_C: i64 = 34 |
| 178 | const MB_BSLASH_C: i64 = 92 |
| 259 | const MB_ARENA_MAX: i64 = NXA_SMALL_MAX |
| 260 | const MB_S_W_ARENA: i64 = 17 |
| 261 | const MB_S_W_PAGE: i64 = 18 |
| 262 | const MB_S_W_UNK: i64 = 19 |
| 263 | const MB_S_W_TOT: i64 = 20 |
| 279 | const MB_S_ARENAONLY: i64 = 21 |
| 280 | const MB_S_ARENAONLYSITES: i64 = 22 |
| 294 | const MB_S_HOT: i64 = 23 // union: the worklist |
| 295 | const MB_S_HOT_LOOP: i64 = 24 // reason A: lexically inside a while body |
| 296 | const MB_S_HOT_SRV: i64 = 25 // reason B: in a NON-forking server (handlers run in the long-lived parent) |
| 297 | const MB_S_SRV_FORK: i64 = 26 // EXCLUDED: server that forks per connection -- the child's exit frees it |
| 313 | const MB_RATCHET_PATH: *u8 = "knowledge/status/mmapbal_pagebacked.conf" |
| 314 | const MB_RATCHET_CAP: i64 = 4096 |
| 322 | const MBC_MAX: i64 = 1024 |
| 323 | const MBC_NAMELEN: i64 = 48 |
| 579 | const MB_LOOPDEPTH_MAX: i64 = 64 |
| 591 | const MB_BRACE_OPEN: i64 = 123 |
| 592 | const MB_BRACE_CLOSE: i64 = 125 |
functions
| 106 | func mb_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } |
| 108 | func mb_streq(a: *u8, b: *u8) -> i64 |
| 120 | func mb_slurp(path: *u8, buf: *u8, cap: i64) -> i64 |
| 134 | func mb_line_end(buf: *u8, s: i64, e: i64) -> i64 |
| 143 | func mb_starts(buf: *u8, s: i64, e: i64, needle: *u8) -> i64 called by 1: mb_is_comment |
| 154 | func mb_is_comment(buf: *u8, s: i64, e: i64) -> i64 |
| 179 | func mb_count_range(buf: *u8, s: i64, e: i64, needle: *u8) -> i64 |
| 207 | func mb_owns(buf: *u8, s: i64, e: i64) -> i64 |
| 216 | func mb_put_range(buf: *u8, s: i64, e: i64) -> i64 |
| 221 | func mb_is_nx(nm: *u8, n: i64) -> i64 |
| 229 | func mb_join(dir: *u8, nm: *u8, out: *u8) -> i64 |
| 329 | func mbc_init() -> i64 |
| 338 | func mbc_isident(c: i64) -> i64 |
| 348 | func mbc_scan(buf: *u8, n: i64) -> i64 |
| 413 | func mbc_lookup(buf: *u8, s: i64, e: i64) -> i64 |
| 443 | func mb_weigh_range(buf: *u8, s: i64, e: i64, st: *i64) -> i64 |
| 508 | func mb_read_floor() -> i64 |
| 528 | func mb_write_floor(v: i64) -> i64 |
| 581 | func mb_while_at(buf: *u8, i: i64, e: i64) -> i64 |
| 598 | func mb_loop_allocs(buf: *u8, s: i64, e: i64) -> i64 |
| 655 | func mb_ends_at(buf: *u8, s: i64, e: i64, needle: *u8) -> i64 |
| 673 | func mb_is_initclass(buf: *u8, s: i64, e: i64) -> i64 |
| 690 | func mb_scan_buf(path: *u8, buf: *u8, n: i64, st: *i64, mode: i64) -> i64 |
| 882 | func mb_walk(root: *u8, st: *i64, mode: i64) -> i64 |
| 954 | func main(argc: i64, argv: *i64) -> i64 |