code wiki / _hdl_build / nx_wasm_vm_verify.nx
nx_wasm_vm_verify.nx source
↩ module page · 330 lines · 20274 B
1// nx_wasm_vm_verify.nx -- RUN ANY SHIPPED WASM MODULE IN THE MEMORY IT DECLARES.
2//
3// WHY THIS IS GENERIC AND NOT A SEVENTEENTH COPY. On 2026-08-14 /craft shipped black TWICE. Every
4// stage of its emit pipeline was GREEN and its NATIVE gate reported 59/59, because a native gate
5// sys_mmaps its own arena and therefore never meets the wasm memory bound at all. The browser did,
6// and trapped on the first frame with "index out of bounds".
7// Measured the same day: the estate has ELEVEN *_wasm_vm_gate organs and NOT ONE of them is for a
8// game. They cover the sims and some crypto/codec modules. Every browser-playable wasm game ships a
9// module that byte-checks pass and nobody ever executes -- the identical blind spot, ~17 times over.
10//
11// ★SO THE RULER IS PARAMETERISED, NOT DUPLICATED. One organ, a path argument, no per-module
12// constants anywhere in it. The declared memory is NOT mirrored from the module's source either --
13// wm_parse already reads the wasm memory section and sizes mod.mem from it, so this asks the
14// ARTIFACT what it declares rather than trusting a number typed in a second file. A mirrored page
15// count is exactly the kind of constant that goes stale in one place and takes a page down.
16//
17// WHAT IT PROVES, and only this:
18// PARSES -- the image is a real module
19// DECLARES -- it names a linear memory, and the VM allocates exactly that much
20// RUNS -- the exported entry points execute without leaving that memory (the VM bounds-checks)
21// PAINTS -- for a FRAMEBUFFER module (ww/hh/fb_off exported) the frame carries >1 colour
22// A module with no framebuffer surface is not a renderer, and this says NOT-A-RENDERER and abstains
23// rather than inventing a verdict about a subject it cannot see.
24//
25// usage: nx_wasm_vm_verify <path.wasm> [noinit]
26// noinit -- skip init(), for modules whose world generation cannot finish in an interpreter.
27// nx_wasm_craft is the measured case: 786,432 voxels of noise, >120s. Its own gate
28// (nx_wasm_craft_vm_gate) seeds a minimal scene instead; this flag is the generic
29// escape so a slow init cannot be mistaken for a broken module.
30// exit: 0 GREEN | 1 RED | 3 the file could not be read
31// license_tier: ORIGINAL
32import "nx_syscalls.nx"
33import "nx_wasm_vm.nx"
34import "nx_gate_verdict.nx"
35import "nx_base64.nx"
36
37const WV_COLCAP: i64 = 4096
38// ★THE GAMES DO NOT SHIP A .wasm FILE -- they ship it EMBEDDED, base64, inside the page, which is
39// why a verifier that only takes a path reaches almost none of them. Measured 2026-08-14: nine
40// standalone .wasm exist estate-wide, while ~17 browser-playable games carry theirs inline. Taking
41// the PAGE as input is what makes this cover the artifact a visitor actually downloads.
42// The emitter writes it as: const B="<base64>" (nx_game_page_emit), so that literal is the anchor.
43// ★★★★★★ANCHOR ON THE PAYLOAD, NEVER ON THE EMITTER'S SPELLING. This was `const B="` -- one
44// emitter's variable name -- and MEASURED 2026-08-15 that read 9 pages while 67 shipped pages call
45// WebAssembly.instantiate: racing, pong, explorer, adventure, td, city, shmup, voxelworld, sudoku and
46// viz-zoom all embed via `var b64='...'` and were INVISIBLE. A census built on it reported 4/4 GREEN
47// over 13% of its own subject and said nothing about the rest.
48// Every wasm module begins with the 8 bytes 00 61 73 6D 01 00 00 00 ("\0asm" + version 1), and since
49// the base64 encodes the module from byte zero that prefix is ALWAYS "AGFzbQEAAAA". It is a property
50// of the thing being carried, so it identifies an embedded module in any page from any emitter, past
51// or future, without this tool having to know the template.
52const WV_WASM_B64_MAGIC: *u8 = "AGFzbQEAAAA"
53const WV_APOS: i64 = 39
54const WV_QUOTE: i64 = 34
55// ★THE INTERPRETER'S FRAME BUDGET, set from what the estate's existing wasm-VM gates actually
56// complete: every one of them verifies a 256x256 = 65,536 pixel frame in milliseconds. Four times
57// that is the ceiling here, so a module rendering up to 512x512 is still measured directly and
58// anything larger is DECLINED OUT LOUD rather than left to hang. This bounds OUR throughput, not
59// the module -- which is why it is named for the interpreter and why the refusal prints the number.
60const WV_FRAME_BUDGET: i64 = 262144
61
62// ★★★★★A BUG YOU FIX BY REWRITING THE LINE, RATHER THAN BY EXTRACTING THE FIX, IS A BUG YOU WILL
63// WRITE AGAIN. These two were hand-rolled here, and the identical allocating formatter was hand-rolled
64// again in nx_wasmpage_census and again in nx_wasmfit -- three copies in one session, and nx_mmapbal
65// flagged every one of them as `mmap=2 munmap=0`, a leak per number printed. This file already imports
66// nx_gate_verdict, whose gv_puts/gv_num are measured CLEAN by the same scanner, so the fix is to DELETE
67// the duplicate rather than balance it. Thin wrappers keep every existing call site untouched while
68// there is exactly one implementation underneath.
69func wv_w(s: *u8) -> i64 { return gv_puts(s) }
70func wv_n(v: i64) -> i64 { return gv_num(v) }
71func wv_rd64(m: *u8, o: i64) -> i64 { var v: i64=0; var i: i64=0; while i<8 { v = v | ((m[o+i] as i64) << (i*8)); i=i+1 } return v }
72func wv_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
73func wv_eq(a: *u8, b: *u8) -> i64 {
74 var i: i64 = 0
75 var r: i64 = 2
76 while r == 2 {
77 if a[i] != b[i] { r = 0 } else {
78 if a[i] == (0 as u8) { r = 1 } else { i = i + 1 }
79 }
80 }
81 return r
82}
83// first offset of needle in buf, or -1
84func wv_find(buf: *u8, len: i64, needle: *u8) -> i64 {
85 let nl: i64 = wv_slen(needle)
86 if nl == 0 { return 0 - 1 }
87 var i: i64 = 0
88 while i + nl <= len {
89 var k: i64 = 0
90 var m: i64 = 1
91 while k < nl { if buf[i+k] != needle[k] { m = 0; k = nl } else { k = k + 1 } }
92 if m == 1 { return i }
93 i = i + 1
94 }
95 return 0 - 1
96}
97
98func main(argc: i64, argv: *i64) -> i64 {
99 let ctr: *i64 = gv_ctr()
100 if argc < 2 {
101 wv_w("usage: nx_wasm_vm_verify <path.wasm|page.html> [noinit] [nopaint]\n" as *u8)
102 wv_w(" noinit -- do not call init(); report the FINEST geometry the module can ask for\n" as *u8)
103 wv_w(" nopaint -- answer only whether the framebuffer fits; never attempt a frame\n" as *u8)
104 return 3
105 }
106 let path: *u8 = argv[1] as *u8
107
108 // ★★★★★★A FLAG THAT ACCEPTS ANY SPELLING CANNOT REPORT A TYPO, AND THE CALLER THEN BELIEVES IT
109 // ASKED FOR SOMETHING IT DID NOT. The first cut of this parse was `if argc >= 3 { noinit = 1 }` --
110 // it never looked at the token at all, so ANY third argument (a misspelling, a stray path, a flag
111 // meant for something else) silently turned init off, and a caller that asked for "nopaint" would
112 // have got "noinit" and no warning. An unrecognised flag is now a refusal, because the alternative
113 // is a tool that answers a different question than the one it was asked and says nothing about it.
114 var noinit: i64 = 0
115 var nopaint: i64 = 0
116 var badflag: i64 = 0
117 var ai: i64 = 2
118 while ai < argc {
119 let f: *u8 = argv[ai] as *u8
120 if wv_eq(f, "noinit" as *u8) == 1 { noinit = 1 } else {
121 if wv_eq(f, "nopaint" as *u8) == 1 { nopaint = 1 } else {
122 badflag = 1
123 wv_w(" unrecognised flag: " as *u8); wv_w(f); wv_w("\n" as *u8)
124 }
125 }
126 ai = ai + 1
127 }
128 if badflag == 1 {
129 wv_w(" refusing rather than guessing which question you meant -- known flags: noinit nopaint\n" as *u8)
130 return 3
131 }
132
133 wv_w("=== nx_wasm_vm_verify " as *u8); wv_w(path); wv_w(" ===\n" as *u8)
134
135 let box: *i64 = sys_mmap(16) as *i64
136 var img: *u8 = sys_read_file(path, box)
137 if (img as i64) == 0 {
138 wv_w(" cannot read the module -- nothing to verify\n" as *u8)
139 return 3
140 }
141 wv_w(" file bytes = " as *u8); wv_n(box[0]); wv_w("\n" as *u8)
142
143 // ★IF THIS IS A PAGE, VERIFY THE MODULE THE PAGE ACTUALLY CARRIES. A wasm game does not ship as
144 // a file next to its html -- it is base64 INSIDE it. Reading the page and decoding the embedded
145 // module means this tool measures the exact bytes a visitor downloads, not a build artifact that
146 // may or may not have been the one emitted.
147 let anchor: i64 = wv_find(img, box[0], WV_WASM_B64_MAGIC)
148 if anchor >= 0 {
149 // The magic sits at the START of the base64 literal, so the delimiter is the byte before it --
150 // but walk back rather than assume, and accept EITHER quote: emitters here use both `"` and `'`.
151 // ⚠The scan uses its own cursor and an explicit flag; writing the exit into the cursor would
152 // destroy the position that IS the answer.
153 var qc: i64 = 0
154 var s: i64 = anchor
155 var b: i64 = anchor
156 var scan: i64 = 1
157 while scan == 1 {
158 if b == 0 { scan = 0 } else {
159 let c: i64 = img[b-1] as i64
160 if c == WV_QUOTE { qc = WV_QUOTE; s = b; scan = 0 } else {
161 if c == WV_APOS { qc = WV_APOS; s = b; scan = 0 } else { b = b - 1 }
162 }
163 }
164 }
165 if qc == 0 {
166 wv_w(" found a wasm magic but no enclosing string literal -- refusing to guess its extent\n" as *u8)
167 return 3
168 }
169 var fin: i64 = s
170 var stop: i64 = 0
171 while stop == 0 {
172 if fin >= box[0] { stop = 1 } else {
173 if (img[fin] as i64) == qc { stop = 1 } else { fin = fin + 1 }
174 }
175 }
176 let b64n: i64 = fin - s
177 // 3 bytes out per 4 chars in; allocate the ceiling, never a guessed constant
178 let dec: *u8 = sys_mmap((b64n/4 + 2) * 3)
179 let dn: i64 = b64_decode(((img as i64) + s) as *u8, b64n, dec)
180 wv_w(" EMBEDDED module found: base64 chars = " as *u8); wv_n(b64n)
181 wv_w(" decoded bytes = " as *u8); wv_n(dn); wv_w("\n" as *u8)
182 if dn <= 0 {
183 wv_w(" the embedded blob did not decode -- refusing to guess what shipped\n" as *u8)
184 return 3
185 }
186 img = dec
187 box[0] = dn
188 }
189
190 let mod: *WasmMod = wm_new(img, box[0])
191 let parsed: i64 = (wm_parse(mod) == 0) as i64
192 gv_check("PARSES: the image is a real wasm module" as *u8, parsed, ctr)
193
194 // ★ASK THE ARTIFACT, NOT A SECOND FILE. wm_parse reads the memory section and allocates mod.mem
195 // to exactly the declared minimum, so this number comes from the bytes that ship.
196 wv_w(" declared linear memory = " as *u8); wv_n(mod.mem_bytes)
197 wv_w(" bytes (" as *u8); wv_n(mod.mem_bytes/65536); wv_w(" pages)\n" as *u8)
198 gv_check("DECLARES: the module names a linear memory and the VM sized itself to it" as *u8, (mod.mem_bytes > 0) as i64, ctr)
199
200 let fw: i64 = wm_find_export(mod, "ww" as *u8)
201 let fh: i64 = wm_find_export(mod, "hh" as *u8)
202 let fo: i64 = wm_find_export(mod, "fb_off" as *u8)
203 let fr: i64 = wm_find_export(mod, "render" as *u8)
204 let fi: i64 = wm_find_export(mod, "init" as *u8)
205 var isfb: i64 = 0
206 if fw>=0 { if fh>=0 { if fo>=0 { if fr>=0 { isfb = 1 } } } }
207 wv_w(" exports: ww=" as *u8); wv_n(fw); wv_w(" hh=" as *u8); wv_n(fh)
208 wv_w(" fb_off=" as *u8); wv_n(fo); wv_w(" render=" as *u8); wv_n(fr)
209 wv_w(" init=" as *u8); wv_n(fi); wv_w("\n" as *u8)
210
211 if isfb == 0 {
212 // ★ABSTAIN, DO NOT INVENT -- BUT NAME WHAT IS ACTUALLY THERE. This module exposes no
213 // framebuffer surface, so "does it paint" is not a question about it. Answering anyway is how
214 // a gate ends up reporting a narrower subject than its name implies.
215 // ★AND PRINT THE EXPORTS. A bare "NOT-A-RENDERER" cannot be checked by its reader: it looks
216 // identical whether the module genuinely has no display surface or merely spells its entry
217 // points differently. Listing them turns an assertion into evidence, and the difference
218 // decides whether this tool needs a second naming convention or the module needs a surface.
219 wv_w(" NOT-A-RENDERER: no ww/hh/fb_off/render surface. Its " as *u8); wv_n(mod.n_exports)
220 wv_w(" exports are:\n" as *u8)
221 var e: i64 = 0
222 while e < mod.n_exports {
223 wv_w(" " as *u8)
224 sys_write(1, ((mod.bytes as i64) + mod.exp_name_off[e]) as *u8, mod.exp_name_len[e])
225 wv_w("\n" as *u8)
226 e = e + 1
227 }
228 gv_need("RUNS+PAINTS: needs a framebuffer surface (ww/hh/fb_off/render)" as *u8, 0, ctr)
229 return gv_verdict("WASM-VM-VERIFY" as *u8, ctr, "module parsed and declared its memory; it exposes no framebuffer so rendering was not claimed either way" as *u8)
230 }
231
232 if noinit == 0 { if fi >= 0 { wm_run(mod, "init" as *u8, 0,0,0,0,0, 0) } }
233
234 // ⚠SIZE THE WORK BEFORE DOING IT. ww/hh/fb_off are trivial accessors; render is the expensive
235 // call, so every decision about whether the frame is affordable has to happen BEFORE it. The
236 // first cut of this tool checked the budget AFTER rendering, which is no check at all -- it hung
237 // on craft's 900,000-pixel frame exactly as if the budget did not exist. Cheap questions first.
238 let off: i64 = wm_run(mod, "fb_off" as *u8, 0,0,0,0,0, 0)
239 let w: i64 = wm_run(mod, "ww" as *u8, 0,0,0,0,0, 0)
240 let h: i64 = wm_run(mod, "hh" as *u8, 0,0,0,0,0, 0)
241 let need: i64 = off + w*h*8
242 wv_w(" frame " as *u8); wv_n(w); wv_w("x" as *u8); wv_n(h)
243 wv_w(" at fb_off=" as *u8); wv_n(off); wv_w(" needs=" as *u8); wv_n(need)
244 wv_w(" headroom=" as *u8); wv_n(mod.mem_bytes - need); wv_w("\n" as *u8)
245 let fits: i64 = (need <= mod.mem_bytes) as i64
246 gv_check("RUNS: the framebuffer the module reports lies inside the memory it declares -- a browser traps the instant it does not" as *u8, fits, ctr)
247
248 // ★★★★★★AN ABSTENTION ON A LATER QUESTION MUST NOT ERASE A DEFINITIVE ANSWER TO AN EARLIER ONE.
249 // gv_verdict tests ctr[2] FIRST and returns 3/SKIP before it ever compares passed-vs-run, which is
250 // correct for a gate whose preconditions failed -- but here the fit question is ALREADY ANSWERED at
251 // this point, and answered NO. Falling through to the frame-budget gv_need below would report the
252 // exact /craft outage this tool was built to catch as "I could not look", and any census reading
253 // exit codes would score it as an honest abstention. ★A SKIP THAT CAN SWALLOW A RED IS NOT A THIRD
254 // STATE, IT IS AN AMNESTY. So a module that does not fit returns RED here, unconditionally, before
255 // any question about affordability is even raised.
256 if fits == 0 {
257 wv_w(" the framebuffer does NOT fit: a browser traps on the first out-of-bounds store,\n" as *u8)
258 wv_w(" which is a BLACK PAGE, not a slow one. Frame affordability is not asked -- the module\n" as *u8)
259 wv_w(" is already refuted.\n" as *u8)
260 return gv_verdict("WASM-VM-VERIFY" as *u8, ctr, "the module reports a framebuffer outside the linear memory it declares" as *u8)
261 }
262
263 // ★ANSWER ONLY WHAT WAS ASKED. A census over every shipped page wants the STRUCTURAL question --
264 // does the framebuffer fit -- and that question is fully answered by the check above, which needs
265 // no frame at all. Attempting one anyway is what turned four world pages into TIMEOUTs on the
266 // first census run: the interpreter is orders of magnitude slower per pixel than the browser the
267 // module actually ships to, and a raycasting frame under the pixel budget can still take minutes.
268 // ★A TIMEOUT IS A NON-ANSWER, AND A NON-ANSWER ON THE PAGES THAT MATTER IS A CENSUS THAT MEASURED
269 // NOTHING. Frame CONTENT remains each module's own gate's job, where a cheap frame can be seeded.
270 if nopaint == 1 {
271 wv_w(" nopaint: the caller asked only whether the framebuffer fits, and it does. No frame\n" as *u8)
272 wv_w(" was attempted, so nothing is claimed about what this module draws.\n" as *u8)
273 gv_need("PAINTS: not attempted -- caller asked the structural question only" as *u8, 0, ctr)
274 return gv_verdict("WASM-VM-VERIFY" as *u8, ctr, "the module declares a linear memory and reports a framebuffer that fits inside it" as *u8)
275 }
276
277 // ★REFUSE RATHER THAN HANG, AND SAY THE NUMBER. This VM is an INTERPRETER, so frame cost is not
278 // a detail -- it decides whether this tool answers at all. Measured 2026-08-14: the estate's
279 // existing wasm-VM gates all verify 256x256 = 65,536 pixel frames and complete in milliseconds,
280 // while craft with no init() clamps its quality slot to 1 and asks for 1200x750 = 900,000, which
281 // does not finish. A tool that silently sits there is worse than one that declines: the caller
282 // cannot tell "slow" from "broken", which is exactly the ambiguity that let a black page ship.
283 // The bound is on OUR interpreter's throughput, not on the module, so it is named for that and
284 // the refusal names the remedy: a module this large needs its own gate, which can seed a cheap
285 // frame the way nx_wasm_craft_vm_gate does.
286 if w*h > WV_FRAME_BUDGET {
287 wv_w(" frame is " as *u8); wv_n(w*h); wv_w(" pixels, over the interpreter budget of " as *u8)
288 wv_n(WV_FRAME_BUDGET); wv_w(" -- REFUSING to attempt it.\n" as *u8)
289 wv_w(" remedy: give this module a gate that seeds a cheaper frame (see nx_wasm_craft_vm_gate),\n" as *u8)
290 wv_w(" or call render after an init() that leaves a coarser quality setting.\n" as *u8)
291 gv_need("PAINTS: frame within the interpreter budget" as *u8, 0, ctr)
292 return gv_verdict("WASM-VM-VERIFY" as *u8, ctr, "module parsed, declared its memory and reported a framebuffer that fits; the frame itself was too large for this VM to render inside a gate" as *u8)
293 }
294 wm_run(mod, "render" as *u8, 0,0,0,0,0, 0)
295
296 let seen: *i64 = sys_mmap(WV_COLCAP*8) as *i64
297 var distinct: i64 = 0
298 var p: i64 = 0
299 while p < w*h {
300 let c: i64 = wv_rd64(mod.mem, off + p*8)
301 var j: i64 = 0
302 var dup: i64 = 0
303 while j < distinct { if seen[j]==c { dup=1; j=distinct } else { j=j+1 } }
304 if dup==0 { if distinct < WV_COLCAP { seen[distinct]=c; distinct=distinct+1 } else { p = w*h } }
305 p = p + 1
306 }
307 wv_w(" distinct colours = " as *u8); wv_n(distinct); wv_w("\n" as *u8)
308
309 // ★★★★★★A TOOTH THAT FAILS BECAUSE THE HARNESS REMOVED ITS PRECONDITION IS MEASURING THE HARNESS.
310 // MEASURED 2026-08-15 on voxelworld/index.html: fit PASSED with 1,875,968 bytes of headroom, then
311 // PAINTS failed on a uniform frame and the caller's bucket reported "does not run in the memory it
312 // declares" -- a live, healthy page indicted for a defect it does not have. TWO causes, both in
313 // this tool: `noinit` had deliberately skipped the world generation the frame draws, and render()
314 // is invoked here with all-zero arguments while that module's render takes seven camera
315 // parameters. Under either condition a blank frame is the EXPECTED result of how it was called.
316 // ★A DETECTOR WITH FALSE POSITIVES IS WORSE THAN NONE -- it teaches everyone to ignore it, and this
317 // one would have sent the next reader hunting a bug that was never there.
318 if noinit == 1 {
319 if distinct <= 1 {
320 wv_w(" the frame is uniform, but init() was SKIPPED at this caller's request and render was\n" as *u8)
321 wv_w(" driven with zero arguments, so a blank frame is the expected outcome of HOW IT WAS\n" as *u8)
322 wv_w(" CALLED, not evidence about the module. Drive it from its own gate to ask this.\n" as *u8)
323 gv_need("PAINTS: not observable -- init skipped and render not driven with real arguments" as *u8, 0, ctr)
324 return gv_verdict("WASM-VM-VERIFY" as *u8, ctr, "the module declares a linear memory and reports a framebuffer that fits inside it" as *u8)
325 }
326 }
327 gv_check("PAINTS: the module wrote a VARIED frame -- one that trapped, or wrote nothing, cannot" as *u8, (distinct > 1) as i64, ctr)
328
329 return gv_verdict("WASM-VM-VERIFY" as *u8, ctr, "the shipped module was executed in the memory it declares, not merely inspected as bytes" as *u8)
330}