Released July 29, 2025

CVE-2025-43211

CVSS 6.2 severity medium CWE-770 vector CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Overview

Impact:

Description: Processing web content may lead to a denial-of-service

Researchers: Yuhao Hu, Yan Kang, Chenggang Wu, and Xiaojie Wei

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.

Patch Details

diff --git a/JSTests/stress/string-replace-speculate-string.js b/JSTests/stress/string-replace-speculate-string.js
new file mode 100644
index 000000000000..675232dcd256
--- /dev/null
+++ b/JSTests/stress/string-replace-speculate-string.js
@@ -0,0 +1,15 @@
+//@ runDefault("--thresholdForJITSoon=10", "--thresholdForJITAfterWarmUp=10", "--thresholdForOptimizeAfterWarmUp=100", "--thresholdForOptimizeAfterLongWarmUp=100", "--thresholdForOptimizeSoon=100", "--thresholdForFTLOptimizeAfterWarmUp=1000", "--thresholdForFTLOptimizeSoon=1000", "--validateBCE=true", "--useConcurrentJIT=0", "--watchdog=500", "--watchdog-exception-ok")
+
+for (;;) {
+    (() => {
+        function f() {
+            return undefined;
+        }
+        try { f(); } catch (e) {}
+        const name = f.name;
+        try { name.isWellFormed(); } catch (e) {}
+        name.__proto__.replace(name, name);
+        for (let i = 0; i < 100; i++) {
+        }
+    })()
+}
diff --git a/Source/JavaScriptCore/dfg/DFGFixupPhase.cpp b/Source/JavaScriptCore/dfg/DFGFixupPhase.cpp
index c038929c6190..b79cdf2c5a77 100644
--- a/Source/JavaScriptCore/dfg/DFGFixupPhase.cpp
+++ b/Source/JavaScriptCore/dfg/DFGFixupPhase.cpp
@@ -1756,16 +1756,15 @@ private:
             }
 
             if (node->child2()->shouldSpeculateString()) {
-                m_insertionSet.insertNode(
-                    m_indexInBlock, SpecNone, Check, node->origin,
-                    Edge(node->child2().node(), StringUse));
                 fixEdge<StringUse>(node->child2());
-            } else if (op == StringReplace || op == StringReplaceAll) {
+                break;
+            }
+
+            if (op == StringReplace || op == StringReplaceAll) {
                 if (node->child2()->shouldSpeculateRegExpObject() && m_graph.isWatchingRegExpPrimordialPropertiesWatchpoint(node))
                     addStringReplacePrimordialChecks(node->child2().node());
                 else 
-                    m_insertionSet.insertNode(
-                        m_indexInBlock, SpecNone, ForceOSRExit, node->origin);
+                    m_insertionSet.insertNode(m_indexInBlock, SpecNone, ForceOSRExit, node->origin);
             }
 
             if (node->child1()->shouldSpeculateString()
diff --git a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
index 66c0c10b2a18..ffebf47f729e 100644
--- a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
+++ b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
@@ -13455,22 +13455,41 @@ void SpeculativeJIT::compileStringReplace(Node* node)
         return;
     }
 
-    // If we fixed up the edge of child2, we inserted a Check(@child2, String).
-    OperandSpeculationMode child2SpeculationMode = AutomaticOperandSpeculation;
-    if (node->child2().useKind() == StringUse)
-        child2SpeculationMode = ManualOperandSpeculation;
+    switch (node->child2().useKind()) {
+    case StringUse: {
+        JSValueOperand string(this, node->child1());
+        SpeculateCellOperand search(this, node->child2());
+        JSValueOperand replace(this, node->child3());
+        JSValueRegs stringRegs = string.jsValueRegs();
+        GPRReg searchGPR = search.gpr();
+        JSValueRegs replaceRegs = replace.jsValueRegs();
 
-    JSValueOperand string(this, node->child1());
-    JSValueOperand search(this, node->child2(), child2SpeculationMode);
-    JSValueOperand replace(this, node->child3());
-    JSValueRegs stringRegs = string.jsValueRegs();
-    JSValueRegs searchRegs = search.jsValueRegs();
-    JSValueRegs replaceRegs = replace.jsValueRegs();
+        speculateString(node->child2(), searchGPR);
 
-    flushRegisters();
-    GPRFlushedCallResult result(this);
-    callOperation(node->op() == StringReplaceAll ? operationStringProtoFuncReplaceAllGeneric : operationStringProtoFuncReplaceGeneric, result.gpr(), LinkableConstant::globalObject(*this, node), stringRegs, searchRegs, replaceRegs);
-    cellResult(result.gpr(), node);
+        flushRegisters();
+        GPRFlushedCallResult result(this);
+        callOperation(node->op() == StringReplaceAll ? operationStringProtoFuncReplaceAllGeneric : operationStringProtoFuncReplaceGeneric, result.gpr(), LinkableConstant::globalObject(*this, node), stringRegs, CellValue(searchGPR), replaceRegs);
+        cellResult(result.gpr(), node);
+        break;
+    }
+    case UntypedUse: {
+        JSValueOperand string(this, node->child1());
+        JSValueOperand search(this, node->child2());
+        JSValueOperand replace(this, node->child3());
+        JSValueRegs stringRegs = string.jsValueRegs();
+        JSValueRegs searchRegs = search.jsValueRegs();
+        JSValueRegs replaceRegs = replace.jsValueRegs();
+
+        flushRegisters();
+        GPRFlushedCallResult result(this);
+        callOperation(node->op() == StringReplaceAll ? operationStringProtoFuncReplaceAllGeneric : operationStringProtoFuncReplaceGeneric, result.gpr(), LinkableConstant::globalObject(*this, node), stringRegs, searchRegs, replaceRegs);
+        cellResult(result.gpr(), node);
+        break;
+    }
+    default:
+        DFG_CRASH(m_graph, node, "Bad UseKind");
+        break;
+    }
 }
 
 void SpeculativeJIT::compileStringReplaceString(Node* node)

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2025-43211
Bugzilla 293730
Component JSC
Bug Class LogicError
Severity medium
Commit 58218eebdaf5770a…
Advisory Apple Advisory

Terminology

  • 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

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.

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.

Changed Functions

Function File Change Note
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

Source Files

  • Source/JavaScriptCore/dfg/DFGFixupPhase.cpp
  • Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp

Test Files

  • JSTests/stress/string-replace-speculate-string.js

Regression Test

The fix ships this proof-of-concept / regression test:

diff --git a/JSTests/stress/string-replace-speculate-string.js b/JSTests/stress/string-replace-speculate-string.js
new file mode 100644
index 000000000000..675232dcd256
--- /dev/null
+++ b/JSTests/stress/string-replace-speculate-string.js
@@ -0,0 +1,15 @@
+//@ runDefault("--thresholdForJITSoon=10", "--thresholdForJITAfterWarmUp=10", "--thresholdForOptimizeAfterWarmUp=100", "--thresholdForOptimizeAfterLongWarmUp=100", "--thresholdForOptimizeSoon=100", "--thresholdForFTLOptimizeAfterWarmUp=1000", "--thresholdForFTLOptimizeSoon=1000", "--validateBCE=true", "--useConcurrentJIT=0", "--watchdog=500", "--watchdog-exception-ok")
+
+for (;;) {
+    (() => {
+        function f() {
+            return undefined;
+        }
+        try { f(); } catch (e) {}
+        const name = f.name;
+        try { name.isWellFormed(); } catch (e) {}
+        name.__proto__.replace(name, name);
+        for (let i = 0; i < 100; i++) {
+        }
+    })()
+}