CVE-2024-54543
Overview
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.)
Attack Path
- Tier up to DFG Repeatedly execute a small function so JSC compiles it in the DFG, enabling CSE and array-mode specialization.
- 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.
- 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.
- 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.
- 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).
- 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
clobberizeSource/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. |
indexedPropertyLocToOutOfBoundsSaneChainSource/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 enumSource/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::compileContiguousPutByValSource/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::compileDoublePutByValSource/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp |
modified | Same in-bounds ASSERT/breakpoint hardening on the PutByValAlias branch for double arrays. |
SpeculativeJIT::jumpForTypedArrayOutOfBoundsSource/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/defSource/JavaScriptCore/dfg/DFGCSEPhase.cpp |
modified | Adds verbose dataLogLnIf tracing only; no behavioral change. |
Audit Directions
- Every clobberize def() for indexed/DataView accessesIn 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 sitesGrep 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 logicFind 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 LocationKindsAudit 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.