CVE-2025-43425
Overview
Background
- 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
- 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.
- 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.
- 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.
- 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.
- 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
forJSTests/stress/loop-unrolling-multi-get-and-put-by-offset.js |
modified |
Files Changed
JSTests/stress/loop-unrolling-multi-get-and-put-by-offset.jsSource/JavaScriptCore/dfg/DFGCloneHelper.cppSource/JavaScriptCore/dfg/DFGCloneHelper.h
Audit Directions
- Remaining Common nodes with owned OpInfo dataIn 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 casesAudit 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 phasesBeyond 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 tiersCheck 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.
Patch
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
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();
+}
Regression Test / PoC
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();
+}