Released December 11, 2024

CVE-2024-54543

CVSS 8.8 severity high CWE-787 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

Overview

Impact:

Description: Processing maliciously crafted web content may lead to memory corruption

Researchers: Lukas Bernhard, Gary Kwong, and an anonymous researcher

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.

Patch Details

diff --git a/JSTests/stress/put-by-val-alias-out-of-bounds.js b/JSTests/stress/put-by-val-alias-out-of-bounds.js
new file mode 100644
index 000000000000..ebd74a75ae6d
--- /dev/null
+++ b/JSTests/stress/put-by-val-alias-out-of-bounds.js
@@ -0,0 +1,9 @@
+let array = new Uint32Array(65535);
+function test() {
+    for (let i = 0; i < 1e5; i += 6)
+        array[i] &= 2;
+    return array;
+}
+noInline(test);
+
+test();
\ No newline at end of file
diff --git a/Source/JavaScriptCore/dfg/DFGCSEPhase.cpp b/Source/JavaScriptCore/dfg/DFGCSEPhase.cpp
index 1909d7741fed..1c044e4bbf2c 100644
--- a/Source/JavaScriptCore/dfg/DFGCSEPhase.cpp
+++ b/Source/JavaScriptCore/dfg/DFGCSEPhase.cpp
@@ -524,6 +524,7 @@ private:
     
         bool run(BasicBlock* block)
         {
+            dataLogLnIf(DFGCSEPhaseInternal::verbose, "Starting block: ", block->index);
             m_maps.clear();
             m_changed = false;
             m_block = block;
@@ -607,11 +608,13 @@ private:
     
         void write(AbstractHeap heap)
         {
+            dataLogLnIf(DFGCSEPhaseInternal::verbose, "\tWrite to heap ", heap);
             m_maps.write(heap);
         }
         
         void def(PureValue value)
         {
+            dataLogLnIf(DFGCSEPhaseInternal::verbose, "\tDef of value ", value, " at node ", m_node->index());
             Node* match = m_maps.addPure(value, m_node);
             if (!match)
                 return;
@@ -622,6 +625,7 @@ private:
     
         void def(const HeapLocation& location, const LazyNode& value)
         {
+            dataLogLnIf(DFGCSEPhaseInternal::verbose, "\tDef to ", location, " of value ", value, " at node ", m_node->index());
             LazyNode match = m_maps.addImpure(location, value);
             if (!match)
                 return;
diff --git a/Source/JavaScriptCore/dfg/DFGClobberize.h b/Source/JavaScriptCore/dfg/DFGClobberize.h
index 2fc89697731f..592a668ba9ee 100644
--- a/Source/JavaScriptCore/dfg/DFGClobberize.h
+++ b/Source/JavaScriptCore/dfg/DFGClobberize.h
@@ -60,14 +60,6 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
     //   easiest way of indicating that something cannot be hoisted is to claim
     //   that it side-effects some miscellaneous thing.
     //
-    // - We cannot hoist forward-exiting nodes without some additional effort. I
-    //   believe that what it comes down to is that forward-exiting generally have
-    //   their NodeExitsForward cleared upon hoist, except for forward-exiting
-    //   nodes that take bogus state as their input. Those are substantially
-    //   harder. We disable it for now. In the future we could enable it by having
-    //   versions of those nodes that backward-exit instead, but I'm not convinced
-    //   of the soundness.
-    //
     // - Some nodes lie, and claim that they do not read the JSCell_structureID,
     //   JSCell_typeInfoFlags, etc. These are nodes that use the structure in a way
     //   that does not depend on things that change under structure transitions.
@@ -1116,8 +1108,11 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
             if (mode.mayBeResizableOrGrowableSharedTypedArray()) {
                 write(MiscFields);
                 write(TypedArrayProperties);
-            } else
+            } else {
+                if (mode.isOutOfBounds())
+                    indexedPropertyLoc = indexedPropertyLocToOutOfBoundsSaneChain(indexedPropertyLoc);
                 def(HeapLocation(indexedPropertyLoc, TypedArrayProperties, graph.varArgChild(node, 0), graph.varArgChild(node, 1)), LazyNode(node));
+            }
             return;
         // We should not get an AnyTypedArray in a GetByVal as AnyTypedArray is only created from intrinsics, which
         // are only added from Inline Caching a GetById.
@@ -1167,7 +1162,7 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
             return;
             
         case Array::Int32:
-            if (node->arrayMode().isOutOfBounds()) {
+            if (mode.isOutOfBounds()) {
                 clobberTop();
                 return;
             }
@@ -1175,14 +1170,14 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
             read(Butterfly_vectorLength);
             read(IndexedInt32Properties);
             write(IndexedInt32Properties);
-            if (node->arrayMode().mayStoreToHole())
+            if (mode.mayStoreToHole())
                 write(Butterfly_publicLength);
             def(HeapLocation(indexedPropertyLoc, IndexedInt32Properties, base, index), LazyNode(value));
             def(HeapLocation(IndexedPropertyInt32OutOfBoundsSaneChainLoc, IndexedInt32Properties, base, index), LazyNode(value));
             return;
             
         case Array::Double:
-            if (node->arrayMode().isOutOfBounds()) {
+            if (mode.isOutOfBounds()) {
                 clobberTop();
                 return;
             }
@@ -1190,7 +1185,7 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
             read(Butterfly_vectorLength);
             read(IndexedDoubleProperties);
             write(IndexedDoubleProperties);
-            if (node->arrayMode().mayStoreToHole())
+            if (mode.mayStoreToHole())
                 write(Butterfly_publicLength);
             def(HeapLocation(IndexedPropertyDoubleLoc, IndexedDoubleProperties, base, index), LazyNode(value));
             def(HeapLocation(IndexedPropertyDoubleSaneChainLoc, IndexedDoubleProperties, base, index), LazyNode(value));
@@ -1198,7 +1193,7 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
             return;
             
         case Array::Contiguous:
-            if (node->arrayMode().isOutOfBounds()) {
+            if (mode.isOutOfBounds()) {
                 clobberTop();
                 return;
             }
@@ -1206,7 +1201,7 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
             read(Butterfly_vectorLength);
             read(IndexedContiguousProperties);
             write(IndexedContiguousProperties);
-            if (node->arrayMode().mayStoreToHole())
+            if (mode.mayStoreToHole())
                 write(Butterfly_publicLength);
             def(HeapLocation(indexedPropertyLoc, IndexedContiguousProperties, base, index), LazyNode(value));
             def(HeapLocation(IndexedPropertyJSOutOfBoundsSaneChainLoc, IndexedContiguousProperties, base, index), LazyNode(value));
@@ -1221,12 +1216,12 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
             read(Butterfly_vectorLength);
             read(IndexedArrayStorageProperties);
             write(IndexedArrayStorageProperties);
-            if (node->arrayMode().mayStoreToHole())
+            if (mode.mayStoreToHole())
                 write(Butterfly_publicLength);
             return;
 
         case Array::SlowPutArrayStorage:
-            if (node->arrayMode().mayStoreToHole()) {
+            if (mode.mayStoreToHole()) {
                 clobberTop();
                 return;
             }
@@ -1246,7 +1241,7 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
         case Array::Float16Array:
         case Array::Float32Array:
         case Array::Float64Array:
-            if (node->arrayMode().mayBeResizableOrGrowableSharedTypedArray()) {
+            if (mode.mayBeResizableOrGrowableSharedTypedArray()) {
                 read(TypedArrayProperties);
                 read(MiscFields);
                 write(TypedArrayProperties);
@@ -2266,7 +2261,7 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
             write(MiscFields);
             write(TypedArrayProperties);
         } else {
-            LocationKind indexedPropertyLoc = indexedPropertyLocForResultType(node->result());
+            LocationKind indexedPropertyLoc = indexedPropertyLocToOutOfBoundsSaneChain(indexedPropertyLocForResultType(node->result()));
             def(HeapLocation(indexedPropertyLoc, AbstractHeap(TypedArrayProperties, node->dataViewData().asQuadWord), node->child1(), node->child2(), node->child3()), LazyNode(node));
         }
         return;
diff --git a/Source/JavaScriptCore/dfg/DFGHeapLocation.cpp b/Source/JavaScriptCore/dfg/DFGHeapLocation.cpp
index 3675e7845f7b..21cc6315d8da 100644
--- a/Source/JavaScriptCore/dfg/DFGHeapLocation.cpp
+++ b/Source/JavaScriptCore/dfg/DFGHeapLocation.cpp
@@ -47,254 +47,5 @@ void HeapLocation::dump(PrintStream& out) const
 
 } } // namespace JSC::DFG
 
-namespace WTF {
-
-using namespace JSC::DFG;
-
-void printInternal(PrintStream& out, LocationKind kind)
-{
-    switch (kind) {
-    case InvalidLocationKind:
-        out.print("InvalidLocationKind");
-        return;
-        
-    case InvalidationPointLoc:
-        out.print("InvalidationPointLoc");
-        return;
-        
-    case TypeOfIsObjectLoc:
-        out.print("TypeOfIsObjectLoc");
-        return;
-
-    case TypeOfIsFunctionLoc:
-        out.print("TypeOfIsFunctionLoc");
-        return;
-
-    case IsCallableLoc:

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2024-54543
Bugzilla 282450
Component WebKit
Bug Class OOB
Severity medium
Commit 903f38de509c5f14…
Advisory Apple Advisory

Terminology

  • 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

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.

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.

Changed Functions

Function File Change Note
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.

Regression Test

The fix ships this proof-of-concept / regression test:

diff --git a/JSTests/stress/put-by-val-alias-out-of-bounds.js b/JSTests/stress/put-by-val-alias-out-of-bounds.js
new file mode 100644
index 000000000000..ebd74a75ae6d
--- /dev/null
+++ b/JSTests/stress/put-by-val-alias-out-of-bounds.js
@@ -0,0 +1,9 @@
+let array = new Uint32Array(65535);
+function test() {
+    for (let i = 0; i < 1e5; i += 6)
+        array[i] &= 2;
+    return array;
+}
+noInline(test);
+
+test();
\ No newline at end of file