Medium CVSS 6.5 webkit UAF 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected Safari crash
ComponentJSC
Sub-componentJSC DFG
Bug ClassUAF
Tracker298606
Fix commitf014a3289076 (WebKit/WebKit) +1083/-450
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H
CISA KEVNot listed
CreditedGary Kwong, Hossein Lotfi (@hosselot) of Trend Micro Zero Day Initiative
Disclosed2025-11-03

Background

Butterfly
the out-of-line storage block that holds a JavaScript array’s indexed elements and its Butterfly_publicLength.
Object allocation sinking
a DFG optimization that delays or elides an allocation and reconstructs it at an OSR exit via Materialize* and Phantom* nodes.
Abstract Interpreter (AI)
the DFG’s abstract-value engine that proves per-node types and structures, feeding constant folding and speculation removal.
Indexing type
the tag (ALL_INT32_INDEXING_TYPES, ALL_DOUBLE_INDEXING_TYPES, ALL_CONTIGUOUS_INDEXING_TYPES) that dictates how a butterfly’s raw element cells are physically interpreted.

Root Cause Analysis

The old NewArrayWithConstantSize node conflated two distinct heap operations — allocating a butterfly and allocating the wrapping JS array object — into a single NodeResultJS node whose only size record was a frozen constant (m_opInfo2), and whose clobberize def published ArrayLengthLoc/Butterfly_publicLength as that frozen constant. When object allocation sinking sank such an array and rematerialized it at an OSR exit through MaterializeNewArrayWithConstantSize/PhantomNewArrayWithConstantSize, the AI never verified that the values used to repopulate the materialized butterfly actually matched the array’s indexingType, so an ALL_DOUBLE_INDEXING_TYPES butterfly could be filled with a non-number JSValue (or a boxed value written where raw doubles are expected), producing a type-confused butterfly whose elements are reinterpreted as raw doubles or pointers.

The fix splits the operation into NewButterflyWithSize (NodeResultStorage) and NewArrayWithButterfly (NodeResultJS) so the butterfly is a first-class SSA value the sinking phase can track independently, and it adds a RELEASE_ASSERT(isSubtypeSpeculation(...)) in the AI’s MaterializeNewButterflyWithSize case that requires every materialization child to be a subtype of the validTypes implied by the indexingType. It also stops publishing a frozen-constant length in clobberize, instead defing Butterfly_publicLength from node->child1().node(), keeping the length definition faithful to the real size input. Together these changes make the storage allocation, its length, and its element-type contract explicit and checkable, closing the window in which a sunk/materialized array could be rebuilt with mismatched element representations.

Key insight
The single core mistake was modeling a butterfly-backed array as one opaque node whose sunk-then-materialized reconstruction was never validated against its indexingType, allowing element-representation confusion; the fix promotes the butterfly to its own NodeResultStorage node and enforces isSubtypeSpeculation on every materialization operand.

Attack Path

  1. Create a small constant-size typed array Craft JS that triggers convertToNewArrayWithConstantSize (now convertToNewArrayWithButterfly) for a double- or contiguous-indexed array below MIN_ARRAY_STORAGE_CONSTRUCTION_LENGTH.
  2. Force allocation sinking Structure control flow so the DFG sinks the array allocation and represents it as a Phantom/Materialize pair rather than a live allocation.
  3. Poison the materialization operands Arrange the sunk state so the values slotted into the butterfly do not match the array’s indexingType, exploiting the absent element-type check.
  4. Trigger OSR exit Cause a speculation failure so the array is rematerialized from the mismatched operands, yielding a type-confused butterfly.
  5. Corrupt memory Read or write the confused elements to reinterpret doubles as pointers (or vice versa), gaining out-of-bounds and use-after-free primitives.

Impact Assessment

An attacker who lands this in the DFG JIT gains a type-confusion primitive over a butterfly, escalating to out-of-bounds read/write and use-after-free of heap-allocated objects inside the Safari/WebContent renderer process. Exploitation requires only that the victim process JIT-compile attacker-controlled JavaScript that drives an array through allocation sinking and an OSR exit — no special privileges, purely from web content. The result is renderer memory corruption suitable for a sandbox-constrained code-execution foothold.

