Decode memargs the same way in validation and translation

Validation, translation and instruction skipping now read memarg flags
through one function, readMemargFlags in module.ts. Flags with bit 6 set
are followed by an explicit memory index, which may be 0; flags of 128 or
more are malformed.

The translator refuses a module (WasmTranslateError) when a memarg,
memory.size, memory.grow, memory.init, memory.copy or memory.fill names a
memory other than 0, and when the operand of local.set, local.tee,
global.set, table.set, a load or a store does not have the type of its
destination. Locals are stored with their declared type.
This commit is contained in:
Chris Tate
2026-10-20 01:46:36 -05:00
parent 431a8d6874
commit b3bcbaaf7c
3 changed files with 85 additions and 39 deletions
+7 -16
View File
@@ -20,6 +20,7 @@
* The decoder never evaluates anything and keeps no module contents beyond * The decoder never evaluates anything and keeps no module contents beyond
* the counts and the feature set it reports. It is plain TypeScript that * the counts and the feature set it reports. It is plain TypeScript that
* the native compiler also compiles. */ * the native compiler also compiles. */
import { readMemargFlags } from "./module.js";
/** Proposals the summary can name, in report order. The first group is the /** Proposals the summary can name, in report order. The first group is the
* translatable set; the rest are reported as unsupported. */ * translatable set; the rest are reported as unsupported. */
@@ -1244,19 +1245,9 @@ class FunctionValidator {
/** A memarg. A memory other than 0 stops typing (multi-memory). */ /** A memarg. A memory other than 0 stops typing (multi-memory). */
memarg(naturalAlign: number): void { memarg(naturalAlign: number): void {
const r = this.r; const r = this.r;
let align = r.u32(); const { align, memory } = readMemargFlags(r);
let multi = false; if (align < 0) r.malformed("malformed memop flags");
if (align >= 64 && align < 128) { if (memory !== 0) this.otherMemory(memory);
align -= 64;
multi = true;
} else if (align >= 64) {
r.malformed("malformed memop flags");
}
let memory = 0;
if (multi) {
memory = r.u32();
if (memory !== 0) this.otherMemory(memory);
}
if (this.m.memory64) r.u64(); if (this.m.memory64) r.u64();
else r.u32(); else r.u32();
if (!this.typed) return; if (!this.typed) return;
@@ -1840,9 +1831,9 @@ class FunctionValidator {
scanMemarg(): void { scanMemarg(): void {
const r = this.r; const r = this.r;
const align = r.u32(); const { align, memory } = readMemargFlags(r);
if (align >= 128) r.malformed("malformed memop flags"); if (align < 0) r.malformed("malformed memop flags");
if (align >= 64 && r.u32() !== 0) this.m.feature("multi-memory"); if (memory !== 0) this.m.feature("multi-memory");
if (this.m.memory64) r.u64(); if (this.m.memory64) r.u64();
else r.u32(); else r.u32();
} }
+17 -1
View File
@@ -283,6 +283,20 @@ function decodeUtf8(bytes: Uint8Array, start: number, end: number): string {
return out; return out;
} }
/** The flags of a memarg (the immediates of loads, stores and atomics):
* the alignment exponent and the memory index. Flags with bit 6 set
* (multi-memory) are followed by an explicit memory index, which may be
* 0; flags of 128 or more are malformed and answer align -1 without
* reading further. The offset follows. Validation (binary.ts), translation
* and skipImmediates all read memargs through this function, so they agree
* on where each instruction ends. */
export function readMemargFlags(r: { u32(): number }): { align: number; memory: number } {
const flags = r.u32();
if (flags >= 128) return { align: -1, memory: 0 };
if (flags >= 64) return { align: flags - 64, memory: r.u32() };
return { align: flags, memory: 0 };
}
/** Reads the immediates of opcode `op` (already consumed). Covers the /** Reads the immediates of opcode `op` (already consumed). Covers the
* translatable feature level; anything else was refused before * translatable feature level; anything else was refused before
* translation. */ * translation. */
@@ -372,7 +386,9 @@ export function skipImmediates(c: WasmCursor, op: number): void {
} }
default: default:
if (op >= 0x28 && op <= 0x3e) { if (op >= 0x28 && op <= 0x3e) {
c.u32(); const flags = readMemargFlags(c);
if (flags.align < 0 || flags.memory !== 0)
throw new Error(`wasm: memarg of memory ${flags.memory} is not supported`);
c.u32(); c.u32();
} }
return; return;
+61 -22
View File
@@ -48,6 +48,7 @@ import {
VAL_I64, VAL_I64,
WasmCursor, WasmCursor,
parseWasmModule, parseWasmModule,
readMemargFlags,
skipImmediates, skipImmediates,
type WasmFuncType, type WasmFuncType,
type WasmModule, type WasmModule,
@@ -830,6 +831,39 @@ class FunctionTranslator {
return v; return v;
} }
/** Pops a value that must have type `t`: the type a local, global,
* table or memory store writes. Validation guarantees it; a mismatch
* means the translator and the validator disagree about the code, so
* the module is not translated. */
popType(t: number, what: string): StackValue {
const v = this.pop();
if (v.t !== t) throw new WasmTranslateError(`${what} operand type does not match`);
return v;
}
/** The declared type of local `i` (parameters first). */
localType(i: number): number {
const t = this.localTypes[i];
if (t === undefined) throw new WasmTranslateError(`unknown local ${i}`);
return t;
}
/** The offset of a memarg over memory 0 (the only memory a translatable
* module has). */
memargOffset(c: WasmCursor): number {
const flags = readMemargFlags(c);
if (flags.align < 0 || flags.memory !== 0)
throw new WasmTranslateError(`memarg of memory ${flags.memory} is not supported`);
return c.u32();
}
/** A memory index immediate (memory.size, memory.grow and the bulk
* memory instructions), which must be 0. */
memoryIndex(c: WasmCursor): void {
const index = c.u32();
if (index !== 0) throw new WasmTranslateError(`memory ${index} is not supported`);
}
popN(n: number): StackValue[] { popN(n: number): StackValue[] {
const out = this.stack.splice(this.stack.length - n, n); const out = this.stack.splice(this.stack.length - n, n);
if (out.length !== n) throw new Error("wasm translator: operand stack underflow"); if (out.length !== n) throw new Error("wasm translator: operand stack underflow");
@@ -1540,20 +1574,23 @@ class FunctionTranslator {
} }
case 0x20: { case 0x20: {
const i = c.u32(); const i = c.u32();
const t = this.localTypes[i]!; const t = this.localType(i);
this.push(this.emit(`load ${llType(t)}, ptr %l${i}`), t); this.push(this.emit(`load ${llType(t)}, ptr %l${i}`), t);
return; return;
} }
case 0x21: { case 0x21: {
const i = c.u32(); const i = c.u32();
const v = this.pop(); const t = this.localType(i);
this.add(` store ${llType(v.t)} ${v.v}, ptr %l${i}`); const v = this.popType(t, "local.set");
this.add(` store ${llType(t)} ${v.v}, ptr %l${i}`);
return; return;
} }
case 0x22: { case 0x22: {
const i = c.u32(); const i = c.u32();
const v = this.stack[this.stack.length - 1]!; const t = this.localType(i);
this.add(` store ${llType(v.t)} ${v.v}, ptr %l${i}`); const v = this.popType(t, "local.tee");
this.stack.push(v);
this.add(` store ${llType(t)} ${v.v}, ptr %l${i}`);
return; return;
} }
case 0x23: { case 0x23: {
@@ -1565,7 +1602,9 @@ class FunctionTranslator {
} }
case 0x24: { case 0x24: {
const g = c.u32(); const g = c.u32();
const v = this.pop(); const global = this.m.globals[g];
if (global === undefined) throw new WasmTranslateError(`unknown global ${g}`);
const v = this.popType(global.type, "global.set");
const cell = this.loadPtrSlot(this.mt.globalSlot(g)); const cell = this.loadPtrSlot(this.mt.globalSlot(g));
if (v.t === VAL_EXTERNREF) if (v.t === VAL_EXTERNREF)
this.add(` call void @scr_wasm_global_set_ref(ptr ${cell}, ptr ${v.v})`); this.add(` call void @scr_wasm_global_set_ref(ptr ${cell}, ptr ${v.v})`);
@@ -1583,13 +1622,15 @@ class FunctionTranslator {
} }
case 0x26: { case 0x26: {
const t = c.u32(); const t = c.u32();
const v = this.pop(); const table = this.m.tables[t];
const i = this.pop(); if (table === undefined) throw new WasmTranslateError(`unknown table ${t}`);
const v = this.popType(table.elemType, "table.set");
const i = this.popType(VAL_I32, "table.set");
this.add(` call void @scr_wasm_table_set(ptr %inst, i32 ${t}, i32 ${i.v}, ptr ${v.v})`); this.add(` call void @scr_wasm_table_set(ptr %inst, i32 ${t}, i32 ${i.v}, ptr ${v.v})`);
return; return;
} }
case 0x3f: { case 0x3f: {
c.u32(); this.memoryIndex(c);
const mem = this.loadPtrSlot(OFF_MEMORY); const mem = this.loadPtrSlot(OFF_MEMORY);
const sizePtr = this.emit(`getelementptr inbounds i8, ptr ${mem}, i64 8`); const sizePtr = this.emit(`getelementptr inbounds i8, ptr ${mem}, i64 8`);
const size = this.emit(`load i64, ptr ${sizePtr}, align 8`); const size = this.emit(`load i64, ptr ${sizePtr}, align 8`);
@@ -1598,7 +1639,7 @@ class FunctionTranslator {
return; return;
} }
case 0x40: { case 0x40: {
c.u32(); this.memoryIndex(c);
const delta = this.pop(); const delta = this.pop();
this.push(this.emit(`call i32 @scr_wasm_memory_grow(ptr %inst, i32 ${delta.v})`), VAL_I32); this.push(this.emit(`call i32 @scr_wasm_memory_grow(ptr %inst, i32 ${delta.v})`), VAL_I32);
this.refreshMemory(); this.refreshMemory();
@@ -1641,10 +1682,9 @@ class FunctionTranslator {
break; break;
} }
if (op >= 0x28 && op <= 0x35) { if (op >= 0x28 && op <= 0x35) {
c.u32(); const offset = this.memargOffset(c);
const offset = c.u32();
const [vt, mt, bytes, ext] = LOAD_INFO[op]!; const [vt, mt, bytes, ext] = LOAD_INFO[op]!;
const addr = this.pop(); const addr = this.popType(VAL_I32, "load address");
const p = this.address(addr.v, offset, bytes); const p = this.address(addr.v, offset, bytes);
const line = this.lines.lines.length; const line = this.lines.lines.length;
const loaded = this.emit(`load ${mt}, ptr ${p}, align 1`); const loaded = this.emit(`load ${mt}, ptr ${p}, align 1`);
@@ -1656,11 +1696,10 @@ class FunctionTranslator {
return; return;
} }
if (op >= 0x36 && op <= 0x3e) { if (op >= 0x36 && op <= 0x3e) {
c.u32(); const offset = this.memargOffset(c);
const offset = c.u32(); const [vt, mt, bytes] = STORE_INFO[op]!;
const [, mt, bytes] = STORE_INFO[op]!; const v = this.popType(vt, "store");
const v = this.pop(); const addr = this.popType(VAL_I32, "store address");
const addr = this.pop();
const p = this.address(addr.v, offset, bytes); const p = this.address(addr.v, offset, bytes);
let value = v.v; let value = v.v;
if (mt === "i8" || mt === "i16" || (mt === "i32" && v.t === VAL_I64)) { if (mt === "i8" || mt === "i16" || (mt === "i32" && v.t === VAL_I64)) {
@@ -1960,7 +1999,7 @@ class FunctionTranslator {
case 8: { case 8: {
// memory.init // memory.init
const seg = c.u32(); const seg = c.u32();
c.u32(); this.memoryIndex(c);
const n = this.pop(); const n = this.pop();
const s = this.pop(); const s = this.pop();
const d = this.pop(); const d = this.pop();
@@ -1973,8 +2012,8 @@ class FunctionTranslator {
this.add(` call void @scr_wasm_data_drop(ptr %inst, i32 ${c.u32()})`); this.add(` call void @scr_wasm_data_drop(ptr %inst, i32 ${c.u32()})`);
return; return;
case 10: { case 10: {
c.u32(); this.memoryIndex(c);
c.u32(); this.memoryIndex(c);
const n = this.pop(); const n = this.pop();
const s = this.pop(); const s = this.pop();
const d = this.pop(); const d = this.pop();
@@ -1984,7 +2023,7 @@ class FunctionTranslator {
return; return;
} }
case 11: { case 11: {
c.u32(); this.memoryIndex(c);
const n = this.pop(); const n = this.pop();
const v = this.pop(); const v = this.pop();
const d = this.pop(); const d = this.pop();