CVE-2025-43425
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: an anonymous researcher
Key insight. The bug is a shallow-copy aliasing defect in DFG block cloning: node kinds whose OpInfo points to mutable per-node side-table data were cloned as ‘Common’ (bitwise), so clone and original shared one data object – the fix makes them ‘Special’ and allocates a fresh Bag entry per clone.
Patch Details
diff --git a/JSTests/stress/loop-unrolling-multi-get-and-put-by-offset.js b/JSTests/stress/loop-unrolling-multi-get-and-put-by-offset.js
new file mode 100644
index 000000000000..fe7c028c6b82
--- /dev/null
+++ b/JSTests/stress/loop-unrolling-multi-get-and-put-by-offset.js
@@ -0,0 +1,18 @@
+//@ runDefault("--forceEagerCompilation=true")
+function foo() {
+ const v0 = [];
+ let v1 = 0;
+
+ do {
+ for (let v5 = v1; v5 < 7; v5++) {
+ v0.c **= v5;
+ }
+ v1++;
+ } while (v1 < 9);
+
+ return v0;
+}
+
+for(let i = 0; i < 32; i++) {
+ foo();
+}
diff --git a/Source/JavaScriptCore/dfg/DFGCloneHelper.cpp b/Source/JavaScriptCore/dfg/DFGCloneHelper.cpp
index e49246b8ec63..945752db98c0 100644
--- a/Source/JavaScriptCore/dfg/DFGCloneHelper.cpp
+++ b/Source/JavaScriptCore/dfg/DFGCloneHelper.cpp
@@ -119,24 +119,27 @@ Node* CloneHelper::cloneNodeImpl(BasicBlock* into, Node* node)
return edge ? Edge(cloneNode(into, edge.node()), edge.useKind()) : Edge();
};
- switch (nodeCloneStatusFor(node->op())) {
- case NodeCloneStatus::Common: {
+ auto cloneEdges = [&](Node* node, Node* clone) {
if (node->hasVarArgs()) {
size_t firstChild = m_graph.m_varArgChildren.size();
m_graph.doToAllChildren(node, [&](Edge& edge) {
m_graph.m_varArgChildren.append(cloneEdge(edge));
});
- Node* clone = into->cloneAndAppend(m_graph, node);
clone->children.setFirstChild(firstChild);
return clone;
}
- Node* clone = into->cloneAndAppend(m_graph, node);
clone->child1() = cloneEdge(node->child1());
clone->child2() = cloneEdge(node->child2());
clone->child3() = cloneEdge(node->child3());
return clone;
+ };
+
+ switch (nodeCloneStatusFor(node->op())) {
+ case NodeCloneStatus::Common: {
+ Node* clone = into->cloneAndAppend(m_graph, node);
+ return cloneEdges(node, clone);
}
case NodeCloneStatus::Special:
@@ -144,16 +147,46 @@ Node* CloneHelper::cloneNodeImpl(BasicBlock* into, Node* node)
case Branch: {
Node* clone = into->cloneAndAppend(m_graph, node);
clone->setOpInfo(OpInfo(m_graph.m_branchData.add(WTFMove(*node->branchData()))));
- clone->child1() = cloneEdge(node->child1());
- return clone;
+ return cloneEdges(node, clone);
}
case Switch: {
Node* clone = into->cloneAndAppend(m_graph, node);
SwitchData& cloneData = *m_graph.m_switchData.add();
cloneData = *clone->switchData();
clone->setOpInfo(OpInfo(&cloneData));
- clone->child1() = cloneEdge(node->child1());
- return clone;
+ return cloneEdges(node, clone);
+ }
+ case MultiGetByOffset: {
+ Node* clone = into->cloneAndAppend(m_graph, node);
+ MultiGetByOffsetData& cloneData = *m_graph.m_multiGetByOffsetData.add();
+ cloneData = node->multiGetByOffsetData();
+ clone->setOpInfo(OpInfo(&cloneData));
+ return cloneEdges(node, clone);
+ }
+ case MultiPutByOffset: {
+ Node* clone = into->cloneAndAppend(m_graph, node);
+ MultiPutByOffsetData& cloneData = *m_graph.m_multiPutByOffsetData.add();
+ cloneData = node->multiPutByOffsetData();
+ clone->setOpInfo(OpInfo(&cloneData));
+ return cloneEdges(node, clone);
+ }
+ case CallVarargs:
+ case ConstructVarargs:
+ case TailCallVarargsInlinedCaller:
+ case TailCallForwardVarargsInlinedCaller: {
+ Node* clone = into->cloneAndAppend(m_graph, node);
+ CallVarargsData& cloneData = *m_graph.m_callVarargsData.add();
+ cloneData = *node->callVarargsData();
+ clone->setOpInfo(OpInfo(&cloneData));
+ return cloneEdges(node, clone);
+ }
+ case LoadVarargs:
+ case VarargsLength: {
+ Node* clone = into->cloneAndAppend(m_graph, node);
+ LoadVarargsData& cloneData = *m_graph.m_loadVarargsData.add();
+ cloneData = *node->loadVarargsData();
+ clone->setOpInfo(OpInfo(&cloneData));
+ return cloneEdges(node, clone);
}
default:
RELEASE_ASSERT_NOT_REACHED();
diff --git a/Source/JavaScriptCore/dfg/DFGCloneHelper.h b/Source/JavaScriptCore/dfg/DFGCloneHelper.h
index 2a55975a54f9..24160016493f 100644
--- a/Source/JavaScriptCore/dfg/DFGCloneHelper.h
+++ b/Source/JavaScriptCore/dfg/DFGCloneHelper.h
@@ -173,7 +173,7 @@ BasicBlock* CloneHelper::cloneBlock(BasicBlock* const block, const CustomizeSucc
CLONE_STATUS(Call, Common) \
CLONE_STATUS(CallCustomAccessorGetter, Common) \
CLONE_STATUS(CallDirectEval, Common) \
- CLONE_STATUS(CallVarargs, Common) \
+ CLONE_STATUS(CallVarargs, Special) \
CLONE_STATUS(Check, Common) \
CLONE_STATUS(CheckArray, Common) \
CLONE_STATUS(CheckBadValue, Common) \
@@ -194,7 +194,7 @@ BasicBlock* CloneHelper::cloneBlock(BasicBlock* const block, const CustomizeSucc
CLONE_STATUS(CompareStrictEq, Common) \
CLONE_STATUS(ConstantStoragePointer, Common) \
CLONE_STATUS(Construct, Common) \
- CLONE_STATUS(ConstructVarargs, Common) \
+ CLONE_STATUS(ConstructVarargs, Special) \
CLONE_STATUS(CreateActivation, Common) \
CLONE_STATUS(CreateDirectArguments, Common) \
CLONE_STATUS(CreateRest, Common) \
@@ -271,7 +271,7 @@ BasicBlock* CloneHelper::cloneBlock(BasicBlock* const block, const CustomizeSucc
CLONE_STATUS(JSConstant, Common) \
CLONE_STATUS(Jump, Common) \
CLONE_STATUS(LoadMapValue, Common) \
- CLONE_STATUS(LoadVarargs, Common) \
+ CLONE_STATUS(LoadVarargs, Special) \
CLONE_STATUS(LogicalNot, Common) \
CLONE_STATUS(LoopHint, Common) \
CLONE_STATUS(MakeAtomString, Common) \
@@ -284,8 +284,8 @@ BasicBlock* CloneHelper::cloneBlock(BasicBlock* const block, const CustomizeSucc
CLONE_STATUS(MapSet, Common) \
CLONE_STATUS(MatchStructure, Common) \
CLONE_STATUS(MovHint, Common) \
- CLONE_STATUS(MultiGetByOffset, Common) \
- CLONE_STATUS(MultiPutByOffset, Common) \
+ CLONE_STATUS(MultiGetByOffset, Special) \
+ CLONE_STATUS(MultiPutByOffset, Special) \
CLONE_STATUS(NewArray, Common) \
CLONE_STATUS(NewArrayBuffer, Common) \
CLONE_STATUS(NewArrayWithButterfly, Common) \
@@ -346,9 +346,9 @@ BasicBlock* CloneHelper::cloneBlock(BasicBlock* const block, const CustomizeSucc
CLONE_STATUS(StringSubstring, Common) \
CLONE_STATUS(StrCat, Common) \
CLONE_STATUS(Switch, Special) \
- CLONE_STATUS(TailCallForwardVarargsInlinedCaller, Common) \
+ CLONE_STATUS(TailCallForwardVarargsInlinedCaller, Special) \
CLONE_STATUS(TailCallInlinedCaller, Common) \
- CLONE_STATUS(TailCallVarargsInlinedCaller, Common) \
+ CLONE_STATUS(TailCallVarargsInlinedCaller, Special) \
CLONE_STATUS(ToLength, Common) \
CLONE_STATUS(ToLowerCase, Common) \
CLONE_STATUS(ToPrimitive, Common) \
@@ -370,7 +370,7 @@ BasicBlock* CloneHelper::cloneBlock(BasicBlock* const block, const CustomizeSucc
CLONE_STATUS(ValueRep, Common) \
CLONE_STATUS(ValueSub, Common) \
CLONE_STATUS(ValueToInt32, Common) \
- CLONE_STATUS(VarargsLength, Common) \
+ CLONE_STATUS(VarargsLength, Special) \
CLONE_STATUS(ZombieHint, Common)
} } // namespace JSC::DFG
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2025-43425 |
| Bugzilla | 298851 |
| Component | JSC |
| Bug Class | LogicError |
| Severity | medium |
| Commit | fcd2a8fc20dda5aa… |
| Advisory | Apple Advisory |
Terminology
- DFG JIT — JavaScriptCore’s mid-tier optimizing compiler that builds a Node-based dataflow graph (IR) and applies optimization phases before code generation.
- Node OpInfo / side-table Bag — Auxiliary per-node data (e.g. MultiGetByOffsetData, CallVarargsData) is stored in graph-owned Bags and referenced from the Node via an OpInfo pointer rather than inline.
- Loop unrolling / block cloning — A DFG optimization that duplicates basic blocks (via CloneHelper) so a loop body appears multiple times, requiring every cloned node’s data to be independently copied.
- MultiGetByOffset / MultiPutByOffset — DFG nodes representing polymorphic property loads/stores over a small set of known structures, carrying a data object describing the per-structure cases.
- Shallow vs deep clone — A shallow (bitwise) clone copies the OpInfo pointer so two nodes alias one data object; a deep clone allocates a fresh data object for the clone.
Root Cause Analysis
JSC’s DFG optimizer clones basic blocks (used by loop unrolling / loop peeling) via CloneHelper::cloneNodeImpl. Each DFG Node may carry an OpInfo that is really a pointer into a per-graph side-table ‘Bag’ holding auxiliary data for that node kind – e.g. MultiGetByOffsetData, MultiPutByOffsetData, CallVarargsData, LoadVarargsData, SwitchData, BranchData. The clone classification table (CLONE_STATUS in DFGCloneHelper.h) marked several such node kinds as NodeCloneStatus::Common: MultiGetByOffset, MultiPutByOffset, CallVarargs, ConstructVarargs, LoadVarargs, VarargsLength, TailCallVarargsInlinedCaller, TailCallForwardVarargsInlinedCaller. For ‘Common’ nodes cloneNodeImpl simply calls into->cloneAndAppend(m_graph, node), which bitwise-copies the Node including its OpInfo pointer, and then fixes up the edges. The invariant that was violated is that a cloned node owning mutable per-node side-table data must receive its own freshly allocated copy of that data; instead the clone and the original ended up sharing the same MultiGetByOffsetData/CallVarargsData/etc. object. When a later phase mutates one node’s data (or the two copies are independently register-allocated / fixed up during unrolling), the shared aliased data desynchronizes from the node, producing an inconsistent IR that trips a RELEASE_ASSERT_NOT_REACHED / bad-graph assertion or miscompiles. The fix moves each of these kinds to NodeCloneStatus::Special and adds explicit cases in cloneNodeImpl that allocate a new entry in the corresponding Bag (m_multiGetByOffsetData.add(), m_multiPutByOffsetData.add(), m_callVarargsData.add(), m_loadVarargsData.add()), copy the original’s data into it, and setOpInfo to the fresh copy. Additionally the patch refactors edge cloning into a cloneEdges lambda and applies it uniformly; notably the pre-existing Branch and Switch Special cases only cloned child1() and dropped child2()/child3()/varargs children, so the refactor also fixes under-cloning of edges for those nodes. The regression test loop-unrolling-multi-get-and-put-by-offset.js with –forceEagerCompilation drives repeated MultiPutByOffset (v0.c **= v5) inside an unrolled loop to reach the buggy clone path.
Attack Path
1. Force DFG compilation with loop unrolling
Provide JS that runs a function hot (or use eager compilation) containing a small counted loop the DFG will unroll/peel, as the test does with a do/for loop over v0.c **= v5.
2. Place a side-table-backed node in the loop body
Use an operation the DFG lowers to MultiGetByOffset/MultiPutByOffset (polymorphic property access on a few structures) or a varargs call (CallVarargs/LoadVarargs) inside the loop so cloning must copy its OpInfo side data.
3. Trigger block cloning
Loop unrolling invokes CloneHelper::cloneBlock -> cloneNodeImpl on the body; the node is cloned as ‘Common’, so the clone shares the original’s MultiPutByOffsetData/CallVarargsData pointer.
4. Desynchronize the shared data
Subsequent DFG phases fix up / mutate the cloned node and its aliased side data independently of the original, yielding an inconsistent graph.
5. Reach the crash
The inconsistency is caught by an IR assertion (or leads to a bad compile), producing the ‘unexpected process crash’ in WebContent described by the advisory.
Impact Assessment
As established by the diff the primitive is a DFG IR consistency bug: cloned nodes alias the original’s compiler side-table data, yielding an inconsistent graph that most directly manifests as an assertion-driven crash of the WebContent process (the advisory says ‘unexpected process crash’). Whether this escalates depends on the aliased data: sharing CallVarargsData/LoadVarargsData (which encode argument counts and spill/stack offsets) could in principle drive a miscompiled varargs call that sets up the wrong stack frame, a path toward memory corruption – but that escalation is an inference and is not demonstrated by the patch, which shows only the missing deep copy. The issue is confined to the JS engine in the WebContent process and remains within its sandbox.
Audit Directions
Remaining Common nodes with owned OpInfo data
In DFGCloneHelper.h scan every CLONE_STATUS(…, Common) entry and cross-check DFGNode.h for node kinds whose OpInfo is a pointer into a Graph Bag (grep m_graph.m_Data.add / hasData() accessors like arrayMode/heapPrediction excluded); any such kind still marked Common is a candidate for the same aliasing bug.
Under-cloned edges in Special cases
Audit each Special case in cloneNodeImpl (and any hand-written node duplication) for cloning only child1() while the node can have child2()/child3()/varargs – the Branch and Switch cases had exactly this defect; grep for ‘clone->child1()’ without corresponding child2/child3/hasVarArgs handling.
Other node-duplicating phases
Beyond CloneHelper, review DFG phases that copy or re-emit nodes carrying side-table pointers (loop peeling, tail-duplication, OSR/inlining, constant folding that reuses OpInfo) for the same ‘copy the node but not its Bag data’ mistake.
Varargs metadata sharing at other tiers
Check that CallVarargsData/LoadVarargsData counts and offsets are never shared between distinct call sites after any transformation, since aliased varargs stack metadata is the most memory-relevant consequence.
Changed Functions
| Function | File | Change | Note |
|---|---|---|---|
CloneHelper::cloneNodeImpl |
Source/JavaScriptCore/dfg/DFGCloneHelper.cpp |
modified | Extracts edge cloning into a cloneEdges lambda; adds Special-case clone handling for MultiGetByOffset, MultiPutByOffset, CallVarargs/ConstructVarargs/TailCall(Forward)VarargsInlinedCaller, and LoadVarargs/VarargsLength that allocate fresh Bag entries and setOpInfo to them; makes Branch/Switch use cloneEdges so all children (not just child1) are cloned. |
CLONE_STATUS table (cloneStatusFor macro list) |
Source/JavaScriptCore/dfg/DFGCloneHelper.h |
modified | Reclassifies CallVarargs, ConstructVarargs, LoadVarargs, VarargsLength, TailCallVarargsInlinedCaller, TailCallForwardVarargsInlinedCaller, MultiGetByOffset, MultiPutByOffset from Common to Special so they take the deep-copy clone path. |
Files Changed
Source Files
Source/JavaScriptCore/dfg/DFGCloneHelper.cppSource/JavaScriptCore/dfg/DFGCloneHelper.h
Test Files
JSTests/stress/loop-unrolling-multi-get-and-put-by-offset.js
Regression Test
The fix ships this proof-of-concept / regression test:
diff --git a/JSTests/stress/loop-unrolling-multi-get-and-put-by-offset.js b/JSTests/stress/loop-unrolling-multi-get-and-put-by-offset.js
new file mode 100644
index 000000000000..fe7c028c6b82
--- /dev/null
+++ b/JSTests/stress/loop-unrolling-multi-get-and-put-by-offset.js
@@ -0,0 +1,18 @@
+//@ runDefault("--forceEagerCompilation=true")
+function foo() {
+ const v0 = [];
+ let v1 = 0;
+
+ do {
+ for (let v5 = v1; v5 < 7; v5++) {
+ v0.c **= v5;
+ }
+ v1++;
+ } while (v1 < 9);
+
+ return v0;
+}
+
+for(let i = 0; i < 32; i++) {
+ foo();
+}