code wiki / _hdl_build / nx_dup_source_check_v3.nx

nx_dup_source_check_v3.nx source

↩ module page · 309 lines · 16203 B

1// ORPHAN FORK -- NOT THE FILE THAT RUNS. Adjudicated 2026-08-16 by measurement, not preference: 2// * ZERO references to "nx_dup_source_check_v3" anywhere in buildroot -- nx_shelltool over 56,497 3// files, corpus_complete=1. Nothing imports it, no registry or conf names it. 4// * The REGISTERED tool's banner ("scope DISCOVERED; allocation O(pairs) not O(files)") is emitted 5// by nx_dup_source_check.nx, NOT by this file's header text. The name that ships is the SHORT one. 6// ==> nx_dup_source_check.nx is CANONICAL. This file is dead weight kept only because retiring it 7// needs nx_retire_path (rename-only, reversible), which the seat that found this could not call. 8// ACTION: `nx_retire_path retire buildroot/runtime/_hdl_build/nx_dup_source_check_v3.nx`. 9// A THIRD variant exists laptop-side (nxc2/runtime/_hdl_build/nx_dup_source_check_v3.nx) whose anchors 10// match NEITHER NAS copy -- three forks of one organ. That is why its ARENA-OVERRUN went unfixed for so 11// long: any seat repairing it had a one-in-three chance of editing a copy nothing executes. 12// The slot-bound fix below was applied here anyway, so the forks cannot diverge FURTHER on this defect. 13// ★A DUPLICATE-DETECTOR THAT IS ITSELF TRIPLICATED CANNOT REPORT ITS OWN CONDITION. 14// 15// nx_dup_source_check.nx v3 -- ANTI-CLOBBER cross-tree source-dup detector. 16// 17// 🚨v3 EXISTS BECAUSE v2 WAS AN OOM BOMB THAT TOOK THE NAS DOWN (2026-07-20). Root cause, stated plainly: 18// v2 widened the scan from 2 hardcoded dirs to ALL discovered source trees (78 pairs) -- a good change -- but 19// REUSED v1's `exists()` helper UNCHANGED, and that helper did `sys_mmap(4096)` PER CALL and never unmapped. 20// v1 called it ~15k times (ONE pair) = ~60MB = survivable. v2 called it ~250k times = ~1GB LEAKED PER RUN, on a 21// cron. Result: NAS userspace starved -- kernel still SYN-ACKs but sshd gives no banner, HTTPS returns status=0, 22// even DSM stops answering. 23// 24// ★THE LESSON, so it is never repeated: CHANGING A CALLER'S FAN-OUT RE-COSTS EVERY CALLEE. A 2->78 pair change 25// is a 17x amplifier on an O(files) helper. Reviewing only the code you WROTE is not enough -- you must re-cost 26// the code you INHERITED AND AMPLIFIED. And note what the v2 selftest could NOT do: it was GREEN 4/4 because 27// fixture dirs hold 2-3 files, so it tested the ALGORITHM at scale and never the RESOURCE at scale. A scale-law 28// tooth that asserts only correctness-on-big-input is HALF a tooth; the other half is a RESOURCE bound. 29// 30// v3 FIX: every path buffer is allocated ONCE PER PAIR (or once per walk) and passed down, never per file. 31// Allocation profile is now O(pairs + dirs), not O(files), and it is DECLARED in the output envelope so the 32// property is inspectable rather than assumed. Detection semantics and the emitted " DUP basename: " line are 33// UNCHANGED (rule 19) -- nx_favela_census counts that exact substring out of knowledge/status/dup_source.log. 34// expect_exit:0 when clean. 35import "nx_syscalls.nx" 36import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc) 37const K_MAGIC_131072: i64 = 131072 38const K_MAGIC_4096: i64 = 4096 39 40func dp_w(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 } 41// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer 42// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the 43// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls). 44// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign. 45func dp_wn(v: i64) -> i64 { nxi_out(v); return 0 } 46 47func ends_nx(nm: *u8) -> i64 { 48 var n: i64=0; while nm[n]!=(0 as u8){n=n+1} 49 if n < 3 { return 0 } 50 if nm[n-3]==(46 as u8) { if nm[n-2]==(110 as u8) { if nm[n-1]==(120 as u8) { return 1 } } } 51 return 0 52} 53 54func join_path(buf: *u8, dir: *u8, name: *u8) -> i64 { 55 var o: i64=0; var i: i64=0 56 while dir[i]!=(0 as u8) { buf[o]=dir[i]; o=o+1; i=i+1 } 57 buf[o]=47 as u8; o=o+1 58 i=0; while name[i]!=(0 as u8) { buf[o]=name[i]; o=o+1; i=i+1 } 59 buf[o]=0 as u8 60 return 0 61} 62 63// ★v3: takes a CALLER-OWNED scratch buffer. No allocation here -- this is the hot path (once per .nx file). 64func exists(dir: *u8, name: *u8, scratch: *u8) -> i64 { 65 join_path(scratch, dir, name) 66 let fd: i64=sys_openat_rd(scratch) 67 if fd >= 0 { sys_close(fd); return 1 } 68 return 0 69} 70 71func is_dot(nm: *u8) -> i64 { 72 if nm[0]==(46 as u8) { 73 if nm[1]==(0 as u8) { return 1 } 74 if nm[1]==(46 as u8) { if nm[2]==(0 as u8) { return 1 } } 75 } 76 return 0 77} 78 79func str_copy(dst: *u8, src: *u8) -> i64 { var i: i64=0; while src[i]!=(0 as u8) { dst[i]=src[i]; i=i+1 } dst[i]=0 as u8; return i } 80 81// Does `s` (with its NUL) fit in a slot of `cap` bytes? Stops at cap, so it costs O(cap) not O(len) 82// and cannot itself walk off the end of a pathological string. Returns 1 fits, 0 does not. 83// Exists because str_copy above is UNBOUNDED BY DESIGN (the right primitive for a sized destination) 84// -- the caller owes the bound, and two callers were not paying it. 85func path_fits(s: *u8, cap: i64) -> i64 { 86 var i: i64 = 0 87 while i < cap { if s[i] == (0 as u8) { return 1 } i = i + 1 } 88 return 0 89} 90 91// ★v3: exactly TWO allocations per pair (dirent buffer + path scratch), regardless of file count. 92// Output contract UNCHANGED from v1/v2 (rule 19). 93func scan(dirA: *u8, dirB: *u8, verbose: i64) -> i64 { 94 let fd: i64=sys_openat_rd(dirA) 95 if fd < 0 { return 0 } 96 let dbuf: *u8=sys_mmap(K_MAGIC_131072) 97 let pbuf: *u8=sys_mmap(K_MAGIC_4096) 98 var dups: i64=0 99 var go: i64=1 100 while go == 1 { 101 let nr: i64=sys_getdents64(fd, dbuf, K_MAGIC_131072) 102 if nr <= 0 { go = 0 } else { 103 var off: i64=0 104 while off < nr { 105 let rec: *u8=(dbuf as i64 + off) as *u8 106 let ty: i64=dirent_type(rec) 107 let nm: *u8=dirent_name(rec) 108 if ty != 4 { 109 if ends_nx(nm) == 1 { 110 if exists(dirB, nm, pbuf) == 1 { 111 dups = dups + 1 112 if verbose == 1 { dp_w(" DUP basename: "); dp_w(nm); dp_w(" (present in both "); dp_w(dirA); dp_w(" and "); dp_w(dirB); dp_w(")\n" as *u8) } 113 } 114 } 115 } 116 off = off + dirent_reclen(rec) 117 } 118 } 119 } 120 sys_close(fd) 121 return dups 122} 123 124func wfile(path: *u8) -> i64 { let fd: i64=sys_openat_wr(path, 420); if fd>=0 { sys_write(fd, "x" as *u8, 1); sys_close(fd) } return 0 } 125 126// BFS-discover dirs holding >=1 .nx. Buffers hoisted; allocation is O(1) per walk, not per entry. 127func discover(root: *u8, out: *u8, maxd: i64, maxdepth: i64, cap: *i64) -> i64 { 128 let QCAP: i64 = 256 129 let qbuf: *u8 = sys_mmap(QCAP*256) 130 let qdep: *i64 = sys_mmap(QCAP*8) as *i64 131 let dbuf: *u8 = sys_mmap(K_MAGIC_131072) 132 let child: *u8 = sys_mmap(K_MAGIC_4096) 133 var qh: i64 = 0 134 var qt: i64 = 0 135 cap[0] = 0 136 str_copy((qbuf as i64) as *u8, root); qdep[0]=0; qt=1 137 var nd: i64 = 0 138 while qh < qt { 139 let cur: *u8 = (qbuf as i64 + qh*256) as *u8 140 let cdep: i64 = qdep[qh] 141 qh = qh + 1 142 let fd: i64 = sys_openat_rd(cur) 143 if fd >= 0 { 144 var hasnx: i64 = 0 145 var go: i64 = 1 146 while go == 1 { 147 let nr: i64 = sys_getdents64(fd, dbuf, K_MAGIC_131072) 148 if nr <= 0 { go = 0 } else { 149 var off: i64 = 0 150 while off < nr { 151 let rec: *u8 = (dbuf as i64 + off) as *u8 152 let ty: i64 = dirent_type(rec) 153 let nm: *u8 = dirent_name(rec) 154 if ty == 4 { 155 if is_dot(nm) == 0 { 156 if cdep < maxdepth { 157 if qt < QCAP { 158 join_path(child, cur, nm) 159 // SLOT-BOUND CHECK (2026-08-16), same defect and same fix as 160 // nx_dup_source_check.nx: `child` is a 4096-byte buffer, each 161 // queue slot is 256, and str_copy runs to the NUL with NO 162 // bound. TWO symptoms, ONE defect -- a moderate overrun stays 163 // inside the 65536-byte qbuf and SILENTLY corrupts the next 164 // queue entry while the tool still prints GREEN; near QCAP it 165 // leaves the arena and trips ARENA-OVERRUN. Refusing here reuses 166 // this tool's OWN truncation signal so the loud RED verdict 167 // fires instead of a wrong GREEN. Bite-proven on the sibling 168 // with a 284-byte fixture: 5/cap_hit=0/GREEN -> 4/cap_hit=1/RED. 169 if path_fits(child, 256) == 1 { 170 str_copy((qbuf as i64 + qt*256) as *u8, child) 171 qdep[qt] = cdep + 1 172 qt = qt + 1 173 } else { cap[0] = 1 } 174 } else { cap[0] = 1 } 175 } 176 } 177 } else { 178 if ends_nx(nm) == 1 { hasnx = 1 } 179 } 180 off = off + dirent_reclen(rec) 181 } 182 } 183 } 184 sys_close(fd) 185 if hasnx == 1 { 186 // same slot bound as the queue write above: `out` is maxd slots of 256 bytes, and the 187 // LAST slot has nothing after it to absorb an overrun. 188 if nd < maxd { 189 if path_fits(cur, 256) == 1 { str_copy((out as i64 + nd*256) as *u8, cur); nd = nd + 1 } 190 else { cap[0] = 1 } 191 } 192 else { cap[0] = 1 } 193 } 194 } 195 } 196 return nd 197} 198 199func selftest() -> i64 { 200 // T1 v1 REGRESSION: two-dir case still finds exactly one dup, ignores the non-dup 201 sys_mkdir("/tmp/nxdupA" as *u8, 0x1ed) 202 sys_mkdir("/tmp/nxdupB" as *u8, 0x1ed) 203 wfile("/tmp/nxdupA/foo.nx" as *u8) 204 wfile("/tmp/nxdupA/bar.nx" as *u8) 205 wfile("/tmp/nxdupB/foo.nx" as *u8) 206 let dups: i64=scan("/tmp/nxdupA" as *u8, "/tmp/nxdupB" as *u8, 0) 207 if dups != 1 { dp_w(" SELFTEST FAIL T1: expected 1 dup, got "); dp_wn(dups); dp_w("\n" as *u8); return 0 } 208 // T2 nested-tree tooth (the seq281 class v1 was blind to) 209 sys_mkdir("/tmp/nxdup3" as *u8, 0x1ed) 210 sys_mkdir("/tmp/nxdup3/_hdl_build" as *u8, 0x1ed) 211 sys_mkdir("/tmp/nxdup3/runtime" as *u8, 0x1ed) 212 wfile("/tmp/nxdup3/only_here.nx" as *u8) 213 wfile("/tmp/nxdup3/_hdl_build/organ.nx" as *u8) 214 wfile("/tmp/nxdup3/runtime/organ.nx" as *u8) 215 let tbl: *u8 = sys_mmap(16*256) 216 let cap: *i64 = sys_mmap(16) as *i64 217 let n: i64 = discover("/tmp/nxdup3" as *u8, tbl, 16, 5, cap) 218 if n != 3 { dp_w(" SELFTEST FAIL T2a: expected 3 trees, got "); dp_wn(n); dp_w("\n" as *u8); return 0 } 219 var found: i64 = 0 220 var i: i64 = 0 221 while i < n { 222 var j: i64 = i + 1 223 while j < n { found = found + scan((tbl as i64 + i*256) as *u8, (tbl as i64 + j*256) as *u8, 0); j = j + 1 } 224 i = i + 1 225 } 226 if found != 1 { dp_w(" SELFTEST FAIL T2b: nested dup not caught, got "); dp_wn(found); dp_w("\n" as *u8); return 0 } 227 // T3 NON-VACUOUS negative control: v1's hardcoded pair reports ZERO here 228 let v1blind: i64 = scan("/tmp/nxdup3" as *u8, "/tmp/nxdup3/_hdl_build" as *u8, 0) 229 if v1blind != 0 { dp_w(" SELFTEST FAIL T3: negative control expected 0, got "); dp_wn(v1blind); dp_w("\n" as *u8); return 0 } 230 // T4 truncation must be LOUD, never silent 231 let tbl2: *u8 = sys_mmap(4*256) 232 let cap2: *i64 = sys_mmap(16) as *i64 233 discover("/tmp/nxdup3" as *u8, tbl2, 1, 5, cap2) 234 if cap2[0] != 1 { dp_w(" SELFTEST FAIL T4: silent cap\n" as *u8); return 0 } 235 // ★T5 RESOURCE TOOTH -- the half of the scale law v2 was missing. 600 files in ONE dir: v2 would have 236 // allocated 600 x 4096 here (per-file mmap); v3 allocates TWO buffers for the whole pair. This asserts the 237 // hot loop is allocation-free by exercising it at a file count a fixture normally never reaches. 238 sys_mkdir("/tmp/nxdupR" as *u8, 0x1ed) 239 sys_mkdir("/tmp/nxdupS" as *u8, 0x1ed) 240 let nmb: *u8 = sys_mmap(256) 241 let dg: *u8 = sys_mmap(16) 242 // ⚠LM-030: a string literal may NOT be indexed in expression position (CONST[i] crashes nx_cc with 243 // "unexpected operator token kind=47"). Bind it to a local pointer FIRST, then index the local. 244 let pfx: *u8 = "/tmp/nxdupR/f" as *u8 245 var k: i64 = 0 246 while k < 600 { 247 // build /tmp/nxdupR/f<k>.nx -- ZERO allocations in this loop (nmb + dg are hoisted above), which is the 248 // whole point of the tooth: if the hot path allocated per iteration this fixture would show it. 249 var p: i64 = 0 250 while pfx[p]!=(0 as u8) { nmb[p]=pfx[p]; p=p+1 } 251 var t: i64 = k 252 var dn: i64 = 0 253 if t==0 { dg[0]=48 as u8; dn=1 } 254 while t>0 { dg[dn]=(48+(t%10)) as u8; t=t/10; dn=dn+1 } 255 var q: i64 = 0 256 while q<dn { nmb[p]=dg[dn-1-q]; p=p+1; q=q+1 } 257 nmb[p]=46 as u8; nmb[p+1]=110 as u8; nmb[p+2]=120 as u8; nmb[p+3]=0 as u8 258 wfile(nmb) 259 k = k + 1 260 } 261 let big: i64 = scan("/tmp/nxdupR" as *u8, "/tmp/nxdupS" as *u8, 0) 262 if big != 0 { dp_w(" SELFTEST FAIL T5: expected 0 dups vs empty dir, got "); dp_wn(big); dp_w("\n" as *u8); return 0 } 263 return 1 264} 265 266func main() -> i64 { 267 dp_w("nx_dup_source_check v3 (cross-tree source-dup detector -- scope DISCOVERED; allocation O(pairs) not O(files))\n" as *u8) 268 if selftest() != 1 { dp_w("verdict=RED (self-test of the detector failed)\n" as *u8); sys_exit(1); return 1 } 269 dp_w(" self-test OK (v1 regression + nested-tree tooth + non-vacuous negative control + loud-cap + 600-file RESOURCE tooth)\n" as *u8) 270 let MAXD: i64 = 24 271 let MAXDEPTH: i64 = 6 272 let tbl: *u8 = sys_mmap(MAXD*256) 273 let cap: *i64 = sys_mmap(16) as *i64 274 dp_w(" discovering source trees under buildroot/ ...\n" as *u8) 275 let nd: i64 = discover("buildroot" as *u8, tbl, MAXD, MAXDEPTH, cap) 276 var i: i64 = 0 277 while i < nd { dp_w(" source tree: "); dp_w((tbl as i64 + i*256) as *u8); dp_w("\n" as *u8); i = i + 1 } 278 dp_w(" source trees discovered: "); dp_wn(nd); dp_w("\n" as *u8) 279 var realdups: i64 = 0 280 var pairs: i64 = 0 281 i = 0 282 while i < nd { 283 var j: i64 = i + 1 284 while j < nd { 285 realdups = realdups + scan((tbl as i64 + i*256) as *u8, (tbl as i64 + j*256) as *u8, 1) 286 pairs = pairs + 1 287 j = j + 1 288 } 289 i = i + 1 290 } 291 dp_w(" cross-tree source-dup basenames found: "); dp_wn(realdups); dp_w("\n" as *u8) 292 dp_w(" ENVELOPE: pairs_compared="); dp_wn(pairs) 293 dp_w(" trees="); dp_wn(nd) 294 dp_w(" tree_cap="); dp_wn(MAXD) 295 dp_w(" depth_cap="); dp_wn(MAXDEPTH) 296 dp_w(" cap_hit="); dp_wn(cap[0]) 297 dp_w(" allocs=O(pairs+dirs) NOT O(files) [v2 regression guard: the per-file mmap in exists() leaked ~1GB/run and starved the host]") 298 dp_w(" (counts are per-pair: one basename in three trees counts twice -- an upper bound on PAIRWISE hazards, not a distinct-basename count)\n" as *u8) 299 if cap[0] == 1 { 300 dp_w("verdict=RED (TRUNCATED -- a bound was hit; this scan is INCOMPLETE, raise MAXD/MAXDEPTH before trusting it)\n" as *u8) 301 sys_exit(3); return 3 302 } 303 if realdups == 0 { 304 dp_w("verdict=GREEN (detection proven; no cross-tree source-dup clobber hazards)\n" as *u8) 305 sys_exit(0); return 0 306 } 307 dp_w("verdict=RED (clobber hazard(s) present -- reconcile each basename to ONE canonical source dir; see debt seq207/seq281/seq284)\n" as *u8) 308 sys_exit(3); return 3 309}