From b3bcbaaf7cea09e9a17582ad9249f887db081f2a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 20 Oct 2026 01:46:36 -0500 Subject: [PATCH] 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. --- packages/compiler/src/wasm/binary.ts | 23 +++---- packages/compiler/src/wasm/module.ts | 18 +++++- packages/compiler/src/wasm/translate.ts | 83 ++++++++++++++++++------- 3 files changed, 85 insertions(+), 39 deletions(-) diff --git a/packages/compiler/src/wasm/binary.ts b/packages/compiler/src/wasm/binary.ts index 5ddc28cc9..ad5297739 100644 --- a/packages/compiler/src/wasm/binary.ts +++ b/packages/compiler/src/wasm/binary.ts @@ -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(); } diff --git a/packages/compiler/src/wasm/module.ts b/packages/compiler/src/wasm/module.ts index 3b0dd54c0..d7ec38701 100644 --- a/packages/compiler/src/wasm/module.ts +++ b/packages/compiler/src/wasm/module.ts @@ -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; diff --git a/packages/compiler/src/wasm/translate.ts b/packages/compiler/src/wasm/translate.ts index e0ce27be5..813dab84a 100644 --- a/packages/compiler/src/wasm/translate.ts +++ b/packages/compiler/src/wasm/translate.ts @@ -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();