Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in V8
DescriptionInappropriate implementation in V8
ComponentV8
Bug ClassLogic Error
Action tierwatch — corruption primitive but gated (flag) — not default-reachable
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.
Tracker484527367
Fix commitb54c7841e2cd (v8/v8) +44/-11
CWECWE-284
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-02-14 (per vendor advisory)
Fix landed2026-02-23 (public git)
Disclosed2026-04-07
Patch gap43 days — fix public in source before the advisory

Background

Maglev / Turbolev
V8’s Maglev graph builder (MaglevGraphBuilder) and the --turbolev pipeline that reuses it; both build deopt metadata describing how to reconstruct interpreter state on deoptimization.
`DeoptFrame` and lazy-deopt continuations
a DeoptFrame records the state to restore on deopt; builtin calls can push LazyDeoptFrameScopes so that on lazy deopt the frame is wrapped (“continued”) with the builtin’s continuation frames.
Use counts / `AddDeoptUseToScopeData`
every ValueNode referenced by a deopt frame must have its use recorded so the value is kept live (not freed, DCE’d, or reused) until the deopt materializes it; AddDeoptUseToScopeData records those uses for a frame’s FrameData.
`RecursivelyWrapDeoptFrameWithContinuations`
walks a chain of LazyDeoptFrameScopes, allocating a nested DeoptFrame per continuation scope so a deopt can rebuild each builtin continuation.

Root Cause Analysis

RecursivelyWrapDeoptFrameWithContinuations recursively wraps a base DeoptFrame with one DeoptFrame per parent LazyDeoptFrameScope, embedding each scope’s data() into the new frames. Pre-patch it was a free function in an anonymous namespace taking Zone* zone, and it never called AddDeoptUseToScopeData for the scope data it wove into the frames — so values referenced by nested builtin continuation frames had their deopt uses left uncounted. The violated invariant is that every value live across a deopt frame (including nested continuation frames) must have a recorded deopt use so it is preserved through register allocation/optimization; with the count missing, such a value could be treated as dead and freed/overwritten while a continuation frame still needs it, so on deopt the frame materializes a stale or wrong value.

The fix promotes the function to a MaglevGraphBuilder member (dropping the zone parameter in favor of zone()) so it can call AddDeoptUseToScopeData(parent_scope->data()) for each continuation scope during the recursion, and relaxes AddDeoptUseToScopeData to take a const DeoptFrame::FrameData& so the const scope data can be passed. This restores correct use counting for every nested continuation frame.

Key insight
The single mistake was that the recursive continuation-frame wrapper never recorded deopt uses for the nested continuation scopes’ data, undercounting the liveness of values needed on deopt; the fix makes the function a member and calls AddDeoptUseToScopeData for each parent_scope->data() so those uses are counted.

Attack Path

  1. Build nested builtin continuations Craft JS that nests builtin calls whose lazy-deopt continuations stack, so RecursivelyWrapDeoptFrameWithContinuations walks multiple LazyDeoptFrameScopes (the PoC uses Array.prototype.forEach passed as the callback to forEach with a second array, under --turbolev).
  2. Optimize the function Tier the function up (%OptimizeFunctionOnNextCall with --turbolev) so Maglev builds the deopt frames and their continuation wrappers.
  3. Undercount a value's deopt use Because the pre-patch wrapper omits AddDeoptUseToScopeData for the continuation scope data, a ValueNode referenced only by a nested continuation frame is left with too low a use count.
  4. Free/reuse the still-needed value Optimization or register allocation treats the undercounted value as no longer used and frees or overwrites it while a continuation frame still references it (inferred; the diff shows the missing use-count increment, not the specific reclamation site).
  5. Deopt materializes a stale value On lazy deopt through that builtin continuation, the frame reconstructs interpreter state from the clobbered value, producing incorrect/confused state (inferred as the exploitable consequence).

Impact Assessment

