code wiki / _hdl_build / nx_dup_source_check_v3.nx
nx_dup_source_check_v3.nx source
↩ module page · 268 lines · 12961 B
1// nx_dup_source_check.nx v3 -- ANTI-CLOBBER cross-tree source-dup detector.
2//
3// 🚨v3 EXISTS BECAUSE v2 WAS AN OOM BOMB THAT TOOK THE NAS DOWN (2026-07-20). Root cause, stated plainly:
4// v2 widened the scan from 2 hardcoded dirs to ALL discovered source trees (78 pairs) -- a good change -- but
5// REUSED v1's `exists()` helper UNCHANGED, and that helper did `sys_mmap(4096)` PER CALL and never unmapped.
6// v1 called it ~15k times (ONE pair) = ~60MB = survivable. v2 called it ~250k times = ~1GB LEAKED PER RUN, on a
7// cron. Result: NAS userspace starved -- kernel still SYN-ACKs but sshd gives no banner, HTTPS returns status=0,
8// even DSM stops answering.
9//
10// ★THE LESSON, so it is never repeated: CHANGING A CALLER'S FAN-OUT RE-COSTS EVERY CALLEE. A 2->78 pair change
11// is a 17x amplifier on an O(files) helper. Reviewing only the code you WROTE is not enough -- you must re-cost
12// the code you INHERITED AND AMPLIFIED. And note what the v2 selftest could NOT do: it was GREEN 4/4 because
13// fixture dirs hold 2-3 files, so it tested the ALGORITHM at scale and never the RESOURCE at scale. A scale-law
14// tooth that asserts only correctness-on-big-input is HALF a tooth; the other half is a RESOURCE bound.
15//
16// v3 FIX: every path buffer is allocated ONCE PER PAIR (or once per walk) and passed down, never per file.
17// Allocation profile is now O(pairs + dirs), not O(files), and it is DECLARED in the output envelope so the
18// property is inspectable rather than assumed. Detection semantics and the emitted " DUP basename: " line are
19// UNCHANGED (rule 19) -- nx_favela_census counts that exact substring out of knowledge/status/dup_source.log.
20// expect_exit:0 when clean.
21import "nx_syscalls.nx"
22import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc)
23const K_MAGIC_131072: i64 = 131072
24const K_MAGIC_4096: i64 = 4096
25
26func 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 }
27// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
28// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
29// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
30// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
31func dp_wn(v: i64) -> i64 { nxi_out(v); return 0 }
32
33func ends_nx(nm: *u8) -> i64 {
34 var n: i64=0; while nm[n]!=(0 as u8){n=n+1}
35 if n < 3 { return 0 }
36 if nm[n-3]==(46 as u8) { if nm[n-2]==(110 as u8) { if nm[n-1]==(120 as u8) { return 1 } } }
37 return 0
38}
39
40func join_path(buf: *u8, dir: *u8, name: *u8) -> i64 {
41 var o: i64=0; var i: i64=0
42 while dir[i]!=(0 as u8) { buf[o]=dir[i]; o=o+1; i=i+1 }
43 buf[o]=47 as u8; o=o+1
44 i=0; while name[i]!=(0 as u8) { buf[o]=name[i]; o=o+1; i=i+1 }
45 buf[o]=0 as u8
46 return 0
47}
48
49// ★v3: takes a CALLER-OWNED scratch buffer. No allocation here -- this is the hot path (once per .nx file).
50func exists(dir: *u8, name: *u8, scratch: *u8) -> i64 {
51 join_path(scratch, dir, name)
52 let fd: i64=sys_openat_rd(scratch)
53 if fd >= 0 { sys_close(fd); return 1 }
54 return 0
55}
56
57func is_dot(nm: *u8) -> i64 {
58 if nm[0]==(46 as u8) {
59 if nm[1]==(0 as u8) { return 1 }
60 if nm[1]==(46 as u8) { if nm[2]==(0 as u8) { return 1 } }
61 }
62 return 0
63}
64
65func 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 }
66
67// ★v3: exactly TWO allocations per pair (dirent buffer + path scratch), regardless of file count.
68// Output contract UNCHANGED from v1/v2 (rule 19).
69func scan(dirA: *u8, dirB: *u8, verbose: i64) -> i64 {
70 let fd: i64=sys_openat_rd(dirA)
71 if fd < 0 { return 0 }
72 let dbuf: *u8=sys_mmap(K_MAGIC_131072)
73 let pbuf: *u8=sys_mmap(K_MAGIC_4096)
74 var dups: i64=0
75 var go: i64=1
76 while go == 1 {
77 let nr: i64=sys_getdents64(fd, dbuf, K_MAGIC_131072)
78 if nr <= 0 { go = 0 } else {
79 var off: i64=0
80 while off < nr {
81 let rec: *u8=(dbuf as i64 + off) as *u8
82 let ty: i64=dirent_type(rec)
83 let nm: *u8=dirent_name(rec)
84 if ty != 4 {
85 if ends_nx(nm) == 1 {
86 if exists(dirB, nm, pbuf) == 1 {
87 dups = dups + 1
88 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) }
89 }
90 }
91 }
92 off = off + dirent_reclen(rec)
93 }
94 }
95 }
96 sys_close(fd)
97 return dups
98}
99
100func 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 }
101
102// BFS-discover dirs holding >=1 .nx. Buffers hoisted; allocation is O(1) per walk, not per entry.
103func discover(root: *u8, out: *u8, maxd: i64, maxdepth: i64, cap: *i64) -> i64 {
104 let QCAP: i64 = 256
105 let qbuf: *u8 = sys_mmap(QCAP*256)
106 let qdep: *i64 = sys_mmap(QCAP*8) as *i64
107 let dbuf: *u8 = sys_mmap(K_MAGIC_131072)
108 let child: *u8 = sys_mmap(K_MAGIC_4096)
109 var qh: i64 = 0
110 var qt: i64 = 0
111 cap[0] = 0
112 str_copy((qbuf as i64) as *u8, root); qdep[0]=0; qt=1
113 var nd: i64 = 0
114 while qh < qt {
115 let cur: *u8 = (qbuf as i64 + qh*256) as *u8
116 let cdep: i64 = qdep[qh]
117 qh = qh + 1
118 let fd: i64 = sys_openat_rd(cur)
119 if fd >= 0 {
120 var hasnx: i64 = 0
121 var go: i64 = 1
122 while go == 1 {
123 let nr: i64 = sys_getdents64(fd, dbuf, K_MAGIC_131072)
124 if nr <= 0 { go = 0 } else {
125 var off: i64 = 0
126 while off < nr {
127 let rec: *u8 = (dbuf as i64 + off) as *u8
128 let ty: i64 = dirent_type(rec)
129 let nm: *u8 = dirent_name(rec)
130 if ty == 4 {
131 if is_dot(nm) == 0 {
132 if cdep < maxdepth {
133 if qt < QCAP {
134 join_path(child, cur, nm)
135 str_copy((qbuf as i64 + qt*256) as *u8, child)
136 qdep[qt] = cdep + 1
137 qt = qt + 1
138 } else { cap[0] = 1 }
139 }
140 }
141 } else {
142 if ends_nx(nm) == 1 { hasnx = 1 }
143 }
144 off = off + dirent_reclen(rec)
145 }
146 }
147 }
148 sys_close(fd)
149 if hasnx == 1 {
150 if nd < maxd { str_copy((out as i64 + nd*256) as *u8, cur); nd = nd + 1 }
151 else { cap[0] = 1 }
152 }
153 }
154 }
155 return nd
156}
157
158func selftest() -> i64 {
159 // T1 v1 REGRESSION: two-dir case still finds exactly one dup, ignores the non-dup
160 sys_mkdir("/tmp/nxdupA" as *u8, 0x1ed)
161 sys_mkdir("/tmp/nxdupB" as *u8, 0x1ed)
162 wfile("/tmp/nxdupA/foo.nx" as *u8)
163 wfile("/tmp/nxdupA/bar.nx" as *u8)
164 wfile("/tmp/nxdupB/foo.nx" as *u8)
165 let dups: i64=scan("/tmp/nxdupA" as *u8, "/tmp/nxdupB" as *u8, 0)
166 if dups != 1 { dp_w(" SELFTEST FAIL T1: expected 1 dup, got "); dp_wn(dups); dp_w("\n" as *u8); return 0 }
167 // T2 nested-tree tooth (the seq281 class v1 was blind to)
168 sys_mkdir("/tmp/nxdup3" as *u8, 0x1ed)
169 sys_mkdir("/tmp/nxdup3/_hdl_build" as *u8, 0x1ed)
170 sys_mkdir("/tmp/nxdup3/runtime" as *u8, 0x1ed)
171 wfile("/tmp/nxdup3/only_here.nx" as *u8)
172 wfile("/tmp/nxdup3/_hdl_build/organ.nx" as *u8)
173 wfile("/tmp/nxdup3/runtime/organ.nx" as *u8)
174 let tbl: *u8 = sys_mmap(16*256)
175 let cap: *i64 = sys_mmap(16) as *i64
176 let n: i64 = discover("/tmp/nxdup3" as *u8, tbl, 16, 5, cap)
177 if n != 3 { dp_w(" SELFTEST FAIL T2a: expected 3 trees, got "); dp_wn(n); dp_w("\n" as *u8); return 0 }
178 var found: i64 = 0
179 var i: i64 = 0
180 while i < n {
181 var j: i64 = i + 1
182 while j < n { found = found + scan((tbl as i64 + i*256) as *u8, (tbl as i64 + j*256) as *u8, 0); j = j + 1 }
183 i = i + 1
184 }
185 if found != 1 { dp_w(" SELFTEST FAIL T2b: nested dup not caught, got "); dp_wn(found); dp_w("\n" as *u8); return 0 }
186 // T3 NON-VACUOUS negative control: v1's hardcoded pair reports ZERO here
187 let v1blind: i64 = scan("/tmp/nxdup3" as *u8, "/tmp/nxdup3/_hdl_build" as *u8, 0)
188 if v1blind != 0 { dp_w(" SELFTEST FAIL T3: negative control expected 0, got "); dp_wn(v1blind); dp_w("\n" as *u8); return 0 }
189 // T4 truncation must be LOUD, never silent
190 let tbl2: *u8 = sys_mmap(4*256)
191 let cap2: *i64 = sys_mmap(16) as *i64
192 discover("/tmp/nxdup3" as *u8, tbl2, 1, 5, cap2)
193 if cap2[0] != 1 { dp_w(" SELFTEST FAIL T4: silent cap\n" as *u8); return 0 }
194 // ★T5 RESOURCE TOOTH -- the half of the scale law v2 was missing. 600 files in ONE dir: v2 would have
195 // allocated 600 x 4096 here (per-file mmap); v3 allocates TWO buffers for the whole pair. This asserts the
196 // hot loop is allocation-free by exercising it at a file count a fixture normally never reaches.
197 sys_mkdir("/tmp/nxdupR" as *u8, 0x1ed)
198 sys_mkdir("/tmp/nxdupS" as *u8, 0x1ed)
199 let nmb: *u8 = sys_mmap(256)
200 let dg: *u8 = sys_mmap(16)
201 // âš LM-030: a string literal may NOT be indexed in expression position (CONST[i] crashes nx_cc with
202 // "unexpected operator token kind=47"). Bind it to a local pointer FIRST, then index the local.
203 let pfx: *u8 = "/tmp/nxdupR/f" as *u8
204 var k: i64 = 0
205 while k < 600 {
206 // build /tmp/nxdupR/f<k>.nx -- ZERO allocations in this loop (nmb + dg are hoisted above), which is the
207 // whole point of the tooth: if the hot path allocated per iteration this fixture would show it.
208 var p: i64 = 0
209 while pfx[p]!=(0 as u8) { nmb[p]=pfx[p]; p=p+1 }
210 var t: i64 = k
211 var dn: i64 = 0
212 if t==0 { dg[0]=48 as u8; dn=1 }
213 while t>0 { dg[dn]=(48+(t%10)) as u8; t=t/10; dn=dn+1 }
214 var q: i64 = 0
215 while q<dn { nmb[p]=dg[dn-1-q]; p=p+1; q=q+1 }
216 nmb[p]=46 as u8; nmb[p+1]=110 as u8; nmb[p+2]=120 as u8; nmb[p+3]=0 as u8
217 wfile(nmb)
218 k = k + 1
219 }
220 let big: i64 = scan("/tmp/nxdupR" as *u8, "/tmp/nxdupS" as *u8, 0)
221 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 }
222 return 1
223}
224
225func main() -> i64 {
226 dp_w("nx_dup_source_check v3 (cross-tree source-dup detector -- scope DISCOVERED; allocation O(pairs) not O(files))\n" as *u8)
227 if selftest() != 1 { dp_w("verdict=RED (self-test of the detector failed)\n" as *u8); sys_exit(1); return 1 }
228 dp_w(" self-test OK (v1 regression + nested-tree tooth + non-vacuous negative control + loud-cap + 600-file RESOURCE tooth)\n" as *u8)
229 let MAXD: i64 = 24
230 let MAXDEPTH: i64 = 6
231 let tbl: *u8 = sys_mmap(MAXD*256)
232 let cap: *i64 = sys_mmap(16) as *i64
233 dp_w(" discovering source trees under buildroot/ ...\n" as *u8)
234 let nd: i64 = discover("buildroot" as *u8, tbl, MAXD, MAXDEPTH, cap)
235 var i: i64 = 0
236 while i < nd { dp_w(" source tree: "); dp_w((tbl as i64 + i*256) as *u8); dp_w("\n" as *u8); i = i + 1 }
237 dp_w(" source trees discovered: "); dp_wn(nd); dp_w("\n" as *u8)
238 var realdups: i64 = 0
239 var pairs: i64 = 0
240 i = 0
241 while i < nd {
242 var j: i64 = i + 1
243 while j < nd {
244 realdups = realdups + scan((tbl as i64 + i*256) as *u8, (tbl as i64 + j*256) as *u8, 1)
245 pairs = pairs + 1
246 j = j + 1
247 }
248 i = i + 1
249 }
250 dp_w(" cross-tree source-dup basenames found: "); dp_wn(realdups); dp_w("\n" as *u8)
251 dp_w(" ENVELOPE: pairs_compared="); dp_wn(pairs)
252 dp_w(" trees="); dp_wn(nd)
253 dp_w(" tree_cap="); dp_wn(MAXD)
254 dp_w(" depth_cap="); dp_wn(MAXDEPTH)
255 dp_w(" cap_hit="); dp_wn(cap[0])
256 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]")
257 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)
258 if cap[0] == 1 {
259 dp_w("verdict=RED (TRUNCATED -- a bound was hit; this scan is INCOMPLETE, raise MAXD/MAXDEPTH before trusting it)\n" as *u8)
260 sys_exit(3); return 3
261 }
262 if realdups == 0 {
263 dp_w("verdict=GREEN (detection proven; no cross-tree source-dup clobber hazards)\n" as *u8)
264 sys_exit(0); return 0
265 }
266 dp_w("verdict=RED (clobber hazard(s) present -- reconcile each basename to ONE canonical source dir; see debt seq207/seq281/seq284)\n" as *u8)
267 sys_exit(3); return 3
268}