Files Changed

  • JSTests/stress/array-allocation-elimination-closure-capture.js
  • JSTests/stress/array-allocation-elimination-conditional-usage.js
  • JSTests/stress/array-allocation-elimination-cross-function.js
  • Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h
  • Source/JavaScriptCore/dfg/DFGBackwardsPropagationPhase.cpp
  • Source/JavaScriptCore/dfg/DFGClobberize.h
  • Source/JavaScriptCore/dfg/DFGCloneHelper.h
  • Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp
  • Source/JavaScriptCore/dfg/DFGDoesGC.cpp
  • Source/JavaScriptCore/dfg/DFGFixupPhase.cpp
  • Source/JavaScriptCore/dfg/DFGLoopUnrollingPhase.cpp
  • Source/JavaScriptCore/dfg/DFGMayExit.cpp
  • Source/JavaScriptCore/dfg/DFGNode.cpp
  • Source/JavaScriptCore/dfg/DFGNode.h
  • Source/JavaScriptCore/dfg/DFGNodeType.h
  • Source/JavaScriptCore/dfg/DFGObjectAllocationSinkingPhase.cpp
  • Source/JavaScriptCore/dfg/DFGObjectMaterializationData.h
  • Source/JavaScriptCore/dfg/DFGOperations.cpp
  • Source/JavaScriptCore/dfg/DFGOperations.h
  • Source/JavaScriptCore/dfg/DFGPredictionPropagationPhase.cpp
  • Source/JavaScriptCore/dfg/DFGPromotedHeapLocation.h
  • Source/JavaScriptCore/dfg/DFGSafeToExecute.h
  • Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
  • Source/JavaScriptCore/dfg/DFGSpeculativeJIT.h
  • Source/JavaScriptCore/dfg/DFGSpeculativeJIT32_64.cpp
  • Source/JavaScriptCore/dfg/DFGSpeculativeJIT64.cpp
  • Source/JavaScriptCore/dfg/DFGStoreBarrierInsertionPhase.cpp
  • Source/JavaScriptCore/dfg/DFGUseKind.h
  • Source/JavaScriptCore/dfg/DFGValidate.cpp
  • Source/JavaScriptCore/ftl/FTLCapabilities.cpp
  • Source/JavaScriptCore/ftl/FTLExitTimeObjectMaterialization.cpp
  • Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
  • Source/JavaScriptCore/ftl/FTLOperations.cpp
  • Source/JavaScriptCore/runtime/IndexingType.cpp
  • Source/JavaScriptCore/runtime/IndexingType.h

Audit Directions

  • Conflated allocation nodes
    Audit any DFG node that bundles storage and object allocation in one op; ensure the butterfly is a distinct SSA value so sinking and materialization cannot desynchronize.
  • Unvalidated materialization operands
    For every Materialize* case in the AI, confirm operand SpeculatedTypes are asserted against the target’s indexingType/structure before the butterfly or object is reconstructed.
  • Frozen-constant heap `def`s
    Review clobberize defs that publish Butterfly_publicLength or element locations as frozen constants; verify the definition tracks the real dynamic input rather than a compile-time value that a later phase could rely on unsafely.
diff --git a/Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h b/Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h
index 751179787d0c..19f700a8773f 100644
--- a/Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h
+++ b/Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h
@@ -3725,10 +3725,30 @@ bool AbstractInterpreter<AbstractStateType>::executeEffects(unsigned clobberLimi
         break;
     }
 
