Medium CVSS 4.3 webkit Logic Error 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected process crash
ComponentJSC Wasm
Bug ClassLogic Error
Tracker298496
Fix commit11eaa3910d44 (WebKit/WebKit) +289/-215
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:L
CISA KEVNot listed
Creditedrheza (@ginggilBesel)
Disclosed2025-11-03

Background

MacroAssembler scratch register
A register the JIT reserves for internal use when lowering a single macro instruction; code must declare AllowMacroScratchRegisterUsage before any macro op is permitted to clobber it.
AllowMacroScratchRegisterUsage / DisallowMacroScratchRegisterUsage
RAII guard objects that toggle, for their lexical scope, whether emitted macro instructions may use (and thus overwrite) the scratch register.
Patchpoint generator
A callback the B3/Air backend invokes at code-emission time to emit custom machine code for a call site, receiving the physical register/stack assignments of its inputs via StackmapGenerationParams.
WebAssembly tail call
A call that reuses the caller’s stack frame instead of pushing a new one; the JIT must shuffle arguments into their final positions and adjust SP without a valid intermediate frame, making register/stack scratch management delicate.
OMG JIT
JavaScriptCore’s Optimizing Maximal-Grade WebAssembly tier that recompiles hot wasm functions through the B3/Air optimizing backend.

Root Cause Analysis

The patch fixes an unsafe use of the MacroAssembler scratch register in the OMG (optimizing) WebAssembly JIT during two related code-generation paths: the indirect-call patchpoint generator in emitIndirectCall and the tail-call setup helper prepareForTailCallImpl (both the 64-bit WasmOMGIRGenerator.cpp and the 32/64-bit WasmOMGIRGenerator32_64.cpp variants). WebKit’s MacroAssembler reserves a dedicated scratch/tmp register whose use is gated by AllowMacroScratchRegisterUsage / DisallowMacroScratchRegisterUsage RAII guards; any macro instruction emitted while AllowMacroScratchRegisterUsage is in scope is permitted to silently clobber that register.

The invariant that was violated is that the scratch register must not be marked allowable while it still holds a live input value. In emitIndirectCall, AllowMacroScratchRegisterUsage was constructed at the very top of the patchpoint generator, so the subsequent storeWasmCalleeToCalleeCallFrame (which may itself materialize the callee value via the scratch register) and the surrounding sequencing could clobber the callee-code pointer held in a param GPR before the farJump consumed it.

The fix moves the AllowMacroScratchRegisterUsage construction to after storeWasmCalleeToCalleeCallFrame, with the comment ‘Allow scratch after the callee is stored, which could be in the scratch register.’ In prepareForTailCallImpl the same class of bug existed across the whole argument-shuffling routine: the old code held a single AllowMacroScratchRegisterUsage over the entire body while it restored callee-saves, spilled/moved tail-call arguments, and finally adjusted SP and jumped, so an argument that happened to live in the scratch (tmp) register could be destroyed by any macro instruction that reached for scratch. The rewrite hoists the frame-size and spill-area computation up front, then wraps the input-live regions in explicit DisallowMacroScratchRegisterUsage scopes (‘Nothing before saving tmp can use the scratch register since it might clobber an input’ and ‘Nothing after restoring tmp can use the scratch register since it might clobber an input’), spills tmp to a dedicated stack slot (renamed tmpSpill -> tmpSpillOffsetRelativeToOriginalSP and clobbersTmp -> tmpNeedsSaving) when it is a live argument, and only re-enables scratch in the narrow window where it is safe. The net effect is that no live wasm argument or callee pointer can be silently overwritten by an internal scratch-register spill during tail-call/indirect-call lowering, which previously produced miscompiled machine code and a crash on the added regression test omg-tail-call-clobber-scratch-register.js.

Key insight
The scratch register is only safe to enable once no input value lives in it: the bug was holding AllowMacroScratchRegisterUsage across regions where the callee pointer or a tail-call argument still occupied the scratch/tmp register, so an internal spill silently destroyed live state.

