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}