CVE-2025-43212
Overview
Background
- OMG tier
- JavaScriptCore’s optimizing WebAssembly compiler that produces the fastest machine code for hot Wasm functions after they tier up from BBQ.
- Tail call (return_call)
- A Wasm instruction that transfers control to a callee reusing the current frame; when the callee has a different argument area the stack layout must be re-adjusted.
- callFrameRegister / frameSize
- cfr points at the current frame base; the SP invariant for a frame is sp == cfr - frameSize, so all stack slots are addressable at fixed offsets.
- Link task
- A deferred closure run during LinkBuffer finalization when late-known values (call targets, final frame sizes) are available, used here to both patch the call and restore SP.
- unlinkedWasmToWasmCalls
- The table of near-call sites patched at link time to the resolved callee entry point for direct wasm-to-wasm calls.
Root Cause Analysis
The bug is in the OMG (Optimized Machine-code Generator, the top Wasm tier) call emitter OMGIRGenerator::addCall in Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp. addCall emits a direct wasm-to-wasm call and registers a link task that patches the near-call target once the callee is laid out. The invariant at issue is the caller’s calling convention around the stack pointer: after a call returns (or after control comes back into the current frame’s code region), the stack pointer must equal callFrameRegister - frameSize so that all subsequent stack-relative accesses in the frame are correct. The reproducer performs a WebAssembly tail call (return_call, opcode 0x12) from ‘main’ to a callee whose signature has far fewer parameters/results than the caller (the caller type has 26 params, the tail-called helper type has 0), executed in a countdown loop of 100000 iterations to force OMG tier-up (–jitPolicyScale=0, –useConcurrentJIT=0). When a tail call targets a function with a different (smaller) argument area, the shared/near-call and argument-shuffling code can leave the stack pointer adjusted to the callee’s frame layout rather than the caller’s, so on the surrounding OMG-generated code path the SP no longer matches callFrameRegister - frameSize. With a desynchronized SP, subsequent frame accesses and the next stack-limit/overflow reasoning operate on wrong addresses, producing an unexpected crash (and, more broadly, stack-pointer corruption).
The fix adds, inside the link task run after the call is emitted, jit.addPtr(TrustedImm32(-params.code().frameSize()), GPRInfo::callFrameRegister, MacroAssembler::stackPointerRegister), i.e. it explicitly recomputes sp = callFrameRegister - frameSize, restoring the caller-frame SP invariant after the (tail) call sequence. Because params.code().frameSize() is only known late, the restore is deferred into the link task alongside the call-target patch. NOTE: the exact tail-call vs normal-call framing detail is partly inference from the test name (omg-tail-call-to-function-with-less-arguments) and the single-line SP-restore fix; the diff itself only shows the added SP recomputation in addCall’s link task.
Attack Path
- Ship a crafted Wasm module A web page compiles and instantiates a module (WebAssembly.Module/Instance) containing a function that tail-calls (return_call) another function whose signature has fewer arguments/results, as encoded in the reproducer’s byte array.
- Force OMG compilation Call the exported ‘main’ with a large counter (f(100000)) under aggressive JIT settings so the hot function tiers up from BBQ to OMG and addCall emits the vulnerable call sequence.
- Execute the mismatched tail call At runtime the tail call shuffles arguments for the smaller callee and leaves the stack pointer set to the callee’s layout instead of the caller’s callFrameRegister - frameSize.
- Operate on a desynced SP Subsequent OMG code in the frame accesses stack slots relative to the wrong SP, corrupting stack-relative reads/writes and/or the stack-overflow accounting.
- Crash the WebContent process The SP desynchronization leads to an unexpected Safari/WebContent crash, the denial-of-service reported by the CVE.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
OMGIRGenerator::addCallSource/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp |
modified | In the post-call link task (which also patches the near-call target via unlinkedWasmToWasmCalls), added jit.addPtr(-params.code().frameSize(), callFrameRegister, stackPointerRegister) to restore the caller frame's stack pointer (sp = cfr - frameSize) after the call, using the late-known frameSize. |
Files Changed
JSTests/wasm/stress/omg-tail-call-to-function-with-less-arguments.jsSource/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp
Audit Directions
- Other SP-affecting emitters in OMGIn WasmOMGIRGenerator.cpp grep for stackPointerRegister / addPtr(…frameSize…) / callFrameRegister around addCall, addCallIndirect, addCallRef and tail-call helpers to confirm each call sequence restores sp == cfr - frameSize.
- Tail-call argument-area mismatchesAudit all return_call / return_call_indirect / return_call_ref codegen for callees with fewer/more args than the caller; grep for ‘params.code().frameSize()’ and shuffle/relocation logic that changes SP without a matching restore.
- Cross-tier consistency (BBQ)Compare with WasmBBQJIT.cpp tail-call handling (see CVE-2025-24162) to check whether the same tier-specific SP/binding cleanup is uniformly applied; tail calls were a repeated 2025 defect area.
- Link-task SP assumptionsSearch addLinkTask closures across the Wasm JIT for ones that patch call targets but assume SP is already correct; verify frameSize-dependent SP restores are present wherever a call can perturb the stack pointer.