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}