// check-module.mjs: a module's verdict under WAI's WAISM profile // (wai/extensions/waism.md), with no dependency. It is the checker a page // runs before it instantiates a decoder module, and the second implementation beside the // Rust checker of this directory, written from the profile's text and from WebAssembly 3.0, // sharing no code with it. It generalises wai-vector's check-mvp.mjs, which reads a module // of WebAssembly 1.0, to every construct of WebAssembly 3.0 and to the profile's rules. // // The verdict for a module at a level under caps (§4.3) is one line of ASCII: `held`, or // `refused R `: // 1. R1: a module longer than `module_bytes` is `refused R1 0`, and is not read further. // 2. R2: a module that does not decode under WebAssembly 3.0's binary format, or is not // valid, within the validation limits of §4.2, is `refused R2 -`. This file decodes // every section and instruction and validates them as WebAssembly 3.0 does, the types // of garbage collection, exception handling and typed function references included. // 3. R3 to R12 over the module's items in binary order: the first item that breaks one is // refused at its offset, by the lowest-numbered rule it breaks. // 4. Then R6 where the module defines no memory, and R10 where an export of §5 is // missing, at the offset equal to the module's length. // // In a page: import { check, levelOf } from './check-module.mjs'; // check(bytes, 'integer') → 'held' | 'refused R ' // From a shell: node check-module.mjs [--level integer|float] [--caps NAME=VALUE,...] FILE... // node check-module.mjs --cases DIR // // With --level, one verdict line a file; without it, the level a sink holds the module at // (§6, check 9b) or the verdict that refuses it. --cases checks every line of DIR/cases.txt // (` `; the module is DIR/.wasm). Exit status: 0 when // every module is held (with --cases, when every verdict is the one stated), 1 otherwise, // 2 for a usage or input error. /** The profile's caps (§4.2). */ export const PROFILE_CAPS = Object.freeze({ module_bytes: 16777216, functions: 50000, memory_pages: 65536 }); /** The validation limits of §4.2, part of R2. */ const LIMIT = Object.freeze({ types: 1000000, functions: 1000000, globals: 1000000, tags: 1000000, tables: 100000, memories: 100, elems: 100000, datas: 100000, segmentItems: 10000000, name: 100000, body: 7654321, locals: 50000, params: 1000, results: 1000, fields: 10000, catches: 10000, depth: 63, interface: 999998, }); /** The exports of §5: name, kind (0 a function, 2 the memory) and a function's type. */ const ABI = [ ['memory', 2, null], ['wai_abi', 0, [[], ['i32']]], ['wai_alloc', 0, [['i32'], ['i32']]], ['wai_run', 0, [['i32', 'i32', 'i32', 'i32', 'i32', 'i32'], ['i32']]], ['wai_out_ptr', 0, [[], ['i32']]], ['wai_out_len', 0, [[], ['i32']]], ]; // A module outside R2: one that does not decode, or is not valid. The message is for a // reader of this file; the verdict carries none of it. class Refused2 extends Error {} function bad(why) { throw new Refused2(why); } // ------------------------------------------------------------------------------ bytes class Reader { constructor(b, at, end) { this.b = b; this.at = at; this.end = end; } eof() { return this.at >= this.end; } byte() { if (this.at >= this.end) bad('unexpected end'); return this.b[this.at++]; } peek() { if (this.at >= this.end) bad('unexpected end'); return this.b[this.at]; } skip(n) { if (n > this.end - this.at) bad('unexpected end'); this.at += n; } // Unsigned LEB128 of 32 bits: at most 5 bytes, the unused bits of the last zero. u32() { let v = 0, scale = 1; for (let i = 0; i < 5; i++) { const c = this.byte(); if (i === 4 && (c & 0xf0) !== 0) bad('u32 too large'); v += (c & 0x7f) * scale; if (!(c & 0x80)) return v; scale *= 128; } return v; } // Unsigned LEB128 of 64 bits, exact, as a BigInt: at most 10 bytes. u64() { let v = 0n, shift = 0n; for (let i = 0; i < 10; i++) { const c = this.byte(); if (i === 9 && (c & 0xfe) !== 0) bad('u64 too large'); v |= BigInt(c & 0x7f) << shift; if (!(c & 0x80)) return v; shift += 7n; } return v; } // The same encoding, as a Number: exact below 2^53, which is all a comparison here needs. u64n() { let v = 0, scale = 1; for (let i = 0; i < 10; i++) { const c = this.byte(); if (i === 9 && (c & 0xfe) !== 0) bad('u64 too large'); v += (c & 0x7f) * scale; if (!(c & 0x80)) return v; scale *= 128; } return v; } // Signed LEB128: at most `max` bytes, the unused bits of the last a sign extension; the // last byte's bits from `keep` up must all equal bit `keep - 1`. Returns the value as a // Number (s33 is exact; s32 and s64 values are not used). signed(max, keep) { let v = 0, scale = 1; for (let i = 0; i < max; i++) { const c = this.byte(); if (i === max - 1) { if (c & 0x80) bad('signed LEB128 too long'); const high = c >> (keep - 1); // bit keep-1 up to bit 6 if (high !== 0 && high !== (0x7f >> (keep - 1))) bad('signed LEB128 too large'); } v += (c & 0x7f) * scale; scale *= 128; if (!(c & 0x80)) return (c & 0x40) ? v - scale : v; } return v; } s32() { return this.signed(5, 4); } s33() { return this.signed(5, 5); } s64() { return this.signed(10, 1); } // A name: a vector of bytes that is UTF-8, at most the name limit long. name() { const n = this.u32(); if (n > LIMIT.name) bad('name too long'); if (n > this.end - this.at) bad('unexpected end'); utf8(this.b, this.at, this.at + n); const s = this.at; this.at += n; return String.fromCharCode.apply(null, Array.from(this.b.subarray(s, s + n))); } } // Valid UTF-8: no overlong form, no surrogate, nothing above U+10FFFF. function utf8(b, at, end) { while (at < end) { const c = b[at]; if (c < 0x80) { at++; continue; } let n, min; if (c >= 0xc2 && c <= 0xdf) { n = 1; min = 0x80; } else if (c >= 0xe0 && c <= 0xef) { n = 2; min = 0x800; } else if (c >= 0xf0 && c <= 0xf4) { n = 3; min = 0x10000; } else bad('not UTF-8'); if (at + n >= end) bad('not UTF-8'); let cp = c & (0x3f >> n); for (let i = 1; i <= n; i++) { const d = b[at + i]; if ((d & 0xc0) !== 0x80) bad('not UTF-8'); cp = cp * 64 + (d & 0x3f); } if (cp < min || cp > 0x10ffff || (cp >= 0xd800 && cp <= 0xdfff)) bad('not UTF-8'); at += n + 1; } } // ------------------------------------------------------------------------------ types // // A value type is a string ('i32', 'i64', 'f32', 'f64', 'v128'), 'bot' (the unknown type of // an unreachable operand), or a reference {nullable, heap}: heap is an abstract heap type // ('func', 'nofunc', 'extern', 'noextern', 'any', 'eq', 'i31', 'struct', 'array', 'none', // 'exn', 'noexn', or 'bot') or a canonical type id, a number: two type indices whose // definitions are equivalent (iso-recursively, by their recursion groups) have one id. const ABSTRACT = { 0x70: 'func', 0x6f: 'extern', 0x6e: 'any', 0x6d: 'eq', 0x6c: 'i31', 0x6b: 'struct', 0x6a: 'array', 0x69: 'exn', 0x73: 'nofunc', 0x72: 'noextern', 0x71: 'none', 0x74: 'noexn', }; const NUMTYPE = { 0x7f: 'i32', 0x7e: 'i64', 0x7d: 'f32', 0x7c: 'f64', 0x7b: 'v128' }; const FUNCREF = Object.freeze({ nullable: true, heap: 'func' }); const BOTREF = Object.freeze({ nullable: false, heap: 'bot' }); const isValTypeByte = (c) => c in NUMTYPE || c in ABSTRACT || c === 0x63 || c === 0x64; const isRef = (t) => typeof t === 'object'; const isFloat = (t) => t === 'f32' || t === 'f64'; function analyze(b) { const findings = []; // {at, rule, int?, cap?, value?} const canon = []; // canonical types: {kind, params, results, fields, final, sup, depth} const groups = new Map(); const types = []; // type index → canonical id const funcs = []; // function index → canonical id of its type const tables = []; // {at: 'i32'|'i64', elem} const mems = []; // {at} const tags = []; // canonical id const globals = []; // {type, mut} const elems = []; // reference type const refs = new Set(); let dataCount = null, dataSegments = null, definedFuncs = null, codeEntries = null; let nImportedFuncs = 0, definedMems = 0, iface = 1; const exported = new Set(), abiSeen = new Set(); const valueItem = (t, at) => { if (isFloat(t)) findings.push({ at, rule: 3, int: true }); else if (isRef(t)) findings.push({ at, rule: 9 }); }; const addInterface = (n) => { iface += n; if (iface > LIMIT.interface + 1) bad('interface size'); }; const funcType = (x) => { if (x >= types.length) bad('unknown type'); const id = types[x]; if (canon[id].kind !== 'func') bad('not a function type'); return canon[id]; }; // -------------------------------------------------------------------- subtyping function subHeap(a, c) { if (a === c || a === 'bot') return true; if (c === 'bot') return false; if (typeof a === 'number') { if (typeof c === 'number') { for (let x = canon[a].sup; x !== null; x = canon[x].sup) if (x === c) return true; return false; } const k = canon[a].kind; if (k === 'func') return c === 'func'; if (k === 'struct') return c === 'struct' || c === 'eq' || c === 'any'; return c === 'array' || c === 'eq' || c === 'any'; } if (typeof c === 'number') return canon[c].kind === 'func' ? a === 'nofunc' : a === 'none'; switch (a) { case 'none': return c === 'i31' || c === 'struct' || c === 'array' || c === 'eq' || c === 'any'; case 'i31': case 'struct': case 'array': return c === 'eq' || c === 'any'; case 'eq': return c === 'any'; case 'nofunc': return c === 'func'; case 'noextern': return c === 'extern'; case 'noexn': return c === 'exn'; default: return false; } } function sub(a, c) { if (a === 'bot') return true; if (typeof a === 'string' || typeof c === 'string') return a === c; return (!a.nullable || c.nullable) && subHeap(a.heap, c.heap); } const same = (a, c) => (typeof a === 'string' ? a === c : typeof c === 'object' && a.nullable === c.nullable && a.heap === c.heap); const sameStorage = (a, c) => same(a, c); const subStorage = (a, c) => (a === 'i8' || a === 'i16' || c === 'i8' || c === 'i16' ? a === c : sub(a, c)); const fieldMatch = (f, g) => f.mut === g.mut && (f.mut ? sameStorage(f.st, g.st) : subStorage(f.st, g.st)); function compMatch(e, s) { if (e.kind !== s.kind) return false; if (e.kind === 'func') { return e.params.length === s.params.length && e.results.length === s.results.length && e.params.every((t, i) => sub(s.params[i], t)) && e.results.every((t, i) => sub(t, s.results[i])); } if (e.kind === 'struct') return e.fields.length >= s.fields.length && s.fields.every((g, i) => fieldMatch(e.fields[i], g)); return fieldMatch(e.fields[0], s.fields[0]); } function top(h) { if (typeof h === 'number') return canon[h].kind === 'func' ? 'func' : 'any'; switch (h) { case 'func': case 'nofunc': return 'func'; case 'extern': case 'noextern': return 'extern'; case 'exn': case 'noexn': return 'exn'; default: return 'any'; } } const unpack = (st) => (st === 'i8' || st === 'i16' ? 'i32' : st); const defaultable = (t) => !isRef(t) || t.nullable; // -------------------------------------------------------------------- reading types function heapType(r, raw) { const c = r.peek(); if (c in ABSTRACT) { r.at++; return ABSTRACT[c]; } const x = r.s33(); if (x < 0) bad('bad heap type'); if (raw) return { mod: x }; if (x >= types.length) bad('unknown type'); return types[x]; } function valType(r, raw) { const c = r.byte(); if (c in NUMTYPE) return NUMTYPE[c]; if (c in ABSTRACT) return { nullable: true, heap: ABSTRACT[c] }; if (c === 0x63 || c === 0x64) return { nullable: c === 0x63, heap: heapType(r, raw) }; bad('bad value type'); } function refType(r) { const t = valType(r, false); if (!isRef(t)) bad('not a reference type'); return t; } function storageType(r) { const c = r.peek(); if (c === 0x78) { r.at++; return 'i8'; } if (c === 0x77) { r.at++; return 'i16'; } return valType(r, true); } function mutability(r) { const m = r.byte(); if (m > 1) bad('bad mutability'); return m === 1; } // One subtype of a type entry; the value types of a plain function type with their offsets. function subType(r) { let final = true, supers = []; let c = r.byte(); if (c === 0x50 || c === 0x4f) { final = c === 0x4f; const n = r.u32(); if (n > 1) bad('more than one supertype'); for (let i = 0; i < n; i++) supers.push(r.u32()); c = r.byte(); } const items = []; if (c === 0x60) { const lists = []; for (const limit of [LIMIT.params, LIMIT.results]) { const n = r.u32(); if (n > limit) bad('too many parameters or results'); const ts = []; for (let i = 0; i < n; i++) { const at = r.at; const t = valType(r, true); items.push([t, at]); ts.push(t); } lists.push(ts); } return { final, supers, kind: 'func', params: lists[0], results: lists[1], fields: null, items }; } if (c === 0x5f) { const n = r.u32(); if (n > LIMIT.fields) bad('too many fields'); const fields = []; for (let i = 0; i < n; i++) { const st = storageType(r); fields.push({ st, mut: mutability(r) }); } return { final, supers, kind: 'struct', params: null, results: null, fields, items }; } if (c === 0x5e) { const st = storageType(r); return { final, supers, kind: 'array', params: null, results: null, fields: [{ st, mut: mutability(r) }], items }; } bad('bad composite type'); } // A recursion group: bounds, canonical ids (one per equivalent group), then validity. function defineGroup(subs) { const base = types.length, n = subs.length; const hk = (h) => (typeof h === 'string' ? h : (h.mod >= base ? 'r' + (h.mod - base) : 'c' + types[h.mod])); const vk = (t) => (typeof t === 'string' ? t : (t.nullable ? '?' : '!') + hk(t.heap)); const check = (t) => { if (isRef(t) && typeof t.heap === 'object' && t.heap.mod >= base + n) bad('unknown type'); }; subs.forEach((s, k) => { for (const x of s.supers) if (x >= base + k) bad('supertypes must be defined before subtypes'); (s.params || []).forEach(check); (s.results || []).forEach(check); (s.fields || []).forEach((f) => check(f.st)); }); const key = subs.map((s) => (s.final ? 'F' : 'N') + s.supers.map((x) => hk({ mod: x })).join(',') + ':' + s.kind + '(' + (s.kind === 'func' ? s.params.map(vk).join(',') + ')(' + s.results.map(vk).join(',') : s.fields.map((f) => (f.mut ? 'm' : 'c') + vk(f.st)).join(',')) + ')').join('|'); let start = groups.get(key); const fresh = start === undefined; if (fresh) { start = canon.length; groups.set(key, start); const cv = (t) => (typeof t === 'string' ? t : { nullable: t.nullable, heap: typeof t.heap === 'string' ? t.heap : (t.heap.mod >= base ? start + t.heap.mod - base : types[t.heap.mod]) }); for (const s of subs) { canon.push({ kind: s.kind, final: s.final, depth: 0, sup: s.supers.length ? (s.supers[0] >= base ? start + s.supers[0] - base : types[s.supers[0]]) : null, params: s.params && s.params.map(cv), results: s.results && s.results.map(cv), fields: s.fields && s.fields.map((f) => ({ st: f.st === 'i8' || f.st === 'i16' ? f.st : cv(f.st), mut: f.mut })), }); } } for (let k = 0; k < n; k++) types.push(start + k); if (!fresh) return; for (let k = 0; k < n; k++) { const e = canon[start + k]; if (e.sup === null) continue; const s = canon[e.sup]; if (s.final) bad('final supertype'); if (!compMatch(e, s)) bad('subtype does not match its supertype'); e.depth = s.depth + 1; if (e.depth > LIMIT.depth) bad('subtype too deep'); } } function limits(r, i64Bound) { const flags = r.byte(); if (flags !== 0 && flags !== 1 && flags !== 4 && flags !== 5) bad('bad limits'); const at = flags & 4 ? 'i64' : 'i32'; const read = () => (at === 'i64' ? r.u64() : BigInt(r.u32())); const min = read(); const max = flags & 1 ? read() : null; if (max !== null && min > max) bad('minimum above maximum'); const bound = at === 'i64' ? i64Bound : (i64Bound === null ? null : 65536n); if (bound !== null && (min > bound || (max !== null && max > bound))) bad('memory too large'); return { at, min, max }; } const memType = (r) => limits(r, 1n << 48n); function tableType(r) { const elem = refType(r); const l = limits(r, null); return { at: l.at, min: l.min, max: l.max, elem }; } function globalType(r) { const type = valType(r, false); return { type, mut: mutability(r) }; } // -------------------------------------------------------------------- constant expressions function constExpr(r, expect) { const st = []; const pop = (t) => { if (!st.length) bad('constant: empty stack'); const v = st.pop(); if (!sub(v, t)) bad('constant: type mismatch'); return v; }; for (;;) { const at = r.at, op = r.byte(); switch (op) { case 0x0b: if (st.length !== 1 || !sub(st[0], expect)) bad('constant: type mismatch'); return; case 0x41: r.s32(); st.push('i32'); break; case 0x42: r.s64(); st.push('i64'); break; case 0x43: r.skip(4); st.push('f32'); findings.push({ at, rule: 3, int: true }); break; case 0x44: r.skip(8); st.push('f64'); findings.push({ at, rule: 3, int: true }); break; case 0x23: { const x = r.u32(); if (x >= globals.length) bad('unknown global'); if (globals[x].mut) bad('constant: mutable global'); st.push(globals[x].type); break; } case 0xd0: st.push({ nullable: true, heap: heapType(r, false) }); break; case 0xd2: { const x = r.u32(); if (x >= funcs.length) bad('unknown function'); refs.add(x); st.push({ nullable: false, heap: funcs[x] }); break; } case 0x6a: case 0x6b: case 0x6c: pop('i32'); pop('i32'); st.push('i32'); break; case 0x7c: case 0x7d: case 0x7e: pop('i64'); pop('i64'); st.push('i64'); break; case 0xfd: { if (r.u32() !== 12) bad('constant expression required'); r.skip(16); st.push('v128'); break; } case 0xfb: { const s = r.u32(); findings.push({ at, rule: 12 }); if (s === 0 || s === 1) { const x = r.u32(), e = gcType(x, 'struct'); if (s === 0) for (let i = e.fields.length - 1; i >= 0; i--) pop(unpack(e.fields[i].st)); else if (!e.fields.every((f) => defaultable(unpack(f.st)))) bad('not defaultable'); st.push({ nullable: false, heap: types[x] }); } else if (s === 6 || s === 7 || s === 8) { const x = r.u32(), e = gcType(x, 'array'); if (s === 6) { pop('i32'); pop(unpack(e.fields[0].st)); } else if (s === 7) { pop('i32'); if (!defaultable(unpack(e.fields[0].st))) bad('not defaultable'); } else { const k = r.u32(); for (let i = 0; i < k; i++) pop(unpack(e.fields[0].st)); } st.push({ nullable: false, heap: types[x] }); } else if (s === 28) { pop('i32'); st.push({ nullable: false, heap: 'i31' }); } else if (s === 26 || s === 27) { const from = s === 26 ? 'extern' : 'any', to = s === 26 ? 'any' : 'extern'; const v = pop({ nullable: true, heap: from }); st.push({ nullable: v.nullable, heap: to }); } else bad('constant expression required'); break; } default: bad('constant expression required'); } } } function gcType(x, kind) { if (x >= types.length) bad('unknown type'); const e = canon[types[x]]; if (e.kind !== kind) bad(`not a ${kind} type`); return e; } // -------------------------------------------------------------------- function bodies function body(r, typeId) { const ft = canon[typeId]; const locals = ft.params.slice(); let count = locals.length; for (let g = r.u32(); g > 0; g--) { const n = r.u32(); const at = r.at; const t = valType(r, false); valueItem(t, at); count += n; if (count > LIMIT.locals) bad('too many locals'); for (let i = 0; i < n; i++) locals.push(t); } const inited = new Uint8Array(locals.length); for (let i = 0; i < locals.length; i++) inited[i] = i < ft.params.length || defaultable(locals[i]) ? 1 : 0; const initLog = []; const vals = [], ctrls = []; const pushCtrl = (kind, start, end) => { ctrls.push({ kind, start, end, height: vals.length, unreachable: false, init: initLog.length }); for (const t of start) vals.push(t); }; const popVal = () => { const f = ctrls[ctrls.length - 1]; if (vals.length === f.height) { if (f.unreachable) return 'bot'; bad('type mismatch: empty stack'); } return vals.pop(); }; const pop = (t) => { const v = popVal(); if (!sub(v, t)) bad('type mismatch'); return v; }; const popVals = (ts) => { const out = new Array(ts.length); for (let i = ts.length - 1; i >= 0; i--) out[i] = pop(ts[i]); return out; }; const pushVals = (ts) => { for (const t of ts) vals.push(t); }; const popRef = () => { const v = popVal(); if (v === 'bot') return BOTREF; if (!isRef(v)) bad('type mismatch: not a reference'); return v; }; const popCtrl = () => { const f = ctrls[ctrls.length - 1]; popVals(f.end); if (vals.length !== f.height) bad('type mismatch: values left at the end of a block'); ctrls.pop(); while (initLog.length > f.init) inited[initLog.pop()] = 0; return f; }; const labelTypes = (l) => { if (l >= ctrls.length) bad('unknown label'); const f = ctrls[ctrls.length - 1 - l]; return f.kind === 'loop' ? f.start : f.end; }; const unreachable = () => { const f = ctrls[ctrls.length - 1]; vals.length = f.height; f.unreachable = true; }; const blockType = () => { const at = r.at, c = r.peek(); if (c === 0x40) { r.at++; return [[], []]; } if (isValTypeByte(c)) { const t = valType(r, false); if (isFloat(t)) findings.push({ at, rule: 3, int: true }); return [[], [t]]; } const x = r.s33(); if (x < 0) bad('bad block type'); const e = funcType(x); return [e.params, e.results]; }; const resultsMatch = (from, to) => from.length === to.length && from.every((t, i) => sub(t, to[i])); const memarg = (natural) => { let flags = r.u32(); let mem = 0; if ((flags & 0x40) !== 0) { flags -= 0x40; mem = r.u32(); } if (flags >= 64) bad('alignment too large'); const offset = r.u64n(); if (mem >= mems.length) bad('unknown memory'); if (flags > natural) bad('alignment above natural'); const at = mems[mem].at; if (at === 'i32' && offset > 0xffffffff) bad('offset out of range'); return at; }; const memIdx = () => { const m = r.u32(); if (m >= mems.length) bad('unknown memory'); return mems[m].at; }; const table = () => { const t = r.u32(); if (t >= tables.length) bad('unknown table'); return tables[t]; }; const dataIdx = () => { const d = r.u32(); if (dataCount === null) bad('data count section required'); if (d >= dataCount) bad('unknown data segment'); }; const elemIdx = () => { const e = r.u32(); if (e >= elems.length) bad('unknown element segment'); return elems[e]; }; const tag = () => { const x = r.u32(); if (x >= tags.length) bad('unknown tag'); return canon[tags[x]]; }; const local = () => { const x = r.u32(); if (x >= locals.length) bad('unknown local'); return x; }; const global = () => { const x = r.u32(); if (x >= globals.length) bad('unknown global'); return globals[x]; }; const callee = () => { const f = r.u32(); if (f >= funcs.length) bad('unknown function'); return canon[funcs[f]]; }; const labelMatch = (ts, l) => { const lt = labelTypes(l); if (!resultsMatch(ts, lt)) bad('type mismatch: catch'); }; const minAt = (a, c) => (a === 'i64' && c === 'i64' ? 'i64' : 'i32'); pushCtrl('func', [], ft.results); for (;;) { if (r.eof()) bad('function body ends before its end'); const at = r.at, op = r.byte(); if (op >= 0x45 && op <= 0xc4) { const n = NUMERIC[op]; if (!n) bad('unknown opcode'); if (n[3]) findings.push({ at, rule: 3, int: true }); popVals(n[1]); pushVals(n[2]); } else if (op >= 0x28 && op <= 0x3e) { const [, t, natural, store] = LOADSTORE[op - 0x28]; if (t === 'f32' || t === 'f64') findings.push({ at, rule: 3, int: true }); const a = memarg(natural); if (store) { pop(t); pop(a); } else { pop(a); vals.push(t); } } else switch (op) { case 0x00: unreachable(); break; case 0x01: break; case 0x02: case 0x03: { const [p, q] = blockType(); popVals(p); pushCtrl(op === 0x02 ? 'block' : 'loop', p, q); break; } case 0x04: { const [p, q] = blockType(); pop('i32'); popVals(p); pushCtrl('if', p, q); break; } case 0x05: { const f = popCtrl(); if (f.kind !== 'if') bad('else without if'); pushCtrl('else', f.start, f.end); break; } case 0x08: { findings.push({ at, rule: 12 }); const e = tag(); popVals(e.params); unreachable(); break; } case 0x0a: { findings.push({ at, rule: 12 }); pop({ nullable: true, heap: 'exn' }); unreachable(); break; } case 0x0b: { let f = ctrls[ctrls.length - 1]; if (f.kind === 'if') { popCtrl(); pushCtrl('else', f.start, f.end); } f = popCtrl(); pushVals(f.end); break; } case 0x0c: { popVals(labelTypes(r.u32())); unreachable(); break; } case 0x0d: { const lt = labelTypes(r.u32()); pop('i32'); popVals(lt); pushVals(lt); break; } case 0x0e: { const n = r.u32(); const ls = []; for (let i = 0; i < n; i++) ls.push(r.u32()); const dflt = r.u32(); pop('i32'); const arity = labelTypes(dflt).length; for (const l of ls) { const lt = labelTypes(l); if (lt.length !== arity) bad('br_table arity'); pushVals(popVals(lt)); } popVals(labelTypes(dflt)); unreachable(); break; } case 0x0f: popVals(ft.results); unreachable(); break; case 0x10: { const e = callee(); popVals(e.params); pushVals(e.results); break; } case 0x11: case 0x13: { if (op === 0x13) findings.push({ at, rule: 12 }); const e = funcType(r.u32()), t = table(); if (!sub(t.elem, FUNCREF)) bad('call_indirect of a table not of functions'); pop(t.at); popVals(e.params); if (op === 0x11) pushVals(e.results); else { if (!resultsMatch(e.results, ft.results)) bad('return_call_indirect results'); unreachable(); } break; } case 0x12: { findings.push({ at, rule: 12 }); const e = callee(); popVals(e.params); if (!resultsMatch(e.results, ft.results)) bad('return_call results'); unreachable(); break; } case 0x14: case 0x15: { findings.push({ at, rule: 12 }); const x = r.u32(), e = funcType(x); pop({ nullable: true, heap: types[x] }); popVals(e.params); if (op === 0x14) pushVals(e.results); else { if (!resultsMatch(e.results, ft.results)) bad('return_call_ref results'); unreachable(); } break; } case 0x1a: popVal(); break; case 0x1b: { pop('i32'); const t1 = popVal(), t2 = popVal(); const num = (t) => t === 'bot' || t === 'i32' || t === 'i64' || t === 'f32' || t === 'f64'; const vec = (t) => t === 'bot' || t === 'v128'; if (!((num(t1) && num(t2)) || (vec(t1) && vec(t2)))) bad('select of references'); if (t1 !== t2 && t1 !== 'bot' && t2 !== 'bot') bad('select type mismatch'); vals.push(t1 === 'bot' ? t2 : t1); break; } case 0x1c: { if (r.u32() !== 1) bad('select with other than one type'); const tat = r.at; const t = valType(r, false); if (isFloat(t)) findings.push({ at: tat, rule: 3, int: true }); pop('i32'); pop(t); pop(t); vals.push(t); break; } case 0x1f: { findings.push({ at, rule: 12 }); const [p, q] = blockType(); const n = r.u32(); if (n > LIMIT.catches) bad('too many catches'); for (let i = 0; i < n; i++) { const kind = r.byte(); if (kind > 3) bad('bad catch'); const e = kind < 2 ? tag() : null; const l = r.u32(); const ts = e ? e.params.slice() : []; if (kind === 1 || kind === 3) ts.push({ nullable: false, heap: 'exn' }); labelMatch(ts, l); } popVals(p); pushCtrl('block', p, q); break; } case 0x20: { const x = local(); if (!inited[x]) bad('uninitialized local'); vals.push(locals[x]); break; } case 0x21: case 0x22: { const x = local(); pop(locals[x]); if (!inited[x]) { inited[x] = 1; initLog.push(x); } if (op === 0x22) vals.push(locals[x]); break; } case 0x23: vals.push(global().type); break; case 0x24: { const g = global(); if (!g.mut) bad('global.set of an immutable global'); pop(g.type); break; } case 0x25: { const t = table(); pop(t.at); vals.push(t.elem); break; } case 0x26: { findings.push({ at, rule: 7 }); const t = table(); pop(t.elem); pop(t.at); break; } case 0x3f: vals.push(memIdx()); break; case 0x40: { const a = memIdx(); pop(a); vals.push(a); break; } case 0x41: r.s32(); vals.push('i32'); break; case 0x42: r.s64(); vals.push('i64'); break; case 0x43: r.skip(4); findings.push({ at, rule: 3, int: true }); vals.push('f32'); break; case 0x44: r.skip(8); findings.push({ at, rule: 3, int: true }); vals.push('f64'); break; case 0xd0: vals.push({ nullable: true, heap: heapType(r, false) }); break; case 0xd1: popRef(); vals.push('i32'); break; case 0xd2: { const x = r.u32(); if (x >= funcs.length) bad('unknown function'); if (!refs.has(x)) bad('undeclared function reference'); vals.push({ nullable: false, heap: funcs[x] }); break; } case 0xd3: { findings.push({ at, rule: 12 }); const eq = { nullable: true, heap: 'eq' }; pop(eq); pop(eq); vals.push('i32'); break; } case 0xd4: { findings.push({ at, rule: 12 }); const v = popRef(); vals.push({ nullable: false, heap: v.heap }); break; } case 0xd5: { findings.push({ at, rule: 12 }); const lt = labelTypes(r.u32()); const v = popRef(); popVals(lt); pushVals(lt); vals.push({ nullable: false, heap: v.heap }); break; } case 0xd6: { findings.push({ at, rule: 12 }); const lt = labelTypes(r.u32()); const last = lt[lt.length - 1]; if (!lt.length || !isRef(last)) bad('br_on_non_null label'); pop({ nullable: true, heap: last.heap }); const rest = lt.slice(0, -1); popVals(rest); pushVals(rest); break; } case 0xfb: gcOp(at); break; case 0xfc: miscOp(at); break; case 0xfd: simdOp(at); break; default: bad('unknown opcode'); } if (ctrls.length === 0) { if (!r.eof()) bad('instructions after the end of the function'); return; } } function miscOp(at) { const s = r.u32(); if (s <= 7) { findings.push({ at, rule: 3, int: true }); pop(s < 2 || s === 4 || s === 5 ? 'f32' : 'f64'); vals.push(s < 4 ? 'i32' : 'i64'); return; } switch (s) { case 8: { dataIdx(); const a = memIdx(); pop('i32'); pop('i32'); pop(a); return; } case 9: dataIdx(); return; case 10: { const d = memIdx(), c = memIdx(); pop(minAt(d, c)); pop(c); pop(d); return; } case 11: { const a = memIdx(); pop(a); pop('i32'); pop(a); return; } case 12: { const e = elemIdx(), t = table(); if (!sub(e, t.elem)) bad('table.init type'); pop('i32'); pop('i32'); pop(t.at); return; } case 13: elemIdx(); return; case 14: { const d = table(), c = table(); if (!sub(c.elem, d.elem)) bad('table.copy type'); pop(minAt(d.at, c.at)); pop(c.at); pop(d.at); return; } case 15: { findings.push({ at, rule: 7 }); const t = table(); pop(t.at); pop(t.elem); vals.push(t.at); return; } case 16: vals.push(table().at); return; case 17: { findings.push({ at, rule: 7 }); const t = table(); pop(t.at); pop(t.elem); pop(t.at); return; } default: bad('unknown opcode'); } } function gcOp(at) { const s = r.u32(); findings.push({ at, rule: 12 }); const ref = (x, nullable = true) => ({ nullable, heap: types[x] }); const field = (e, i) => { if (i >= e.fields.length) bad('unknown field'); return e.fields[i]; }; const packed = (f) => f.st === 'i8' || f.st === 'i16'; switch (s) { case 0: { const x = r.u32(), e = gcType(x, 'struct'); for (let i = e.fields.length - 1; i >= 0; i--) pop(unpack(e.fields[i].st)); vals.push(ref(x, false)); return; } case 1: { const x = r.u32(), e = gcType(x, 'struct'); if (!e.fields.every((f) => defaultable(unpack(f.st)))) bad('not defaultable'); vals.push(ref(x, false)); return; } case 2: case 3: case 4: { const x = r.u32(), e = gcType(x, 'struct'), f = field(e, r.u32()); if ((s === 2) === packed(f)) bad('struct.get of a packed field, or get_s of an unpacked one'); pop(ref(x)); vals.push(unpack(f.st)); return; } case 5: { const x = r.u32(), e = gcType(x, 'struct'), f = field(e, r.u32()); if (!f.mut) bad('immutable field'); pop(unpack(f.st)); pop(ref(x)); return; } case 6: { const x = r.u32(), e = gcType(x, 'array'); pop('i32'); pop(unpack(e.fields[0].st)); vals.push(ref(x, false)); return; } case 7: { const x = r.u32(), e = gcType(x, 'array'); if (!defaultable(unpack(e.fields[0].st))) bad('not defaultable'); pop('i32'); vals.push(ref(x, false)); return; } case 8: { const x = r.u32(), e = gcType(x, 'array'), n = r.u32(); for (let i = 0; i < n; i++) pop(unpack(e.fields[0].st)); vals.push(ref(x, false)); return; } case 9: case 18: { const x = r.u32(), e = gcType(x, 'array'); dataIdx(); if (isRef(e.fields[0].st)) bad('array of references from data'); if (s === 18 && !e.fields[0].mut) bad('immutable array'); if (s === 9) { pop('i32'); pop('i32'); vals.push(ref(x, false)); } else { pop('i32'); pop('i32'); pop('i32'); pop(ref(x)); } return; } case 10: case 19: { const x = r.u32(), e = gcType(x, 'array'), et = elemIdx(); const st = e.fields[0].st; if (!isRef(st) || !sub(et, st)) bad('element type'); if (s === 19 && !e.fields[0].mut) bad('immutable array'); if (s === 10) { pop('i32'); pop('i32'); vals.push(ref(x, false)); } else { pop('i32'); pop('i32'); pop('i32'); pop(ref(x)); } return; } case 11: case 12: case 13: { const x = r.u32(), f = gcType(x, 'array').fields[0]; if ((s === 11) === packed(f)) bad('array.get of a packed element, or get_s of an unpacked one'); pop('i32'); pop(ref(x)); vals.push(unpack(f.st)); return; } case 14: { const x = r.u32(), f = gcType(x, 'array').fields[0]; if (!f.mut) bad('immutable array'); pop(unpack(f.st)); pop('i32'); pop(ref(x)); return; } case 15: pop({ nullable: true, heap: 'array' }); vals.push('i32'); return; case 16: { const x = r.u32(), f = gcType(x, 'array').fields[0]; if (!f.mut) bad('immutable array'); pop('i32'); pop(unpack(f.st)); pop('i32'); pop(ref(x)); return; } case 17: { const x = r.u32(), d = gcType(x, 'array').fields[0], y = r.u32(), c = gcType(y, 'array').fields[0]; if (!d.mut) bad('immutable array'); if (!subStorage(c.st, d.st)) bad('array.copy types'); pop('i32'); pop('i32'); pop(ref(y)); pop('i32'); pop(ref(x)); return; } case 20: case 21: case 22: case 23: { const h = heapType(r, false); pop({ nullable: true, heap: top(h) }); vals.push(s < 22 ? 'i32' : { nullable: s === 23, heap: h }); return; } case 24: case 25: { const flags = r.byte(); if (flags > 3) bad('bad cast flags'); const lt = labelTypes(r.u32()); const rt1 = { nullable: (flags & 1) !== 0, heap: heapType(r, false) }; const rt2 = { nullable: (flags & 2) !== 0, heap: heapType(r, false) }; if (!sub(rt2, rt1)) bad('br_on_cast types'); const diff = { nullable: rt2.nullable ? false : rt1.nullable, heap: rt1.heap }; const last = lt[lt.length - 1]; if (!lt.length || !isRef(last)) bad('br_on_cast label'); if (!sub(s === 24 ? rt2 : diff, last)) bad('br_on_cast label type'); pop(rt1); const rest = lt.slice(0, -1); popVals(rest); pushVals(rest); vals.push(s === 24 ? diff : rt2); return; } case 26: case 27: { const v = pop({ nullable: true, heap: s === 26 ? 'extern' : 'any' }); const to = s === 26 ? 'any' : 'extern'; vals.push(v === 'bot' || v.heap === 'bot' ? { nullable: false, heap: to } : { nullable: v.nullable, heap: to }); return; } case 28: pop('i32'); vals.push({ nullable: false, heap: 'i31' }); return; case 29: case 30: pop({ nullable: true, heap: 'i31' }); vals.push('i32'); return; default: bad('unknown opcode'); } } function simdOp(at) { const s = r.u32(); const e = SIMD[s]; if (!e) bad('unknown vector opcode'); const [name, sig, a1, a2] = e; if (name.includes('f32') || name.includes('f64')) findings.push({ at, rule: 3, int: true }); if (s >= 0x100) findings.push({ at, rule: 4 }); const lane = (n) => { if (r.byte() >= n) bad('lane index out of range'); }; switch (sig) { case 'load': { const a = memarg(a1); pop(a); vals.push('v128'); return; } case 'store': { const a = memarg(a1); pop('v128'); pop(a); return; } case 'const': r.skip(16); vals.push('v128'); return; case 'shuffle': for (let i = 0; i < 16; i++) lane(32); pop('v128'); pop('v128'); vals.push('v128'); return; case 'splat': pop(a1); vals.push('v128'); return; case 'extract': lane(a2); pop('v128'); vals.push(a1); return; case 'replace': lane(a2); pop(a1); pop('v128'); vals.push('v128'); return; case 'loadlane': { const a = memarg(a1); lane(a2); pop('v128'); pop(a); vals.push('v128'); return; } case 'storelane': { const a = memarg(a1); lane(a2); pop('v128'); pop(a); return; } case 'un': pop('v128'); vals.push('v128'); return; case 'bin': pop('v128'); pop('v128'); vals.push('v128'); return; case 'tern': pop('v128'); pop('v128'); pop('v128'); vals.push('v128'); return; case 'test': pop('v128'); vals.push('i32'); return; case 'shift': pop('i32'); pop('v128'); vals.push('v128'); return; default: bad('unknown vector opcode'); } } } // -------------------------------------------------------------------- the sections const r = new Reader(b, 0, b.length); if (b.length < 8 || b[0] !== 0 || b[1] !== 0x61 || b[2] !== 0x73 || b[3] !== 0x6d) bad('no preamble'); if (b[4] !== 1 || b[5] !== 0 || b[6] !== 0 || b[7] !== 0) bad('not version 1 of the binary format'); r.at = 8; // The order of the sections of WebAssembly 3.0, by id. const ORDER = { 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 13: 6, 6: 7, 7: 8, 8: 9, 9: 10, 12: 11, 10: 12, 11: 13 }; let last = 0; while (!r.eof()) { const idAt = r.at, id = r.byte(), size = r.u32(); if (size > r.end - r.at) bad('section beyond the module'); const s = new Reader(b, r.at, r.at + size); r.at += size; if (id === 0) { s.name(); continue; } if (!ORDER[id]) bad('unknown section'); if (ORDER[id] <= last) bad('section out of order'); last = ORDER[id]; switch (id) { case 1: { const n = s.u32(); if (n > LIMIT.types) bad('too many types'); for (let i = 0; i < n; i++) { const at = s.at, first = s.peek(); let subs; if (first === 0x4e) { s.at++; const k = s.u32(); if (types.length + k > LIMIT.types) bad('too many types'); subs = []; for (let j = 0; j < k; j++) subs.push(subType(s)); } else { if (types.length + 1 > LIMIT.types) bad('too many types'); subs = [subType(s)]; } if (first !== 0x60) findings.push({ at, rule: 12 }); else for (const [t, tat] of subs[0].items) valueItem(t, tat); defineGroup(subs); } break; } case 2: { for (let n = s.u32(); n > 0; n--) { const at = s.at; s.name(); s.name(); findings.push({ at, rule: 5 }); const kind = s.byte(); if (kind === 0x00) { const x = s.u32(), e = funcType(x); funcs.push(types[x]); nImportedFuncs++; if (funcs.length > LIMIT.functions) bad('too many functions'); addInterface(2 + e.params.length + e.results.length); } else if (kind === 0x01) { tables.push(tableType(s)); if (tables.length > LIMIT.tables) bad('too many tables'); addInterface(1); } else if (kind === 0x02) { mems.push(memType(s)); if (mems.length > LIMIT.memories) bad('too many memories'); addInterface(1); } else if (kind === 0x03) { globals.push(globalType(s)); if (globals.length > LIMIT.globals) bad('too many globals'); addInterface(1); } else if (kind === 0x04) { if (s.byte() !== 0) bad('bad tag attribute'); const x = s.u32(), e = funcType(x); if (e.results.length) bad('tag type with results'); tags.push(types[x]); if (tags.length > LIMIT.tags) bad('too many tags'); addInterface(2 + e.params.length); } else bad('bad import kind'); } break; } case 3: { const n = s.u32(); if (funcs.length + n > LIMIT.functions) bad('too many functions'); findings.push({ at: idAt, rule: 11, cap: 'functions', value: n }); for (let i = 0; i < n; i++) { const x = s.u32(); funcType(x); funcs.push(types[x]); } definedFuncs = n; break; } case 4: { const n = s.u32(); if (tables.length + n > LIMIT.tables) bad('too many tables'); for (let i = 0; i < n; i++) { const at = s.at; let init = false; if (s.peek() === 0x40) { s.at++; if (s.byte() !== 0) bad('bad table encoding'); init = true; } const t = tableType(s); if (init) constExpr(s, t.elem); else if (!t.elem.nullable) bad('table of a non-defaultable type without an initialiser'); const fixed = i === 0 && t.elem.nullable && t.elem.heap === 'func' && t.at === 'i32' && t.max !== null && t.max === t.min; if (!fixed) findings.push({ at, rule: 7 }); tables.push(t); } break; } case 5: { const n = s.u32(); if (mems.length + n > LIMIT.memories) bad('too many memories'); for (let i = 0; i < n; i++) { const at = s.at, m = memType(s); mems.push(m); definedMems++; if (i > 0 || m.at === 'i64' || m.max === null) findings.push({ at, rule: 6 }); else findings.push({ at, rule: 6, cap: 'memory_pages', value: Number(m.max) }); } break; } case 13: { findings.push({ at: idAt, rule: 12 }); const n = s.u32(); if (tags.length + n > LIMIT.tags) bad('too many tags'); for (let i = 0; i < n; i++) { if (s.byte() !== 0) bad('bad tag attribute'); const x = s.u32(); if (funcType(x).results.length) bad('tag type with results'); tags.push(types[x]); } break; } case 6: { const n = s.u32(); if (globals.length + n > LIMIT.globals) bad('too many globals'); for (let i = 0; i < n; i++) { const at = s.at, g = globalType(s); valueItem(g.type, at); constExpr(s, g.type); globals.push(g); } break; } case 7: { for (let n = s.u32(); n > 0; n--) { const at = s.at, nm = s.name(), kind = s.byte(), x = s.u32(); if (exported.has(nm)) bad('duplicate export name'); exported.add(nm); let ty = null; if (kind === 0) { if (x >= funcs.length) bad('unknown function'); refs.add(x); ty = canon[funcs[x]]; addInterface(2 + ty.params.length + ty.results.length); } else if (kind === 1) { if (x >= tables.length) bad('unknown table'); addInterface(1); } else if (kind === 2) { if (x >= mems.length) bad('unknown memory'); addInterface(1); } else if (kind === 3) { if (x >= globals.length) bad('unknown global'); addInterface(1); } else if (kind === 4) { if (x >= tags.length) bad('unknown tag'); const e = canon[tags[x]]; addInterface(2 + e.params.length + e.results.length); } else bad('bad export kind'); const abi = ABI.find(([name]) => name === nm); let ok = false; if (abi) { abiSeen.add(nm); ok = kind === abi[1] && (abi[2] === null || (ty.final && ty.sup === null && ty.params.length === abi[2][0].length && ty.params.every((t, i) => t === abi[2][0][i]) && ty.results.length === abi[2][1].length && ty.results.every((t, i) => t === abi[2][1][i]))); } if (!ok) findings.push({ at, rule: 10 }); } break; } case 8: { findings.push({ at: idAt, rule: 8 }); const x = s.u32(); if (x >= funcs.length) bad('unknown function'); const e = canon[funcs[x]]; if (e.params.length || e.results.length) bad('start function type'); break; } case 9: { const n = s.u32(); if (n > LIMIT.elems) bad('too many element segments'); for (let i = 0; i < n; i++) { const flags = s.u32(); if (flags > 7) bad('bad element segment flags'); const active = (flags & 1) === 0, exprs = (flags & 4) !== 0; let t = null; if (active) { const ti = flags & 2 ? s.u32() : 0; if (ti >= tables.length) bad('unknown table'); t = tables[ti]; constExpr(s, t.at); } let type; if (!exprs) { if (flags & 3) { if (s.byte() !== 0) bad('bad element kind'); } type = { nullable: false, heap: 'func' }; const k = s.u32(); if (k > LIMIT.segmentItems) bad('too many segment items'); for (let j = 0; j < k; j++) { const x = s.u32(); if (x >= funcs.length) bad('unknown function'); refs.add(x); } } else { type = flags & 3 ? refType(s) : FUNCREF; const k = s.u32(); if (k > LIMIT.segmentItems) bad('too many segment items'); for (let j = 0; j < k; j++) constExpr(s, type); } if (t && !sub(type, t.elem)) bad('element segment type'); elems.push(type); } break; } case 12: { dataCount = s.u32(); if (dataCount > LIMIT.datas) bad('too many data segments'); break; } case 10: { const n = s.u32(); if (n !== (definedFuncs ?? 0)) bad('function and code sections differ'); codeEntries = n; for (let i = 0; i < n; i++) { const size = s.u32(); if (size > s.end - s.at) bad('unexpected end'); if (size > LIMIT.body) bad('function body too large'); body(new Reader(b, s.at, s.at + size), funcs[nImportedFuncs + i]); s.at += size; } break; } case 11: { const n = s.u32(); if (n > LIMIT.datas) bad('too many data segments'); if (dataCount !== null && dataCount !== n) bad('data count and data section differ'); dataSegments = n; for (let i = 0; i < n; i++) { const flags = s.u32(); if (flags === 0 || flags === 2) { const m = flags === 2 ? s.u32() : 0; if (m >= mems.length) bad('unknown memory'); constExpr(s, mems[m].at); } else if (flags !== 1) bad('bad data segment flags'); s.skip(s.u32()); } break; } default: bad('unknown section'); } if (!s.eof()) bad('section size mismatch'); } if ((definedFuncs ?? 0) !== (codeEntries ?? 0)) bad('function and code sections differ'); if (dataCount !== null && dataSegments === null && dataCount !== 0) bad('data count without data'); if (definedMems === 0) findings.push({ at: b.length, rule: 6 }); if (abiSeen.size < ABI.length) findings.push({ at: b.length, rule: 10 }); return findings; } // ------------------------------------------------------------------------------ tables // Scalar numeric instructions 0x45..0xc4: [name, params, results, float]. const NUMERIC = []; { const def = (op, name, p, q) => { NUMERIC[op] = [name, p, q, name.includes('f32') || name.includes('f64')]; }; const rel = ['eq', 'ne', 'lt_s', 'lt_u', 'gt_s', 'gt_u', 'le_s', 'le_u', 'ge_s', 'ge_u']; const frel = ['eq', 'ne', 'lt', 'gt', 'le', 'ge']; const ibin = ['add', 'sub', 'mul', 'div_s', 'div_u', 'rem_s', 'rem_u', 'and', 'or', 'xor', 'shl', 'shr_s', 'shr_u', 'rotl', 'rotr']; const fun = ['abs', 'neg', 'ceil', 'floor', 'trunc', 'nearest', 'sqrt']; const fbin = ['add', 'sub', 'mul', 'div', 'min', 'max', 'copysign']; def(0x45, 'i32.eqz', ['i32'], ['i32']); rel.forEach((n, i) => def(0x46 + i, 'i32.' + n, ['i32', 'i32'], ['i32'])); def(0x50, 'i64.eqz', ['i64'], ['i32']); rel.forEach((n, i) => def(0x51 + i, 'i64.' + n, ['i64', 'i64'], ['i32'])); frel.forEach((n, i) => def(0x5b + i, 'f32.' + n, ['f32', 'f32'], ['i32'])); frel.forEach((n, i) => def(0x61 + i, 'f64.' + n, ['f64', 'f64'], ['i32'])); ['clz', 'ctz', 'popcnt'].forEach((n, i) => def(0x67 + i, 'i32.' + n, ['i32'], ['i32'])); ibin.forEach((n, i) => def(0x6a + i, 'i32.' + n, ['i32', 'i32'], ['i32'])); ['clz', 'ctz', 'popcnt'].forEach((n, i) => def(0x79 + i, 'i64.' + n, ['i64'], ['i64'])); ibin.forEach((n, i) => def(0x7c + i, 'i64.' + n, ['i64', 'i64'], ['i64'])); fun.forEach((n, i) => def(0x8b + i, 'f32.' + n, ['f32'], ['f32'])); fbin.forEach((n, i) => def(0x92 + i, 'f32.' + n, ['f32', 'f32'], ['f32'])); fun.forEach((n, i) => def(0x99 + i, 'f64.' + n, ['f64'], ['f64'])); fbin.forEach((n, i) => def(0xa0 + i, 'f64.' + n, ['f64', 'f64'], ['f64'])); const conv = [ [0xa7, 'i32.wrap_i64', 'i64', 'i32'], [0xa8, 'i32.trunc_f32_s', 'f32', 'i32'], [0xa9, 'i32.trunc_f32_u', 'f32', 'i32'], [0xaa, 'i32.trunc_f64_s', 'f64', 'i32'], [0xab, 'i32.trunc_f64_u', 'f64', 'i32'], [0xac, 'i64.extend_i32_s', 'i32', 'i64'], [0xad, 'i64.extend_i32_u', 'i32', 'i64'], [0xae, 'i64.trunc_f32_s', 'f32', 'i64'], [0xaf, 'i64.trunc_f32_u', 'f32', 'i64'], [0xb0, 'i64.trunc_f64_s', 'f64', 'i64'], [0xb1, 'i64.trunc_f64_u', 'f64', 'i64'], [0xb2, 'f32.convert_i32_s', 'i32', 'f32'], [0xb3, 'f32.convert_i32_u', 'i32', 'f32'], [0xb4, 'f32.convert_i64_s', 'i64', 'f32'], [0xb5, 'f32.convert_i64_u', 'i64', 'f32'], [0xb6, 'f32.demote_f64', 'f64', 'f32'], [0xb7, 'f64.convert_i32_s', 'i32', 'f64'], [0xb8, 'f64.convert_i32_u', 'i32', 'f64'], [0xb9, 'f64.convert_i64_s', 'i64', 'f64'], [0xba, 'f64.convert_i64_u', 'i64', 'f64'], [0xbb, 'f64.promote_f32', 'f32', 'f64'], [0xbc, 'i32.reinterpret_f32', 'f32', 'i32'], [0xbd, 'i64.reinterpret_f64', 'f64', 'i64'], [0xbe, 'f32.reinterpret_i32', 'i32', 'f32'], [0xbf, 'f64.reinterpret_i64', 'i64', 'f64'], [0xc0, 'i32.extend8_s', 'i32', 'i32'], [0xc1, 'i32.extend16_s', 'i32', 'i32'], [0xc2, 'i64.extend8_s', 'i64', 'i64'], [0xc3, 'i64.extend16_s', 'i64', 'i64'], [0xc4, 'i64.extend32_s', 'i64', 'i64'], ]; for (const [op, name, p, q] of conv) def(op, name, [p], [q]); } // Loads and stores 0x28..0x3e: [name, value type, natural alignment, store]. const LOADSTORE = [ ['i32.load', 'i32', 2, false], ['i64.load', 'i64', 3, false], ['f32.load', 'f32', 2, false], ['f64.load', 'f64', 3, false], ['i32.load8_s', 'i32', 0, false], ['i32.load8_u', 'i32', 0, false], ['i32.load16_s', 'i32', 1, false], ['i32.load16_u', 'i32', 1, false], ['i64.load8_s', 'i64', 0, false], ['i64.load8_u', 'i64', 0, false], ['i64.load16_s', 'i64', 1, false], ['i64.load16_u', 'i64', 1, false], ['i64.load32_s', 'i64', 2, false], ['i64.load32_u', 'i64', 2, false], ['i32.store', 'i32', 2, true], ['i64.store', 'i64', 3, true], ['f32.store', 'f32', 2, true], ['f64.store', 'f64', 3, true], ['i32.store8', 'i32', 0, true], ['i32.store16', 'i32', 1, true], ['i64.store8', 'i64', 0, true], ['i64.store16', 'i64', 1, true], ['i64.store32', 'i64', 2, true], ]; // Vector instructions, by their opcode after 0xfd: [name, signature, argument, argument]. const SIMD = []; { const d = (op, name, sig, a1, a2) => { SIMD[op] = [name, sig, a1, a2]; }; [[0x00, 'v128.load', 4], [0x01, 'v128.load8x8_s', 3], [0x02, 'v128.load8x8_u', 3], [0x03, 'v128.load16x4_s', 3], [0x04, 'v128.load16x4_u', 3], [0x05, 'v128.load32x2_s', 3], [0x06, 'v128.load32x2_u', 3], [0x07, 'v128.load8_splat', 0], [0x08, 'v128.load16_splat', 1], [0x09, 'v128.load32_splat', 2], [0x0a, 'v128.load64_splat', 3], [0x5c, 'v128.load32_zero', 2], [0x5d, 'v128.load64_zero', 3]].forEach(([op, n, a]) => d(op, n, 'load', a)); d(0x0b, 'v128.store', 'store', 4); d(0x0c, 'v128.const', 'const'); d(0x0d, 'i8x16.shuffle', 'shuffle'); d(0x0e, 'i8x16.swizzle', 'bin'); [['i8x16', 'i32'], ['i16x8', 'i32'], ['i32x4', 'i32'], ['i64x2', 'i64'], ['f32x4', 'f32'], ['f64x2', 'f64']] .forEach(([s, t], i) => d(0x0f + i, s + '.splat', 'splat', t)); [[0x15, 'i8x16.extract_lane_s', 'extract', 'i32', 16], [0x16, 'i8x16.extract_lane_u', 'extract', 'i32', 16], [0x17, 'i8x16.replace_lane', 'replace', 'i32', 16], [0x18, 'i16x8.extract_lane_s', 'extract', 'i32', 8], [0x19, 'i16x8.extract_lane_u', 'extract', 'i32', 8], [0x1a, 'i16x8.replace_lane', 'replace', 'i32', 8], [0x1b, 'i32x4.extract_lane', 'extract', 'i32', 4], [0x1c, 'i32x4.replace_lane', 'replace', 'i32', 4], [0x1d, 'i64x2.extract_lane', 'extract', 'i64', 2], [0x1e, 'i64x2.replace_lane', 'replace', 'i64', 2], [0x1f, 'f32x4.extract_lane', 'extract', 'f32', 4], [0x20, 'f32x4.replace_lane', 'replace', 'f32', 4], [0x21, 'f64x2.extract_lane', 'extract', 'f64', 2], [0x22, 'f64x2.replace_lane', 'replace', 'f64', 2], ].forEach(([op, n, sig, t, l]) => d(op, n, sig, t, l)); const irel = ['eq', 'ne', 'lt_s', 'lt_u', 'gt_s', 'gt_u', 'le_s', 'le_u', 'ge_s', 'ge_u']; irel.forEach((n, i) => d(0x23 + i, 'i8x16.' + n, 'bin')); irel.forEach((n, i) => d(0x2d + i, 'i16x8.' + n, 'bin')); irel.forEach((n, i) => d(0x37 + i, 'i32x4.' + n, 'bin')); ['eq', 'ne', 'lt', 'gt', 'le', 'ge'].forEach((n, i) => { d(0x41 + i, 'f32x4.' + n, 'bin'); d(0x47 + i, 'f64x2.' + n, 'bin'); }); d(0x4d, 'v128.not', 'un'); ['and', 'andnot', 'or', 'xor'].forEach((n, i) => d(0x4e + i, 'v128.' + n, 'bin')); d(0x52, 'v128.bitselect', 'tern'); d(0x53, 'v128.any_true', 'test'); [8, 16, 32, 64].forEach((w, i) => { d(0x54 + i, `v128.load${w}_lane`, 'loadlane', i, 16 >> i); d(0x58 + i, `v128.store${w}_lane`, 'storelane', i, 16 >> i); }); const rows = [ [0x5e, 'f32x4.demote_f64x2_zero', 'un'], [0x5f, 'f64x2.promote_low_f32x4', 'un'], [0x60, 'i8x16.abs', 'un'], [0x61, 'i8x16.neg', 'un'], [0x62, 'i8x16.popcnt', 'un'], [0x63, 'i8x16.all_true', 'test'], [0x64, 'i8x16.bitmask', 'test'], [0x65, 'i8x16.narrow_i16x8_s', 'bin'], [0x66, 'i8x16.narrow_i16x8_u', 'bin'], [0x67, 'f32x4.ceil', 'un'], [0x68, 'f32x4.floor', 'un'], [0x69, 'f32x4.trunc', 'un'], [0x6a, 'f32x4.nearest', 'un'], [0x6b, 'i8x16.shl', 'shift'], [0x6c, 'i8x16.shr_s', 'shift'], [0x6d, 'i8x16.shr_u', 'shift'], [0x6e, 'i8x16.add', 'bin'], [0x6f, 'i8x16.add_sat_s', 'bin'], [0x70, 'i8x16.add_sat_u', 'bin'], [0x71, 'i8x16.sub', 'bin'], [0x72, 'i8x16.sub_sat_s', 'bin'], [0x73, 'i8x16.sub_sat_u', 'bin'], [0x74, 'f64x2.ceil', 'un'], [0x75, 'f64x2.floor', 'un'], [0x76, 'i8x16.min_s', 'bin'], [0x77, 'i8x16.min_u', 'bin'], [0x78, 'i8x16.max_s', 'bin'], [0x79, 'i8x16.max_u', 'bin'], [0x7a, 'f64x2.trunc', 'un'], [0x7b, 'i8x16.avgr_u', 'bin'], [0x7c, 'i16x8.extadd_pairwise_i8x16_s', 'un'], [0x7d, 'i16x8.extadd_pairwise_i8x16_u', 'un'], [0x7e, 'i32x4.extadd_pairwise_i16x8_s', 'un'], [0x7f, 'i32x4.extadd_pairwise_i16x8_u', 'un'], [0x80, 'i16x8.abs', 'un'], [0x81, 'i16x8.neg', 'un'], [0x82, 'i16x8.q15mulr_sat_s', 'bin'], [0x83, 'i16x8.all_true', 'test'], [0x84, 'i16x8.bitmask', 'test'], [0x85, 'i16x8.narrow_i32x4_s', 'bin'], [0x86, 'i16x8.narrow_i32x4_u', 'bin'], [0x87, 'i16x8.extend_low_i8x16_s', 'un'], [0x88, 'i16x8.extend_high_i8x16_s', 'un'], [0x89, 'i16x8.extend_low_i8x16_u', 'un'], [0x8a, 'i16x8.extend_high_i8x16_u', 'un'], [0x8b, 'i16x8.shl', 'shift'], [0x8c, 'i16x8.shr_s', 'shift'], [0x8d, 'i16x8.shr_u', 'shift'], [0x8e, 'i16x8.add', 'bin'], [0x8f, 'i16x8.add_sat_s', 'bin'], [0x90, 'i16x8.add_sat_u', 'bin'], [0x91, 'i16x8.sub', 'bin'], [0x92, 'i16x8.sub_sat_s', 'bin'], [0x93, 'i16x8.sub_sat_u', 'bin'], [0x94, 'f64x2.nearest', 'un'], [0x95, 'i16x8.mul', 'bin'], [0x96, 'i16x8.min_s', 'bin'], [0x97, 'i16x8.min_u', 'bin'], [0x98, 'i16x8.max_s', 'bin'], [0x99, 'i16x8.max_u', 'bin'], [0x9b, 'i16x8.avgr_u', 'bin'], [0x9c, 'i16x8.extmul_low_i8x16_s', 'bin'], [0x9d, 'i16x8.extmul_high_i8x16_s', 'bin'], [0x9e, 'i16x8.extmul_low_i8x16_u', 'bin'], [0x9f, 'i16x8.extmul_high_i8x16_u', 'bin'], [0xa0, 'i32x4.abs', 'un'], [0xa1, 'i32x4.neg', 'un'], [0xa3, 'i32x4.all_true', 'test'], [0xa4, 'i32x4.bitmask', 'test'], [0xa7, 'i32x4.extend_low_i16x8_s', 'un'], [0xa8, 'i32x4.extend_high_i16x8_s', 'un'], [0xa9, 'i32x4.extend_low_i16x8_u', 'un'], [0xaa, 'i32x4.extend_high_i16x8_u', 'un'], [0xab, 'i32x4.shl', 'shift'], [0xac, 'i32x4.shr_s', 'shift'], [0xad, 'i32x4.shr_u', 'shift'], [0xae, 'i32x4.add', 'bin'], [0xb1, 'i32x4.sub', 'bin'], [0xb5, 'i32x4.mul', 'bin'], [0xb6, 'i32x4.min_s', 'bin'], [0xb7, 'i32x4.min_u', 'bin'], [0xb8, 'i32x4.max_s', 'bin'], [0xb9, 'i32x4.max_u', 'bin'], [0xba, 'i32x4.dot_i16x8_s', 'bin'], [0xbc, 'i32x4.extmul_low_i16x8_s', 'bin'], [0xbd, 'i32x4.extmul_high_i16x8_s', 'bin'], [0xbe, 'i32x4.extmul_low_i16x8_u', 'bin'], [0xbf, 'i32x4.extmul_high_i16x8_u', 'bin'], [0xc0, 'i64x2.abs', 'un'], [0xc1, 'i64x2.neg', 'un'], [0xc3, 'i64x2.all_true', 'test'], [0xc4, 'i64x2.bitmask', 'test'], [0xc7, 'i64x2.extend_low_i32x4_s', 'un'], [0xc8, 'i64x2.extend_high_i32x4_s', 'un'], [0xc9, 'i64x2.extend_low_i32x4_u', 'un'], [0xca, 'i64x2.extend_high_i32x4_u', 'un'], [0xcb, 'i64x2.shl', 'shift'], [0xcc, 'i64x2.shr_s', 'shift'], [0xcd, 'i64x2.shr_u', 'shift'], [0xce, 'i64x2.add', 'bin'], [0xd1, 'i64x2.sub', 'bin'], [0xd5, 'i64x2.mul', 'bin'], [0xd6, 'i64x2.eq', 'bin'], [0xd7, 'i64x2.ne', 'bin'], [0xd8, 'i64x2.lt_s', 'bin'], [0xd9, 'i64x2.gt_s', 'bin'], [0xda, 'i64x2.le_s', 'bin'], [0xdb, 'i64x2.ge_s', 'bin'], [0xdc, 'i64x2.extmul_low_i32x4_s', 'bin'], [0xdd, 'i64x2.extmul_high_i32x4_s', 'bin'], [0xde, 'i64x2.extmul_low_i32x4_u', 'bin'], [0xdf, 'i64x2.extmul_high_i32x4_u', 'bin'], [0xe0, 'f32x4.abs', 'un'], [0xe1, 'f32x4.neg', 'un'], [0xe3, 'f32x4.sqrt', 'un'], [0xe4, 'f32x4.add', 'bin'], [0xe5, 'f32x4.sub', 'bin'], [0xe6, 'f32x4.mul', 'bin'], [0xe7, 'f32x4.div', 'bin'], [0xe8, 'f32x4.min', 'bin'], [0xe9, 'f32x4.max', 'bin'], [0xea, 'f32x4.pmin', 'bin'], [0xeb, 'f32x4.pmax', 'bin'], [0xec, 'f64x2.abs', 'un'], [0xed, 'f64x2.neg', 'un'], [0xef, 'f64x2.sqrt', 'un'], [0xf0, 'f64x2.add', 'bin'], [0xf1, 'f64x2.sub', 'bin'], [0xf2, 'f64x2.mul', 'bin'], [0xf3, 'f64x2.div', 'bin'], [0xf4, 'f64x2.min', 'bin'], [0xf5, 'f64x2.max', 'bin'], [0xf6, 'f64x2.pmin', 'bin'], [0xf7, 'f64x2.pmax', 'bin'], [0xf8, 'i32x4.trunc_sat_f32x4_s', 'un'], [0xf9, 'i32x4.trunc_sat_f32x4_u', 'un'], [0xfa, 'f32x4.convert_i32x4_s', 'un'], [0xfb, 'f32x4.convert_i32x4_u', 'un'], [0xfc, 'i32x4.trunc_sat_f64x2_s_zero', 'un'], [0xfd, 'i32x4.trunc_sat_f64x2_u_zero', 'un'], [0xfe, 'f64x2.convert_low_i32x4_s', 'un'], [0xff, 'f64x2.convert_low_i32x4_u', 'un'], // The relaxed vector instructions (R4), 0x100 to 0x113. [0x100, 'i8x16.relaxed_swizzle', 'bin'], [0x101, 'i32x4.relaxed_trunc_f32x4_s', 'un'], [0x102, 'i32x4.relaxed_trunc_f32x4_u', 'un'], [0x103, 'i32x4.relaxed_trunc_f64x2_s_zero', 'un'], [0x104, 'i32x4.relaxed_trunc_f64x2_u_zero', 'un'], [0x105, 'f32x4.relaxed_madd', 'tern'], [0x106, 'f32x4.relaxed_nmadd', 'tern'], [0x107, 'f64x2.relaxed_madd', 'tern'], [0x108, 'f64x2.relaxed_nmadd', 'tern'], [0x109, 'i8x16.relaxed_laneselect', 'tern'], [0x10a, 'i16x8.relaxed_laneselect', 'tern'], [0x10b, 'i32x4.relaxed_laneselect', 'tern'], [0x10c, 'i64x2.relaxed_laneselect', 'tern'], [0x10d, 'f32x4.relaxed_min', 'bin'], [0x10e, 'f32x4.relaxed_max', 'bin'], [0x10f, 'f64x2.relaxed_min', 'bin'], [0x110, 'f64x2.relaxed_max', 'bin'], [0x111, 'i16x8.relaxed_q15mulr_s', 'bin'], [0x112, 'i16x8.relaxed_dot_i8x16_i7x16_s', 'bin'], [0x113, 'i32x4.relaxed_dot_i8x16_i7x16_add_s', 'tern'], ]; for (const [op, n, sig] of rows) d(op, n, sig); } /** The names of every instruction this file decodes, for a reader that compares tables. */ export function instructionNames() { return { numeric: NUMERIC.map((e, op) => e && [op, e[0]]).filter(Boolean), loadstore: LOADSTORE.map((e, i) => [0x28 + i, e[0]]), simd: SIMD.map((e, op) => e && [op, e[0]]).filter(Boolean), }; } // ------------------------------------------------------------------------------ the verdict /** * The analysis of a module's bytes: `null` where it breaks R2, else every item and absence * that breaks one of R3 to R12, each with the level and the cap it depends on. */ export function analyse(bytes) { try { return analyze(bytes); } catch (e) { if (e instanceof Refused2) return null; throw e; } } /** The verdict from an analysis, for a module of `length` bytes, at `level` under `caps`. */ export function verdictOf(analysis, length, level, caps = PROFILE_CAPS) { if (length > caps.module_bytes) return 'refused R1 0'; if (analysis === null) return 'refused R2 -'; let best = null; for (const f of analysis) { if (f.int && level !== 'integer') continue; if (f.cap && !(f.value > caps[f.cap])) continue; if (!best || f.at < best.at || (f.at === best.at && f.rule < best.rule)) best = f; } return best ? `refused R${best.rule} ${best.at}` : 'held'; } /** The module's verdict at `level` ('integer' or 'float') under `caps` (§4.3). */ export function check(bytes, level, caps = PROFILE_CAPS) { if (level !== 'integer' && level !== 'float') throw new Error(`no level ${level}`); if (bytes.length > caps.module_bytes) return 'refused R1 0'; return verdictOf(analyse(bytes), bytes.length, level, caps); } /** * The level a sink holds the module at (§6, check 9b): { level: 'integer' | 'float' | null, * verdict }, the verdict being that of the last level checked. */ export function levelOf(bytes, caps = PROFILE_CAPS) { const a = bytes.length > caps.module_bytes ? null : analyse(bytes); const int = verdictOf(a, bytes.length, 'integer', caps); if (int === 'held') return { level: 'integer', verdict: int }; if (!int.startsWith('refused R3 ')) return { level: null, verdict: int }; const float = verdictOf(a, bytes.length, 'float', caps); return { level: float === 'held' ? 'float' : null, verdict: float }; } /** The profile's caps with `name=value` changes, comma-joined; `-` for none. */ export function parseCaps(text) { const caps = { ...PROFILE_CAPS }; if (text === '-') return caps; for (const part of text.split(',')) { const [name, value] = part.split('='); if (!(name in caps) || !/^[0-9]+$/.test(value ?? '')) return null; caps[name] = Number(value); } return caps; } const LEVELS = { integer: 'integer', float: 'float', 'waism/1-int': 'integer', 'waism/1-det': 'float' }; // ------------------------------------------------------------------------------ from a shell async function main(argv) { const { readFileSync } = await import('node:fs'); const { join } = await import('node:path'); const read = (p) => { try { return new Uint8Array(readFileSync(p)); } catch (e) { console.error(`check-module: ${p}: ${e.message}`); process.exit(2); } }; const usage = () => { console.error('usage: node check-module.mjs [--level integer|float] [--caps NAME=VALUE,...] FILE...\n node check-module.mjs --cases DIR'); process.exit(2); }; if (argv.length === 2 && argv[0] === '--cases') { const list = join(argv[1], 'cases.txt'); let text; try { text = readFileSync(list, 'utf8'); } catch (e) { console.error(`check-module: ${list}: ${e.message}`); process.exit(2); } let checked = 0, wrong = 0; const cache = new Map(); for (const raw of text.split('\n')) { const line = raw.split(' #')[0].trim(); if (!line || line.startsWith('#')) continue; const m = /^(\S+) (\S+) (\S+) (.+)$/.exec(line); const level = m && LEVELS[m[2]]; const caps = m && parseCaps(m[3]); if (!m || !level || !caps) { console.error(`check-module: ${list}: not a case: ${line}`); process.exit(2); } if (!cache.has(m[1])) { const bytes = read(join(argv[1], `${m[1]}.wasm`)); cache.set(m[1], { length: bytes.length, analysis: analyse(bytes) }); } const { length, analysis } = cache.get(m[1]); const got = verdictOf(analysis, length, level, caps); checked++; if (got !== m[4]) { wrong++; console.log(`${m[1]} ${level} ${m[3]}: ${got}, not ${m[4]}`); } } console.log(wrong ? `check-module: ${wrong} of ${checked} verdicts differ from ${list}` : `check-module: ${checked} verdicts, each the one ${list} states`); process.exit(wrong ? 1 : 0); } let level = null, caps = PROFILE_CAPS; const files = []; for (let i = 0; i < argv.length; i++) { if (argv[i] === '--level') { level = LEVELS[argv[++i]]; if (!level) usage(); } else if (argv[i] === '--caps') { caps = parseCaps(argv[++i] ?? ''); if (!caps) usage(); } else if (argv[i].startsWith('--')) usage(); else files.push(argv[i]); } if (!files.length) usage(); let allHeld = true; for (const f of files) { const bytes = read(f); let line; if (level) { line = check(bytes, level, caps); allHeld &&= line === 'held'; } else { const { level: held, verdict } = levelOf(bytes, caps); if (held) line = `held at the ${held} level (${held === 'integer' ? 'waism/1-int' : 'waism/1-det'})`; else { line = verdict; allHeld = false; } } console.log(files.length > 1 ? `${f}: ${line}` : line); } process.exit(allHeld ? 0 : 1); } if (typeof process === 'object' && process.argv && process.argv[1]) { const { pathToFileURL } = await import('node:url'); const { realpathSync } = await import('node:fs'); let self; try { self = pathToFileURL(realpathSync(process.argv[1])).href; } catch { self = null; } if (self === import.meta.url) await main(process.argv.slice(2)); }