Medium CVSS 7.5 webkit Type Confusion 🔧 Commit mapped

Overview

Medium
Severity
7.5
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected process crash
ComponentJSC Wasm
Bug ClassType Confusion
Tracker282180
Fix commit6146215d9220 (WebKit/WebKit)
CWECWE-125, CWE-787 (Out-of-bounds read, Out-of-bounds write)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
CISA KEVNot listed
CreditedXiangwei Zhang of Tencent Security YUNDING LAB, linjy of HKUS3Lab and chluo of WHUSecLab, and an anonymous researcher
Disclosed2024-12-11

Background

BBQ JIT
JavaScriptCore’s baseline wasm optimizing tier with its own register allocator.
Register bindings
m_gprBindings/m_fprBindings map physical registers to the live values they currently hold.
consume / unbind
Releases a value’s register binding so the allocator no longer assumes that register holds it.
Unreachable region
Wasm code after an unconditional branch/throw that is still validated and compiled, with special control handling.

Root Cause Analysis

In JavaScriptCore’s Wasm BBQ JIT, register allocation tracks which GPRs/FPRs are bound to live stack values via m_gprBindings/m_fprBindings. When compiling exception-handling catch blocks, the register-binding state must be reset consistently on entry to the catch and when finishing an unreachable region, or the JIT’s model of which registers hold which values diverges from reality.

Pre-patch, addCatch and addCatchAll did not unbind the currently-bound registers on entry, while addEndToUnreachable open-coded a loop that consumed all bindings. That asymmetry meant compiling a catch (especially in an unreachable context, per the compile-unreachable-catch test) left stale register bindings, so subsequent codegen could treat a register as holding a value it no longer does — a JIT register-state inconsistency that miscompiles and crashes.

The fix factors the binding-clearing into unbindAllRegisters() (consuming every non-none GPR/FPR binding) and calls it at the start of addCatch and addCatchAll as well as reusing it in addEndToUnreachable, so all three paths reset register state identically.

The restored invariant is that entering a catch handler or an unreachable end clears the register bindings, keeping the BBQ JIT’s register model consistent.

Key insight
BBQ-JIT catch handlers didn’t clear register bindings on entry the way the unreachable-end path did, so stale bindings miscompiled the catch; unifying on unbindAllRegisters() restores a consistent register model.

Attack Path

  1. Craft wasm with unreachable catch Build a module whose function has an exception-handling catch/catch_all reached in an unreachable code region.
  2. Compile via BBQ JIT Instantiation compiles the function; addCatch/addCatchAll run without clearing prior register bindings.
  3. Diverge register state Stale GPR/FPR bindings persist into the catch codegen, so a register is assumed to hold a value it no longer holds.
  4. Miscompile / corrupt The resulting code uses the wrong register contents, crashing or corrupting memory in the WebContent process.

Impact Assessment

A BBQ-JIT register-state inconsistency compiling catch handlers (advisory class LogicError) that miscompiles code, reachable from crafted wasm in the WebContent process. A JIT register mismatch is a corruption-class primitive, though building a controlled result needs precise register shaping; the observable is a crash. Confined to WebContent; rated medium.

Changed Functions

FunctionChangeNotes
BBQJIT::addCatch / addCatchAll
Source/JavaScriptCore/wasm/WasmBBQJIT.cpp
modified Call unbindAllRegisters() on entry so catch handlers start from a clean register-binding state.
BBQJIT::unbindAllRegisters (+ addEndToUnreachable)
Source/JavaScriptCore/wasm/WasmBBQJIT.cpp
added Factors the consume-all-bindings loop into one helper reused by the unreachable-end path, unifying register-state reset.

Audit Directions

  • Other control-entry binding resets
    grep WasmBBQJIT for control-flow entries (loop/block/try/delegate) that should unbindAllRegisters() but hand-roll or omit it.
  • Unreachable-region codegen
    Audit addEndToUnreachable and unreachable catch/delegate paths for register/stack-state assumptions.
  • consume() coverage
    Review addArray*/addStruct*/call paths for values whose registers are not consumed, leaving stale bindings.

Original Bug Report

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