code wiki / _hdl_build / _simsize_gate.nx

_simsize_gate.nx source

↩ module page · 288 lines · 12949 B

1// _simsize_gate.nx -- STRUCT-SIZE-vs-ALLOCATION gate. NO mocks. 2// 3// WHY THIS EXISTS (measured 2026-08-07): FIFTEEN live sources allocated NxRv64imSim with a 4// hardcoded `sys_mmap(128)`. 128 bytes is 16 i64 slots; the struct had grown to 26 fields = 208 5// bytes. So `halted` (offset 128), `halt_code` (136) and `steps` (144) -- THE THREE FIELDS EVERY 6// BOOT VERDICT IS COMPUTED FROM -- were written and read OUT OF BOUNDS. It hid for months inside 7// 4 KiB page slack and surfaced only when NXA_SMALL_MAX went 64 -> 256 and a 128-byte request began 8// being served from the shared small arena, landing the overrun on a live neighbour. The symptom 9// was nx_boot_run_sov reporting steps=1953655343 = 0x74726976 = the ASCII "virt" of the virtio 10// magic the driver under test had just read. 11// 12// A HARDCODED BYTE SIZE FOR A STRUCT THAT KEEPS GROWING IS AN OUT-OF-BOUNDS WRITE ON A TIMER. 13// 14// The remedy is not a bigger number -- a bigger number just resets the timer. The remedy is a 15// RULER: re-derive the field count FROM SOURCE on every run and refuse if the declared allocation 16// constant cannot hold it. 17// 18// T1 REAL -- NxRv64imSim fields*8 <= NX_RV64IM_SIM_BYTES, BOTH parsed from rv64im_min_sim.nx. 19// T2 NEG CTL -- a fixture whose struct provably does NOT fit its constant must be REFUSED. 20// Without this tooth the gate would score its loudest PASS when it parsed nothing. 21// T3 PARSER -- a fixture with a KNOWN field count must return EXACTLY that count. T1+T2 alone 22// are both satisfied by a parser that always returns 0; this tooth kills that. 23// 24// Evidence -> knowledge/status/simsize.log (SIMSIZEGATE row). Sovereign. license_tier: ORIGINAL 25import "nx_syscalls.nx" 26import "nx_gate_verdict.nx" 27 28// The SOURCE tree is buildroot/runtime/...; the serving root's runtime/_hdl_build/ holds only 29// EMITTED binaries. A gate that reads source must say so, and must try both anchors because it is 30// run BOTH from the serving root (directly) and from buildroot/ (via nx_sov_build_run, which 31// anchors CWD there). Reading zero bytes is INSTRUMENT-BLIND, never a pass -- see T1. 32const SG_SRC: *u8 = "buildroot/runtime/_hdl_build/rv64im_min_sim.nx" 33const SG_SRC_ALT: *u8 = "runtime/_hdl_build/rv64im_min_sim.nx" 34const SG_FIX: *u8 = "/tmp/_simsize_fixture.nx" 35const SG_LOG: *u8 = "knowledge/status/simsize.log" 36const SG_CAP: i64 = 262144 37 38func sg_p(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 } 39func sg_fp(fd: i64, s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(fd,s,n); return 0 } 40// Prints the SIGN. The idiom copy-pasted across the gate family does `if m<0 { m = 0-m }` and then 41// prints the magnitude, so a -1 NOT-FOUND sentinel renders as "1" -- indistinguishable from a real 42// value of 1. MEASURED 2026-08-07: this gate's first run printed `fields=1 declared=1` when both 43// were actually -1 (the source path was wrong and nothing had parsed), which reads as a plausible 44// tiny struct instead of an instrument failure. 45// A NUMBER PRINTER THAT DROPS THE SIGN MAKES A SENTINEL INDISTINGUISHABLE FROM A VALUE. 46func sg_fn(fd: i64, v: i64) -> i64 { 47 let bb: *u8 = sys_mmap(32) 48 var m: i64 = v 49 var neg: i64 = 0 50 if m < 0 { m = 0 - m; neg = 1 } 51 let t: *u8 = sys_mmap(32) 52 var k: i64 = 0 53 if m == 0 { t[0] = 48 as u8; k = 1 } 54 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 55 var o: i64 = 0 56 if neg == 1 { bb[0] = 45 as u8; o = 1 } 57 var i: i64 = 0 58 while i < k { bb[o + i] = t[k - 1 - i]; i = i + 1 } 59 sys_write(fd, bb, k + o) 60 return 0 61} 62 63func sg_strlen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n } 64 65func sg_read(path: *u8, buf: *u8, cap: i64) -> i64 { 66 let fd: i64 = sys_openat_rd(path) 67 if fd < 0 { return 0 } 68 var n: i64 = 0 69 var go: i64 = 1 70 while go == 1 { 71 let r: i64 = sys_read(fd, (buf as i64 + n) as *u8, cap - 1 - n) 72 if r <= 0 { go = 0 } else { n = n + r } 73 if n >= cap - 1 { go = 0 } 74 } 75 sys_close(fd) 76 return n 77} 78 79// index of the first occurrence of pat in buf[from..n), else -1. 80func sg_find(buf: *u8, n: i64, pat: *u8, pl: i64, from: i64) -> i64 { 81 if pl <= 0 { return 0 - 1 } 82 var i: i64 = from 83 while i + pl <= n { 84 var k: i64 = 0 85 var hit: i64 = 1 86 while k < pl { if buf[i+k] != pat[k] { hit = 0; k = pl } else { k = k + 1 } } 87 if hit == 1 { return i } 88 i = i + 1 89 } 90 return 0 - 1 91} 92 93// Count the i64-sized fields of `struct <sname>` in buf. Returns -1 if the struct is not found -- 94// DISTINCT from 0, so "I could not parse it" can never be mistaken for "it has no fields". 95// A field line is one whose first non-space character is neither '}' nor '/', and which contains a 96// ':'. Continuation comment lines (leading '/') and section banners are therefore skipped, exactly 97// as a human reading the declaration would skip them. 98func sg_struct_fields(buf: *u8, n: i64, sname: *u8) -> i64 { 99 let key: *u8 = sys_mmap(256) 100 let pre: *u8 = "struct " as *u8 101 var w: i64 = 0 102 var q: i64 = 0 103 while pre[q] != (0 as u8) { key[w] = pre[q]; w = w + 1; q = q + 1 } 104 q = 0 105 while sname[q] != (0 as u8) { key[w] = sname[q]; w = w + 1; q = q + 1 } 106 key[w] = 32 as u8 107 w = w + 1 108 key[w] = 0 as u8 109 let at: i64 = sg_find(buf, n, key, w, 0) 110 if at < 0 { return 0 - 1 } 111 // advance to the end of the declaration line 112 var i: i64 = at 113 var adv: i64 = 1 114 while adv == 1 { 115 if i >= n { adv = 0 } else { 116 if buf[i] == (10 as u8) { i = i + 1; adv = 0 } else { i = i + 1 } 117 } 118 } 119 var fields: i64 = 0 120 var scan: i64 = 1 121 while scan == 1 { 122 if i >= n { scan = 0 } else { 123 // line is [i, le) 124 var le: i64 = i 125 var fe: i64 = 1 126 while fe == 1 { 127 if le >= n { fe = 0 } else { 128 if buf[le] == (10 as u8) { fe = 0 } else { le = le + 1 } 129 } 130 } 131 // first non-space 132 var p: i64 = i 133 var fs: i64 = 1 134 while fs == 1 { 135 if p >= le { fs = 0 } else { 136 if buf[p] == (32 as u8) { p = p + 1 } else { 137 if buf[p] == (9 as u8) { p = p + 1 } else { fs = 0 } 138 } 139 } 140 } 141 if p >= le { 142 i = le + 1 143 } else { 144 if buf[p] == (125 as u8) { 145 scan = 0 146 } else { 147 if buf[p] == (47 as u8) { 148 i = le + 1 149 } else { 150 var c: i64 = p 151 var found: i64 = 0 152 while c < le { if buf[c] == (58 as u8) { found = 1; c = le } else { c = c + 1 } } 153 if found == 1 { fields = fields + 1 } 154 i = le + 1 155 } 156 } 157 } 158 } 159 } 160 return fields 161} 162 163// Parse `const <cname>: i64 = <decimal>`. Returns -1 if absent. 164func sg_const_val(buf: *u8, n: i64, cname: *u8) -> i64 { 165 let key: *u8 = sys_mmap(256) 166 let pre: *u8 = "const " as *u8 167 var w: i64 = 0 168 var q: i64 = 0 169 while pre[q] != (0 as u8) { key[w] = pre[q]; w = w + 1; q = q + 1 } 170 q = 0 171 while cname[q] != (0 as u8) { key[w] = cname[q]; w = w + 1; q = q + 1 } 172 key[w] = 0 as u8 173 let at: i64 = sg_find(buf, n, key, w, 0) 174 if at < 0 { return 0 - 1 } 175 var i: i64 = at 176 var fe: i64 = 1 177 while fe == 1 { 178 if i >= n { return 0 - 1 } 179 if buf[i] == (61 as u8) { fe = 0 } else { 180 if buf[i] == (10 as u8) { return 0 - 1 } 181 i = i + 1 182 } 183 } 184 i = i + 1 185 var ws: i64 = 1 186 while ws == 1 { 187 if i >= n { ws = 0 } else { 188 if buf[i] == (32 as u8) { i = i + 1 } else { 189 if buf[i] == (9 as u8) { i = i + 1 } else { ws = 0 } 190 } 191 } 192 } 193 if i >= n { return 0 - 1 } 194 var val: i64 = 0 195 var digits: i64 = 0 196 var go: i64 = 1 197 while go == 1 { 198 if i >= n { go = 0 } else { 199 let c: i64 = buf[i] as i64 200 if c >= 48 { if c <= 57 { val = val * 10 + (c - 48); digits = digits + 1; i = i + 1 } else { go = 0 } } else { go = 0 } 201 } 202 } 203 if digits == 0 { return 0 - 1 } 204 return val 205} 206 207// Write the fixture used by T2 and T3. Two structs with KNOWN field counts and two constants: 208// FixtureBad needs 4*8=32 and declares 16 (must be REFUSED); FixtureOk needs 2*8=16 and declares 64. 209func sg_write_fixture() -> i64 { 210 let fd: i64 = sys_openat_wr(SG_FIX, 0x1a4) 211 if fd < 0 { return 0 - 1 } 212 sg_fp(fd, "// generated by _simsize_gate -- fixture, not a build input\n" as *u8) 213 sg_fp(fd, "struct FixtureBad {\n" as *u8) 214 sg_fp(fd, " // a banner comment that must NOT be counted\n" as *u8) 215 sg_fp(fd, " a: i64\n b: i64\n" as *u8) 216 sg_fp(fd, " // a continuation comment that must NOT be counted\n" as *u8) 217 sg_fp(fd, " c: i64\n d: i64\n}\n" as *u8) 218 sg_fp(fd, "const FIXTURE_BAD_BYTES: i64 = 16\n" as *u8) 219 sg_fp(fd, "struct FixtureOk {\n a: i64\n b: i64\n}\n" as *u8) 220 sg_fp(fd, "const FIXTURE_OK_BYTES: i64 = 64\n" as *u8) 221 sys_close(fd) 222 return 0 223} 224 225func main() -> i64 { 226 sg_p("=== struct-size vs allocation gate (NxRv64imSim) ===\n" as *u8) 227 228 let src: *u8 = sys_mmap(SG_CAP) 229 var sn: i64 = sg_read(SG_SRC, src, SG_CAP) 230 if sn <= 0 { sn = sg_read(SG_SRC_ALT, src, SG_CAP) } 231 if sn <= 0 { sg_p(" INSTRUMENT-BLIND: could not read the sim source from either anchor\n" as *u8) } 232 let fields: i64 = sg_struct_fields(src, sn, "NxRv64imSim" as *u8) 233 let need: i64 = fields * 8 234 let declared: i64 = sg_const_val(src, sn, "NX_RV64IM_SIM_BYTES" as *u8) 235 236 sg_p(" NxRv64imSim fields=" as *u8); sg_fn(1, fields) 237 sg_p(" need=" as *u8); sg_fn(1, need) 238 sg_p("B declared=" as *u8); sg_fn(1, declared); sg_p("B\n" as *u8) 239 240 // T1: the real check. Both operands must have PARSED (>0) -- a -1 from either side is an 241 // INSTRUMENT failure and must not be allowed to score a pass. 242 var t1: i64 = 0 243 if fields > 0 { if declared > 0 { if need <= declared { t1 = 1 } } } 244 245 sg_write_fixture() 246 let fx: *u8 = sys_mmap(SG_CAP) 247 let fn2: i64 = sg_read(SG_FIX, fx, SG_CAP) 248 let bad_fields: i64 = sg_struct_fields(fx, fn2, "FixtureBad" as *u8) 249 let bad_declared: i64 = sg_const_val(fx, fn2, "FIXTURE_BAD_BYTES" as *u8) 250 let ok_fields: i64 = sg_struct_fields(fx, fn2, "FixtureOk" as *u8) 251 252 // T2: NEGATIVE CONTROL. FixtureBad needs 32B and declares 16B, so the SAME comparison that 253 // scored T1 must come out FALSE here. If this tooth passes, the ruler cannot bite. 254 var t2: i64 = 0 255 if bad_fields > 0 { if bad_declared > 0 { if bad_fields * 8 > bad_declared { t2 = 1 } } } 256 257 // T3: the counter is EXACT, not merely non-zero. FixtureBad has exactly 4 fields and FixtureOk 258 // exactly 2, and the fixture deliberately contains a banner comment and a continuation comment 259 // that must not be counted. 260 var t3: i64 = 0 261 if bad_fields == 4 { if ok_fields == 2 { t3 = 1 } } 262 263 sg_p(" fixture: bad_fields=" as *u8); sg_fn(1, bad_fields) 264 sg_p(" (expect 4) ok_fields=" as *u8); sg_fn(1, ok_fields); sg_p(" (expect 2)\n" as *u8) 265 266 let ctr: *i64 = gv_ctr() 267 gv_check("T1 NxRv64imSim fits its allocation constant (fields*8 <= NX_RV64IM_SIM_BYTES, both parsed from source)" as *u8, t1, ctr) 268 gv_check("T2 NEG CTL: a struct that does NOT fit its constant is REFUSED (fixture needs 32B, declares 16B)" as *u8, t2, ctr) 269 gv_check("T3 the field counter is EXACT (fixture 4 and 2), and skips banner + continuation comments" as *u8, t3, ctr) 270 let rc: i64 = gv_verdict("SIMSIZEGATE" as *u8, ctr, "struct-size vs allocation: the NxRv64imSim field count is re-derived FROM SOURCE and checked against the declared NX_RV64IM_SIM_BYTES, so growing the struct past its allocation is caught mechanically instead of becoming an out-of-bounds write hidden by page slack. probe=struct-allocation-fit" as *u8) 271 272 let lfd: i64 = sys_openat_append(SG_LOG, 0x1a4) 273 if lfd >= 0 { 274 sg_fp(lfd, "SIMSIZEGATE verdict=" as *u8) 275 if rc == 0 { sg_fp(lfd, "GREEN" as *u8) } else { sg_fp(lfd, "RED" as *u8) } 276 sg_fp(lfd, " struct=NxRv64imSim fields=" as *u8); sg_fn(lfd, fields) 277 sg_fp(lfd, " need_bytes=" as *u8); sg_fn(lfd, need) 278 sg_fp(lfd, " declared_bytes=" as *u8); sg_fn(lfd, declared) 279 sg_fp(lfd, " headroom_fields=" as *u8); sg_fn(lfd, (declared - need) / 8) 280 sg_fp(lfd, " negctl=" as *u8); sg_fn(lfd, t2) 281 sg_fp(lfd, " exactcount=" as *u8); sg_fn(lfd, t3) 282 sg_fp(lfd, " epoch=" as *u8); sg_fn(lfd, sys_now_realtime_sec()) 283 sg_fp(lfd, "\n" as *u8) 284 sys_close(lfd) 285 } 286 sys_exit(rc) 287 return rc 288}