Released November 3, 2025

CVE-2025-43425

CVSS 4.3 severity medium CWE-119 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L

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.cpp
  • Source/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(); 
+}