CVE-2026-0900
Overview
Background
- `Maglev`
- V8’s mid-tier optimizing JIT that compiles a bytecode-derived IR (
maglev-ir) into machine code, relying on static value-type analysis to elide runtime checks. - The hole (`TheHole`)
- a special internal V8 sentinel value that must never escape to user code; it marks, among other things, a
letbinding still in its temporal dead zone (TDZ). - `Phi`
- an IR node that merges values arriving from multiple control-flow predecessors; a loop phi merges back-edge values and an exception phi (
is_exception_phi()) merges the value of a variable at the point an exception unwinds into a catch block. - `CanBeTheHoleValue` / `Tribool`
- the analysis (returning
kTrue/kFalse/kMaybe) that decides whether a node may hold the hole, and thus whether a hole check can be safely removed.
Root Cause Analysis
When reasoning about whether a Phi in maglev-ir.cc could carry TheHole, the analysis special-cased only loop phis: for any non-loop phi it inspected each input and, if none could be the hole, concluded the phi could not be the hole either (can_be_the_hole = false). This logic ignored exception phis, whose value is materialized at the moment an exception unwinds into a catch block, so a let variable that is still in its TDZ (e.g. y when control jumps into the try region before the let y = 1 binding executes) can flow into the exception phi as the hole even though the phi’s ordinary inputs appear hole-free. The optimizer therefore wrongly proved the phi non-hole and elided the required hole check, letting TheHole propagate as if it were a legitimate value — a type confusion.
The fix extends the guard to !phi->is_loop_phi() && !phi->is_exception_phi(), so exception phis fall through to the conservative kMaybe result and the hole check is retained.
!phi->is_exception_phi() to the condition so those phis are never assumed hole-free.Attack Path
- Craft TDZ-crossing control flow
Write a function like
foothat uses aswitchfallthrough to enter atryblock referencing aletbinding (y) whose declaration was skipped, so the binding is stillTheHolewhen the exception path runs. - Force an exception phi
The
try/catchstructure makes Maglev create an exception phi foryat the catch merge, whose true value can be the hole even though its explicit inputs look hole-free. - Trigger optimization
Warm up with
%PrepareFunctionForOptimizationand then%OptimizeMaglevOnNextCall, so Maglev’s hole analysis runs and, missing the exception-phi case, elides the hole check. - Leak / confuse the hole
Invoke the optimized function so
TheHoleflows past the removed check into subsequent operations as a real value, yielding type confusion inside the JIT-compiled code.
Impact Assessment
TheHole sentinel be treated as an ordinary object value — a primitive that typically bootstraps to memory corruption and renderer-process compromise. Exploitation requires only that the page trigger Maglev optimization of a function with the specific TDZ-plus-exception control flow, achievable purely from untrusted web content with no special privileges.Changed Functions
| Function | Change | Notes |
|---|---|---|
switchtest/mjsunit/maglev/regress-465730465.js |
modified |
Files Changed
src/maglev/maglev-ir.cctest/mjsunit/maglev/regress-465730465.js
Audit Directions
- Incomplete phi-kind enumerationAudit every analysis in
maglev-irthat branches onis_loop_phi()for whether it also needs to account foris_exception_phi(), since exception phis carry values not represented by their nominal inputs. - Hole-check elisionReview all sites that prove a value cannot be
TheHoleand drop a hole check; confirm each covers TDZ-in-catch and other unwinding paths, not just straight-line dataflow. - Input-derived value proofsFlag optimizations that conclude a merge node’s type solely from its enumerated inputs, and verify no control-flow-injected value (exception, deopt, or OSR entry) can introduce a state the inputs do not reflect.
Patch
From 5a96a3530f02f495eb17e5effa877aade940e8a2 Mon Sep 17 00:00:00 2001
From: Victor Gomes <victorgomes@chromium.org>
Date: Thu, 04 Dec 2025 09:25:48 +0100
Subject: [PATCH] [maglev] Exception phis can be the hole
Fixed: 465730465
Change-Id: If1deb6975acd515c2209d24549bc7642535ecd5b
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7223977
Commit-Queue: Victor Gomes <victorgomes@chromium.org>
Auto-Submit: Victor Gomes <victorgomes@chromium.org>
Commit-Queue: Marja Hölttä <marja@chromium.org>
Reviewed-by: Marja Hölttä <marja@chromium.org>
Cr-Commit-Position: refs/heads/main@{#104099}
---
diff --git a/src/maglev/maglev-ir.cc b/src/maglev/maglev-ir.cc
index ec83ede..fa8d01e 100644
--- a/src/maglev/maglev-ir.cc
+++ b/src/maglev/maglev-ir.cc
@@ -628,7 +628,7 @@
return Tribool::kMaybe;
}
if (const Phi* phi = TryCast<Phi>()) {
- if (!phi->is_loop_phi()) {
+ if (!phi->is_loop_phi() && !phi->is_exception_phi()) {
bool can_be_the_hole = false;
for (ConstInput input : phi->inputs()) {
if (input.node()->IsTheHole() != Tribool::kFalse) {
diff --git a/test/mjsunit/maglev/regress-465730465.js b/test/mjsunit/maglev/regress-465730465.js
new file mode 100644
index 0000000..b2fbb29
--- /dev/null
+++ b/test/mjsunit/maglev/regress-465730465.js
@@ -0,0 +1,39 @@
+// Copyright 2025 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
+
+let glob;
+
+function empty() {}
+
+function foo(a) {
+ glob = 0;
+ var x;
+ switch (a) {
+ case 1:
+ let y = 1;
+ case 2:
+ try {
+ y = 2;
+ empty();
+ glob++;
+ } catch (e) {}
+ try {
+ x = y;
+ } catch (e) {
+ empty();
+ glob++;
+ }
+ }
+}
+
+%PrepareFunctionForOptimization(foo);
+foo(1);
+assertEquals(1, glob);
+foo(2);
+assertEquals(1, glob);
+%OptimizeMaglevOnNextCall(foo);
+foo(2);
+assertEquals(1, glob);
Regression Test / PoC
diff --git a/test/mjsunit/maglev/regress-465730465.js b/test/mjsunit/maglev/regress-465730465.js
new file mode 100644
index 0000000..b2fbb29
--- /dev/null
+++ b/test/mjsunit/maglev/regress-465730465.js
@@ -0,0 +1,39 @@
+// Copyright 2025 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
+
+let glob;
+
+function empty() {}
+
+function foo(a) {
+ glob = 0;
+ var x;
+ switch (a) {
+ case 1:
+ let y = 1;
+ case 2:
+ try {
+ y = 2;
+ empty();
+ glob++;
+ } catch (e) {}
+ try {
+ x = y;
+ } catch (e) {
+ empty();
+ glob++;
+ }
+ }
+}
+
+%PrepareFunctionForOptimization(foo);
+foo(1);
+assertEquals(1, glob);
+foo(2);
+assertEquals(1, glob);
+%OptimizeMaglevOnNextCall(foo);
+foo(2);
+assertEquals(1, glob);
Original Bug Report
V8 correctness failure in sources: 25
Detailed Report: https://clusterfuzz.com/testcase?key=5392979434864640
Fuzzer: foozzie_js_fuzzer Job Type: v8_foozzie_v2 Platform Id: linux
Crash Type: V8 correctness failure Crash Address: Crash State: sources: 25
Sanitizer: address (ASAN)
Regressed: https://clusterfuzz.com/revisions?job=v8_foozzie_v2&range=104001:104002
Reproducer Testcase: https://clusterfuzz.com/download?testcase_id=5392979434864640
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.