code wiki / _hdl_build / nx_constscan_beat.nx
nx_constscan_beat.nx source
↩ module page · 281 lines · 10156 B
1// nx_constscan_beat.nx -- STANDING guard for LM-030, the const-pointer direct-index SILENT MISCOMPILE.
2//
3// WHY A BEAT: on 2026-07-30 I reproduced LM-030 with a controlled probe/control pair -- CONST[i] compiles
4// clean, runs, and returns WRONG DATA with no diagnostic anywhere -- then scanned all 17,501 .nx in the tree
5// and found hits=0. Clean TODAY is not a property; it is a snapshot. A defect whose only symptom is a wrong
6// number needs a continuous guard, because the first reintroduction is invisible by construction.
7//
8// WHY THE DETECTOR WAS DARK, which is the real lesson: nx_doc_constscan already existed and had even measured
9// the same garbage value, but it was SOURCE-ONLY -- never built, never in tool_allowlist.conf, never on the
10// sweep rail. A detector nobody can run is not a guard. This beat is the missing rail.
11//
12// Discovers + batches + aggregates ONLY. It forks the proven ./nx_doc_constscan_run.elf exactly as
13// nx_store_fold_beat forks nx_store_compact, so the DETECTOR keeps sole ownership of what counts as a hit.
14// Batching exists because the runner takes files as argv and the tree is ~17.5k files -- one exec cannot hold
15// them, so this is xargs-in-an-organ, not a re-implementation.
16//
17// Sweep-row contract: exit 0 = zero LM-030 sites; exit 1 = at least one, surfaced RED with the count.
18// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
19import "nx_syscalls.nx"
20import "nx_tool_run.nx"
21
22const CB_DIRBUF: i64 = 262144
23const CB_ARENA: i64 = 1048576
24const CB_OUTCAP: i64 = 262144
25const CB_PATHCAP: i64 = 1024
26const CB_BATCH: i64 = 96
27const CB_AVSLOTS: i64 = 100
28const CB_STDOUT: i64 = 1
29const CB_NL: i64 = 10
30
31func cb_len(s: *u8) -> i64 {
32 var n: i64 = 0
33 while s[n] != (0 as u8) {
34 n = n + 1
35 }
36 return n
37}
38func cb_puts(s: *u8) -> i64 {
39 sys_write(CB_STDOUT, s, cb_len(s))
40 return 0
41}
42func cb_putn(v: i64) -> i64 {
43 let t: *u8 = sys_mmap(32)
44 let b: *u8 = sys_mmap(32)
45 var m: i64 = v
46 if m < 0 {
47 m = 0 - m
48 }
49 var k: i64 = 0
50 if m == 0 {
51 t[0] = 48 as u8
52 k = 1
53 }
54 while m > 0 {
55 t[k] = (48 + (m % 10)) as u8
56 m = m / 10
57 k = k + 1
58 }
59 var i: i64 = 0
60 while i < k {
61 b[i] = t[k - 1 - i]
62 i = i + 1
63 }
64 sys_write(CB_STDOUT, b, k)
65 sys_munmap(t, 32)
66 sys_munmap(b, 32)
67 return 0
68}
69// name ends in .nx and is not a shadow copy (.migrated/.premigrate/.bak/.retired/MOVED/SHADOW)
70// FIXTURE EXCLUSION (2026-07-30). Two files in the tree contain LM-030 ON PURPOSE and must not turn a
71// standing guard RED forever: nx_constidx_probe.nx is the positive control that PROVES the detector can fire
72// (its safe twin nx_constidx_ctrl.nx must stay clean), and nx_doc_constscan_gate.nx carries KAT fixture
73// STRINGS containing the offending pattern as test input. Excluding a deliberate fixture is what lint
74// exclusions are for; excluding it is NOT the same as suppressing a finding, and the exclusion is BY NAME so
75// a real defect anywhere else still fires. ⚠RESIDUAL, filed not hidden: cs_scan skips // comments but NOT
76// string literals, which is why the gate's fixture strings match at all -- root fix is a string-literal skip.
77// Compare two NUL-terminated strings. Stops at EITHER terminator, which is the whole point: the
78// version this replaced walked the LITERAL's length while indexing the CANDIDATE, so a candidate
79// shorter than the literal was read past its NUL. dirent names live inside the getdents buffer, so
80// a short name near the end of that mapping meant reading off the end of the mapping itself --
81// SIGSEGV, error 4 (read), at a page-aligned address, every ~7 minutes on the beat schedule.
82// Keeping this loud was deliberate: page-per-allocation FAULTS on such an over-read, whereas the
83// small-allocation arena would quietly hand back a neighbour's bytes and return a wrong answer.
84func cb_streq(x: *u8, y: *u8) -> i64 {
85 var i: i64 = 0
86 while x[i] != (0 as u8) {
87 if y[i] != x[i] { return 0 }
88 i = i + 1
89 }
90 if y[i] != (0 as u8) { return 0 }
91 return 1
92}
93func cb_is_fixture(nm: *u8) -> i64 {
94 if cb_streq("nx_constidx_probe.nx" as *u8, nm) == 1 { return 1 }
95 if cb_streq("nx_doc_constscan_gate.nx" as *u8, nm) == 1 { return 1 }
96 return 0
97}
98func cb_is_scannable(nm: *u8) -> i64 {
99 let n: i64 = cb_len(nm)
100 if n < 4 {
101 return 0
102 }
103 if cb_is_fixture(nm) == 1 {
104 return 0
105 }
106 if nm[n - 3] != (46 as u8) {
107 return 0
108 }
109 if nm[n - 2] != (110 as u8) {
110 return 0
111 }
112 if nm[n - 1] != (120 as u8) {
113 return 0
114 }
115 return 1
116}
117// sum every "hits=N" the runner printed in this batch
118func cb_hits(buf: *u8, n: i64) -> i64 {
119 let key: *u8 = "hits=" as *u8
120 let kl: i64 = 5
121 var total: i64 = 0
122 var i: i64 = 0
123 while i + kl < n {
124 var ok: i64 = 1
125 var k: i64 = 0
126 while k < kl {
127 if buf[i + k] != key[k] {
128 ok = 0
129 k = kl
130 } else {
131 k = k + 1
132 }
133 }
134 if ok == 1 {
135 var v: i64 = 0
136 var j: i64 = i + kl
137 var go: i64 = 1
138 while go == 1 {
139 if j >= n {
140 go = 0
141 } else {
142 let c: i64 = buf[j]
143 var d: i64 = 0
144 if c >= 48 {
145 if c <= 57 {
146 d = 1
147 }
148 }
149 if d == 1 {
150 v = v * 10 + (c - 48)
151 j = j + 1
152 } else {
153 go = 0
154 }
155 }
156 }
157 total = total + v
158 i = j
159 } else {
160 i = i + 1
161 }
162 }
163 return total
164}
165
166func main(argc: i64, argv: *i64) -> i64 {
167 let runner: *u8 = "./nx_doc_constscan_run.elf" as *u8
168 let dbuf: *u8 = sys_mmap(CB_DIRBUF)
169 let arena: *u8 = sys_mmap(CB_ARENA)
170 let av: *i64 = sys_mmap(8 * CB_AVSLOTS) as *i64
171 let out: *u8 = sys_mmap(CB_OUTCAP)
172 let ol: *i64 = sys_mmap(16) as *i64
173 var scanned: i64 = 0
174 var hits: i64 = 0
175 var batches: i64 = 0
176 var failed: i64 = 0
177
178 var d: i64 = 0
179 while d < 2 {
180 var dir: *u8 = "buildroot/runtime" as *u8
181 if d == 1 {
182 dir = "buildroot/runtime/_hdl_build" as *u8
183 }
184 let dl: i64 = cb_len(dir)
185 let fd: i64 = sys_openat_rd(dir)
186 if fd < 0 {
187 cb_puts("CONSTSCAN-BEAT RED cannot open " as *u8)
188 cb_puts(dir)
189 cb_puts("\n" as *u8)
190 return 1
191 }
192 var ao: i64 = 0
193 var nb_in: i64 = 0
194 av[0] = runner as i64
195 var go: i64 = 1
196 while go == 1 {
197 let nb: i64 = sys_getdents64(fd, dbuf, CB_DIRBUF)
198 if nb <= 0 {
199 go = 0
200 } else {
201 var off: i64 = 0
202 while off < nb {
203 let rec: *u8 = (dbuf as i64 + off) as *u8
204 let rl: i64 = dirent_reclen(rec)
205 if rl <= 0 {
206 off = nb
207 } else {
208 let nm: *u8 = dirent_name(rec)
209 if cb_is_scannable(nm) == 1 {
210 let want: i64 = ao + dl + cb_len(nm) + 2
211 if want < CB_ARENA {
212 let start: i64 = ao
213 var z: i64 = 0
214 while z < dl {
215 arena[ao] = dir[z]
216 ao = ao + 1
217 z = z + 1
218 }
219 arena[ao] = 47 as u8
220 ao = ao + 1
221 var y: i64 = 0
222 while nm[y] != (0 as u8) {
223 arena[ao] = nm[y]
224 ao = ao + 1
225 y = y + 1
226 }
227 arena[ao] = 0 as u8
228 ao = ao + 1
229 nb_in = nb_in + 1
230 av[nb_in] = (arena as i64) + start
231 scanned = scanned + 1
232 if nb_in >= CB_BATCH {
233 av[nb_in + 1] = 0
234 let rc: i64 = tr_run_capture(runner, av, out, CB_OUTCAP - 1, ol)
235 if rc != 0 {
236 failed = failed + 1
237 }
238 hits = hits + cb_hits(out, ol[0])
239 batches = batches + 1
240 nb_in = 0
241 ao = 0
242 }
243 }
244 }
245 off = off + rl
246 }
247 }
248 }
249 }
250 sys_close(fd)
251 if nb_in > 0 {
252 av[nb_in + 1] = 0
253 let rc2: i64 = tr_run_capture(runner, av, out, CB_OUTCAP - 1, ol)
254 if rc2 != 0 {
255 failed = failed + 1
256 }
257 hits = hits + cb_hits(out, ol[0])
258 batches = batches + 1
259 }
260 d = d + 1
261 }
262
263 cb_puts("CONSTSCAN-BEAT scanned=" as *u8)
264 cb_putn(scanned)
265 cb_puts(" batches=" as *u8)
266 cb_putn(batches)
267 cb_puts(" batch_failures=" as *u8)
268 cb_putn(failed)
269 cb_puts(" LM030_hits=" as *u8)
270 cb_putn(hits)
271 if hits > 0 {
272 cb_puts(" verdict=RED-const-pointer-indexed-directly-SILENT-MISCOMPILE-bind-to-a-local-let\n" as *u8)
273 return 1
274 }
275 if failed > 0 {
276 cb_puts(" verdict=RED-a-batch-FAILED-coverage-incomplete-do-not-read-hits-as-zero\n" as *u8)
277 return 1
278 }
279 cb_puts(" verdict=GREEN-no-LM-030-sites\n" as *u8)
280 return 0
281}