Differential behavior: wasm `switch` opcode traps Unimplemented instead of executing (stack-switching)
Categories
(Core :: JavaScript: WebAssembly, defect)
Tracking
()
People
(Reporter: decoder, Unassigned)
References
(Blocks 1 open bug)
Details
(4 keywords)
Attachments
(2 files)
Summary
The wasm switch instruction (from the stack-switching / typed-continuations proposal) passes validation and the module compiles successfully, but SpiderMonkey's Ion wasm compiler has only a stub for it: FunctionCompiler::emitSwitch unconditionally emits Trap::Unimplemented after decoding the operands. Any module that dynamically executes switch raises WebAssembly.RuntimeError: unimplemented instruction executed, whereas V8 (d8 --experimental-wasm-wasmfx) correctly executes the opcode.
This is a behavioral divergence, not a memory-corruption bug (see memory-safety analysis below).
Root Cause
js/src/wasm/WasmIonCompile.cpp, FunctionCompiler::emitSwitch (line 9972):
bool FunctionCompiler::emitSwitch() {
uint32_t contTypeIndex;
uint32_t tagIndex;
MDefinition* cont;
DefVector args;
HandlerExprVector handlers;
if (!iter().readSwitch(&contTypeIndex, &tagIndex, &args, &cont)) {
return false;
}
if (inDeadCode()) {
return true;
}
// TODO: Not yet implemented.
unimplementedTrap(); // <-- always emits Trap::Unimplemented
return true;
}
The entire stack-switching opcode family was landed in Bug 2023217 with several Ion operations left as TODO stubs that call unimplementedTrap(): emitContBind (line 9623), emitResumeThrow (line 9934), emitResumeThrowRef (line 9953), emitSwitch (line 9972), and the (on $tag switch) handler kind inside emitResume (line 9787). The validator accepts these opcodes (so modules are well-formed and instantiable), but execution traps.
Reproduction
Tested on mozilla-central rev b754e06486cf (2026-05-21), fuzzing-asan-opt build.
# SpiderMonkey (Ion) -- traps
dist/bin/js --fuzzing-safe --setpref=wasm_stack_switching=true --wasm-compiler=optimizing poc.js
# => WebAssembly.RuntimeError: unimplemented instruction executed (exit 3)
# V8 -- executes the switch opcode
d8 --experimental-wasm-wasmfx poc.js
# => PASS (exit 0)
Differential evaluator result:
differs: true
message: Discrepancy: shell B succeeded but shell A failed (A=FAIL exit 3, B=OK exit 0)
Note: --setpref=wasm_stack_switching=true is required (defaults to false on all channels). Without it the module is rejected at compile time.
Two test artifacts are attached:
poc.js-- minimal module isolatingemitSwitchspecifically (primary)poc_coroutine.js-- complete switch-based coroutine demonstrating V8 runs it end-to-end
Memory-Safety Analysis
The stub is memory-safe. Two non-obvious aspects were checked:
-
Iterator/MIR desync:
readSwitchdoes not mark the iterator unreachable, butunimplementedTrap()setscurBlock_ = nullptr. This "iterator live / MIR dead" state is absorbed by Ion's structured dead-code handling -- all MIR helpers guard oninDeadCode()/curBlock_, and dead defs cannot reach live MIR. Confirmed: the debug ASan build compiles all tested switch modules without assertions. -
Stale
inlinedCallerOffsetsIndex_: The phantom-frame bug family in this file is unreachable viaswitchbecause cross-function inlining only runs inCompileMode::LazyTiering, while stack-switching requires the optimizing compiler to be force-selected -- the two prerequisites are mutually exclusive.
Suggested Fix
Implement switch in Ion (build the switch/landing-pad MIR analogously to emitResume/emitSuspend), or reject the instruction at validation time when the optimizing compiler cannot yet handle it. The same applies to the sibling stubs emitContBind, emitResumeThrow, emitResumeThrowRef, and the (on $tag switch) handler kind in emitResume.
| Reporter | ||
Comment 1•3 months ago
|
||
| Reporter | ||
Comment 2•3 months ago
|
||
Updated•3 months ago
|
Comment 3•3 months ago
|
||
Unimplemented means we haven't implemented it yet :) The normal stack-switching bug will track finishing the feature.
Comment 4•3 months ago
|
||
I think it's a duplicate of Implement switch
Updated•12 days ago
|
Description
•