CVE-2024-54505
Overview
Impact:
Description: Processing maliciously crafted web content may lead to memory corruption
Researchers: Gary Kwong
Key insight. ToString/CallStringConstructor unconditionally marked their operand as UsesAsOther even when it could not be an ‘other’ type, feeding inconsistent usage flags into representation choices; gating the flag on SpecOther fixes the miscompilation.
Patch Details
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++;
+}
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;
}
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2024-54505 |
| Bugzilla | 282661 |
| Component | JSC |
| Bug Class | TypeConfusion |
| Severity | medium |
| Commit | 2d5e29d47324834c… |
| Advisory | Apple Advisory |
Terminology
- 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
1. Reach the DFG
Run JS with a ToString/String() over a value whose DFG use kind cannot be an ‘other’ type.
2. Mis-propagate UsesAsOther
Backward propagation unconditionally marks the operand UsesAsOther, an inconsistent usage signal.
3. Choose a wrong representation
A later phase picks a value representation/conversion based on the incorrect flags.
4. Type confusion
The value is handled as the wrong representation, a memory-corruption primitive in the WebContent process (inferred escalation).
Impact Assessment
A DFG usage-flag miscompilation in the WebContent process that the advisory rates as memory corruption via type confusion. The shown change corrects the flag propagation; the concrete corrupting path downstream is not in this diff, so the demonstrated defect is an incorrect optimization input rather than a fully-shown write primitive, though the class (JSC type confusion) is high-value.
Audit Directions
Unconditional usage-flag merges
Audit DFGBackwardsPropagationPhase for other nodes that merge NodeBytecodeUsesAsOther/AsNumber unconditionally regardless of the operand’s use kind / possible types.
Flag-driven representation
Trace consumers of NodeBytecodeUsesAsOther to confirm they tolerate (or the producers correctly gate) the flag on SpecOther.
Changed Functions
| Function | File | Change | Note |
|---|---|---|---|
BackwardsPropagationPhase (ToString / CallStringConstructor) |
Source/JavaScriptCore/dfg/DFGBackwardsPropagationPhase.cpp |
modified | Merges NodeBytecodeUsesAsOther only when typeFilterFor(child1 useKind) & SpecOther, instead of unconditionally, so the usage flag matches the operand’s possible types. |
Files Changed
Source Files
Source/JavaScriptCore/dfg/DFGBackwardsPropagationPhase.cpp
Test Files
JSTests/stress/string-add-conversion-unused.js
Regression Test
The fix ships this proof-of-concept / regression test:
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++;
+}