Medium CVSS 6.5 webkit Logic Error 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected Safari crash
ComponentJSC LLInt
Bug ClassLogic Error
Tracker292621
Fix commit946696720edc (WebKit/WebKit) +67/-18
CWECWE-119, CWE-120 (Buffer bounds error, Buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H
CISA KEVNot listed
CreditedGoogle V8 Security Team
Disclosed2025-07-29

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:

  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.'
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.

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.

Changed Functions

FunctionChangeNotes
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/JavaScriptCore/llint/WebAssembly.asm
  • Source/JavaScriptCore/wasm/js/JSToWasm.cpp

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.

Original Bug Report

The reporter's bug is still restricted on the tracker.