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 |
|---|---|---|
CloneHelper::cloneNodeImplSource/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
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.