CVE-2024-54505
Overview
Background
- Backwards propagation / usage flags
- A DFG phase that records how values are used (NodeBytecodeUsesAsNumber/AsOther/…) to guide later representation and conversion choices.
- SpecOther
- The speculated type for ‘other’ primitives (null/undefined); UsesAsOther is only meaningful when the operand can be SpecOther.
- ToString / CallStringConstructor
- DFG nodes for String() coercion whose operand-usage flags feed downstream optimization.
Root Cause Analysis
This fixes an incorrect backward-propagation of usage flags for ToString/CallStringConstructor in the DFG that could mislead value-representation decisions into a type confusion. DFGBackwardsPropagationPhase walks the graph backward to record how each value is used (NodeBytecodeUses* flags), which downstream phases consult to pick number/other representations and conversions. For ToString and CallStringConstructor, the pre-patch code unconditionally merged NodeBytecodeUsesAsNumber | NodeBytecodeUsesAsOther | NodeBytecodeNeedsNaNOrInfinity onto the operand (child1). Marking the operand as UsesAsOther even when its use kind cannot actually be an ‘other’ (null/undefined, SpecOther) type feeds an inconsistent usage signal into later optimization: the representation/conversion chosen for the operand based on these flags becomes inconsistent with how the value is really produced/consumed, and that mismatch is exploitable as a type confusion (a value handled as the wrong representation), which the advisory rates as memory corruption.
The fix gates the UsesAsOther flag on the operand actually being able to be SpecOther: if (typeFilterFor(node->child1().useKind()) & SpecOther) node->child1()->mergeFlags(NodeBytecodeUsesAsOther); and always merges UsesAsNumber | NeedsNaNOrInfinity.
The restored invariant is that the UsesAsOther usage flag is propagated only when the operand’s use kind admits an ‘other’ type, so downstream representation choices are consistent. INFERENCE: the precise downstream miscompilation that turns the wrong flag into a corrupting type confusion is not contained in this one-hunk diff; the patch establishes the incorrect unconditional UsesAsOther propagation as the root cause.
Attack Path
- Reach the DFG Run JS with a ToString/String() over a value whose DFG use kind cannot be an ‘other’ type.
- Mis-propagate UsesAsOther Backward propagation unconditionally marks the operand UsesAsOther, an inconsistent usage signal.
- Choose a wrong representation A later phase picks a value representation/conversion based on the incorrect flags.
- Type confusion The value is handled as the wrong representation, a memory-corruption primitive in the WebContent process (inferred escalation).
Impact Assessment
Files Changed
JSTests/stress/string-add-conversion-unused.jsSource/JavaScriptCore/dfg/DFGBackwardsPropagationPhase.cpp
Audit Directions
- Unconditional usage-flag mergesAudit DFGBackwardsPropagationPhase for other nodes that merge NodeBytecodeUsesAsOther/AsNumber unconditionally regardless of the operand’s use kind / possible types.
- Flag-driven representationTrace consumers of NodeBytecodeUsesAsOther to confirm they tolerate (or the producers correctly gate) the flag on SpecOther.
Patch
diff --git a/Source/JavaScriptCore/dfg/DFGBackwardsPropagationPhase.cpp b/Source/JavaScriptCore/dfg/DFGBackwardsPropagationPhase.cpp
index d1c2540344cd..e2eb6b334eaf 100644
--- a/Source/JavaScriptCore/dfg/DFGBackwardsPropagationPhase.cpp
+++ b/Source/JavaScriptCore/dfg/DFGBackwardsPropagationPhase.cpp
@@ -528,7 +528,9 @@ private:
case ToString:
case CallStringConstructor: {
- node->child1()->mergeFlags(NodeBytecodeUsesAsNumber | NodeBytecodeUsesAsOther | NodeBytecodeNeedsNaNOrInfinity);
+ if (typeFilterFor(node->child1().useKind()) & SpecOther)
+ node->child1()->mergeFlags(NodeBytecodeUsesAsOther);
+ node->child1()->mergeFlags(NodeBytecodeUsesAsNumber | NodeBytecodeNeedsNaNOrInfinity);
break;
}
diff --git a/JSTests/stress/string-add-conversion-unused.js b/JSTests/stress/string-add-conversion-unused.js
new file mode 100644
index 000000000000..96e1377859a0
--- /dev/null
+++ b/JSTests/stress/string-add-conversion-unused.js
@@ -0,0 +1,14 @@
+var x = [, 9999999999];
+var count = 0;
+for (
+ y = 10;
+ (function () {
+ z = y;
+ " " + x[y];
+ return 9;
+ })();
+ y--
+) {
+ if (count >= 99) break;
+ count++;
+}
Regression Test / PoC
diff --git a/JSTests/stress/string-add-conversion-unused.js b/JSTests/stress/string-add-conversion-unused.js
new file mode 100644
index 000000000000..96e1377859a0
--- /dev/null
+++ b/JSTests/stress/string-add-conversion-unused.js
@@ -0,0 +1,14 @@
+var x = [, 9999999999];
+var count = 0;
+for (
+ y = 10;
+ (function () {
+ z = y;
+ " " + x[y];
+ return 9;
+ })();
+ y--
+) {
+ if (count >= 99) break;
+ count++;
+}