An attacker running crafted JavaScript optimized via Maglev/Turbolev can cause deopt continuation frames to reference values whose liveness was undercounted, so a value needed to reconstruct interpreter state may be freed or reused, corrupting the restored state on deoptimization in the renderer process. The advisory classifies this as “Inappropriate implementation in V8” (LogicError) with no stated exploited-in-the-wild status; the precondition is tiering a function with nested builtin-continuation deopts up to the optimizer. Any escalation to a memory-corruption read/write primitive or code execution is (inferred) and not demonstrated by the patch.

Changed Functions

FunctionChangeNotes
if
src/maglev/maglev-graph-builder.cc
modified

Files Changed

  • src/maglev/maglev-graph-builder.cc
  • src/maglev/maglev-graph-builder.h
  • test/mjsunit/turbolev/regress-484527367.js

Audit Directions

  • Deopt use-count completeness
    Audit every path that constructs or wraps DeoptFrames (especially continuation/inlining wrappers) to confirm AddDeoptUse/AddDeoptUseToScopeData is invoked for all embedded values, not just the outermost frame.
  • Helpers that cannot record uses
    Review free/namespace-local helpers that build IR referencing values but lack access to the builder’s use-counting methods, since they are prone to silently omitting liveness bookkeeping.
  • Nested builtin continuations
    Exercise builtins whose lazy-deopt continuations stack (e.g. array iteration builtins passed as callbacks) under Maglev/Turbolev, watching for values kept live by only the innermost continuation frame.
From b54c7841e2cdb2c3a8c7c315daa41ca99ecd7329 Mon Sep 17 00:00:00 2001
From: Darius Mercadier <dmercadier@chromium.org>
Date: Mon, 23 Feb 2026 16:14:11 +0100
Subject: [PATCH] [*lev] Correctly update use counts for nested builtin continuations

Fixed: 484527367
Change-Id: I4cda8c1bbb2788fca06b564eae509511aec0957e
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7593054
Reviewed-by: Victor Gomes <victorgomes@chromium.org>
Auto-Submit: Darius Mercadier <dmercadier@chromium.org>
Commit-Queue: Darius Mercadier <dmercadier@chromium.org>
Cr-Commit-Position: refs/heads/main@{#105381}
---

diff --git a/src/maglev/maglev-graph-builder.cc b/src/maglev/maglev-graph-builder.cc
index 3718e10..832ec14 100644
--- a/src/maglev/maglev-graph-builder.cc
+++ b/src/maglev/maglev-graph-builder.cc
@@ -1542,19 +1542,19 @@
   return caller_details_->deopt_frame;
 }
 
-namespace {
-DeoptFrame* RecursivelyWrapDeoptFrameWithContinuations(
-    Zone* zone, const DeoptFrame& frame,
+DeoptFrame* MaglevGraphBuilder::RecursivelyWrapDeoptFrameWithContinuations(
+    const DeoptFrame& frame,
     const MaglevGraphBuilder::LazyDeoptFrameScope* parent_scope) {
   if (!parent_scope) {
-    return zone->New<DeoptFrame>(frame);
+    return zone()->New<DeoptFrame>(frame);
   }
 
-  return zone->New<DeoptFrame>(parent_scope->data(),
-                               RecursivelyWrapDeoptFrameWithContinuations(
-                                   zone, frame, parent_scope->parent()));
+  AddDeoptUseToScopeData(parent_scope->data());
+
+  return zone()->New<DeoptFrame>(parent_scope->data(),
+                                 RecursivelyWrapDeoptFrameWithContinuations(
+                                     frame, parent_scope->parent()));
 }
-}  // namespace
 
 DeoptFrame* MaglevGraphBuilder::GetLatestCheckpointedFrame() {
   if (in_prologue_) {
@@ -1581,7 +1581,7 @@
       latest_checkpointed_frame_ = zone()->New<DeoptFrame>(
           deopt_scope->data(),
           RecursivelyWrapDeoptFrameWithContinuations(
-              zone(), *latest_checkpointed_frame_, deopt_scope->parent()));
+              *latest_checkpointed_frame_, deopt_scope->parent()));
     }
   }
   return latest_checkpointed_frame_;
