// Read a WebAssembly module and refuse it unless it is WebAssembly 1.0 and nothing // later: every section, type, import, export, segment and instruction is read, and // anything a later proposal added is a finding. No dependency: the binary format is // decoded here. // // node check-mvp.mjs [--package] [--functions ] // // Always checked: // - sections: only the eleven of 1.0 and custom sections, in order (no DataCount // section, no tag section); // - types: function types over i32, i64, f32 and f64, with at most one result; // - one memory and one table at most, neither shared nor 64-bit; a table of // function references; // - no mutable global imported or exported; // - element and data segments of the 1.0 kind only (active, memory or table 0); // - constant expressions of one constant or one global.get; // - every instruction of every function is one of 1.0's, with 1.0's immediates: // no SIMD, no atomics, no bulk memory, no sign-extension operators, no // saturating conversions, no reference types, no multi-value blocks, no tail // calls, no exceptions, and a call_indirect that names table 0 in one byte; // - a `target_features` section, if present, declares none of them in use. // // With --package, the rules of a module a page instantiates with no glue script: // - no imports; // - no exported function takes or returns i64, which a caller could pass only // through the engine's later BigInt interface; // - no floating-point type, constant or instruction anywhere. // // With --functions, the exported functions are exactly the names given. // // Prints what it found. Exit status 0: the module passes; 1: it does not; 2: usage, // or a module this reader cannot decode. import { createHash } from 'node:crypto'; import { readFileSync } from 'node:fs'; let file = null, pkg = false, functions = null, usage = false; for (let a = process.argv.slice(2), i = 0; i < a.length; i++) { if (a[i] === '--package') pkg = true; else if (a[i] === '--functions' && i + 1 < a.length) functions = a[++i].split(',').filter(Boolean); else if (!a[i].startsWith('--') && file === null) file = a[i]; else usage = true; } if (usage || file === null) { console.error('usage: node check-mvp.mjs [--package] [--functions ]'); process.exit(2); } const I32 = 0x7f, I64 = 0x7e, F32 = 0x7d, F64 = 0x7c; const NUM = new Set([I32, I64, F32, F64]); class Reader { constructor(b, at = 0, end = b.length) { this.b = b; this.at = at; this.end = end; } eof() { return this.at >= this.end; } byte() { if (this.at >= this.end) throw new Error(`truncated at byte ${this.at}`); return this.b[this.at++]; } // An unsigned LEB128 of at most `max` bytes. u(max = 5) { let r = 0, s = 1; for (let i = 0; i < max; i++) { const c = this.byte(); r += (c & 0x7f) * s; s *= 128; if (!(c & 0x80)) return r; } throw new Error(`a LEB128 longer than ${max} bytes before byte ${this.at}`); } skip(n) { if (this.at + n > this.end) throw new Error(`truncated at byte ${this.at}`); this.at += n; } sub(n) { const r = new Reader(this.b, this.at, this.at + n); this.skip(n); return r; } name() { const n = this.u(); const s = new TextDecoder().decode(this.b.subarray(this.at, this.at + n)); this.skip(n); return s; } } // What was found, by feature: a count and the first place. const found = new Map(); function finding(feature, where) { const f = found.get(feature); if (f) f.count += 1; else found.set(feature, { count: 1, where }); } const SECTION = ['custom', 'type', 'import', 'function', 'table', 'memory', 'global', 'export', 'start', 'element', 'code', 'data']; const types = []; // { params: [valtype], results: [valtype] } const funcTypes = []; // function index -> type index, imports first const globalsMutable = []; // global index -> mutable, imports first const exports = []; // { name, kind, index } const imports = []; // "module.field" let tables = 0, memories = 0, floats = 0, bodies = 0, instructions = 0; let declared = null; // the target_features section's entries function valtype(t, where) { if (NUM.has(t)) { if (t === F32 || t === F64) floats += 1; return; } if (t === 0x7b) finding('simd128 (the type v128)', where); else if (t === 0x70 || t === 0x6f) finding('reference-types (a reference as a value type)', where); else finding(`a value type 0x${t.toString(16)} that 1.0 does not have`, where); } function limits(r, what, where) { const flag = r.u(); if (flag & 0x02) finding(`threads (a shared ${what})`, where); if (flag & 0x04) finding(`memory64 (a 64-bit ${what})`, where); if (flag > 0x07) finding(`${what} limits with flags 0x${flag.toString(16)}`, where); r.u(10); if (flag & 0x01) r.u(10); } // A constant expression: one constant or one global.get, then end. function constExpr(r, where) { const op = r.byte(); if (op === 0x41) r.u(5); else if (op === 0x42) r.u(10); else if (op === 0x43) { r.skip(4); floats += 1; } else if (op === 0x44) { r.skip(8); floats += 1; } else if (op === 0x23) r.u(); else { finding('a constant expression beyond one constant or global.get (extended-const, or a reference)', where); throw new Error('stop'); } if (r.byte() !== 0x0b) { finding('a constant expression beyond one constant or global.get (extended-const)', where); throw new Error('stop'); } } const FLOAT_OP = (op) => op === 0x2a || op === 0x2b || op === 0x38 || op === 0x39 || (op >= 0x5b && op <= 0x66) || (op >= 0x8b && op <= 0xa6) || (op >= 0xa8 && op <= 0xab) || (op >= 0xae && op <= 0xbf); function blockType(r, where) { const t = r.byte(); if (t === 0x40) return; if (NUM.has(t)) { valtype(t, where); return; } if (t === 0x7b || t === 0x70 || t === 0x6f) { valtype(t, where); return; } // Anything else is a type index, as a signed LEB128. finding('multivalue (a block whose type is a type index)', where); if (t & 0x80) { r.at -= 1; r.u(5); } } // One function body's instructions. Returns false when it met an instruction whose // immediates this reader does not know, and stopped. function body(r, where) { while (!r.eof()) { const at = r.at; const op = r.byte(); const here = `${where}, byte 0x${at.toString(16)}`; instructions += 1; if (op === 0x02 || op === 0x03 || op === 0x04) blockType(r, here); else if (op === 0x0c || op === 0x0d || op === 0x10 || (op >= 0x20 && op <= 0x24)) r.u(); else if (op === 0x0e) { for (let n = r.u(); n > 0; n--) r.u(); r.u(); } else if (op === 0x11) { r.u(); const t = r.byte(); if (t !== 0x00) { finding('reference-types (a call_indirect that names a table, or encodes table 0 in more than one byte)', here); if (t & 0x80) { r.at -= 1; r.u(5); } } } else if (op >= 0x28 && op <= 0x3e) { const align = r.u(); if (align >= 64) { finding('multi-memory (a load or store that names a memory)', here); r.u(); } r.u(10); if (FLOAT_OP(op)) floats += 1; } else if (op === 0x3f || op === 0x40) { const m = r.byte(); if (m !== 0x00) { finding('multi-memory (memory.size or memory.grow that names a memory, or encodes memory 0 in more than one byte)', here); if (m & 0x80) { r.at -= 1; r.u(5); } } } else if (op === 0x41) r.u(5); else if (op === 0x42) r.u(10); else if (op === 0x43) { r.skip(4); floats += 1; } else if (op === 0x44) { r.skip(8); floats += 1; } else if (op === 0x00 || op === 0x01 || op === 0x05 || op === 0x0b || op === 0x0f || op === 0x1a || op === 0x1b) { /* no immediate */ } else if (op >= 0x45 && op <= 0xbf) { if (FLOAT_OP(op)) floats += 1; } else if (op >= 0xc0 && op <= 0xc4) finding('sign-ext (i32.extend8_s and its kin)', here); else if (op === 0xfc) { const sub = r.u(); if (sub <= 7) finding('nontrapping-fptoint (a saturating conversion)', here); else if (sub === 8) { finding('bulk-memory (memory.init)', here); r.u(); r.byte(); } else if (sub === 9) { finding('bulk-memory (data.drop)', here); r.u(); } else if (sub === 10) { finding('bulk-memory (memory.copy)', here); r.byte(); r.byte(); } else if (sub === 11) { finding('bulk-memory (memory.fill)', here); r.byte(); } else if (sub === 12) { finding('bulk-memory (table.init)', here); r.u(); r.u(); } else if (sub === 13) { finding('bulk-memory (elem.drop)', here); r.u(); } else if (sub === 14) { finding('bulk-memory (table.copy)', here); r.u(); r.u(); } else if (sub <= 17) { finding('reference-types (table.grow, table.size or table.fill)', here); r.u(); } else { finding(`an instruction 0xfc ${sub} that 1.0 does not have`, here); return false; } } else { const feature = op === 0xfd ? 'simd128 (a vector instruction)' : op === 0xfe ? 'threads (an atomic instruction)' : op === 0xfb ? 'gc (a struct, array or cast instruction)' : (op >= 0x06 && op <= 0x0a) || op === 0x18 || op === 0x19 || op === 0x1f ? 'exception-handling' : op === 0x12 || op === 0x13 ? 'tail-call' : op === 0x14 || op === 0x15 ? 'typed function references (call_ref)' : op === 0x1c || op === 0x25 || op === 0x26 || (op >= 0xd0 && op <= 0xd6) ? 'reference-types (a typed select, a table access or a reference instruction)' : `an instruction 0x${op.toString(16)} that 1.0 does not have`; finding(feature, here); return false; } } if (r.b[r.end - 1] !== 0x0b) finding('a function body that does not end with end', where); return true; } function section(id, r) { const where = `the ${SECTION[id]} section`; if (id === 1) { for (let n = r.u(), i = 0; i < n; i++) { const form = r.byte(); if (form !== 0x60) { finding(`a type of form 0x${form.toString(16)} (1.0 has function types only)`, `${where}, type ${i}`); throw new Error('stop'); } const t = { params: [], results: [] }; for (let k = r.u(); k > 0; k--) { const v = r.byte(); valtype(v, `${where}, type ${i}`); t.params.push(v); } for (let k = r.u(); k > 0; k--) { const v = r.byte(); valtype(v, `${where}, type ${i}`); t.results.push(v); } if (t.results.length > 1) finding('multivalue (a function type with more than one result)', `${where}, type ${i}`); types.push(t); } } else if (id === 2) { for (let n = r.u(), i = 0; i < n; i++) { const name = `${r.name()}.${r.name()}`; imports.push(name); const kind = r.byte(); if (kind === 0) funcTypes.push(r.u()); else if (kind === 1) { const t = r.byte(); if (t !== 0x70) finding('reference-types (a table that is not of function references)', `${where}, ${name}`); limits(r, 'table', `${where}, ${name}`); tables += 1; } else if (kind === 2) { limits(r, 'memory', `${where}, ${name}`); memories += 1; } else if (kind === 3) { valtype(r.byte(), `${where}, ${name}`); const mutable = r.byte() === 1; globalsMutable.push(mutable); if (mutable) finding('mutable-globals (a mutable global imported)', `${where}, ${name}`); } else { finding(`an import of kind ${kind} (1.0 has functions, tables, memories and globals)`, `${where}, ${name}`); throw new Error('stop'); } } } else if (id === 3) { for (let n = r.u(); n > 0; n--) funcTypes.push(r.u()); } else if (id === 4) { for (let n = r.u(), i = 0; i < n; i++) { const t = r.byte(); if (t !== 0x70) finding('reference-types (a table that is not of function references)', `${where}, table ${i}`); limits(r, 'table', `${where}, table ${i}`); tables += 1; } } else if (id === 5) { for (let n = r.u(), i = 0; i < n; i++) { limits(r, 'memory', `${where}, memory ${i}`); memories += 1; } } else if (id === 6) { for (let n = r.u(), i = 0; i < n; i++) { valtype(r.byte(), `${where}, global ${i}`); globalsMutable.push(r.byte() === 1); constExpr(r, `${where}, global ${i}`); } } else if (id === 7) { for (let n = r.u(); n > 0; n--) { const name = r.name(); const kind = r.byte(); const index = r.u(); exports.push({ name, kind, index }); if (kind > 3) finding(`an export of kind ${kind} (1.0 has functions, tables, memories and globals)`, `${where}, ${name}`); } } else if (id === 8) { r.u(); } else if (id === 9) { for (let n = r.u(), i = 0; i < n; i++) { const flag = r.u(); if (flag !== 0) { finding(`bulk-memory or reference-types (an element segment of kind ${flag}; 1.0 has kind 0)`, `${where}, segment ${i}`); throw new Error('stop'); } constExpr(r, `${where}, segment ${i}`); for (let k = r.u(); k > 0; k--) r.u(); } } else if (id === 10) { for (let n = r.u(), i = 0; i < n; i++) { const f = r.sub(r.u()); const w = `function ${funcTypes.length - n + i}`; for (let k = f.u(); k > 0; k--) { f.u(); valtype(f.byte(), `${w}, its locals`); } bodies += 1; body(f, w); } } else if (id === 11) { for (let n = r.u(), i = 0; i < n; i++) { const flag = r.u(); if (flag !== 0) { finding(`bulk-memory (a data segment of kind ${flag}; 1.0 has kind 0)`, `${where}, segment ${i}`); throw new Error('stop'); } constExpr(r, `${where}, segment ${i}`); r.skip(r.u()); } } } const b = readFileSync(file); let decoded = true; try { if (b.length < 8 || b.readUInt32LE(0) !== 0x6d736100) throw new Error('not a WebAssembly module (no \\0asm)'); if (b.readUInt32LE(4) !== 1) throw new Error(`binary format version ${b.readUInt32LE(4)}, not 1`); const r = new Reader(b, 8); let last = 0; while (!r.eof()) { const id = r.byte(); const s = r.sub(r.u()); if (id === 0) { const name = s.name(); if (name === 'target_features') { declared = []; for (let n = s.u(); n > 0; n--) { const prefix = String.fromCharCode(s.byte()); declared.push(prefix + s.name()); } } continue; } if (id === 12) { finding('bulk-memory (a DataCount section)', 'section 12'); continue; } if (id === 13) { finding('exception-handling (a tag section)', 'section 13'); continue; } if (id > 13) throw new Error(`section id ${id}`); if (id <= last) throw new Error(`section ${id} after section ${last}`); last = id; try { section(id, s); if (!s.eof()) throw new Error(`${s.end - s.at} bytes left over in the ${SECTION[id]} section`); } catch (e) { // A finding that ends a section's reading says so with 'stop'. if (e.message !== 'stop') throw e; } } } catch (e) { decoded = false; console.error(`check-mvp: ${file}: cannot decode: ${e.message}`); } if (!decoded) process.exit(2); if (tables > 1) finding('reference-types (more than one table)', 'the module'); if (memories > 1) finding('multi-memory (more than one memory)', 'the module'); for (const e of exports) { if (e.kind === 3 && globalsMutable[e.index]) finding('mutable-globals (a mutable global exported)', `the export ${e.name}`); } // The linker's own account of what the module uses: nothing later than 1.0 may be in // use ('+') or required ('='). mutable-globals is declared by every module with a // stack pointer; the module's imports and exports were checked for it above. for (const d of declared || []) { if ((d[0] === '+' || d[0] === '=') && d.slice(1) !== 'mutable-globals') finding(`${d.slice(1)} (declared in use by the target_features section)`, 'the target_features section'); } const exportedFunctions = exports.filter((e) => e.kind === 0).map((e) => e.name).sort(); const policy = []; if (pkg) { if (imports.length > 0) policy.push(`it imports ${imports.join(', ')}; a package imports nothing`); for (const e of exports.filter((x) => x.kind === 0)) { const t = types[funcTypes[e.index]]; if (t && [...t.params, ...t.results].includes(I64)) policy.push(`the export ${e.name} takes or returns i64`); } if (floats > 0) policy.push(`floating point: ${floats} types, constants or instructions; a package has none`); } if (functions) { const want = [...functions].sort(); const extra = exportedFunctions.filter((n) => !want.includes(n)); const missing = want.filter((n) => !exportedFunctions.includes(n)); if (extra.length > 0) policy.push(`it exports ${extra.join(', ')}, which the list does not name`); if (missing.length > 0) policy.push(`it does not export ${missing.join(', ')}`); } console.log(`${file}: ${b.length} bytes, SHA-256 ${createHash('sha256').update(b).digest('hex')}`); console.log(` functions: ${bodies} bodies, ${instructions} instructions read`); console.log(` imports: ${imports.length === 0 ? 'none' : imports.join(', ')}`); console.log(` exported functions: ${exportedFunctions.join(', ') || 'none'}`); console.log(` other exports: ${exports.filter((e) => e.kind !== 0).map((e) => e.name).join(', ') || 'none'}`); console.log(` floating point: ${floats === 0 ? 'none' : `${floats} types, constants or instructions`}`); console.log(` target_features: ${declared === null ? 'no such section' : declared.join(' ') || 'empty'}`); if (found.size === 0 && policy.length === 0) { console.log(' WebAssembly 1.0: every section, type, segment and instruction is 1.0\'s'); if (pkg) console.log(' a package: no imports, no i64 at an exported function, no floating point'); if (functions) console.log(' the exported functions are the list\'s'); process.exit(0); } for (const [feature, f] of found) console.error(` NOT 1.0: ${feature}: ${f.count} time${f.count === 1 ? '' : 's'}, first in ${f.where}`); for (const p of policy) console.error(` NOT A PACKAGE: ${p}`); console.error(`check-mvp: ${file} fails`); process.exit(1);