CVE-2024-9403
Overview
Background
- JIT entry stub
- The machine-code trampoline generated by
GenerateJitEntrythat lets JIT-compiled JavaScript call directly into a WebAssembly exported function. - `ExitFooterFrame`
- A fixed-layout frame that SpiderMonkey pushes when transitioning out of generated code, whose
ExitFooterFrame::Size()reserves space for bookkeeping such as the exit reason and saved frame pointer. - `WasmStackAlignment`
- The required byte alignment for the wasm ABI stack, which arguments passed on the stack must respect for correct and safe access.
- Stack argument area
- The region of the stack reserved by
StackArgBytesForWasmABIto hold call arguments that do not fit in registers.
Root Cause Analysis
In GenerateJitEntry, the stub computed the amount of stack space to reserve by adding the raw ExitFooterFrame::Size() to the byte count returned by StackArgBytesForWasmABI(funcType), producing normalBytesNeeded (and similarly oolBytesNeeded for the out-of-line coercion path). Because ExitFooterFrame::Size() is not necessarily a multiple of WasmStackAlignment, the code that subsequently placed the aligned stack-argument area could round the argument base up for alignment, causing that area to overlap the memory belonging to the ExitFooterFrame. Writes to stack arguments then clobbered the footer frame (or vice versa), corrupting the frame’s saved state and violating the invariant that the exit footer and the argument region occupy disjoint stack ranges.
The fix precomputes AlignedExitFooterFrameSize as AlignBytes(ExitFooterFrame::Size(), WasmStackAlignment) and uses that aligned value in both normalBytesNeeded and oolBytesNeeded, guaranteeing the argument area starts on an aligned boundary that lies strictly above the footer frame so the two regions can no longer overlap.
ExitFooterFrame::Size() while the argument area was placed at an aligned offset, so alignment padding silently overlapped the footer frame; the fix rounds the footer size up to WasmStackAlignment before adding argument bytes, keeping the regions disjoint.Attack Path
- Reach the JIT entry stub
Attacker-controlled JavaScript repeatedly calls a WebAssembly exported function so the engine generates and executes the
GenerateJitEntrytrampoline. - Choose a colliding signature
By defining a wasm function whose
funcTypeyields aStackArgBytesForWasmABIsize that forces alignment padding, the attacker makes the aligned stack-argument base overlap theExitFooterFrame. - Trigger the overlap
During the transition, argument marshalling writes into the overlapping region, overwriting fields of the
ExitFooterFramesuch as saved frame-pointer or exit-reason data. - Corrupt control state The clobbered footer frame is later consumed during the exit, producing a corrupted stack view that can redirect execution or misinterpret memory.
- Escalate to code execution With controlled overwrite of frame metadata, the attacker steers the corruption toward a controllable pointer to achieve arbitrary code execution in the content process.
Impact Assessment
ExitFooterFrame state on the stack, which with sufficient effort can be leveraged into arbitrary code execution. The corruption occurs in the sandboxed content process during a JIT-to-wasm call, and the only precondition is the ability to define and invoke a wasm export with a signature whose stack-argument sizing triggers the misaligned overlap.Files Changed
js/src/wasm/WasmStubs.cpp
Audit Directions
- Unaligned size arithmeticSearch for stack- or frame-size computations that add a raw
::Size()to an aligned region without wrapping it inAlignBytes, since downstream alignment can create overlap. - Stub frame layoutReview other trampoline generators in
js/src/wasm/WasmStubs.cppand the Jit exit-frame code that mixExitFooterFramereservations withStackArgBytesForWasmABIresults for the same disjointness invariant. - ABI alignment assumptionsAudit places relying on
WasmStackAlignment(or analogous alignment constants) where the base offset is aligned but the reserved size is not, to catch silent padding-induced aliasing.
Patch
diff --git a/js/src/wasm/WasmStubs.cpp b/js/src/wasm/WasmStubs.cpp index 748717f01e4..70c6c98d919 100644 --- a/js/src/wasm/WasmStubs.cpp +++ b/js/src/wasm/WasmStubs.cpp @@ -944,15 +944,18 @@ static bool GenerateJitEntry(MacroAssembler& masm, size_t funcExportIndex, MOZ_ASSERT(masm.framePushed() == 0); + // Avoid overlapping aligned stack arguments area with ExitFooterFrame. + const unsigned AlignedExitFooterFrameSize = + AlignBytes(ExitFooterFrame::Size(), WasmStackAlignment); unsigned normalBytesNeeded = - ExitFooterFrame::Size() + StackArgBytesForWasmABI(funcType); + AlignedExitFooterFrameSize + StackArgBytesForWasmABI(funcType); MIRTypeVector coerceArgTypes; MOZ_ALWAYS_TRUE(coerceArgTypes.append(MIRType::Int32)); MOZ_ALWAYS_TRUE(coerceArgTypes.append(MIRType::Pointer)); MOZ_ALWAYS_TRUE(coerceArgTypes.append(MIRType::Pointer)); unsigned oolBytesNeeded = - ExitFooterFrame::Size() + StackArgBytesForWasmABI(coerceArgTypes); + AlignedExitFooterFrameSize + StackArgBytesForWasmABI(coerceArgTypes); unsigned bytesNeeded = std::max(normalBytesNeeded, oolBytesNeeded);