Medium webkit Type Confusion 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactProcessing maliciously crafted web content may lead to an unexpected Safari crash
DescriptionThe issue was addressed with improved memory handling.
ComponentJSC
Bug ClassType Confusion
Tracker318348
Fix commit6c004fb89bd7 (webkit) +196/-38
CISA KEVNot listed
CreditedOpenAI Codex Security - Amy Burnett
Disclosed2026-09-14

Background

DFG arguments elimination
DFGArgumentsEliminationPhase (Source/JavaScriptCore/dfg/DFGArgumentsEliminationPhase.cpp) removes allocations of arguments objects when the optimizer can prove they never escape, leaving only the data needed to recover (materialize) the object if an OSR exit later needs it.
OSR exit / recovery
When speculated types fail, execution bails from optimized code back to the baseline/LLInt; any eliminated object must be rebuilt from values the compiler kept “OSR-live” at that point.
Catch entrypoint / exceptional exit
a try/catch catch handler is reachable by an exceptional edge that the DFG does not model as a normal CFG successor. A value can therefore be OSR-live at a block’s terminal (because the catch block will read it) yet absent from liveAtTail (which only sees CFG successors).
`removeViaKill` / `clobberStack`
when a node clobbers stack slots, the phase must mark every value whose source slots are clobbered so the recovery data is preserved rather than being read back from overwritten slots.

Root Cause Analysis

The invariant is that before a block clobbers stack slots, arguments-elimination must account for every candidate that is still live-out of the block, so a clobber of a candidate’s source slots is noticed and its recovery kept intact. The buggy code iterated only combinedLiveness.liveAtTail[block]. Because liveAtTail is built purely from CFG successors’ liveAtHead, a candidate kept alive solely by an exceptional exit to a catch entrypoint (a non-CFG successor) is missing from that set — yet it is genuinely OSR-live at the terminal. The phase therefore failed to register the clobber of such a candidate’s source slots. The patch introduces bytecodeLivenessAtTerminal(graph, block) (factored out of the no-successor case in CombinedLiveness) and, in the clobberStack branch, computes possiblyLiveOut = bytecodeLivenessAtTerminal(...) unioned with liveAtTail, iterating that superset through removeViaKill. This closes the gap for exception-only-live candidates.

The companion change in DFGOSRAvailabilityAnalysisPhase::executeNode adds killHeaps(data->count) and killHeaps(data->start + i) for LoadVarargs/ForwardVarargs: when varargs are spread into locals, the availability of the heap/stack slots they overwrite is now invalidated, so stale availability records can no longer point recovery at slots that LoadVarargs has since overwritten. Together these mean that when an arguments object (eliminated candidate a) must be recovered at an OSR exit into a catch block, it is no longer recovered from stack slots that a subsequent LoadVarargs for a different arguments object (b) overwrote. Before the fix, the recovered a would contain b’s attacker-controlled spread values instead of its own — i.e. a materialized JS object populated with arbitrary attacker-chosen JSValue bits. The committed tests run under --validateFTLOSRExitLiveness=true and assert the recovered array equals its original markers, confirming that unpatched builds recover wrong values.

The advisory calls this an “unexpected Safari crash,” but the diff shows a liveness/availability correctness error whose sink is OSR recovery producing JSValues from clobbered stack slots — a type-safety hazard, not merely a crash.

Key insight
liveAtTail only reflects CFG successors, so a candidate kept alive only by an exceptional exit to a catch entrypoint was invisible to arguments-elimination’s clobber accounting, letting a later LoadVarargs overwrite the slots from which the eliminated object was recovered.

Attack Path

  1. Shape the IR Attacker JS builds a function where a strict arguments object (a) is created, a second varargs call (b = arg.apply(...)) spills into overlapping locals, and both live inside a try whose catch reads a (as in the committed five/eight tests).
  2. Tier up Warm the function so it is DFG/FTL-compiled; arguments elimination removes a’s allocation and records recovery from its source stack slots.
  3. Force the exceptional edge Call an invalid callee ((3881)(b)) to throw, triggering OSR exit into the catch block — the non-CFG edge that liveAtTail ignored.
  4. Clobber then recover The intervening LoadVarargs for b has overwritten a’s source slots (availability not killed; clobber not registered), so a is materialized from b’s spread values.
  5. Primitive The recovered arguments object exposes attacker-controlled JSValue bits as object contents; crafting those bits yields a fake-object / type-confusion primitive (escalation to addrof/fakeobj inferred, not proven by the diff).

Impact Assessment

Reachable from unprivileged web content in the renderer (JSC). The diff substantiates that an eliminated arguments object can be recovered at a catch-block OSR exit from stack slots overwritten by a different varargs spread, so the materialized object contains attacker-influenced JSValues. That is a type-confusion / fake-JSValue primitive materially stronger than the advisory’s “crash” label. Full exploitation (controlled addrof/fakeobj, arbitrary R/W) is a plausible but inferred escalation; the diff proves the incorrect recovery, not the end-to-end primitive. Preconditions: JIT tier-up and an exceptional exit into a catch that reads the eliminated object.

