Medium CVSS 4.3 webkit Logic Error 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected process crash
ComponentJSC DFG
Bug ClassLogic Error
Tracker298851
Fix commitfcd2a8fc20dd (WebKit/WebKit) +67/-16
CWECWE-119 (Buffer bounds error)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L
CISA KEVNot listed
Creditedan anonymous researcher
Disclosed2025-11-03

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.

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.

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.

Changed Functions

FunctionChangeNotes
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

  • JSTests/stress/loop-unrolling-multi-get-and-put-by-offset.js
  • Source/JavaScriptCore/dfg/DFGCloneHelper.cpp
  • Source/JavaScriptCore/dfg/DFGCloneHelper.h

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.

Original Bug Report

The reporter's bug is still restricted on the tracker.