The name replaces the working one across the specification, the two checkers and their planted set. The planted module that carries a profile name in a custom section is rebuilt with build.py. CI's step label stays ASCII. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1374 lines
67 KiB
JavaScript
1374 lines
67 KiB
JavaScript
// 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<n> <offset>`:
|
|
// 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<n> <offset>'
|
|
// 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
|
|
// (`<case> <level> <caps> <verdict>`; the module is DIR/<case>.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));
|
|
}
|