CVE-2025-43213
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Google V8 Security Team
Key insight. The JS-to-Wasm entry thunk must present a valid boxed callee in the frame’s Callee slot before any call that can trigger a stack walk (operationJSToWasmEntryWrapperBuildFrame), and must load/cage the Wasm memory registers only after the frame is built — the fix is purely a reordering plus save/restore of the Callee slot to keep the frame walkable and the memory registers fresh.
Patch Details
diff --git a/Source/JavaScriptCore/llint/WebAssembly.asm b/Source/JavaScriptCore/llint/WebAssembly.asm
index 61f0b45d3d4d..ea2c71f220f0 100644
--- a/Source/JavaScriptCore/llint/WebAssembly.asm
+++ b/Source/JavaScriptCore/llint/WebAssembly.asm
@@ -578,17 +578,6 @@ end
loadp Callee[cfr], ws0 # WebAssemblyFunction*
loadp WebAssemblyFunction::m_instance[ws0], wasmInstance
- # Memory
- if ARM64 or ARM64E
- loadpairq JSWebAssemblyInstance::m_cachedMemory[wasmInstance], memoryBase, boundsCheckingSize
- elsif X86_64
- loadp JSWebAssemblyInstance::m_cachedMemory[wasmInstance], memoryBase
- loadp JSWebAssemblyInstance::m_cachedBoundsCheckingSize[wasmInstance], boundsCheckingSize
- end
- if not ARMv7
- cagedPrimitiveMayBeNull(memoryBase, wa0)
- end
-
# Allocate stack space
loadi WebAssemblyFunction::m_frameSize[ws0], wa0
subp sp, wa0, wa0
@@ -608,15 +597,51 @@ if ASSERT_ENABLED
end)
end
+ # a0 = current stack frame position
+ move sp, a0
+
+ # Save wasmInstance and put the correct Callee into the stack for building the frame
+ storep wasmInstance, CodeBlock[cfr]
+
+if JSVALUE64
+ loadp Callee[cfr], memoryBase
+ transferp WebAssemblyFunction::m_boxedJSToWasmCallee[ws0], Callee[cfr]
+else
+ # Store old Callee to the stack temporarily
+ loadp Callee[cfr], ws1
+ push ws1, ws1
+ loadp WebAssemblyFunction::m_boxedJSToWasmCallee[ws0], ws1
+ storep ws1, Callee[cfr]
+end
+
# Prepare frame
move ws0, a2
move cfr, a1
- move sp, a0
cCall3(_operationJSToWasmEntryWrapperBuildFrame)
+ # Restore Callee slot
+if JSVALUE64
+ storep memoryBase, Callee[cfr]
+else
+ loadp [sp], ws0
+ addp 2 * SlotSize, sp
+ storep ws0, Callee[cfr]
+end
+
btpnz r1, .buildEntryFrameThrew
move r0, ws0
+ # Memory
+ if ARM64 or ARM64E
+ loadpairq JSWebAssemblyInstance::m_cachedMemory[wasmInstance], memoryBase, boundsCheckingSize
+ elsif X86_64
+ loadp JSWebAssemblyInstance::m_cachedMemory[wasmInstance], memoryBase
+ loadp JSWebAssemblyInstance::m_cachedBoundsCheckingSize[wasmInstance], boundsCheckingSize
+ end
+ if not ARMv7
+ cagedPrimitiveMayBeNull(memoryBase, wa0)
+ end
+
# Arguments
forEachWasmArgumentGPR(macro (index, gpr1, gpr2)
diff --git a/Source/JavaScriptCore/wasm/js/JSToWasm.cpp b/Source/JavaScriptCore/wasm/js/JSToWasm.cpp
index 4d17f96b9fed..e3559f4561e1 100644
--- a/Source/JavaScriptCore/wasm/js/JSToWasm.cpp
+++ b/Source/JavaScriptCore/wasm/js/JSToWasm.cpp
@@ -241,11 +241,6 @@ MacroAssemblerCodeRef<JITThunkPtrTag> createJSToWasmJITShared()
jit.loadPtr(CCallHelpers::addressFor(CallFrameSlot::callee), GPRInfo::regWS0);
jit.loadPtr(CCallHelpers::Address(GPRInfo::regWS0, WebAssemblyFunction::offsetOfInstance()), GPRInfo::wasmContextInstancePointer);
- // Memory
-#if USE(JSVALUE64)
- jit.loadPair64(GPRInfo::wasmContextInstancePointer, CCallHelpers::TrustedImm32(JSWebAssemblyInstance::offsetOfCachedMemory()), GPRInfo::wasmBaseMemoryPointer, GPRInfo::wasmBoundsCheckingSizeRegister);
- jit.cageConditionally(Gigacage::Primitive, GPRInfo::wasmBaseMemoryPointer, GPRInfo::wasmBoundsCheckingSizeRegister, GPRInfo::regWA0);
-#endif
// Now, the current frame is fully set up for exceptions.
// Allocate stack space
@@ -259,15 +254,44 @@ MacroAssemblerCodeRef<JITThunkPtrTag> createJSToWasmJITShared()
stackOverflow.append(jit.branchPtr(CCallHelpers::LessThanOrEqual, GPRInfo::regWS1, CCallHelpers::Address(GPRInfo::wasmContextInstancePointer, JSWebAssemblyInstance::offsetOfSoftStackLimit())));
jit.move(GPRInfo::regWS1, CCallHelpers::stackPointerRegister);
+ jit.move(CCallHelpers::stackPointerRegister, GPRInfo::argumentGPR0);
+
+ jit.storePtr(GPRInfo::wasmContextInstancePointer, CCallHelpers::addressFor(CallFrameSlot::codeBlock));
+
+ // Save the current Callee before putting in our boxed callee for the stack visitor
+#if USE(JSVALUE64)
+ jit.loadPtr(CCallHelpers::addressFor(CallFrameSlot::callee), GPRInfo::wasmBaseMemoryPointer);
+ jit.transferPtr(CCallHelpers::Address(GPRInfo::regWS0, WebAssemblyFunction::offsetOfBoxedJSToWasmCallee()), CCallHelpers::addressFor(CallFrameSlot::callee));
+#else
+ jit.loadPtr(CCallHelpers::addressFor(CallFrameSlot::callee), GPRInfo::regWS1);
+ jit.push(GPRInfo::regWS1);
+ jit.push(GPRInfo::regWS1);
+ jit.transferPtr(CCallHelpers::Address(GPRInfo::regWS0, WebAssemblyFunction::offsetOfBoxedJSToWasmCallee()), CCallHelpers::addressFor(CallFrameSlot::callee));
+#endif
// Prepare frame
- jit.setupArguments<decltype(operationJSToWasmEntryWrapperBuildFrame)>(CCallHelpers::stackPointerRegister, GPRInfo::callFrameRegister, GPRInfo::regWS0);
+ jit.setupArguments<decltype(operationJSToWasmEntryWrapperBuildFrame)>(GPRInfo::argumentGPR0, GPRInfo::callFrameRegister, GPRInfo::regWS0);
jit.callOperation<OperationPtrTag>(operationJSToWasmEntryWrapperBuildFrame);
+
+ // Restore Callee slot regardless
+#if USE(JSVALUE64)
+ jit.storePtr(GPRInfo::wasmBaseMemoryPointer, CCallHelpers::addressFor(CallFrameSlot::callee));
+#else
+ jit.transferPtr(CCallHelpers::Address(CCallHelpers::stackPointerRegister, 0), CCallHelpers::addressFor(CallFrameSlot::callee));
+ jit.addPtr(CCallHelpers::TrustedImm32(2 * 8), CCallHelpers::stackPointerRegister);
+#endif
+
static_assert(CCallHelpers::operationExceptionRegister<operationJSToWasmEntryWrapperBuildFrame>() != InvalidGPRReg, "We don't have a VM readily available so we rely on exception being returned");
JIT_COMMENT(jit, "Exception check: ", CCallHelpers::operationExceptionRegister<operationJSToWasmEntryWrapperBuildFrame>());
buildEntryFrameThrew.append(jit.branchTestPtr(CCallHelpers::NonZero, CCallHelpers::operationExceptionRegister<operationJSToWasmEntryWrapperBuildFrame>()));
jit.move(GPRInfo::returnValueGPR, GPRInfo::regWS0);
+ // Memory
+#if USE(JSVALUE64)
+ jit.loadPair64(GPRInfo::wasmContextInstancePointer, CCallHelpers::TrustedImm32(JSWebAssemblyInstance::offsetOfCachedMemory()), GPRInfo::wasmBaseMemoryPointer, GPRInfo::wasmBoundsCheckingSizeRegister);
+ jit.cageConditionally(Gigacage::Primitive, GPRInfo::wasmBaseMemoryPointer, GPRInfo::wasmBoundsCheckingSizeRegister, GPRInfo::regWA0);
+#endif
+
#if CPU(ARM64)
jit.loadPair64(CCallHelpers::Address(CCallHelpers::stackPointerRegister, 0 * 8), GPRInfo::regWA0, GPRInfo::regWA1);
jit.loadPair64(CCallHelpers::Address(CCallHelpers::stackPointerRegister, 2 * 8), GPRInfo::regWA2, GPRInfo::regWA3);
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2025-43213 |
| Bugzilla | 292621 |
| Component | JSC |
| Bug Class | LogicError |
| Severity | medium |
| Commit | 946696720edc253e… |
| Advisory | Apple Advisory |
Terminology
- JS-to-Wasm entry thunk — The generated trampoline (JIT or LLInt) that transitions a call from JavaScript into an exported WebAssembly function and builds the Wasm entry frame.
- CallFrameSlot::callee — The frame slot holding the current callee; stack visitors read it to identify a frame, so it must contain a recognizable (boxed) callee at all times a walk can occur.
- Boxed JS-to-Wasm callee — A specially tagged callee value (WebAssemblyFunction::offsetOfBoxedJSToWasmCallee) that the stack walker understands, as opposed to the raw WebAssemblyFunction pointer.
- cageConditionally / cagedPrimitiveMayBeNull — Gigacage pointer-caging applied to the Wasm memory base so out-of-bounds pointer arithmetic stays confined to the primitive cage region.
- Cached memory base / bounds size — Per-instance values (JSWebAssemblyInstance::m_cachedMemory / m_cachedBoundsCheckingSize) loaded into dedicated registers for Wasm memory access and bounds checks.
Root Cause Analysis
The bug is in the JS-to-Wasm entry thunk, implemented twice: the JIT thunk createJSToWasmJITShared in Source/JavaScriptCore/wasm/js/JSToWasm.cpp and the equivalent LLInt path in Source/JavaScriptCore/llint/WebAssembly.asm. This thunk runs when JavaScript calls an exported WebAssembly function; it sets up the Wasm frame and then calls the C++ helper operationJSToWasmEntryWrapperBuildFrame to finish building the entry frame. Two ordering/consistency invariants were violated. First, the frame’s Callee slot: while the helper runs it may walk the stack (for exception handling, stack-overflow reporting, or GC), and a stack visitor must find a well-formed, recognizable callee in CallFrameSlot::callee. Before the patch the Callee slot still held the raw WebAssemblyFunction* pointer rather than the boxed JS-to-Wasm callee that the visitor expects, so a stack walk during operationJSToWasmEntryWrapperBuildFrame could misinterpret the frame and crash. Second, the cached memory registers: the thunk previously loaded the instance’s cached memory base and bounds-checking size (JSWebAssemblyInstance::offsetOfCachedMemory) and caged the pointer (cageConditionally / cagedPrimitiveMayBeNull) BEFORE calling the frame-build helper; those dedicated Wasm registers (wasmBaseMemoryPointer, wasmBoundsCheckingSizeRegister) are not preserved across the C++ call and, worse, the helper could grow/relocate memory, so the values loaded early were stale by the time Wasm code ran. The fix reorders and adds book-keeping: (1) it stores wasmContextInstancePointer into CallFrameSlot::codeBlock, then saves the current Callee and transfers WebAssemblyFunction::offsetOfBoxedJSToWasmCallee() into CallFrameSlot::callee before the helper call, so any stack visitor during operationJSToWasmEntryWrapperBuildFrame sees the correct boxed callee; (2) it restores the original Callee slot immediately after the helper returns; and (3) it moves the cachedMemory load + cageConditionally to AFTER the helper call, so the memory base/size registers are populated with fresh, post-frame-build values. The .asm change mirrors this exactly (storep wasmInstance, CodeBlock[cfr]; transferp m_boxedJSToWasmCallee into Callee[cfr]; restore afterward; move the m_cachedMemory load + cagedPrimitiveMayBeNull below the cCall3). The violated invariant is thus ’the frame must be walkable with a valid boxed callee across the entry-frame-building call, and memory registers must be loaded from the instance only after the frame is built.'
Attack Path
1. Call an exported Wasm function from JS
A web page instantiates a WebAssembly module and calls an exported function from JavaScript, entering the JS-to-Wasm thunk (createJSToWasmJITShared or the LLInt WebAssembly.asm path).
2. Provoke a stack walk during frame build
Arrange conditions (e.g. stack near the soft stack limit / stack overflow, an exception, or a GC) so operationJSToWasmEntryWrapperBuildFrame triggers a stack visitor while the entry frame is being constructed.
3. Visitor reads the wrong Callee
Pre-patch, CallFrameSlot::callee still holds the raw WebAssemblyFunction* instead of the boxed JS-to-Wasm callee, so the stack visitor misinterprets the partially built frame.
4. Crash the process
The malformed-frame interpretation (or use of stale/uncaged memory registers loaded too early) produces an unexpected Safari/WebContent crash — the reported denial-of-service.
Impact Assessment
The patch establishes a frame-consistency defect: a stack visitor running during entry-frame construction could read an invalid Callee, and memory registers were loaded before the frame-build call, yielding an unexpected crash (DoS) confined to the WebContent (renderer) process where the Wasm engine runs. Because the mishandled state involves the stack visitor’s view of a frame and the Wasm memory base/caging registers, a corruption-oriented escalation cannot be categorically excluded, but the diff only demonstrates a crash and the reordering fix; any path beyond DoS is inference, not shown. The caging (cageConditionally) that limits Wasm memory pointers is preserved and merely reordered, so the memory-safety confinement remains intact. Standard heap-grooming/ACE background is not what this commit proves.
Audit Directions
Other entry/exit thunks and the Callee slot
Grep the Wasm and JS entry thunks (JSToWasm.cpp, WasmToJS.cpp, WebAssembly.asm) for CallFrameSlot::callee / offsetOfBoxedJSToWasmCallee and verify a valid boxed callee is installed before any callOperation/cCall that could walk the stack.
Register lifetime across callOperation
Audit thunks that load wasmBaseMemoryPointer / wasmBoundsCheckingSize (offsetOfCachedMemory / m_cachedBoundsCheckingSize) before a C++ helper call; these callee-clobbered or instance-mutating registers should be (re)loaded after the call, as this patch enforces.
JIT vs LLInt parity
Diff createJSToWasmJITShared against WebAssembly.asm for every entry path to ensure the frame-slot and memory-load ordering match across both implementations, since this bug required fixing both in lockstep.
Stack visitors during frame build
Trace operationJSToWasmEntryWrapperBuildFrame and its stack-overflow/exception/GC paths to enumerate where a StackVisitor can run mid-frame-construction, then check other trampolines for the same ‘walkable frame’ precondition.
Changed Functions
| Function | File | Change | Note |
|---|---|---|---|
createJSToWasmJITShared |
Source/JavaScriptCore/wasm/js/JSToWasm.cpp |
modified | Reordered the entry thunk: store wasmContextInstancePointer into CallFrameSlot::codeBlock and swap in WebAssemblyFunction::offsetOfBoxedJSToWasmCallee() as the Callee before calling operationJSToWasmEntryWrapperBuildFrame (so a stack walk sees a valid boxed callee), restore the original Callee after, and move the cachedMemory load + cageConditionally to after the helper call so memory base/size are fresh. |
JS-to-Wasm entry thunk (LLInt) |
Source/JavaScriptCore/llint/WebAssembly.asm |
modified | Mirror of the C++ change: storep wasmInstance,CodeBlock[cfr]; transferp m_boxedJSToWasmCallee into Callee[cfr] before cCall3(_operationJSToWasmEntryWrapperBuildFrame); restore Callee after; relocate the m_cachedMemory load and cagedPrimitiveMayBeNull to run after the frame is built. |
Files Changed
Source Files
Source/JavaScriptCore/llint/WebAssembly.asmSource/JavaScriptCore/wasm/js/JSToWasm.cpp