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 counts and the feature set it reports. It is plain TypeScript that
* the native compiler also compiles. */
import { readMemargFlags } from "./module.js";
/** Proposals the summary can name, in report order. The first group is the
* 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). */
memarg(naturalAlign: number): void {
const r = this.r;
let align = r.u32();
let multi = false;
if (align >= 64 && align < 128) {
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);
}
const { align, memory } = readMemargFlags(r);
if (align < 0) r.malformed("malformed memop flags");
if (memory !== 0) this.otherMemory(memory);
if (this.m.memory64) r.u64();
else r.u32();
if (!this.typed) return;
@@ -1840,9 +1831,9 @@ class FunctionValidator {
scanMemarg(): void {
const r = this.r;
const align = r.u32();
if (align >= 128) r.malformed("malformed memop flags");
if (align >= 64 && r.u32() !== 0) this.m.feature("multi-memory");
const { align, memory } = readMemargFlags(r);
if (align < 0) r.malformed("malformed memop flags");
if (memory !== 0) this.m.feature("multi-memory");
if (this.m.memory64) r.u64();
else r.u32();
}
+17 -1
View File
@@ -283,6 +283,20 @@ function decodeUtf8(bytes: Uint8Array, start: number, end: number): string {
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
* translatable feature level; anything else was refused before
* translation. */
@@ -372,7 +386,9 @@ export function skipImmediates(c: WasmCursor, op: number): void {
}
default:
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();
}
return;
+61 -22
View File
@@ -48,6 +48,7 @@ import {
VAL_I64,
WasmCursor,
parseWasmModule,
readMemargFlags,
skipImmediates,
type WasmFuncType,
type WasmModule,
@@ -830,6 +831,39 @@ class FunctionTranslator {
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[] {
const out = this.stack.splice(this.stack.length - n, n);
if (out.length !== n) throw new Error("wasm translator: operand stack underflow");
@@ -1540,20 +1574,23 @@ class FunctionTranslator {
}
case 0x20: {
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);
return;
}
case 0x21: {
const i = c.u32();
const v = this.pop();
this.add(` store ${llType(v.t)} ${v.v}, ptr %l${i}`);
const t = this.localType(i);
const v = this.popType(t, "local.set");
this.add(` store ${llType(t)} ${v.v}, ptr %l${i}`);
return;
}
case 0x22: {
const i = c.u32();
const v = this.stack[this.stack.length - 1]!;
this.add(` store ${llType(v.t)} ${v.v}, ptr %l${i}`);
const t = this.localType(i);
const v = this.popType(t, "local.tee");
this.stack.push(v);
this.add(` store ${llType(t)} ${v.v}, ptr %l${i}`);
return;
}
case 0x23: {
@@ -1565,7 +1602,9 @@ class FunctionTranslator {
}
case 0x24: {
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));
if (v.t === VAL_EXTERNREF)
this.add(` call void @scr_wasm_global_set_ref(ptr ${cell}, ptr ${v.v})`);
@@ -1583,13 +1622,15 @@ class FunctionTranslator {
}
case 0x26: {
const t = c.u32();
const v = this.pop();
const i = this.pop();
const table = this.m.tables[t];
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})`);
return;
}
case 0x3f: {
c.u32();
this.memoryIndex(c);
const mem = this.loadPtrSlot(OFF_MEMORY);
const sizePtr = this.emit(`getelementptr inbounds i8, ptr ${mem}, i64 8`);
const size = this.emit(`load i64, ptr ${sizePtr}, align 8`);
@@ -1598,7 +1639,7 @@ class FunctionTranslator {
return;
}
case 0x40: {
c.u32();
this.memoryIndex(c);
const delta = this.pop();
this.push(this.emit(`call i32 @scr_wasm_memory_grow(ptr %inst, i32 ${delta.v})`), VAL_I32);
this.refreshMemory();
@@ -1641,10 +1682,9 @@ class FunctionTranslator {
break;
}
if (op >= 0x28 && op <= 0x35) {
c.u32();
const offset = c.u32();
const offset = this.memargOffset(c);
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 line = this.lines.lines.length;
const loaded = this.emit(`load ${mt}, ptr ${p}, align 1`);
@@ -1656,11 +1696,10 @@ class FunctionTranslator {
return;
}
if (op >= 0x36 && op <= 0x3e) {
c.u32();
const offset = c.u32();
const [, mt, bytes] = STORE_INFO[op]!;
const v = this.pop();
const addr = this.pop();
const offset = this.memargOffset(c);
const [vt, mt, bytes] = STORE_INFO[op]!;
const v = this.popType(vt, "store");
const addr = this.popType(VAL_I32, "store address");
const p = this.address(addr.v, offset, bytes);
let value = v.v;
if (mt === "i8" || mt === "i16" || (mt === "i32" && v.t === VAL_I64)) {
@@ -1960,7 +1999,7 @@ class FunctionTranslator {
case 8: {
// memory.init
const seg = c.u32();
c.u32();
this.memoryIndex(c);
const n = this.pop();
const s = 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()})`);
return;
case 10: {
c.u32();
c.u32();
this.memoryIndex(c);
this.memoryIndex(c);
const n = this.pop();
const s = this.pop();
const d = this.pop();
@@ -1984,7 +2023,7 @@ class FunctionTranslator {
return;
}
case 11: {
c.u32();
this.memoryIndex(c);
const n = this.pop();
const v = this.pop();
const d = this.pop();