CVE-2026-8570
Overview
Background
- Maglev
- V8’s mid-tier optimizing JIT that builds an SSA-style graph and, under
--maglev-untagged-phis, may represent aPhiin a raw untagged (e.g. Int32) form instead of a boxed tagged value. - Static type vs. KNA type
unmerged_phi->type()is a node’s conservative staticNodeType, whereas the known-node-aspects (KNA) type fromunmerged_aspects.GetTypeUncheckedis the refined type learned along a control path, for example once aCheckedSmiUntaghas proven a value is aSmi.- `SetUseRequiresSmi`
- a flag on an input to a loop
Phithat records the backedge value is used as aSmi, forcing later type/untagging decisions to stay consistent with that constraint. - `Smi` untagging
- replacing a tagged small-integer
Phiwith a raw machine integer, which is only sound if every value flowing in is truly aSmi.
Root Cause Analysis
When merging a loop backedge in src/maglev/maglev-interpreter-frame-state.cc, a prior fix (crrev.com/c/7633545) tried to call SetUseRequiresSmi whenever the backedge of a Phi had Smi type, but it queried unmerged_phi->type(), the node’s static type, rather than its KNA type. For non-loop phis the static type is typically NodeType::kAny, while the KNA type only becomes NodeType::kSmi once a CheckedSmiUntag is seen on the path, so the NodeTypeIs(unmerged_phi->type(), NodeType::kSmi) guard failed to fire and SetUseRequiresSmi was never called. Missing that flag lets Maglev untag the Phi as a Smi based on a CheckedSmiUntag, then later invalidate the very type assumption that justified the untagging without re-checking it, leaving an untagged Phi that can carry a non-Smi value.
The fix computes unmerged_type from unmerged_aspects.GetTypeUnchecked(builder->broker(), unmerged) (the KNA type) and tests NodeTypeIs(unmerged_type, NodeType::kSmi), so the flag is set whenever the path-refined type is Smi; a DCHECK also asserts the static type is not kSmi in the fallthrough, since that case would have been caught above.
SetUseRequiresSmi from the backedge Phi’s static type (unmerged_phi->type()) instead of its path-sensitive KNA type; the fix reads the KNA type via unmerged_aspects.GetTypeUnchecked and passes it to NodeTypeIs, so Smi-ness introduced by a CheckedSmiUntag is no longer missed.Attack Path
- Craft a loop with a conditionally-changing phi
Author a hot loop where a variable (
x) starts as aSmibut is reassigned to a largeInt32-but-non-Smivalue (0x4e000000) partway through iteration, feeding a backedgePhi. - Trigger a CheckedSmiUntag on the phi
Use the value in an operation that inserts a
CheckedSmiUntag(e.g.x | 0and indexing likedelete ArrayBuffer[x]), which populates the phi’s KNA type asNodeType::kSmieven though its static type staysNodeType::kAny. - Bypass the Smi-use flag
Because the buggy check consulted
unmerged_phi->type(), theSmiKNA type is not detected, soSetUseRequiresSmiis skipped and Maglev leaves the untaggedPhiwithout the constraint that would keep itsSmiassumption honest. - Force unsound untagging
Maglev untags the
Phito a raw integer under--maglev-untagged-phis, but the invariant that all backedge values areSmiis silently violated when the non-Smivalue flows in. - Achieve type confusion
The untagged
Phiis subsequently treated as aSmi/tagged value, confusing a raw integer with a tagged pointer and yielding type confusion and potential out-of-bounds access.
Impact Assessment
Phi untagged as a Smi can hold a non-Smi value and be misinterpreted as tagged, enabling memory corruption / out-of-bounds access. Exploitation runs in the renderer process and requires only that the victim execute attacker-controlled script that gets tiered up to Maglev with --maglev-untagged-phis behavior, using a loop shaped to introduce a CheckedSmiUntag-derived Smi KNA type on a backedge Phi.Changed Functions
| Function | Change | Notes |
|---|---|---|
fortest/mjsunit/maglev/regress-490353576.js |
modified | |
iftest/mjsunit/maglev/regress-490353576.js |
modified |
Files Changed
src/maglev/maglev-interpreter-frame-state.cctest/mjsunit/maglev/regress-490353576.js
Audit Directions
- Static type vs. KNA type at mergesReview every Maglev decision made at phi/backedge merges (untagging,
SetUseRequiresSmi,SetUseRequiresHeapObject) that reads a node’s statictype()where the path-refined KNA type fromGetTypeUncheckedis the correct source. - Loop-phi soundness constraintsAudit places where a type refinement (e.g. from
CheckedSmiUntag) can later be invalidated, ensuring a corresponding use-constraint flag is recorded so representation choices stay sound across the backedge. - Non-loop vs. loop phi assumptionsLook for code that assumes static and refined types coincide “more or less” for phis, which holds for loop phis but not non-loop phis whose static type is typically
kAny.
Patch
From 54fec2bb063997d5f545c90b8b342b5d620c994f Mon Sep 17 00:00:00 2001
From: Darius Mercadier <dmercadier@chromium.org>
Date: Tue, 17 Mar 2026 11:20:30 +0100
Subject: [PATCH] [maglev] Fix backedge Smi type recording
https://crrev.com/c/7633545 tried to make sure that we call
SetUseRequiresSmi when the backedge of a Phi has Smi type, but it was
using the static type of the backedge rather than its KNA type. For
loop phis, it's more or less the same, but for non-loop phis, the
static type is typically Any, whereas the KNA type is populated as
soon as a CheckedSmiUntag is encountered (as is the case in the
repro).
Fixed: 490353576
Change-Id: I4ed65d4f3791c3c9b4307cf9997c2367f8dfbf56
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7665886
Auto-Submit: Darius Mercadier <dmercadier@chromium.org>
Reviewed-by: Leszek Swirski <leszeks@chromium.org>
Commit-Queue: Darius Mercadier <dmercadier@chromium.org>
Cr-Commit-Position: refs/heads/main@{#105897}
---
diff --git a/src/maglev/maglev-interpreter-frame-state.cc b/src/maglev/maglev-interpreter-frame-state.cc
index 595d75c..2d3a2f9 100644
--- a/src/maglev/maglev-interpreter-frame-state.cc
+++ b/src/maglev/maglev-interpreter-frame-state.cc
@@ -1000,13 +1000,13 @@
return;
}
DCHECK_EQ(result->owner(), owner);
- NodeType type =
+ NodeType unmerged_type =
unmerged_aspects.GetTypeUnchecked(builder->broker(), unmerged);
unmerged = EnsureTagged(builder, unmerged_aspects, unmerged,
predecessors_[predecessors_so_far_]);
result->set_input(predecessor_count_ - 1, unmerged);
- result->merge_post_loop_type(type);
+ result->merge_post_loop_type(unmerged_type);
// We've just merged the backedge, which means that future uses of this Phi
// will be after the loop, so we can now promote `post_loop_type` to the
// regular `type`.
@@ -1021,7 +1021,7 @@
// sminess.
if (result->uses_require_smi()) {
unmerged_phi->SetUseRequiresSmi();
- } else if (NodeTypeIs(unmerged_phi->type(), NodeType::kSmi)) {
+ } else if (NodeTypeIs(unmerged_type, NodeType::kSmi)) {
// The backedge has Smi type, but it's possible that this is only true
// because {result} itself is eventually known to be a Smi, thanks to for
// instance a CheckedSmiUntag. If we don't set {use_requires_smi}, then
@@ -1029,6 +1029,10 @@
// invalidating the type of the backedge, and in turn potentially
// invalidating some Phi untagging without us realizing it.
unmerged_phi->SetUseRequiresSmi();
+ } else {
+ // If the static type of {unmerged_phi} was kSmi, we should have gone into
+ // the branch above and called SetUseRequiresSmi.
+ DCHECK(!NodeTypeIs(unmerged_phi->type(), NodeType::kSmi));
}
if (result->uses_require_heap_object()) {
unmerged_phi->SetUseRequiresHeapObject();
diff --git a/test/mjsunit/maglev/regress-490353576.js b/test/mjsunit/maglev/regress-490353576.js
new file mode 100644
index 0000000..c912a2c
--- /dev/null
+++ b/test/mjsunit/maglev/regress-490353576.js
@@ -0,0 +1,21 @@
+// 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: --no-concurrent-recompilation --maglev-untagged-phis
+
+function foo() {
+ let x = 0;
+ var y = 0x4e000000;
+ for (let i = 0; i < 250; i++) {
+ if (i == 100) {
+ x = y;
+ }
+ delete ArrayBuffer[x];
+ x | 0;
+ }
+
+ return x;
+}
+
+assertEquals(1308622848, foo());
Regression Test / PoC
diff --git a/test/mjsunit/maglev/regress-490353576.js b/test/mjsunit/maglev/regress-490353576.js
new file mode 100644
index 0000000..c912a2c
--- /dev/null
+++ b/test/mjsunit/maglev/regress-490353576.js
@@ -0,0 +1,21 @@
+// 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: --no-concurrent-recompilation --maglev-untagged-phis
+
+function foo() {
+ let x = 0;
+ var y = 0x4e000000;
+ for (let i = 0; i < 250; i++) {
+ if (i == 100) {
+ x = y;
+ }
+ delete ArrayBuffer[x];
+ x | 0;
+ }
+
+ return x;
+}
+
+assertEquals(1308622848, foo());
Original Bug Report
V8 correctness failure in sources: d3
Detailed Report: https://clusterfuzz.com/testcase?key=5547061260779520
Fuzzer: foozzie_js_fuzzer Job Type: v8_foozzie_v2 Platform Id: linux
Crash Type: V8 correctness failure Crash Address: Crash State: sources: d3
Sanitizer: address (ASAN)
Regressed: https://clusterfuzz.com/revisions?job=v8_foozzie_v2&range=105097:105098
Reproducer Testcase: https://clusterfuzz.com/download?testcase_id=5547061260779520
Issue filed automatically.
To reproduce this, please build the target in this report and run it against the reproducer testcase. Please use the GN arguments provided at bottom of this report when building the binary.
If you have trouble reproducing, please also export the environment variables listed under “[Environment]” in the crash stacktrace.
If you have any feedback on reproducing test cases, let us know at https://forms.gle/Yh3qCYFveHj6E5jz5 so we can improve.