Changed Functions

FunctionChangeNotes
if
Source/JavaScriptCore/dfg/DFGArgumentsEliminationPhase.cpp
modified
if
Source/JavaScriptCore/dfg/DFGForAllKills.h
modified
for
Source/JavaScriptCore/dfg/DFGOSRAvailabilityAnalysisPhase.cpp
modified
for
JSTests/stress/arguments-elimination-inlined-load-varargs-preserves-recoveries.js
modified
for
JSTests/stress/arguments-elimination-load-varargs-kills-promoted-recoveries.js
modified

Files Changed

  • JSTests/stress/arguments-elimination-inlined-load-varargs-preserves-recoveries.js
  • JSTests/stress/arguments-elimination-load-varargs-kills-promoted-recoveries.js
  • 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

Audit Directions

  • CFG-successor-only liveness
    Audit other DFG phases that read combinedLiveness.liveAtTail[block] directly (e.g. object allocation sinking, other elimination phases) for the same blind spot toward exception/catch-entrypoint liveness.
  • Availability vs. slot-overwriting nodes
    Review every node in LocalOSRAvailabilityCalculator::executeNode that writes locals (varargs, PutStack, spread/apply paths) to confirm a matching killHeaps invalidation.
  • Catch-entrypoint recovery
    Look at all eliminated/sunk-object recovery that can be forced through a try/catch OSR exit, validating under --validateFTLOSRExitLiveness=true.
diff --git a/Source/JavaScriptCore/dfg/DFGArgumentsEliminationPhase.cpp b/Source/JavaScriptCore/dfg/DFGArgumentsEliminationPhase.cpp
index 8c15c56bca82..fc5c7f42d320 100644
--- a/Source/JavaScriptCore/dfg/DFGArgumentsEliminationPhase.cpp
+++ b/Source/JavaScriptCore/dfg/DFGArgumentsEliminationPhase.cpp
@@ -33,6 +33,7 @@
 #include "DFGArgumentsUtilities.h"
 #include "DFGBlockMapInlines.h"
 #include "DFGClobberize.h"
+#include "DFGCombinedLiveness.h"
 #include "DFGForAllKills.h"
 #include "DFGGraph.h"
 #include "DFGInsertionSet.h"
@@ -722,7 +723,16 @@ private:
             }
 
             if (clobberStack) {
-                for (Node* node : combinedLiveness.liveAtTail[block])
+                // liveAtTail is the union of the CFG successors' liveAtHead, but a candidate can be kept
+                // alive solely by an exceptional exit to a catch entrypoint, which the DFG models as a
+                // non-CFG successor. Such a candidate is OSR-live at the terminal yet absent from
+                // liveAtTail, so a clobber of its source slots in this block would otherwise go
+                // unnoticed. Cover that gap with the nodes live at the terminal but dead on the tail.
+                // FIXME: If this is ever too conservative we can just calculate the locals used by
+                // the catch block for the terminal.
+                NodeSet possiblyLiveOut = bytecodeLivenessAtTerminal(m_graph, block);
+                possiblyLiveOut.addAll(combinedLiveness.liveAtTail[block]);
+                for (Node* node : possiblyLiveOut)
                     removeViaKill(block, block->size(), node);
 
                 for (unsigned nodeIndex = 0; nodeIndex < block->size(); ++nodeIndex) {
diff --git a/Source/JavaScriptCore/dfg/DFGCombinedLiveness.cpp b/Source/JavaScriptCore/dfg/DFGCombinedLiveness.cpp
index f0ec68bb1fbb..f74caec9339d 100644
--- a/Source/JavaScriptCore/dfg/DFGCombinedLiveness.cpp
+++ b/Source/JavaScriptCore/dfg/DFGCombinedLiveness.cpp
@@ -62,6 +62,13 @@ NodeSet liveNodesAtHead(Graph& graph, BasicBlock* block)
     return seen;
 }
 
+NodeSet bytecodeLivenessAtTerminal(Graph& graph, BasicBlock* block)
+{
+    NodeSet seen;
+    addBytecodeLiveness(graph, block->ssa->availabilityAtTail, seen, block->last());
+    return seen;
+}
+
 CombinedLiveness::CombinedLiveness(Graph& graph)
     : liveAtHead(graph)
     , liveAtTail(graph)
@@ -81,11 +88,8 @@ CombinedLiveness::CombinedLiveness(Graph& graph)
         // Unreachable
         //
         // And things may definitely be live in bytecode at that point in the program.
-        if (!block->numSuccessors()) {
-            NodeSet seen;
-            addBytecodeLiveness(graph, block->ssa->availabilityAtTail, seen, block->last());
-            liveAtTail[block] = seen;
-        }
+        if (!block->numSuccessors())
+            liveAtTail[block] = bytecodeLivenessAtTerminal(graph, block);
     }
     
     // Now compute the liveAtTail by unifying the liveAtHead of the successors.
