CVE-2025-31215
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Jiming Wang and Jikai Ren
Key insight. The abstract interpreter under-specified the result structure of single-child NewArrayWithSpread (only recognizing the PhantomNewArrayBuffer-contiguous case as CopyOnWrite) while FTL always allocates a CopyOnWrite contiguous array, so the AI’s model diverged from the emitted allocation; the fix unifies both the lowering and the AI so the proven structure always matches what is built.
Patch Details
diff --git a/JSTests/stress/dfg-ai-should-reduce-new-array-with-spread-structures.js b/JSTests/stress/dfg-ai-should-reduce-new-array-with-spread-structures.js
new file mode 100644
index 000000000000..5f1699bb4ccb
--- /dev/null
+++ b/JSTests/stress/dfg-ai-should-reduce-new-array-with-spread-structures.js
@@ -0,0 +1,11 @@
+//@ runDefault("--thresholdForFTLOptimizeAfterWarmUp=1000", "--useConcurrentJIT=0")
+for (let i = 0; i < 1000; ++i) {
+ (() => {
+ for (let v8 = 0; v8 < 25; v8++) {}
+ const v10 = [...[1,2,3,4,5]];
+ v10[2] = 3;
+ function f11(a12) { return a12;}
+ class C13 extends f11 {}
+ v10.at(3745);
+ })();
+}
diff --git a/Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h b/Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h
index 52dc45bfa761..78cd51b229cd 100644
--- a/Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h
+++ b/Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h
@@ -3594,7 +3594,8 @@ bool AbstractInterpreter<AbstractStateType>::executeEffects(unsigned clobberLimi
m_graph.globalObjectFor(node->origin.semantic)->arrayStructureForIndexingTypeDuringAllocation(node->indexingType()));
break;
- case NewArrayWithSpread:
+ case NewArrayWithSpread: {
+ auto* globalObject = m_graph.globalObjectFor(node->origin.semantic);
if (m_graph.isWatchingHavingABadTimeWatchpoint(node)) {
// We've compiled assuming we're not having a bad time, so to be consistent
// with StructureRegisterationPhase we must say we produce an original array
@@ -3602,29 +3603,14 @@ bool AbstractInterpreter<AbstractStateType>::executeEffects(unsigned clobberLimi
#if USE(JSVALUE64)
BitVector* bitVector = node->bitVector();
if (node->numChildren() == 1 && bitVector->get(0)) {
- Edge use = m_graph.varArgChild(node, 0);
- if (use->op() == PhantomSpread) {
- if (use->child1()->op() == PhantomNewArrayBuffer) {
- auto* immutableButterfly = use->child1()->castOperand<JSImmutableButterfly*>();
- if (hasContiguous(immutableButterfly->indexingType())) {
- setForNode(node, m_graph.globalObjectFor(node->origin.semantic)->originalArrayStructureForIndexingType(CopyOnWriteArrayWithContiguous));
- break;
- }
- }
- }
+ setForNode(node, globalObject->originalArrayStructureForIndexingType(CopyOnWriteArrayWithContiguous));
+ break;
}
#endif
- JSGlobalObject* globalObject = m_graph.globalObjectFor(node->origin.semantic);
- RegisteredStructureSet structureSet;
- structureSet.add(m_graph.registerStructure(globalObject->originalArrayStructureForIndexingType(ArrayWithContiguous)));
- structureSet.add(m_graph.registerStructure(globalObject->originalArrayStructureForIndexingType(CopyOnWriteArrayWithContiguous)));
- setForNode(node, structureSet);
- } else {
- setForNode(node,
- m_graph.globalObjectFor(node->origin.semantic)->arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous));
}
-
+ setForNode(node, globalObject->arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous));
break;
+ }
case Spread:
switch (node->child1()->op()) {
diff --git a/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp b/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
index fee6e1b55eba..2b1ec8f74135 100644
--- a/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
+++ b/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
@@ -9547,53 +9547,37 @@ IGNORE_CLANG_WARNINGS_END
if (m_node->numChildren() == 1 && bitVector->get(0)) {
Edge use = m_graph.varArgChild(m_node, 0);
+ LValue immutableButterfly = nullptr;
if (use->op() == PhantomSpread) {
- if (use->child1()->op() == PhantomNewArrayBuffer) {
- auto* immutableButterfly = use->child1()->castOperand<JSImmutableButterfly*>();
- if (hasContiguous(immutableButterfly->indexingType())) {
- RegisteredStructure structure = m_graph.registerStructure(globalObject->originalArrayStructureForIndexingType(CopyOnWriteArrayWithContiguous));
- LBasicBlock slowPath = m_out.newBlock();
- LBasicBlock continuation = m_out.newBlock();
-
- LValue fastArray = allocateObject<JSArray>(structure, m_out.constIntPtr(immutableButterfly->toButterfly()), slowPath);
- ValueFromBlock fastResult = m_out.anchor(fastArray);
- m_out.jump(continuation);
-
- m_out.appendTo(slowPath, continuation);
- LValue slowArray = vmCall(Int64, operationNewArrayBuffer, m_vmValue, weakStructure(structure), frozenPointer(use->child1()->cellOperand()));
- ValueFromBlock slowResult = m_out.anchor(slowArray);
- m_out.jump(continuation);
-
- m_out.appendTo(continuation);
-
- mutatorFence();
- setJSValue(m_out.phi(pointerType(), slowResult, fastResult));
- return;
- }
+ if (use->child1()->op() == PhantomNewArrayBuffer)
+ immutableButterfly = createContiguousImmutableButterflyFromPhantomNewArrayBuffer(globalObject, use->child1().node());
+ else {
+ RELEASE_ASSERT(use->child1()->op() == PhantomCreateRest);
+ immutableButterfly = createContiguousImmutableButterflyFromPhantomCreateRest(globalObject, use->child1().node());
}
} else {
// If a node is producing JSImmutableButterfly, it must be contiguous.
- LValue immutableButterfly = lowCell(use);
+ immutableButterfly = lowCell(use);
+ }
- RegisteredStructure structure = m_graph.registerStructure(globalObject->arrayStructureForIndexingTypeDuringAllocation(CopyOnWriteArrayWithContiguous));
- LBasicBlock slowPath = m_out.newBlock();
- LBasicBlock continuation = m_out.newBlock();
+ RegisteredStructure structure = m_graph.registerStructure(globalObject->arrayStructureForIndexingTypeDuringAllocation(CopyOnWriteArrayWithContiguous));
+ LBasicBlock slowPath = m_out.newBlock();
+ LBasicBlock continuation = m_out.newBlock();
- LValue fastArray = allocateObject<JSArray>(structure, toButterfly(immutableButterfly), slowPath);
- ValueFromBlock fastResult = m_out.anchor(fastArray);
- m_out.jump(continuation);
+ LValue fastArray = allocateObject<JSArray>(structure, toButterfly(immutableButterfly), slowPath);
+ ValueFromBlock fastResult = m_out.anchor(fastArray);
+ m_out.jump(continuation);
- m_out.appendTo(slowPath, continuation);
- LValue slowArray = vmCall(Int64, operationNewArrayBuffer, m_vmValue, weakStructure(structure), immutableButterfly);
- ValueFromBlock slowResult = m_out.anchor(slowArray);
- m_out.jump(continuation);
+ m_out.appendTo(slowPath, continuation);
+ LValue slowArray = vmCall(Int64, operationNewArrayBuffer, m_vmValue, weakStructure(structure), immutableButterfly);
+ ValueFromBlock slowResult = m_out.anchor(slowArray);
+ m_out.jump(continuation);
- m_out.appendTo(continuation);
+ m_out.appendTo(continuation);
- mutatorFence();
- setJSValue(m_out.phi(pointerType(), slowResult, fastResult));
- return;
- }
+ mutatorFence();
+ setJSValue(m_out.phi(pointerType(), slowResult, fastResult));
+ return;
}
for (unsigned i = 0; i < m_node->numChildren(); ++i) {
@@ -9933,50 +9917,103 @@ IGNORE_CLANG_WARNINGS_END
compileCreateInternalFieldObject<JSAsyncGenerator>(operationCreateAsyncGenerator);
}
- void compileSpread()
+ LValue createContiguousImmutableButterflyFromPhantomNewArrayBuffer(JSGlobalObject* globalObject, Node* newArrayBufferNode)
{
- JSGlobalObject* globalObject = m_graph.globalObjectFor(m_origin.semantic);
- if (m_node->child1()->op() == PhantomNewArrayBuffer) {
- ASSERT(m_graph.isWatchingHavingABadTimeWatchpoint(m_node->child1().node()));
+ ASSERT(newArrayBufferNode->op() == PhantomNewArrayBuffer);
+ auto* immutableButterfly = newArrayBufferNode->castOperand<JSImmutableButterfly*>();
+ if (hasContiguous(immutableButterfly->indexingType()))
+ return frozenPointer(newArrayBufferNode->cellOperand());
- // FIXME: JSImmutableButterfly::createFromArray should support re-using non contiguous indexing types as well.
- auto* immutableButterfly = m_node->child1()->castOperand<JSImmutableButterfly*>();
- if (hasContiguous(immutableButterfly->indexingType())) {
- setJSValue(frozenPointer(m_node->child1()->cellOperand()));
- return;
- }
+ LBasicBlock slowAllocation = m_out.newBlock();
+ LBasicBlock continuation = m_out.newBlock();
- LBasicBlock slowAllocation = m_out.newBlock();
- LBasicBlock continuation = m_out.newBlock();
+ ASSERT(immutableButterfly->length() <= MAX_STORAGE_VECTOR_LENGTH);
- ASSERT(immutableButterfly->length() <= MAX_STORAGE_VECTOR_LENGTH);
+ LValue fastImmutableButterflyValue = allocateVariableSizedCell<JSImmutableButterfly>(
+ m_out.constIntPtr(JSImmutableButterfly::allocationSize(immutableButterfly->length())),
+ m_graph.m_vm.immutableButterflyStructure(CopyOnWriteArrayWithContiguous), slowAllocation);
+ LValue fastImmutableButterflyStorage = toButterfly(fastImmutableButterflyValue);
+ m_out.store32(m_out.constInt32(immutableButterfly->length()), fastImmutableButterflyStorage, m_heaps.Butterfly_publicLength);
+ m_out.store32(m_out.constInt32(immutableButterfly->length()), fastImmutableButterflyStorage, m_heaps.Butterfly_vectorLength);
+ ValueFromBlock fastImmutableButterfly = m_out.anchor(fastImmutableButterflyValue);
+ m_out.jump(continuation);
- LValue fastImmutableButterflyValue = allocateVariableSizedCell<JSImmutableButterfly>(
- m_out.constIntPtr(JSImmutableButterfly::allocationSize(immutableButterfly->length())),
- m_graph.m_vm.immutableButterflyStructure(CopyOnWriteArrayWithContiguous), slowAllocation);
- LValue fastImmutableButterflyStorage = toButterfly(fastImmutableButterflyValue);
- m_out.store32(m_out.constInt32(immutableButterfly->length()), fastImmutableButterflyStorage, m_heaps.Butterfly_publicLength);
- m_out.store32(m_out.constInt32(immutableButterfly->length()), fastImmutableButterflyStorage, m_heaps.Butterfly_vectorLength);
- ValueFromBlock fastImmutableButterfly = m_out.anchor(fastImmutableButterflyValue);
- m_out.jump(continuation);
+ LBasicBlock lastNext = m_out.appendTo(slowAllocation, continuation);
+ ValueFromBlock slowImmutableButterfly = m_out.anchor(vmCall(pointerType(), operationCreateImmutableButterfly, weakPointer(globalObject), m_out.constInt32(immutableButterfly->length())));
+ m_out.jump(continuation);
- LBasicBlock lastNext = m_out.appendTo(slowAllocation, continuation);
- ValueFromBlock slowImmutableButterfly = m_out.anchor(vmCall(pointerType(), operationCreateImmutableButterfly, weakPointer(globalObject), m_out.constInt32(immutableButterfly->length())));
- m_out.jump(continuation);
+ m_out.appendTo(continuation, lastNext);
+ LValue immutableButterflyValue = m_out.phi(pointerType(), fastImmutableButterfly, slowImmutableButterfly);
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2025-31215 |
| Bugzilla | 288814 |
| Component | JSC |
| Bug Class | TypeConfusion |
| Severity | medium |
| Commit | ac09d743b1828ad9… |
| Advisory | Apple Advisory |
Terminology
- DFG/FTL abstract interpreter (AI) — A dataflow analysis in JSC’s optimizing JIT that proves facts (types, structures) about values so later phases can drop checks and specialize code; its proofs must soundly match what the backend actually produces.
- NewArrayWithSpread / Spread — DFG nodes that construct an array from spread elements (e.g. […x]); Spread produces the intermediate immutable butterfly and NewArrayWithSpread assembles the final array.
- JSImmutableButterfly / CopyOnWrite array — A shared, immutable backing store (butterfly) used by copy-on-write arrays (CopyOnWriteArrayWithContiguous) so array literals can share storage until mutated.
- HavingABadTime watchpoint — A JSGlobalObject watchpoint that fires when array prototype/indexed behavior is tampered with; while valid, the JIT may assume original, well-behaved array structures.
- PhantomNewArrayBuffer / PhantomCreateRest — Phantom (materialization-elided) DFG nodes representing an array-literal buffer or a rest-parameter array that a Spread consumes without allocating the intermediate object.
- Structure — JSC’s descriptor of an object’s shape and indexing type; JITted code specializes on structures, so a wrong structure assumption yields type confusion or out-of-bounds indexed access.
Root Cause Analysis
The bug is in JSC’s DFG abstract interpreter, specifically the AbstractInterpreter handling of the NewArrayWithSpread node in DFGAbstractInterpreterInlines.h. When compiling under the HavingABadTime watchpoint (i.e., array prototypes are unmodified) and the node has a single spread child whose bit is set, the FTL lowering (compileNewArrayWithSpread in FTLLowerDFGToB3.cpp) produces a CopyOnWrite contiguous array in essentially all sub-cases: a PhantomSpread of a PhantomNewArrayBuffer, a PhantomSpread of a PhantomCreateRest, or a directly-lowered immutable butterfly. The abstract interpreter, however, only proved the result was a CopyOnWrite original array (originalArrayStructureForIndexingType(CopyOnWriteArrayWithContiguous)) for the narrow case where the spread wrapped a PhantomNewArrayBuffer with a contiguous indexing type; for every other single-child spread shape it fell through to a broader/less-precise structure set (originalArrayStructureForIndexingType(ArrayWithContiguous) plus the CopyOnWrite variant, or the non-original ArrayWithContiguous during allocation). This is an inconsistency between what the FTL backend actually allocates and what the abstract value model claims: the AI over- or mis-approximated the result structure relative to the concrete allocation, violating the invariant that the abstract interpreter’s proven structure must match (soundly and precisely) the structure the backend emits. Downstream optimizations that trust the AI’s structure set (for example CSE/structure checks and array-access lowering such as the .at() access in the test) can then operate on an array whose real structure differs from the assumed one. The fix refactors compileNewArrayWithSpread so both the PhantomNewArrayBuffer and PhantomCreateRest cases go through shared helpers (createContiguousImmutableButterflyFromPhantomNewArrayBuffer / …FromPhantomCreateRest) that always yield a contiguous CopyOnWrite immutable butterfly, and correspondingly simplifies the AbstractInterpreter so that for the single-set-bit spread case it uniformly sets the node’s structure to originalArrayStructureForIndexingType(CopyOnWriteArrayWithContiguous), and otherwise to arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous). This realigns the abstract structure with what is actually allocated, closing the mismatch. It is an inference (not shown verbatim in the diff) that the concrete downstream misuse is a type-confusion/OOB via an array-access node trusting the wrong structure; the diff shows the AI/lowering mismatch and a regression test that exercises spread + element write + out-of-range .at().
Attack Path
1. Construct a single-child spread array literal
Use a construct like […[1,2,3,4,5]] (PhantomNewArrayBuffer spread) or spreading rest arguments (PhantomCreateRest) so the DFG builds a NewArrayWithSpread node with one child whose bit vector bit 0 is set.
2. Force FTL compilation under the good-time watchpoint
Warm the enclosing function so it tiers up to FTL while the HavingABadTime watchpoint is still valid (the test uses –thresholdForFTLOptimizeAfterWarmUp=1000 and a 1000-iteration warmup with an inner counting loop).
3. Trigger the AI/allocation structure mismatch
In the spread shape not covered by the narrow AI case, the abstract interpreter models a structure set that does not match the CopyOnWrite contiguous array the FTL backend actually allocates.
4. Feed the array to a structure-trusting operation
Mutate and index the array (v10[2]=3 then v10.at(3745) in the test, plus a class extending a returned function to shape the graph) so an optimized array-access relies on the AI’s incorrect structure assumption.
5. Provoke the fault
The access proceeds under a wrong structure/indexing assumption, producing an out-of-bounds or type-confused access that crashes the WebContent process (the demonstrated outcome).
Impact Assessment
The patch corrects a mismatch between the abstract interpreter’s proven result structure for single-child NewArrayWithSpread and the CopyOnWrite contiguous array the FTL backend actually allocates. A structure/indexing-type misprediction that downstream array accesses trust is a classic route to type confusion or out-of-bounds indexed access, which in JSC is a strong primitive that has historically been escalated toward arbitrary read/write and RCE; however, the diff and regression test only demonstrate a crash, and any escalation beyond that is inference, not established by the commit. The bug is confined to the sandboxed WebContent (renderer) process. The advisory characterizes it as an unexpected process crash, consistent with a controlled fault, while the underlying AI-imprecision class is the concerning part.
Audit Directions
Re-audit NewArrayWithSpread AI vs. FTL allocation parity
In DFGAbstractInterpreterInlines.h and FTLLowerDFGToB3.cpp, diff every branch of compileNewArrayWithSpread/compileSpread against the structure setForNode() calls; confirm originalArrayStructureForIndexingType(CopyOnWriteArrayWithContiguous) vs arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous) is chosen consistently for each spread shape (PhantomSpread-of-PhantomNewArrayBuffer, PhantomSpread-of-PhantomCreateRest, direct immutable butterfly).
Sweep other array-constructing nodes for the same AI/backend divergence
Grep DFGAbstractInterpreterInlines.h for setForNode paired with originalArrayStructureForIndexingType / arrayStructureForIndexingTypeDuringAllocation on NewArray, NewArrayBuffer, NewArrayWithSize, Spread, and CreateRest, and verify each matches the concrete structure used in the corresponding FTL/DFG lowering, especially where a hasContiguous()/indexingType() special case narrows only one branch.
Check the refactored helpers for behavior changes, not just extraction
Compare createContiguousImmutableButterflyFromPhantomCreateRest’s immutableButterflyStructures[arrayIndexFromIndexingType(CopyOnWriteArrayWithContiguous) - NumberOfIndexingShapes] against the original immutableButterflyStructure(CopyOnWriteArrayWithContiguous) call to ensure the structure selection is equivalent, and audit callers of these helpers (compileSpread and compileNewArrayWithSpread) for length/MAX_STORAGE_VECTOR_LENGTH and indexing-type assumptions.
Look for HavingABadTime-gated structure assumptions elsewhere
Grep for isWatchingHavingABadTimeWatchpoint together with structure/indexing-type selection across DFG/FTL; any place that proves an ‘original array’ structure in the AI under this watchpoint but branches its concrete allocation on indexingType()/hasContiguous() is a candidate for the same mismatch.
Changed Functions
| Function | File | Change | Note |
|---|---|---|---|
compileNewArrayWithSpread |
Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp |
modified | For the single-set-bit spread case, replaces inline per-shape logic with calls to the new helpers so both PhantomNewArrayBuffer and PhantomCreateRest paths uniformly build a contiguous CopyOnWrite immutable butterfly and allocate a CopyOnWriteArrayWithContiguous JSArray; unifies the fast/slow allocation and phi. |
createContiguousImmutableButterflyFromPhantomNewArrayBuffer |
Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp |
added | Factored-out helper (from compileSpread) that returns the frozen contiguous immutable butterfly, or allocates a CopyOnWriteArrayWithContiguous immutable butterfly and copies the buffer’s JSValues in when the source indexing type is not already contiguous. |
createContiguousImmutableButterflyFromPhantomCreateRest |
Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp |
added | Factored-out helper (from compileSpread) that builds a contiguous CopyOnWrite immutable butterfly from rest arguments, computing length/start from the inline call frame and copying values in a loop; note it now uses immutableButterflyStructures[…] indexing for the structure. |
compileSpread |
Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp |
modified | Rewritten to delegate to the two new helpers for the PhantomNewArrayBuffer and PhantomCreateRest branches instead of inlining the allocation/copy code, so Spread and NewArrayWithSpread share the same butterfly-construction logic. |
AbstractInterpreter::executeEffects (NewArrayWithSpread case) |
Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h |
modified | Simplifies the abstract value: for the single-set-bit spread case it now uniformly proves originalArrayStructureForIndexingType(CopyOnWriteArrayWithContiguous), and otherwise arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous), removing the over-broad structure set that disagreed with what FTL allocates. |
Files Changed
Source Files
Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.hSource/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
Test Files
JSTests/stress/dfg-ai-should-reduce-new-array-with-spread-structures.js
Regression Test
The fix ships this proof-of-concept / regression test:
diff --git a/JSTests/stress/dfg-ai-should-reduce-new-array-with-spread-structures.js b/JSTests/stress/dfg-ai-should-reduce-new-array-with-spread-structures.js
new file mode 100644
index 000000000000..5f1699bb4ccb
--- /dev/null
+++ b/JSTests/stress/dfg-ai-should-reduce-new-array-with-spread-structures.js
@@ -0,0 +1,11 @@
+//@ runDefault("--thresholdForFTLOptimizeAfterWarmUp=1000", "--useConcurrentJIT=0")
+for (let i = 0; i < 1000; ++i) {
+ (() => {
+ for (let v8 = 0; v8 < 25; v8++) {}
+ const v10 = [...[1,2,3,4,5]];
+ v10[2] = 3;
+ function f11(a12) { return a12;}
+ class C13 extends f11 {}
+ v10.at(3745);
+ })();
+}