CVE-2026-15718
Overview
Background
- SpiderMonkey
- Mozilla’s JavaScript and WebAssembly engine embedded in Firefox/Gecko.
- WASM baseline compiler
- the fast, single-pass
BaseCompilerinWasmBaselineCompile.cppthat emits machine code for WebAssembly with a register-allocating value stack. - `sync()`
- a baseline-compiler operation that spills the in-register portion of the abstract value stack to memory so the physical machine state matches the compiler’s model before a control-flow join or shared helper sequence.
- `array.fill`
- the WebAssembly GC instruction, compiled by
BaseCompiler::emitArrayFill, that writes a repeated value into a contiguous range of a WASM GC array.
Root Cause Analysis
BaseCompiler::emitArrayFill compiled the array.fill operation by emitting a fill loop that used numElements as a countdown loop variable, but it entered that loop without first calling sync() to reconcile the abstract value stack with the physical register/stack state. Because the loop contains a control-flow join (the done label and the back-edge) and manipulates registers such as PreBarrierReg, the baseline compiler’s invariant that operand-stack values held in registers are spilled before branch targets was violated, so the compiler’s model of where operands lived diverged from the code actually generated. This let generated code read or write the wrong registers/stack slots across the loop boundary, corrupting engine state.
The fix adds a single sync() call immediately before the fill loop is emitted, forcing all live register-resident stack values to be flushed so the machine state is consistent at the loop’s control-flow join. With the spill in place the loop’s back-edge and exit see a well-defined stack layout, restoring the compiler’s core register-allocation invariant.
emitArrayFill without spilling the register-resident value stack first, leaving the physical machine state inconsistent with the compiler’s abstract model. Inserting sync() before the loop restores the invariant that all live stack operands are materialized in memory across the join.Attack Path
- Deliver malicious module
A remote page ships a crafted WebAssembly (GC) module containing an
array.fillinstruction positioned so its operands are register-resident on the value stack. - Trigger baseline compilation
The module is compiled by the baseline (
BaseCompiler) tier, invokingemitArrayFilland emitting the fill loop without the missingsync(). - Diverge machine and model state The un-synced loop join causes generated code to use registers/stack slots that no longer match the compiler’s tracking, producing type- or memory-confused JIT code.
- Achieve memory corruption Executing the miscompiled function reads/writes memory outside the intended bounds or with a confused value, giving a controllable corruption primitive.
- Escalate The attacker leverages the corruption (public exploit code exists) toward arbitrary read/write and code execution inside the content process.
Impact Assessment
array.fill code, yielding memory corruption inside the sandboxed content process where SpiderMonkey runs. The metadata rates this critical and notes public exploit code exists; preconditions are only that WebAssembly GC compilation via the baseline tier is reachable and the crafted module is executed. No special privileges or user interaction beyond visiting the page are indicated by the diff.Files Changed
js/src/wasm/WasmBaselineCompile.cpp
Audit Directions
- Missing `sync()` before control-flow joinsAudit other
BaseCompiler::emit*handlers that emit loops,Labels, or branch back-edges to confirm each callssync()(or equivalent spill) before the join when the value stack may hold register-resident operands. - Helper sequences touching special registersReview paths that reserve or free registers like
PreBarrierRegmid-instruction and ensure the abstract stack is reconciled before and after such register churn. - WASM GC array/struct opcodesGive focused review to newer GC opcode emitters (
array.fill,array.copy,array.new, struct writes) whose loops and barriers are prone to the same spill-before-join invariant breaks.
Patch
diff --git a/js/src/wasm/WasmBaselineCompile.cpp b/js/src/wasm/WasmBaselineCompile.cpp
index 6f6fbd72eba..6d94c781ebf 100644
--- a/js/src/wasm/WasmBaselineCompile.cpp
+++ b/js/src/wasm/WasmBaselineCompile.cpp
@@ -8976,6 +8976,8 @@ bool BaseCompiler::emitArrayFill() {
freePtr(RegPtr(PreBarrierReg));
}
+ sync();
+
// Perform the fill loop using `numElements` as the loop variable, counting
// down to zero.
Label done;