CVE-2026-65338

Overview

Advisory: Apple Security Advisory

Impact:

Description: Processing maliciously crafted web content may lead to an unexpected Safari crash

Researchers: OpenAI Codex Security - Amy Burnett

Attribute Value
CVE CVE-2026-65338
Bugzilla 318348
Component JSC
Bug Class LogicError
Severity medium
Commit 26aa84fcd527016d…
Advisory Apple Advisory

Root Cause Analysis

The issue was addressed with improved memory handling.

Files Changed

Source Files

  • Source/JavaScriptCore/dfg/DFGArgumentsEliminationPhase.cpp
  • Source/JavaScriptCore/dfg/DFGCombinedLiveness.cpp
  • Source/JavaScriptCore/dfg/DFGCombinedLiveness.h
  • Source/JavaScriptCore/dfg/DFGForAllKills.h
  • Source/JavaScriptCore/dfg/DFGOSRAvailabilityAnalysisPhase.cpp

Test Files

  • JSTests/stress/arguments-elimination-inlined-load-varargs-preserves-recoveries.js
  • JSTests/stress/arguments-elimination-load-varargs-kills-promoted-recoveries.js

Patch Preview

diff --git a/JSTests/stress/arguments-elimination-inlined-load-varargs-preserves-recoveries.js b/JSTests/stress/arguments-elimination-inlined-load-varargs-preserves-recoveries.js
new file mode 100644
index 000000000000..6a9e21ffb290
--- /dev/null
+++ b/JSTests/stress/arguments-elimination-inlined-load-varargs-preserves-recoveries.js
@@ -0,0 +1,84 @@
+//@ runDefault("--thresholdForJITAfterWarmUp=10", "--thresholdForFTLOptimizeAfterWarmUp=1000", "--useConcurrentJIT=false", "--validateFTLOSRExitLiveness=true")
+
+"use strict";
+
+function shouldBe(actual, expected)
+{
+    if (actual !== expected)
+        throw new Error("bad value: " + actual + ", expected: " + expected);
+}
+
+function five(values1, values2)
+{
+    let result = null;
+    for (let i = 0; i < 5; ++i) {
+        function arg() { "use strict"; return arguments; }
+        const a = arg.apply(undefined, values1);
+        const b = arg.apply(undefined, values2);
+        try {
+            (3881)(b);
+        } catch (error) {
+            a.toString();
+            result = a;
+        }
+    }
+    return result;
+}
+
+function eight(values1, values2)
+{
+    let result = null;
+    for (let i = 0; i < 5; ++i) {
+        function arg() { "use strict"; return arguments; }
+        const a = arg.apply(undefined, values1);
+        const b = arg.apply(undefined, values2);
+        try {
+            (3881)(b);
+        } catch (error) {
+            a.toString();
+            result = a;
+        }
+    }
+    return result;
+}
+
+function filled(length, value)
+{
+    const result = [];
+    for (let i = 0; i < length; ++i)
+        result.push(value);
+    return result;
+}
+
+const fiveMarker = { marker: "five" };
+const eightMarker = { marker: "eight" };
+const seedArray = [{ marker: "seed" }, 1, 2, 3, 4, 5];
+
+const firstFive = filled(5, fiveMarker);
+const overwriteFive = filled(30, fiveMarker);
+overwriteFive[22] = 9;
+
+const firstEight = filled(8, eightMarker);
+const overwriteEight = filled(30, eightMarker);
+overwriteEight[20] = 9;
+
+for (let i = 0; i < testLoopCount; ++i) {
+    five(firstFive, overwriteFive);
+    eight(firstEight, overwriteEight);
+}
+
+const seedValues = filled(30, seedArray);
+seedValues[20] = 9;
+for (let i = 0; i < testLoopCount; ++i)
+    eight(firstEight, seedValues);
+
+const recoveredEight = eight(firstEight, seedValues);
+shouldBe(recoveredEight.length, firstEight.length);
+for (let i = 0; i < firstEight.length; ++i)
+    shouldBe(recoveredEight[i], eightMarker);
+
+const recoveredFive = five(firstFive, overwriteFive);
+shouldBe(recoveredFive.length, firstFive.length);
+for (let i = 0; i < firstFive.length; ++i)
+    shouldBe(recoveredFive[i], fiveMarker);
+shouldBe(recoveredFive[5], undefined);
diff --git a/JSTests/stress/arguments-elimination-load-varargs-kills-promoted-recoveries.js b/JSTests/stress/arguments-elimination-load-varargs-kills-promoted-recoveries.js
new file mode 100644
index 000000000000..aa818cb6d313
--- /dev/null
+++ b/JSTests/stress/arguments-elimination-load-varargs-kills-promoted-recoveries.js
@@ -0,0 +1,88 @@
+//@ runDefault("--thresholdForJITAfterWarmUp=10", "--thresholdForFTLOptimizeAfterWarmUp=1000", "--useConcurrentJIT=false")
+
+"use strict";
+
+function shouldBe(actual, expected)
+{
+    if (actual !== expected)
+        throw new Error("bad value: " + actual + ", expected: " + expected);
+}
+noInline(shouldBe);
+
+function five(values1, values2)
+{
+    let result = null;
+    for (let i = 0; i < 5; ++i) {
+        function arg() { "use strict"; return arguments; }
+        const a = arg.apply(undefined, values1);
+        const b = arg.apply(undefined, values2);
+        try {
+            (3881)(b);
+        } catch (error) {
+            a.toString();
+            result = a;
+        }
+    }
+    return result;
+}
+noInline(five);
+
+function eight(values1, values2)
+{
+    let result = null;
+    for (let i = 0; i < 5; ++i) {
+        function arg() { "use strict"; return arguments; }
+        const a = arg.apply(undefined, values1);
+        const b = arg.apply(undefined, values2);
+        try {
+            (3881)(b);
+        } catch (error) {
+            a.toString();
+            result = a;
+        }
+    }
+    return result;
+}
+noInline(eight);
+
+function filled(length, value)
+{
+    const result = [];
+    for (let i = 0; i < length; ++i)
+        result.push(value);
+    return result;
+}
+noInline(filled);
+
+const fiveMarker = { marker: "five" };
+const eightMarker = { marker: "eight" };
+const seedArray = [{ marker: "seed" }, 1, 2, 3, 4, 5];
+
+const firstFive = filled(5, fiveMarker);
+const overwriteFive = filled(30, fiveMarker);
+overwriteFive[22] = 9;
+
+const firstEight = filled(8, eightMarker);
+const overwriteEight = filled(30, eightMarker);
+overwriteEight[20] = 9;
+
+for (let i = 0; i < testLoopCount; ++i) {
+    five(firstFive, overwriteFive);
+    eight(firstEight, overwriteEight);
+}
+
+const seedValues = filled(30, seedArray);
+seedValues[20] = 9;
+for (let i = 0; i < testLoopCount; ++i)
+    eight(firstEight, seedValues);
+
+const recoveredEight = eight(firstEight, seedValues);
+shouldBe(recoveredEight.length, firstEight.length);
+for (let i = 0; i < firstEight.length; ++i)
+    shouldBe(recoveredEight[i], eightMarker);
+
+const recoveredFive = five(firstFive, overwriteFive);
+shouldBe(recoveredFive.length, firstFive.length);
+for (let i = 0; i < firstFive.length; ++i)
+    shouldBe(recoveredFive[i], fiveMarker);
+shouldBe(recoveredFive[5], undefined);
diff --git a/Source/JavaScriptCore/dfg/DFGArgumentsEliminationPhase.cpp b/Source/JavaScriptCore/dfg/DFGArgumentsEliminationPhase.cpp
index b90a0b9649cf..52567cea948e 100644
--- a/Source/JavaScriptCore/dfg/DFGArgumentsEliminationPhase.cpp
+++ b/Source/JavaScriptCore/dfg/DFGArgumentsEliminationPhase.cpp
@@ -33,6 +33,7 @@
 #include "DFGArgumentsUtilities.h"
 #include <wtf/IndexMap.h>
 #include "DFGClobberize.h"
+#include "DFGCombinedLiveness.h"
 #include "DFGForAllKills.h"
 #include "DFGGraph.h"
 #include "DFGInsertionSet.h"
@@ -722,7 +723,16 @@ class ArgumentsEliminationPhase : public Phase {
             }
 
             if (clobberStack) {