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}