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}