Medium CVSS 8.8 webkit OOB 🔧 Commit mapped

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to memory corruption
ComponentJSC DFG
Bug ClassOOB
Tracker282450
Fix commit903f38de509c (WebKit/WebKit)
CWECWE-787 (Out-of-bounds write)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedLukas Bernhard, Gary Kwong, and an anonymous researcher
Disclosed2024-12-11

Background

DFG CSE
The DFG JIT’s common-subexpression-elimination phase, which removes redundant loads/stores by matching HeapLocations and can convert a redundant store into an aliased store.
HeapLocation / LocationKind
An abstract memory-location key (a LocationKind plus base/index) used by CSE to decide whether two heap accesses touch the same location.
PutByValAlias
A specialized PutByVal node the JIT emits when a store is known to alias a previously bounds-checked access, so it stores directly with no bounds check.
Out-of-bounds sane chain
An array-access mode where an index beyond the length is safe because the prototype chain has no interfering indexed properties, so reads return undefined rather than trapping.
ArrayMode / isInBounds / isOutOfBounds
DFG per-node classification of how an indexed access behaves; PutByValAlias is only valid when the mode is in-bounds.
Butterfly / public length
The out-of-line storage for a JS array/object and its stored length; a bounds-check-free store past public length writes out of bounds.

Root Cause Analysis

The patch fixes a DFG common-subexpression-elimination (CSE) modeling error that let out-of-bounds array stores be miscompiled into bounds-check-free PutByValAlias stores. CSE tracks memory effects via HeapLocation values keyed by a LocationKind plus base/index; two accesses are considered to touch the same abstract location only if their HeapLocations match. For indexed properties, JSC distinguishes ordinary in-bounds accesses from ‘out-of-bounds sane chain’ accesses (where an OOB index is safe because the prototype chain is sane and reads simply return undefined). In DFGClobberize.h the def() calls for PutByVal (Int32/Double/Contiguous and the typed-array path) and for DataViewSet used the in-bounds LocationKind (indexedPropertyLocForResultType) even when node->arrayMode().isOutOfBounds() was true, so an out-of-bounds store and an in-bounds access to the same (base,index) produced identical HeapLocations. The violated invariant is that CSE must never treat an out-of-bounds access as equivalent to an in-bounds one. Because they were conflated, CSE could match an out-of-bounds PutByVal against a prior in-bounds access at the same location and rewrite it to PutByValAlias — the aliased-store form that assumes the bounds were already checked and therefore emits no bounds check at all. In DFGSpeculativeJIT.cpp, compileContiguousPutByVal, compileDoublePutByVal and jumpForTypedArrayOutOfBounds all treat PutByValAlias as unconditionally in-bounds (jumpForTypedArrayOutOfBounds returns an empty Jump(), skipping the check). An attacker-controlled OOB index thus reaches a raw store with no bounds check, giving an out-of-bounds write.

The fix adds indexedPropertyLocToOutOfBoundsSaneChain(), a new IndexedPropertyInt52OutOfBoundsSaneChainLoc kind (and reorders the enum), and routes OOB accesses through the sane-chain LocationKind in clobberize so they can no longer be CSE-matched with in-bounds accesses; this prevents the illegal PutByVal->PutByValAlias conversion. The SpeculativeJIT changes add ASSERT_ENABLED in-bounds assertions/breakpoints on the PutByValAlias paths to catch any future violation of the ‘PutByValAlias implies in-bounds’ invariant. The regression test put-by-val-alias-out-of-bounds.js performs array[i] &= 2 over a Uint32Array(65535) with i running well past the length, exactly the read-modify-write pattern that CSE turns into an aliased OOB store. (The clobberize refactor to a local mode variable and the deletion of printInternal(LocationKind) dead code are non-security cleanups; the CSE dataLog additions are tracing only.)

Key insight
Conflating out-of-bounds sane-chain accesses with in-bounds accesses in CSE’s HeapLocation model let an OOB PutByVal be rewritten as a bounds-check-free PutByValAlias; giving OOB accesses distinct location kinds restores the invariant that PutByValAlias is only ever emitted for genuinely in-bounds stores.

Attack Path

  1. Tier up to DFG Repeatedly execute a small function so JSC compiles it in the DFG, enabling CSE and array-mode specialization.
  2. Emit a read-modify-write on an indexed array Use array[i] op= value (e.g. array[i] &= 2) which lowers to a GetByVal followed by a PutByVal on the same base and index — the pattern CSE tries to alias.
  3. Make the access out-of-bounds sane-chain Drive the index i beyond the array length with a sane prototype chain so the access is compiled in an out-of-bounds mode rather than being rejected.
  4. Get the store turned into PutByValAlias Because the OOB access shares a HeapLocation with an in-bounds access, CSE matches them and rewrites the PutByVal to PutByValAlias, which the SpeculativeJIT compiles with no bounds check.
  5. Perform the out-of-bounds write The bounds-check-free aliased store writes the value at base+index*elementSize past the buffer/butterfly, giving a controlled-offset OOB write (index and value are attacker-controlled).
  6. Escalate to arbitrary R/W and RCE Standard JSC exploitation (background): groom adjacent objects/butterflies, corrupt a length or structure to build addrof/fakeobj and arbitrary read/write, then achieve code execution inside the WebContent process.

