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.
nx_mmapbal scan [dir] walk dir (default buildroot/runtime) for .nx, report unbalanced funcs
license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
dependencies 2 imports · 0 importers
imports: nx_syscalls.nxnx_gate_verdict.nx
imported by: nobody (leaf or entry point)
structs
| none |
consts
| 49 | const MB_DIRBUF: i64 = 262144 |
| 50 | const MB_FILECAP: i64 = 524288 |
| 51 | const MB_PATHCAP: i64 = 1024 |
| 52 | const MB_DIR_TYPE: i64 = 4 |
| 53 | const MB_NL_C: i64 = 10 |
| 54 | const MB_SLASH_C: i64 = 47 |
| 55 | const MB_SPACE_C: i64 = 32 |
| 56 | const MB_TAB_C: i64 = 9 |
| 57 | const MB_DOT_C: i64 = 46 |
| 58 | const MB_N_C: i64 = 110 |
| 59 | const MB_X_C: i64 = 120 |
| 60 | const MB_WORD: i64 = 8 |
| 61 | const MB_ST_SLOTS: i64 = 8 |
| 62 | const MB_S_FILES: i64 = 0 |
| 63 | const MB_S_FUNCS: i64 = 1 |
| 64 | const MB_S_BAD: i64 = 2 |
| 65 | const MB_S_MAIN: i64 = 3 |
| 66 | const MB_S_SKIP: i64 = 4 |
| 67 | const MB_S_SITES: i64 = 5 |
functions
| 69 | func mb_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } |
| 71 | func mb_streq(a: *u8, b: *u8) -> i64 |
| 83 | func mb_slurp(path: *u8, buf: *u8, cap: i64) -> i64 |
| 97 | func mb_line_end(buf: *u8, s: i64, e: i64) -> i64 |
| 106 | func mb_starts(buf: *u8, s: i64, e: i64, needle: *u8) -> i64 called by 1: mb_is_comment |
| 117 | func mb_is_comment(buf: *u8, s: i64, e: i64) -> i64 |
| 130 | func mb_count_range(buf: *u8, s: i64, e: i64, needle: *u8) -> i64 |
| 148 | func mb_owns(buf: *u8, s: i64, e: i64) -> i64 |
| 157 | func mb_put_range(buf: *u8, s: i64, e: i64) -> i64 |
| 162 | func mb_is_nx(nm: *u8, n: i64) -> i64 |
| 170 | func mb_join(dir: *u8, nm: *u8, out: *u8) -> i64 |
| 182 | func mb_scan_buf(path: *u8, buf: *u8, n: i64, st: *i64) -> i64 |
| 227 | func main(argc: i64, argv: *i64) -> i64 |