diff --git a/Source/JavaScriptCore/dfg/DFGCombinedLiveness.h b/Source/JavaScriptCore/dfg/DFGCombinedLiveness.h
index 6172650be220..90b995892493 100644
--- a/Source/JavaScriptCore/dfg/DFGCombinedLiveness.h
+++ b/Source/JavaScriptCore/dfg/DFGCombinedLiveness.h
@@ -35,6 +35,8 @@ namespace JSC { namespace DFG {
 // Returns the set of nodes live at head, both due to DFG and due to bytecode (i.e. OSR exit).
 NodeSet liveNodesAtHead(Graph&, BasicBlock*);
 
+NodeSet bytecodeLivenessAtTerminal(Graph&, BasicBlock*);
+
 // WARNING: This currently does not reason about the liveness of shadow values. The execution
 // semantics of DFG SSA are that an Upsilon stores to the shadow value of a Phi, and the Phi loads
 // from that shadow value. Hence, the shadow values are like variables, and have liveness. The normal
diff --git a/Source/JavaScriptCore/dfg/DFGForAllKills.h b/Source/JavaScriptCore/dfg/DFGForAllKills.h
index 107cad824b56..295dccd00d55 100644
--- a/Source/JavaScriptCore/dfg/DFGForAllKills.h
+++ b/Source/JavaScriptCore/dfg/DFGForAllKills.h
@@ -34,10 +34,6 @@
 
 namespace JSC { namespace DFG {
 
-namespace ForAllKillsInternal {
-constexpr bool verbose = false;
-}
-
 // Utilities for finding the last points where a node is live in DFG SSA. This accounts for liveness due
 // to OSR exit. This is usually used for enumerating over all of the program points where a node is live,
 // by exploring all blocks where the node is live at tail and then exploring all program points where the
@@ -170,34 +166,6 @@ void forAllKilledNodesAtNodeIndex(
         });
 }
 
-// Tells you all of the places to start searching from in a basic block. Gives you the node index at which
-// the value is either no longer live. This pretends that nodes are dead at the end of the block, so that
-// you can use this to do per-basic-block analyses.
-template<typename Functor>
-void forAllKillsInBlock(
-    Graph& graph, const CombinedLiveness& combinedLiveness, BasicBlock* block,
-    const Functor& functor)
-{
-    for (Node* node : combinedLiveness.liveAtTail[block])
-        functor(block->size(), node);
-    
-    LocalOSRAvailabilityCalculator localAvailability(graph);
-    localAvailability.beginBlock(block);
-    // Start running functor at the second node, because the functor is expected to only inspect nodes from the start of
-    // the block up to nodeIndex (exclusive), so if nodeIndex is zero then the functor has nothing to do.
-    for (unsigned nodeIndex = 0; nodeIndex < block->size(); ++nodeIndex) {
-        dataLogLnIf(ForAllKillsInternal::verbose, "local availability at index: ", nodeIndex, " ", localAvailability.m_availability);
-        if (nodeIndex) {
-            forAllKilledNodesAtNodeIndex(
-                graph, localAvailability.m_availability, block, nodeIndex,
-                [&] (Node* node) {
-                    functor(nodeIndex, node);
-                });
-        }
-        localAvailability.executeNode(block->at(nodeIndex));
-    }
-}
-
 } } // namespace JSC::DFG
 
 #endif // ENABLE(DFG_JIT)
diff --git a/Source/JavaScriptCore/dfg/DFGOSRAvailabilityAnalysisPhase.cpp b/Source/JavaScriptCore/dfg/DFGOSRAvailabilityAnalysisPhase.cpp
index 99a63f2af878..57c19824d391 100644
--- a/Source/JavaScriptCore/dfg/DFGOSRAvailabilityAnalysisPhase.cpp
+++ b/Source/JavaScriptCore/dfg/DFGOSRAvailabilityAnalysisPhase.cpp
@@ -359,8 +359,10 @@ void LocalOSRAvailabilityCalculator::executeNode(Node* node)
     case LoadVarargs:
     case ForwardVarargs: {
         LoadVarargsData* data = node->loadVarargsData();
+        killHeaps(data->count);
         m_availability.m_locals.operand(data->count) = Availability(node->child1().node(), FlushedAt(FlushedInt32, data->machineCount));
         for (unsigned i = data->limit; i--;) {
+            killHeaps(data->start + i);
             m_availability.m_locals.operand(data->start + i) =
                 Availability(FlushedAt(FlushedJSValue, data->machineStart.isValid() ? (data->machineStart + i) : VirtualRegister()));
         }
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);
Loading diff…

Regression Test / PoC

shipped with the fix
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);
Loading diff…