@@ -1603,7 +1603,8 @@
                          result_location, result_size);
 }
 
-void MaglevGraphBuilder::AddDeoptUseToScopeData(DeoptFrame::FrameData& data) {
+void MaglevGraphBuilder::AddDeoptUseToScopeData(
+    const DeoptFrame::FrameData& data) {
   switch (data.tag()) {
     case DeoptFrame::FrameType::kInterpretedFrame:
     case DeoptFrame::FrameType::kInlinedArgumentsFrame:
diff --git a/src/maglev/maglev-graph-builder.h b/src/maglev/maglev-graph-builder.h
index b2d9328..223e447 100644
--- a/src/maglev/maglev-graph-builder.h
+++ b/src/maglev/maglev-graph-builder.h
@@ -1511,7 +1511,11 @@
   void AddDeoptUse(VirtualObject* alloc);
   void AddNonEscapingUses(InlinedAllocation* allocation, int use_count);
 
-  void AddDeoptUseToScopeData(DeoptFrame::FrameData& data);
+  void AddDeoptUseToScopeData(const DeoptFrame::FrameData& data);
+
+  DeoptFrame* RecursivelyWrapDeoptFrameWithContinuations(
+      const DeoptFrame& frame,
+      const MaglevGraphBuilder::LazyDeoptFrameScope* parent_scope);
 
   std::optional<VirtualObject*> TryGetNonEscapingArgumentsObject(
       ValueNode* value);
diff --git a/test/mjsunit/turbolev/regress-484527367.js b/test/mjsunit/turbolev/regress-484527367.js
new file mode 100644
index 0000000..5766d96
--- /dev/null
+++ b/test/mjsunit/turbolev/regress-484527367.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 --turbolev
+
+function __wrapTC(f, permissive = true) {
+  try {
+    return f();
+  } catch (e) {
+  }
+}
+
+function foo() {
+  const arr = __wrapTC(() => []);
+  function bar(arr1) {
+    const arr2 = [null,,];
+    arr1.forEach(Array.prototype.forEach, arr2);
+  }
+  __wrapTC(() => bar(arr));
+}
+
+%PrepareFunctionForOptimization(foo);
+foo.apply();
+foo();
+
+%OptimizeFunctionOnNextCall(foo);
+foo();
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/test/mjsunit/turbolev/regress-484527367.js b/test/mjsunit/turbolev/regress-484527367.js
new file mode 100644
index 0000000..5766d96
--- /dev/null
+++ b/test/mjsunit/turbolev/regress-484527367.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 --turbolev
+
+function __wrapTC(f, permissive = true) {
+  try {
+    return f();
+  } catch (e) {
+  }
+}
+
+function foo() {
+  const arr = __wrapTC(() => []);
+  function bar(arr1) {
+    const arr2 = [null,,];
+    arr1.forEach(Array.prototype.forEach, arr2);
+  }
+  __wrapTC(() => bar(arr));
+}
+
+%PrepareFunctionForOptimization(foo);
+foo.apply();
+foo();
+
+%OptimizeFunctionOnNextCall(foo);
+foo();
Loading diff…

Original Bug Report

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

DCHECK failure in use_count_ > 0 in maglev-ir.h

Detailed Report: https://clusterfuzz.com/testcase?key=5109997302120448

Fuzzer: ochang_js_fuzzer Job Type: linux_asan_d8_v8_arm_dbg Platform Id: linux

Crash Type: DCHECK failure Crash Address: Crash State: use_count_ > 0 in maglev-ir.h

Sanitizer: address (ASAN)

Regressed: https://clusterfuzz.com/revisions?job=linux_asan_d8_v8_arm_dbg&range=105182:105183

Reproducer Testcase: https://clusterfuzz.com/download?testcase_id=5109997302120448

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