CVE-2025-12727
Overview
Files Changed
src/maglev/maglev-graph-builder.ccsrc/maglev/maglev-ir.hsrc/maglev/maglev-reducer-inl.h
Patch
From af7644b2de8ca3ccc4069b637b550d076caf4b38 Mon Sep 17 00:00:00 2001 From: Jakob Linke <[email protected]> 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 <[email protected]> Reviewed-by: Victor Gomes <[email protected]> Auto-Submit: Jakob Linke <[email protected]> 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:
Original Bug Report
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 thefloat64value loaded fromarr[i]. TheNodeTypefor this node iskNumberOrOddball(It’s confusing thatkNumberOrOddballsuggests this might be aHeapNumberobject, but in reality, this node can only be of typefloat64).n24 : Phi(r5) [n10, n22]: Represents the node fori, with aNodeTypeofSMI.
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]iskNumberOrOddball, 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
ValueRepresentationof then55node iskHoleyFloat64, soBuildCheckSmi()inserts aCheckHoleyFloat64IsSmi()node. Note: This node only checks whether theFloat64value can be represented as an SMI; it does not guarantee that theValueRepresentationof then55node becomesSMI. In this example, theFloat64value loaded byn55is1.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 <[email protected]>
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]