CVE-2025-43213
Overview
Background
- 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:
- 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;
- it restores the original Callee slot immediately after the helper returns; and
- 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
- 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).
- 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.
- 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.
- 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
createJSToWasmJITSharedSource/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/JavaScriptCore/llint/WebAssembly.asmSource/JavaScriptCore/wasm/js/JSToWasm.cpp
Audit Directions
- Other entry/exit thunks and the Callee slotGrep 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 callOperationAudit 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 parityDiff 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 buildTrace 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.