Impact Assessment

The primitive is a controlled out-of-bounds write on an indexed array/typed array: attacker-chosen index and value stored with the bounds check elided, one of the strongest JSC primitives. Realistic escalation is straightforward to arbitrary read/write (corrupt an adjacent butterfly length or a typed-array vector/length, build addrof/fakeobj) and then to remote code execution. It is reachable purely from crafted JavaScript, matching the CVE’s ‘maliciously crafted web content -> memory corruption’ wording. It is confined to the WebContent process under the WebKit sandbox, so full compromise still needs a separate sandbox escape.

Changed Functions

FunctionChangeNotes
clobberize
Source/JavaScriptCore/dfg/DFGClobberize.h
modified Core fix: PutByVal typed-array and Int32/Double/Contiguous paths, plus DataViewSet, now map out-of-bounds accesses to *OutOfBoundsSaneChain HeapLocation kinds (via indexedPropertyLocToOutOfBoundsSaneChain) so CSE cannot conflate them with in-bounds accesses; also switched to a local `mode` and removed the obsolete forward-exit-hoisting comment.
indexedPropertyLocToOutOfBoundsSaneChain
Source/JavaScriptCore/dfg/DFGHeapLocation.h
added Maps a base indexed LocationKind (Int32/Int52/Double/JS) to its OutOfBoundsSaneChain variant; the mechanism by which OOB accesses now get distinct HeapLocations.
LocationKind enum
Source/JavaScriptCore/dfg/DFGHeapLocation.h
modified Adds IndexedPropertyInt52OutOfBoundsSaneChainLoc (previously missing) and reorders JS/Int52 sane-chain entries so every result type has a distinct OOB sane-chain kind.
printInternal(PrintStream&, LocationKind)
Source/JavaScriptCore/dfg/DFGHeapLocation.cpp
deleted Dead debug dumper removed (and its declaration in the header); non-security cleanup.
SpeculativeJIT::compileContiguousPutByVal
Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
modified On the PutByValAlias branch adds ASSERT(arrayMode.isInBounds()) and an ASSERT_ENABLED branch32/breakpoint that traps if propertyReg is not below public length, hardening the 'alias implies in-bounds' invariant; hoists arrayMode read.
SpeculativeJIT::compileDoublePutByVal
Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
modified Same in-bounds ASSERT/breakpoint hardening on the PutByValAlias branch for double arrays.
SpeculativeJIT::jumpForTypedArrayOutOfBounds
Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
modified For PutByValAlias still emits no bounds check (returns empty Jump) but now asserts arrayMode is in-bounds and non-resizable and adds an ASSERT_ENABLED length check/breakpoint; documents that the alias path legitimately skips the check.
CSEPhase run/write/def
Source/JavaScriptCore/dfg/DFGCSEPhase.cpp
modified Adds verbose dataLogLnIf tracing only; no behavioral change.

Audit Directions

  • Every clobberize def() for indexed/DataView accesses
    In DFGClobberize.h audit each GetByVal/PutByVal/DataViewGet/DataViewSet arm for whether it selects the OutOfBoundsSaneChain LocationKind when arrayMode().isOutOfBounds(); grep for indexedPropertyLocForResultType, indexedPropertyLocToOutOfBoundsSaneChain, and any def(HeapLocation(…)) that ignores isOutOfBounds().
  • PutByValAlias emission sites
    Grep the SpeculativeJIT and FTL for PutByValAlias and confirm every path assumes/asserts isInBounds() and mayBeResizableOrGrowableSharedTypedArray() is false; check jumpForTypedArrayOutOfBounds-style helpers that return an empty Jump().
  • Alias conversion logic
    Find where PutByVal is downgraded to PutByValAlias (ArrayMode/CSE/FixupPhase) and verify the aliasing candidate’s HeapLocation truly matches only in-bounds accesses; look for the LocationKind comparisons and canonicalResultRepresentation usage.
  • Completeness of sane-chain LocationKinds
    Audit the LocationKind enum and indexedPropertyLocToOutOfBoundsSaneChain switch for any result representation lacking a distinct OOB variant (the Int52 case was the missing one); ensure no default/RELEASE_ASSERT_NOT_REACHED path silently reuses an in-bounds kind.

Original Bug Report

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