Attack Path

  1. Deliver crafted wasm module Serve a WebAssembly module whose exported function performs an indirect call or a tail call with an argument layout that places a live value (or the callee-code pointer) into the JIT’s scratch/tmp register, matching the encoding in the regression test.
  2. Force OMG tier-up Drive the function hot (the test uses –jitPolicyScale=0) so it is compiled by the OMG optimizing JIT, exercising emitIndirectCall’s patchpoint generator and prepareForTailCallImpl rather than the interpreter or BBQ tier.
  3. Trigger scratch clobber during lowering During code generation the AllowMacroScratchRegisterUsage window causes a macro instruction (e.g. materializing the callee pointer or an argument move) to overwrite the scratch register while it still holds a live input, producing miscompiled code.
  4. Execute the miscompiled call At runtime the tail/indirect call jumps with a corrupted callee-code pointer or shuffles arguments incorrectly, yielding an unexpected process crash (the observed impact) and, in principle, a corrupted control-flow target.
  5. (Inference) Escalate a controlled clobber If an attacker can influence which value lands in the scratch register and the resulting incorrect jump target or argument, this could in theory be steered beyond a crash; the commit itself only establishes the miscompile-and-crash, so any control-flow-hijack escalation is inferred, not shown.

Impact Assessment

As established by the commit, the primitive is a JIT miscompilation in the wasm OMG tier that clobbers a live register during indirect/tail-call lowering, whose demonstrated effect is an unexpected crash in the WebContent process (the regression test is titled ‘Don’t crash’). Because the clobbered value can be a callee-code pointer used by farJump or a live wasm argument shuffled into the callee frame, there is a plausible but unproven path toward corrupted control flow or argument state; the diff only proves a crash, so treating this as a controlled/uncontrolled crash is the grounded reading and any RCE escalation is inference. It is confined to the WebContent process JS/wasm engine and does not by itself cross the sandbox.

Changed Functions

FunctionChangeNotes
OMGIRGenerator::emitIndirectCall (patchpoint generator lambda)
Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp
modified Moved AllowMacroScratchRegisterUsage from the top of the generator to after storeWasmCalleeToCalleeCallFrame, so the scratch register is only allowed once the callee (which may occupy it) has been stored, just before farJump.
prepareForTailCallImpl
Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp
modified Hoisted frame-size/spill-area setup, wrapped the callee-save-restore/argument-scan and the final SP-adjust/jump in DisallowMacroScratchRegisterUsage scopes, and renamed clobbersTmp->tmpNeedsSaving and tmpSpill->tmpSpillOffsetRelativeToOriginalSP with an eager spill of tmp when it is a live argument.
OMGIRGenerator::emitIndirectCall (patchpoint generator lambda)
Source/JavaScriptCore/wasm/WasmOMGIRGenerator32_64.cpp
modified Same reordering for the 32/64-bit generator: AllowMacroScratchRegisterUsage now constructed after storeWasmCalleeToCalleeCallFrame and before loading the call target.
prepareForTailCallImpl
Source/JavaScriptCore/wasm/WasmOMGIRGenerator32_64.cpp
modified Mirror of the 64-bit fix, additionally guarding the stack-argument stash loop (tailCallPatchpointScratch) inside the DisallowMacroScratchRegisterUsage region.

Files Changed

  • JSTests/wasm/stress/omg-tail-call-clobber-scratch-register.js
  • Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp
  • Source/JavaScriptCore/wasm/WasmOMGIRGenerator32_64.cpp

Audit Directions

  • Other patchpoint generators enabling scratch too early
    In the wasm OMG/BBQ generators grep for ‘AllowMacroScratchRegisterUsage allowScratch(jit)’ placed at the top of a patchpoint setGenerator lambda and check whether any params[…].gpr()/fpr() input is still live (used by a later store/jump) after that point; the fix pattern is to construct the guard only immediately before the instruction that consumes the scratch.
  • Tail-call and CallFrameShuffler scratch handling
    Audit prepareForTailCallImpl analogues and CallFrameShuffler/Air Shuffle code for a single wide AllowMacroScratchRegisterUsage scope surrounding emitRestore(calleeSaves), argument moves and SP adjustment; look for tmp = jit.scratchRegister() where tmp may alias a param GPR without a spill guarded by DisallowMacroScratchRegisterUsage.
  • scratchRegister() aliasing live inputs
    Across MacroAssembler users grep for jit.scratchRegister()/dataTempRegister/addressTempRegister used near loops over params[i].gpr() and verify a spill exists when arg.gpr() == scratch; the renamed tmpNeedsSaving/tmpSpillOffsetRelativeToOriginalSP logic is the reference safe pattern.
  • Missing Disallow scopes around SP transitions
    Search for jit.addPtr(…stackPointerRegister) or farJump sequences that lack a surrounding DisallowMacroScratchRegisterUsage, since adjusting SP or jumping while scratch is enabled and inputs are live is the exact miscompile condition this patch closes.

Original Bug Report

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