CVE-2025-43211
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.cppSource/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++) {
+ }
+ })()
+}