CVE-2024-54543
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