Medium CVSS 6.2 webkit Logic Error 🔧 Commit mapped

Overview

Medium
Severity
6.2
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing web content may lead to a denial-of-service
ComponentJSC DFG
Bug ClassLogic Error
Tracker293730
Fix commit58218eebdaf5 (WebKit/WebKit) +53/-20
CWECWE-770
CVSS vectorCVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
CISA KEVNot listed
CreditedYuhao Hu, Yan Kang, Chenggang Wu, and Xiaojie Wei
Disclosed2025-07-29

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.

Key insight
The vulnerability is a broken contract between the Fixup phase and codegen over StringReplace’s search argument: the Fixup phase marked a StringUse edge while codegen materialized it as a generic JSValue under ManualOperandSpeculation, so no single site actually guaranteed the value was a JSString; the fix collapses this into one explicit speculateString site per useKind.

Attack Path

  1. 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).
  2. 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.
  3. 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.
  4. Trip validation / speculation Under DFG validation (–validateBCE=true) or the speculation machinery the inconsistent guard is detected, driving an assertion / DFG_CRASH.
  5. Crash the WebContent process The abort terminates the renderer, yielding the denial-of-service described by the CVE.

Impact Assessment

As shown in the diff the failure mode is a controlled abort (DFG_CRASH / speculation-validation assertion), so the realistic impact is a denial-of-service crash of the WebContent (renderer) process, matching the CVE description. Whether the underlying operand-shape mismatch (feeding a not-fully-guaranteed value as a String cell into operationStringProtoFuncReplaceGeneric) could be weaponized into a type confusion is not demonstrated by the patch and would be inference; the visible outcome is a crash inside the JS engine, confined to the sandboxed renderer. Heap-grooming / ACE background is not established by this commit — it is a speculation-contract correctness fix.

Changed Functions

FunctionChangeNotes
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::compileStringReplace
Source/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.js
  • Source/JavaScriptCore/dfg/DFGFixupPhase.cpp
  • Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp

Audit Directions

  • Other ManualOperandSpeculation call sites
    Grep 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 combinations
    In 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 compilers
    Audit 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 guards
    Search 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.

Original Bug Report

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