-    case NewArrayWithConstantSize:
-    case MaterializeNewArrayWithConstantSize:
+    case MaterializeNewButterflyWithSize: {
+        SpeculatedType validTypes = [&]() {
+            switch (node->indexingType()) {
+            case ALL_INT32_INDEXING_TYPES: return SpecInt32Only;
+            case ALL_DOUBLE_INDEXING_TYPES: return SpecBytecodeNumber;
+            case ALL_CONTIGUOUS_INDEXING_TYPES: return SpecBytecodeTop;
+            default: break;
+            }
+            RELEASE_ASSERT_NOT_REACHED();
+        }();
+        for (unsigned i = 1; i < node->numChildren(); ++i)
+            RELEASE_ASSERT(isSubtypeSpeculation(forNode(m_graph.varArgChild(node, i)).m_type, validTypes));
+
+        [[fallthrough]];
+    }
+    case NewButterflyWithSize:
+        // We don't represent storage/butterflies in AI.
+        clearForNode(node);
+        break;
+
+    case NewArrayWithButterfly: {
         setForNode(node, m_graph.globalObjectFor(node->origin.semantic)->arrayStructureForIndexingTypeDuringAllocation(node->indexingMode()));
         break;
+    }
 
     case NewTypedArray: {
         switch (node->child1().useKind()) {
@@ -3990,7 +4010,8 @@ bool AbstractInterpreter<AbstractStateType>::executeEffects(unsigned clobberLimi
     }
 
     case PhantomNewObject:
-    case PhantomNewArrayWithConstantSize:
+    case PhantomNewButterflyWithSize:
+    case PhantomNewArrayWithButterfly:
     case PhantomNewFunction:
     case PhantomNewGeneratorFunction:
     case PhantomNewAsyncGeneratorFunction:
diff --git a/Source/JavaScriptCore/dfg/DFGBackwardsPropagationPhase.cpp b/Source/JavaScriptCore/dfg/DFGBackwardsPropagationPhase.cpp
index 87bca15efbd3..21b451f8d2cc 100644
--- a/Source/JavaScriptCore/dfg/DFGBackwardsPropagationPhase.cpp
+++ b/Source/JavaScriptCore/dfg/DFGBackwardsPropagationPhase.cpp
@@ -525,7 +525,6 @@ private:
         case NewTypedArray:
         case NewTypedArrayBuffer:
         case NewArrayWithSize:
-        case NewArrayWithConstantSize:
         case NewArrayWithSpecies:
         case NewArrayWithSizeAndStructure: {
             // Negative zero is not observable. NaN versus undefined are only observable
diff --git a/Source/JavaScriptCore/dfg/DFGClobberize.h b/Source/JavaScriptCore/dfg/DFGClobberize.h
index 61ffe85f1df6..a6f985602b29 100644
--- a/Source/JavaScriptCore/dfg/DFGClobberize.h
+++ b/Source/JavaScriptCore/dfg/DFGClobberize.h
@@ -1924,12 +1924,20 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
         write(HeapObjectCount);
         return;
 
-    case NewArrayWithConstantSize:
-    case PhantomNewArrayWithConstantSize:
-    case MaterializeNewArrayWithConstantSize:
+    case NewButterflyWithSize:
+    case PhantomNewArrayWithButterfly:
+    case PhantomNewButterflyWithSize:
+    case MaterializeNewButterflyWithSize: {
         read(HeapObjectCount);
         write(HeapObjectCount);
-        def(HeapLocation(ArrayLengthLoc, Butterfly_publicLength, node), LazyNode(graph.freeze(jsNumber(node->newArraySize()))));
+        // FIXME: In this phase we say the Array is where the length of the array is def'd but this differs from ObjectAllocationSinking.
+        return;
+    }
+
+    case NewArrayWithButterfly:
+        read(HeapObjectCount);
+        write(HeapObjectCount);
+        def(HeapLocation(ArrayLengthLoc, Butterfly_publicLength, node), LazyNode(node->child1().node()));
         return;
 
     case NewTypedArray:
diff --git a/Source/JavaScriptCore/dfg/DFGCloneHelper.h b/Source/JavaScriptCore/dfg/DFGCloneHelper.h
index f2150b6cfc3f..2a55975a54f9 100644
--- a/Source/JavaScriptCore/dfg/DFGCloneHelper.h
+++ b/Source/JavaScriptCore/dfg/DFGCloneHelper.h
@@ -288,7 +288,7 @@ BasicBlock* CloneHelper::cloneBlock(BasicBlock* const block, const CustomizeSucc
     CLONE_STATUS(MultiPutByOffset, Common) \
     CLONE_STATUS(NewArray, Common) \
     CLONE_STATUS(NewArrayBuffer, Common) \
-    CLONE_STATUS(NewArrayWithConstantSize, Common) \
+    CLONE_STATUS(NewArrayWithButterfly, Common) \
     CLONE_STATUS(NewArrayWithSize, Common) \
     CLONE_STATUS(NewArrayWithSpread, Common) \
     CLONE_STATUS(NewFunction, Common) \
diff --git a/Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp b/Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp
index 28a3847a266b..97fb681b53a4 100644
--- a/Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp
+++ b/Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp
@@ -1106,10 +1106,14 @@ private:
                 if (m_graph.isWatchingHavingABadTimeWatchpoint(node)) {
                     if (node->child1().useKind() == Int32Use && node->child1()->isInt32Constant()) {
                         int32_t length = node->child1()->asInt32();
-                        if (length >= 0
+                        if (0 <= length
                             && length < MIN_ARRAY_STORAGE_CONSTRUCTION_LENGTH
-                            && isNewArrayWithConstantSizeIndexingType(node->indexingType())) {
-                                node->convertToNewArrayWithConstantSize(m_graph, length);
+                            && !hasAnyArrayStorage(node->indexingType())) {
+                                m_interpreter.execute(indexInBlock); // Push CFA over this node after we get the state before.
+                                alreadyHandled = true; // Don't allow the default constant folder to do things to this.
+
+                                Node* butterfly = m_insertionSet.insertNode(indexInBlock, SpecNone, NewButterflyWithSize, node->origin, OpInfo(node->indexingType()), node->child1());
+                                node->convertToNewArrayWithButterfly(m_graph, butterfly);
                                 changed = true;
                         }
                     }
@@ -1676,7 +1680,8 @@ private:
             }
 
             case PhantomNewObject:
-            case PhantomNewArrayWithConstantSize:
+            case PhantomNewArrayWithButterfly:
+            case PhantomNewButterflyWithSize:
             case PhantomNewFunction:
             case PhantomNewGeneratorFunction:
             case PhantomNewAsyncGeneratorFunction:
diff --git a/Source/JavaScriptCore/dfg/DFGDoesGC.cpp b/Source/JavaScriptCore/dfg/DFGDoesGC.cpp
index 8c617adc8a2f..47ec5e1b340d 100644
--- a/Source/JavaScriptCore/dfg/DFGDoesGC.cpp
+++ b/Source/JavaScriptCore/dfg/DFGDoesGC.cpp
@@ -225,7 +225,8 @@ bool doesGC(Graph& graph, Node* node)
     case CheckBadValue:
     case BottomValue:
     case PhantomNewObject:
-    case PhantomNewArrayWithConstantSize:
+    case PhantomNewArrayWithButterfly:
+    case PhantomNewButterflyWithSize:
     case PhantomNewFunction:
     case PhantomNewGeneratorFunction:
     case PhantomNewAsyncFunction:
@@ -413,8 +414,9 @@ bool doesGC(Graph& graph, Node* node)
     case NewArrayWithSpread:
     case NewInternalFieldObject:
     case Spread:
+    case NewButterflyWithSize:
     case NewArrayWithSize:
-    case NewArrayWithConstantSize:
+    case NewArrayWithButterfly:
     case NewArrayWithSpecies:
     case NewArrayWithSizeAndStructure:
     case NewArrayBuffer:
@@ -440,7 +442,7 @@ bool doesGC(Graph& graph, Node* node)
     case EnumeratorNextUpdatePropertyName:
     case EnumeratorNextUpdateIndexAndMode:
     case MaterializeNewObject:
-    case MaterializeNewArrayWithConstantSize:
+    case MaterializeNewButterflyWithSize:
     case MaterializeNewInternalFieldObject:
     case MaterializeCreateActivation:
     case SetFunctionName:
diff --git a/Source/JavaScriptCore/dfg/DFGFixupPhase.cpp b/Source/JavaScriptCore/dfg/DFGFixupPhase.cpp
index b471fd112937..a672363d2dc0 100644
--- a/Source/JavaScriptCore/dfg/DFGFixupPhase.cpp
+++ b/Source/JavaScriptCore/dfg/DFGFixupPhase.cpp
@@ -2064,11 +2064,6 @@ private:
             break;
         }
 
-        case NewArrayWithConstantSize: {
-            watchHavingABadTime(node);
-            break;
-        }
-
         case NewArrayWithSpecies: {
             ArrayMode arrayMode = node->arrayMode().refine(m_graph, node, node->child2()->prediction(), ArrayMode::unusedIndexSpeculatedType);
             node->setArrayMode(arrayMode);
@@ -2664,8 +2659,11 @@ private:
         case Int52Constant:
         case Identity: // This should have been cleaned up.
         case BooleanToNumber:
+        case NewArrayWithButterfly:
+        case NewButterflyWithSize:
         case PhantomNewObject:
-        case PhantomNewArrayWithConstantSize:
+        case PhantomNewButterflyWithSize:
+        case PhantomNewArrayWithButterfly:
         case PhantomNewFunction:
         case PhantomNewGeneratorFunction:
         case PhantomNewAsyncGeneratorFunction:
@@ -2687,7 +2685,7 @@ private:
         case CheckStructureOrEmpty:
         case CheckArrayOrEmpty:
         case MaterializeNewObject:
-        case MaterializeNewArrayWithConstantSize:
+        case MaterializeNewButterflyWithSize:
         case MaterializeCreateActivation:
         case MaterializeNewInternalFieldObject:
         case PutStack:
diff --git a/Source/JavaScriptCore/dfg/DFGLoopUnrollingPhase.cpp b/Source/JavaScriptCore/dfg/DFGLoopUnrollingPhase.cpp
index c34e4908c0d2..324fb26db1c8 100644
--- a/Source/JavaScriptCore/dfg/DFGLoopUnrollingPhase.cpp
+++ b/Source/JavaScriptCore/dfg/DFGLoopUnrollingPhase.cpp
@@ -863,7 +863,8 @@ bool LoopUnrollingPhase::LoopData::isMaterialNode(Graph& graph, Node* node)
     case ZombieHint:
     case ExitOK:
     case PhantomNewObject:
-    case PhantomNewArrayWithConstantSize:
+    case PhantomNewArrayWithButterfly:
+    case PhantomNewButterflyWithSize:
     case PhantomNewFunction:
     case PhantomNewGeneratorFunction:
     case PhantomNewAsyncGeneratorFunction:
diff --git a/Source/JavaScriptCore/dfg/DFGMayExit.cpp b/Source/JavaScriptCore/dfg/DFGMayExit.cpp
index 47f59267376f..f1a39bc8e1fd 100644
--- a/Source/JavaScriptCore/dfg/DFGMayExit.cpp
+++ b/Source/JavaScriptCore/dfg/DFGMayExit.cpp
@@ -71,8 +71,9 @@ ExitMode mayExitImpl(Graph& graph, Node* node, StateType& state)
     case BottomValue:
     case PutHint:
     case PhantomNewObject:
-    case PhantomNewArrayWithConstantSize:
+    case PhantomNewArrayWithButterfly:
     case PhantomNewInternalFieldObject:
+    case PhantomNewButterflyWithSize:
     case PutStack:
     case KillStack:
     case GetStack:
@@ -172,7 +173,7 @@ ExitMode mayExitImpl(Graph& graph, Node* node, StateType& state)
     case CreateActivation:
     case MaterializeCreateActivation:
     case MaterializeNewObject:
-    case MaterializeNewArrayWithConstantSize:
+    case MaterializeNewButterflyWithSize:
     case MaterializeNewInternalFieldObject:
     case NewFunction:
     case NewGeneratorFunction:
@@ -184,7 +185,8 @@ ExitMode mayExitImpl(Graph& graph, Node* node, StateType& state)
     case NewRegExp:
     case NewMap:
     case NewSet:
-    case NewArrayWithConstantSize:
+    case NewArrayWithButterfly:
+    case NewButterflyWithSize:
     case ToNumber:
     case ToNumeric:
     case ToObject:
diff --git a/Source/JavaScriptCore/dfg/DFGNode.cpp b/Source/JavaScriptCore/dfg/DFGNode.cpp
index da08861b5e5e..1cea7ccd0aea 100644
--- a/Source/JavaScriptCore/dfg/DFGNode.cpp
+++ b/Source/JavaScriptCore/dfg/DFGNode.cpp
@@ -268,12 +268,14 @@ void Node::convertToNewArrayWithSize()
     m_opInfo = indexingType;
 }
 
-void Node::convertToNewArrayWithConstantSize(Graph&, uint32_t size)
+void Node::convertToNewArrayWithButterfly(Graph&, Node* butterfly)
 {
     ASSERT(op() == NewArrayWithSize);
-    ASSERT(size < MIN_ARRAY_STORAGE_CONSTRUCTION_LENGTH);
-    setOpAndDefaultFlags(NewArrayWithConstantSize);
-    m_opInfo2 = size;
+    IndexingType indexingType = this->indexingType();
+    setOpAndDefaultFlags(NewArrayWithButterfly);
+    ASSERT(child1()->asInt32() < MIN_ARRAY_STORAGE_CONSTRUCTION_LENGTH);
+    children.child2() = Edge(butterfly);
+    ASSERT_UNUSED(indexingType, indexingType == this->indexingType());
 }
 
 void Node::convertToNewArrayWithSizeAndStructure(Graph& graph, RegisteredStructure structure)
diff --git a/Source/JavaScriptCore/dfg/DFGNode.h b/Source/JavaScriptCore/dfg/DFGNode.h
index abf85a660a2c..51c69a82eabc 100644
--- a/Source/JavaScriptCore/dfg/DFGNode.h
+++ b/Source/JavaScriptCore/dfg/DFGNode.h
@@ -727,17 +727,22 @@ public:
         children.setChild1(index);
     }
 
-    void convertToPhantomNewArrayWithConstantSize()
+    void convertToPhantomNewArrayWithButterfly()
     {
-        ASSERT(m_op == NewArrayWithConstantSize);
-        m_op = PhantomNewArrayWithConstantSize;
-        m_flags &= ~NodeHasVarArgs;
-        m_flags |= NodeMustGenerate;
-        // No need to clear the infos, as the indexing type and array
-        // size are still required for materialization when an OSR exit occurs.
-        children = AdjacencyList();
+        ASSERT(m_op == NewArrayWithButterfly);
+        setOpAndDefaultFlags(PhantomNewArrayWithButterfly);
+        // OpInfo/children shouldn't change and are needed for OSR exit.
+        ASSERT(child1().useKind() == Int32Use);
     }
     
+    void convertToPhantomNewButterflyWithSize()
+    {
+        ASSERT(m_op == NewButterflyWithSize);
+        setOpAndDefaultFlags(PhantomNewButterflyWithSize);
+        // OpInfo/children shouldn't change and are needed for OSR exit.
+        ASSERT(child1().useKind() == Int32Use);
+    }
+
     void convertToPhantomNewObject()
     {
         ASSERT(m_op == NewObject);
@@ -916,7 +921,7 @@ public:
 
     void convertToNewArrayBuffer(FrozenValue* immutableButterfly);
     void convertToNewArrayWithSize();
-    void convertToNewArrayWithConstantSize(Graph&, uint32_t);
+    void convertToNewArrayWithButterfly(Graph&, Node* butterfly);
     void convertToNewArrayWithSizeAndStructure(Graph&, RegisteredStructure);
 
     void convertToNewBoundFunction(FrozenValue*);
@@ -1454,32 +1459,16 @@ public:
         return newArrayBufferData().vectorLengthHint;
     }
 
-    unsigned hasNewArraySize()
-    {
-        switch (op()) {
-        case NewArrayWithConstantSize:
-        case PhantomNewArrayWithConstantSize:
-        case MaterializeNewArrayWithConstantSize:
-            return true;
-        default:
-            return false;
-        }
-    }
-
-    unsigned newArraySize()
-    {
-        ASSERT(hasNewArraySize());
-        return op() == MaterializeNewArrayWithConstantSize ? objectMaterializationData().m_newArraySize : m_opInfo2.as<unsigned>();
-    }
-
     bool hasIndexingType()
     {
         switch (op()) {
         case NewArray:
         case NewArrayWithSize:
-        case NewArrayWithConstantSize:
-        case PhantomNewArrayWithConstantSize:
-        case MaterializeNewArrayWithConstantSize:
+        case NewArrayWithButterfly:
+        case NewButterflyWithSize:
+        case PhantomNewArrayWithButterfly:
+        case PhantomNewButterflyWithSize:
+        case MaterializeNewButterflyWithSize:
         case NewArrayBuffer:
         case PhantomNewArrayBuffer:
         case NewArrayWithSpecies:
@@ -2514,7 +2503,7 @@ public:
     {
         switch (op()) {
         case MaterializeNewObject:
-        case MaterializeNewArrayWithConstantSize:
+        case MaterializeNewButterflyWithSize:
         case MaterializeNewInternalFieldObject:
         case MaterializeCreateActivation:
             return true;
@@ -2602,7 +2591,8 @@ public:
     bool isPhantomAllocation()
     {
         switch (op()) {
-        case PhantomNewArrayWithConstantSize:
+        case PhantomNewArrayWithButterfly:
+        case PhantomNewButterflyWithSize:
         case PhantomNewObject:
         case PhantomDirectArguments:
         case PhantomCreateRest:
diff --git a/Source/JavaScriptCore/dfg/DFGNodeType.h b/Source/JavaScriptCore/dfg/DFGNodeType.h
index 6bbcca98750c..8c7751814389 100644
--- a/Source/JavaScriptCore/dfg/DFGNodeType.h
+++ b/Source/JavaScriptCore/dfg/DFGNodeType.h
@@ -403,13 +403,15 @@ namespace JSC { namespace DFG {
     macro(NewObject, NodeResultJS) \
     macro(NewGenerator, NodeResultJS) \
     macro(NewAsyncGenerator, NodeResultJS) \
+    /* FIXME: A lot of these could likely be consolidated but there's some subtle differences between them, particularly when having a bad time. */ \
     macro(NewArray, NodeResultJS | NodeHasVarArgs) \
     macro(NewArrayWithSpread, NodeResultJS | NodeHasVarArgs) \
     macro(NewArrayWithSpecies, NodeResultJS | NodeMustGenerate) \
     macro(NewArrayWithSize, NodeResultJS | NodeMustGenerate) \
-    macro(NewArrayWithConstantSize, NodeResultJS | NodeMustGenerate) \
     macro(NewArrayWithSizeAndStructure, NodeResultJS | NodeMustGenerate) \
     macro(NewArrayBuffer, NodeResultJS) \
+    macro(NewArrayWithButterfly, NodeResultJS) \
+    macro(NewButterflyWithSize, NodeResultStorage) \
     macro(NewInternalFieldObject, NodeResultJS) \
     macro(NewTypedArray, NodeResultJS | NodeMustGenerate) \
     macro(NewTypedArrayBuffer, NodeResultJS | NodeMustGenerate) \
@@ -425,8 +427,9 @@ namespace JSC { namespace DFG {
     \
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/JSTests/stress/array-allocation-elimination-closure-capture.js b/JSTests/stress/array-allocation-elimination-closure-capture.js
new file mode 100644
index 000000000000..c8235d83e04a
--- /dev/null
+++ b/JSTests/stress/array-allocation-elimination-closure-capture.js
@@ -0,0 +1,207 @@
+function assert(condition, message) {
+    if (!condition)
+        throw new Error(message || "Assertion failed");
+}
+
+// Test array allocation with closure capture scenarios
+
+// Function that creates a closure value
+function createClosureValue(multiplier) {
+    return function(x) {
+        return x * multiplier;
+    };
+}
+
+// Function that allocates an array with fixed size
+function allocateArrayForClosure(baseValue) {
+    const arr = new Array(5);
+    for (let i = 0; i < 5; i++) {
+        arr[i] = baseValue + i;
+    }
+    return arr;
+}
+
+// Function that uses array and captures closure
+function processWithClosure(closureFn, arr) {
+    let result = 0;
+    for (let i = 0; i < arr.length; i++) {
+        result += closureFn(arr[i]);
+    }
+    return result;
+}
+
+// Complex scenario: array allocation, closure capture, and usage all in different functions
+function complexClosureTest(multiplier, baseValue) {
+    const closureFn = createClosureValue(multiplier);
+    const arr = allocateArrayForClosure(baseValue);
+    return processWithClosure(closureFn, arr);
+}
+
+// Scenario where closure modifies array behavior
+function closureModifiesArray(capturedValue) {
+    return function(baseValue) {
+        const arr = new Array(4);
+        for (let i = 0; i < 4; i++) {
+            arr[i] = (baseValue + i) + capturedValue;
+        }
+
+        // Array usage within the closure
+        let sum = 0;
+        for (let i = 0; i < arr.length; i++) {
+            sum += arr[i] * 2;
+        }
+        return sum;
+    };
+}
+
+// Nested closure with array
+function nestedClosureWithArray(outerValue) {
+    return function(middleValue) {
+        return function(baseValue) {
+            const arr = new Array(3);
+            for (let i = 0; i < 3; i++) {
+                arr[i] = baseValue + i + outerValue + middleValue;
+            }
+
+            // Process array using captured values
+            let product = 1;
+            for (let i = 0; i < arr.length; i++) {
+                product *= (arr[i] % 10) || 1; // avoid zero
+            }
+            return product;
+        };
+    };
+}
+
+let escapedArray = null;
+// Array that sometimes escapes through closure
+function conditionalEscapeViaClosure(shouldEscape) {
+
+    return function(baseValue) {
+        const arr = new Array(4);
+        for (let i = 0; i < 4; i++) {
+            arr[i] = baseValue + i * 2;
+        }
+
+        if (shouldEscape) {
+            escapedArray = arr;
+            return arr.length;
+        } else {
+            // Use array locally
+            let sum = 0;
+            for (let i = 0; i < arr.length; i++) {
+                sum += arr[i];
+            }
+            return sum;
+        }
+    };
+}
+
+// Closure that captures array from outer scope
+function captureArrayInClosure(baseValue) {
+    const arr = new Array(6);
+    for (let i = 0; i < 6; i++) {
+        arr[i] = baseValue + i;
+    }
+
+    // Return closure that uses the captured array
+    return function(multiplier) {
+        let result = 0;
+        for (let i = 0; i < arr.length; i++) {
+            result += arr[i] * multiplier;
+        }
+        return result;
+    };
+}
+
+// Multiple arrays with closure interactions
+function multiArrayClosure(factor1, factor2) {
+    return function(baseValue) {
+        const arr1 = new Array(3);
+        const arr2 = new Array(3);
+
+        for (let i = 0; i < 3; i++) {
+            arr1[i] = (baseValue + i) * factor1;
+            arr2[i] = (baseValue + i) * factor2;
+        }
+
+        // Combine arrays using closure variables
+        let combined = 0;
+        for (let i = 0; i < 3; i++) {
+            combined += arr1[i] + arr2[i];
+        }
+        return combined;
+    };
+}
+
+noInline(complexClosureTest);
+noInline(closureModifiesArray);
+noInline(nestedClosureWithArray);
+noInline(conditionalEscapeViaClosure);
+noInline(captureArrayInClosure);
+noInline(multiArrayClosure);
+
+// Run tests
+for (let i = 0; i < testLoopCount; i++) {
+    const baseValue = i % 8;
+    const multiplier = 1 + (i % 3); // multipliers 1-3
+
+    // Test complex closure scenario (array size 5)
+    let result1 = complexClosureTest(multiplier, baseValue);
+    let expected1 = 0;
+    for (let j = 0; j < 5; j++) {
+        expected1 += (baseValue + j) * multiplier;
+    }
+    assert(result1 === expected1, `complexClosureTest(${multiplier}, ${baseValue}) failed: got ${result1}, expected ${expected1}`);
+
+    // Test closure that modifies array behavior (array size 4)
+    const closureFn = closureModifiesArray(i % 5);
+    let result2 = closureFn(baseValue);
+    let expected2 = 0;
+    for (let j = 0; j < 4; j++) {
+        expected2 += (baseValue + j + (i % 5)) * 2;
+    }
+    assert(result2 === expected2, `closureModifiesArray test failed: got ${result2}, expected ${expected2}`);
+
+    // Test nested closure (array size 3)
+    const nestedFn = nestedClosureWithArray(i % 3)(i % 4);
+    let result3 = nestedFn(baseValue);
+    let expected3 = 1;
+    for (let j = 0; j < 3; j++) {
+        let val = (baseValue + j + (i % 3) + (i % 4)) % 10;
+        if (val !== 0) expected3 *= val;
+    }
+    assert(result3 === expected3, `nestedClosureWithArray test failed: got ${result3}, expected ${expected3}`);
+
+    // Test conditional escape via closure - non-escaping case (array size 4)
+    const condEscapeFn = conditionalEscapeViaClosure(false);
+    let result4 = condEscapeFn(baseValue);
+    let expected4 = 0;
+    for (let j = 0; j < 4; j++) {
+        expected4 += baseValue + j * 2;
+    }
+    assert(result4 === expected4, `conditionalEscapeViaClosure(false) test failed: got ${result4}, expected ${expected4}`);
+
+    // Test conditional escape via closure - escaping case (array size 4)
+    const condEscapeFn2 = conditionalEscapeViaClosure(true);
+    let result5 = condEscapeFn2(baseValue);
+    assert(result5 === 4, `conditionalEscapeViaClosure(true) test failed: got ${result5}, expected 4`);
+
+    // Test capture array in closure (array size 6)
+    const capturedFn = captureArrayInClosure(baseValue);
+    let result6 = capturedFn(multiplier);
+    let expected6 = 0;
+    for (let j = 0; j < 6; j++) {
+        expected6 += (baseValue + j) * multiplier;
+    }
+    assert(result6 === expected6, `captureArrayInClosure test failed: got ${result6}, expected ${expected6}`);
+
+    // Test multiple arrays with closure (arrays size 3 each)
+    const multiArrayFn = multiArrayClosure(2, 3);
+    let result7 = multiArrayFn(baseValue);
+    let expected7 = 0;
+    for (let j = 0; j < 3; j++) {
+        expected7 += (baseValue + j) * 2 + (baseValue + j) * 3; // factor1 + factor2
+    }
+    assert(result7 === expected7, `multiArrayClosure test failed: got ${result7}, expected ${expected7}`);
+}
diff --git a/JSTests/stress/array-allocation-elimination-conditional-usage.js b/JSTests/stress/array-allocation-elimination-conditional-usage.js
new file mode 100644
index 000000000000..9875274dbee5
--- /dev/null
+++ b/JSTests/stress/array-allocation-elimination-conditional-usage.js
@@ -0,0 +1,108 @@
+function assert(condition, message) {
+    if (!condition)
+        throw new Error(message || "Assertion failed");
+}
+
+// Test conditional array usage - some branches use array, others don't
+function testConditionalArrayUsage(useArray, writeValue) {
+    const arr = new Array(5);
+
+    if (useArray) {
+        // Branch that uses the array
+        arr[0] = writeValue;
+        arr[2] = writeValue + 10;
+        arr[4] = writeValue + 20;
+
+        let sum = 0;
+        for (let i = 0; i < 5; i++) {
+            if (arr[i] !== undefined) {
+                sum += arr[i];
+            }
+        }
+        return sum;
+    } else {
+        // Branch that doesn't use the array at all
+        return writeValue * 3 + 30; // Same result as the sum above when writeValue = 0
+    }
+}
+
+// Test with nested conditionals
+function testNestedConditionalArray(outer, inner, value) {
+    const arr = new Array(3);
+
+    if (outer) {
+        arr[0] = value;
+
+        if (inner) {
+            arr[1] = value * 2;
+            arr[2] = value * 3;
+            return arr[0] + arr[1] + arr[2]; // value * 6
+        } else {
+            // Only use first element
+            return arr[0]; // value
+        }
+    } else {
+        // Don't use array at all
+        return inner ? value * 6 : value;
+    }
+}
+
+// Test with early return that skips array usage
+function testEarlyReturnArray(shouldReturn, value) {
+    const arr = new Array(4);
+
+    if (shouldReturn) {
+        return value * 42;
+    }
+
+    // This array usage should be eliminated when shouldReturn is true
+    arr[0] = value;
+    arr[1] = value + 1;
+    arr[3] = value + 3;
+
+    return arr[0] + arr[1] + arr[3]; // value * 3 + 4
+}
+
+noInline(testConditionalArrayUsage);
+noInline(testNestedConditionalArray);
+noInline(testEarlyReturnArray);
+
+// Run tests to trigger optimization
+for (let i = 0; i < testLoopCount; i++) {
+    // Test the branch that uses the array
+    let result1 = testConditionalArrayUsage(true, i % 100);
+    let expected1 = (i % 100) + (i % 100 + 10) + (i % 100 + 20);
+    assert(result1 === expected1, `testConditionalArrayUsage(true, ${i % 100}) failed: got ${result1}, expected ${expected1}`);
+
+    // Test the branch that doesn't use the array
+    let result2 = testConditionalArrayUsage(false, i % 100);
+    let expected2 = (i % 100) * 3 + 30;
+    assert(result2 === expected2, `testConditionalArrayUsage(false, ${i % 100}) failed: got ${result2}, expected ${expected2}`);
+
... (truncated)
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker.