High watch ✔︎ verified CVSS 8.8 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped 🧬 TDZ hole-check elided across a branch

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in V8
DescriptionInappropriate implementation in V8
ComponentV8
Sub-componentMaglev
Bug ClassLogic Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety — revised by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed (tier revised from patch-now)
Reviewer notes: Phi mechanism matches the diff, but 'type confusion' and a corruption primitive are stated as fact; the advisory says 'inappropriate implementation' and the diff/test only show an elided TDZ hole check leaking the hole into script.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker465730465
Fix commit5a96a3530f02 (v8/v8) +40/-1
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2025-12-03 (per vendor advisory)
Fix landed2025-12-04 (public git)
Disclosed2026-01-13
Patch gap40 days — fix public in source before the advisory

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 let binding 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.

Key insight
The single mistake is treating “not a loop phi” as sufficient to run the input-based hole analysis, when exception phis are a second phi kind whose value is not captured by their listed inputs; the fix simply adds !phi->is_exception_phi() to the condition so those phis are never assumed hole-free.

Attack Path

  1. Craft TDZ-crossing control flow Write a function like foo that uses a switch fallthrough to enter a try block referencing a let binding (y) whose declaration was skipped, so the binding is still TheHole when the exception path runs.
  2. Force an exception phi The try/catch structure makes Maglev create an exception phi for y at the catch merge, whose true value can be the hole even though its explicit inputs look hole-free.
  3. Trigger optimization Warm up with %PrepareFunctionForOptimization and then %OptimizeMaglevOnNextCall, so Maglev’s hole analysis runs and, missing the exception-phi case, elides the hole check.
  4. Leak / confuse the hole Invoke the optimized function so TheHole flows past the removed check into subsequent operations as a real value, yielding type confusion inside the JIT-compiled code.

Impact Assessment

An attacker who can run scripted JavaScript in the renderer gains a type confusion in Maglev-optimized code, letting the internal 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

FunctionChangeNotes
switch
test/mjsunit/maglev/regress-465730465.js
modified

Files Changed

  • src/maglev/maglev-ir.cc
  • test/mjsunit/maglev/regress-465730465.js

Audit Directions

  • Incomplete phi-kind enumeration
    Audit every analysis in maglev-ir that branches on is_loop_phi() for whether it also needs to account for is_exception_phi(), since exception phis carry values not represented by their nominal inputs.
  • Hole-check elision
    Review all sites that prove a value cannot be TheHole and drop a hole check; confirm each covers TDZ-in-catch and other unwinding paths, not just straight-line dataflow.
  • Input-derived value proofs
    Flag 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.
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);
Loading diff…

Regression Test / PoC

shipped with the fix
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);
Loading diff…

Original Bug Report

reported by 24...@project.gserviceaccount.com

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.

View on issue tracker