CVE-2025-43211
Overview
Background
- DFG JIT
- JavaScriptCore’s optimizing ‘Data Flow Graph’ compiler that speculates on value types and deoptimizes (OSR exit) if speculations fail.
- Fixup phase
- A DFG pass that assigns edge useKinds and inserts type-check/conversion nodes so later phases and codegen can assume specific representations.
- useKind / edge
- A DFG edge annotation (e.g. StringUse, UntypedUse, CellUse) declaring what type/representation a node’s input is guaranteed to have at that point.
- Manual vs Automatic OperandSpeculation
- A codegen flag choosing whether the speculation guard for an operand is emitted here (automatic) or was already emitted upstream so must not be repeated (manual).
- SpeculateCellOperand / speculateString
- Codegen helpers that load a value into a register as a cell and emit an inline type check that it is a JSString, OSR-exiting otherwise.
Root Cause Analysis
The bug is in the DFG JIT’s handling of String.prototype.replace / replaceAll, split across the Fixup phase (Source/JavaScriptCore/dfg/DFGFixupPhase.cpp) and codegen (Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp::compileStringReplace). For the StringReplace/StringReplaceAll node, child2 is the search argument. Before the patch, when child2 shouldSpeculateString(), the Fixup phase inserted an explicit Check(@child2, StringUse) node AND called fixEdge<StringUse>(child2), setting the edge’s useKind to StringUse. Codegen (compileStringReplace) then keyed off node->child2().useKind() == StringUse only to pick ManualOperandSpeculation (meaning ’the speculation guard was already emitted elsewhere, don’t re-emit it here’) and loaded child2 with a plain JSValueOperand as if it were a boxed JSValue. The invariant that broke is the contract between the Fixup phase and codegen about who guarantees, and how, that a StringUse edge actually holds a JSString cell in a cell-shaped register at the call site: the ManualOperandSpeculation path assumed the separately-inserted Check fully pinned the value, but the operand was still materialized as a generic JSValue rather than as a speculated cell, and the interaction with the else-if control flow (StringUse and the StringReplace-primordial/ForceOSRExit branch were mutually exclusive via ’else if’) left the guarding inconsistent. This mismatch let a non-guaranteed value flow into operationStringProtoFuncReplaceGeneric under DFG validation (the test runs with –validateBCE=true), producing an assertion/DFG_CRASH — a reliable controlled crash / DoS.
The fix, on the Fixup side, drops the manually inserted Check node, keeps fixEdge<StringUse>(child2), and adds an explicit break so the StringUse case no longer shares control flow with the StringReplace/ForceOSRExit branch. On the codegen side it rewrites compileStringReplace as a switch on child2’s useKind: for StringUse it loads child2 via SpeculateCellOperand and calls speculateString(child2, searchGPR) to emit the type guard right there, passing the search as a CellValue; for UntypedUse it keeps the plain JSValueOperand generic path; and default now hits DFG_CRASH(‘Bad UseKind’). This restores a single, explicit speculation site and makes the operand shape (cell vs JSValue) match the useKind.
Attack Path
- Reach the StringReplace DFG node From JavaScript, call String.prototype.replace with a search argument the DFG can speculate as a String — the reproducer does name.proto.replace(name, name) where name is f.name (a string).
- Warm up to the DFG tier Run the call in a tight loop (the test uses an infinite for(;;) plus low JIT thresholds via runDefault) so the function tiers up and the StringReplace node is compiled by the DFG SpeculativeJIT.
- Hit the mismatched StringUse path With child2 speculated String, the pre-patch Fixup inserts a Check and marks StringUse while codegen uses ManualOperandSpeculation on a generic JSValueOperand, leaving the search operand’s guarding inconsistent.
- Trip validation / speculation Under DFG validation (–validateBCE=true) or the speculation machinery the inconsistent guard is detected, driving an assertion / DFG_CRASH.
- Crash the WebContent process The abort terminates the renderer, yielding the denial-of-service described by the CVE.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
FixupPhase::fixupNode (StringReplace/StringReplaceAll case)Source/JavaScriptCore/dfg/DFGFixupPhase.cpp |
modified | For child2 speculated String, removed the manually inserted Check(StringUse) node, kept fixEdge<StringUse>(child2), and added 'break' so the StringUse case no longer shares else-if control flow with the RegExp-primordial / ForceOSRExit branch. |
SpeculativeJIT::compileStringReplaceSource/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp |
modified | Replaced the AutomaticOperandSpeculation/ManualOperandSpeculation heuristic with an explicit switch on child2 useKind: StringUse now uses SpeculateCellOperand + speculateString(child2) and passes CellValue(searchGPR); UntypedUse keeps the generic JSValueOperand path; default is DFG_CRASH('Bad UseKind'). |
Files Changed
JSTests/stress/string-replace-speculate-string.jsSource/JavaScriptCore/dfg/DFGFixupPhase.cppSource/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
Audit Directions
- Other ManualOperandSpeculation call sitesGrep DFGSpeculativeJIT.cpp for ‘ManualOperandSpeculation’ and confirm each is paired with an upstream Check/fixEdge that truly pins the operand’s representation; the removed Check here shows how fragile that pairing is.
- Fixup Check-node + fixEdge combinationsIn DFGFixupPhase.cpp grep for insertNode(…Check…) immediately followed by fixEdge<…>() on the same edge; each redundant/overlapping guard is a candidate for the same StringUse-vs-JSValue shape mismatch, especially where control flow uses ’else if'.
- Other string-intrinsic compilersAudit compileStringReplaceString, compileStringSlice, compileStringIndexOf and similar for whether their operands are loaded with SpeculateCellOperand/speculateString matching the edge useKind, or with a generic JSValueOperand that assumes an external guard.
- Missing default/Bad-UseKind guardsSearch DFG codegen ‘compile*’ functions that switch on useKind but lack a DFG_CRASH(‘Bad UseKind’) default; the added default here is what turns an unexpected useKind into a defined crash instead of undefined behavior.