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 Wasm
Bug ClassLogic Error
Tracker293197
Fix commit240b9cb98891 (WebKit/WebKit) +10/-0
CWECWE-119 (Buffer bounds error)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H
CISA KEVNot listed
CreditedNan Wang (@eternalsakura13) and Ziling Chen
Disclosed2025-07-29

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.

Key insight
An OMG wasm-to-wasm call (notably a tail call to a callee with a smaller argument area) left the stack pointer matching the callee’s layout instead of the caller’s; the one-line fix re-establishes sp = callFrameRegister - frameSize in the call’s link task, restoring the fundamental per-frame SP invariant.

Attack Path

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. Crash the WebContent process The SP desynchronization leads to an unexpected Safari/WebContent crash, the denial-of-service reported by the CVE.

Impact Assessment

The patch establishes a stack-pointer desynchronization after a call/tail-call, which as demonstrated yields an unexpected crash (DoS) of the WebContent (renderer) process, matching the CVE. A wrong SP relative to the frame is a stack-memory-safety issue in principle (subsequent reads/writes land at attacker-influenced offsets), so escalation beyond a crash toward controlled stack corruption cannot be fully excluded, but the diff only shows the SP-restore fix and does not demonstrate a controllable OOB write, so any RCE path is inference rather than established. It is confined to the sandboxed renderer where the Wasm JIT executes; heap-grooming/ACE background is not what this commit proves.

Changed Functions

FunctionChangeNotes
OMGIRGenerator::addCall
Source/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.js
  • Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp

Audit Directions

  • Other SP-affecting emitters in OMG
    In 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 mismatches
    Audit 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 assumptions
    Search 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.

Original Bug Report

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