CVE-2026-5863
Overview
Background
- Maglev / Turbolev
- V8’s Maglev graph builder (
MaglevGraphBuilder) and the--turbolevpipeline that reuses it; both build deopt metadata describing how to reconstruct interpreter state on deoptimization. - `DeoptFrame` and lazy-deopt continuations
- a
DeoptFramerecords the state to restore on deopt; builtin calls can pushLazyDeoptFrameScopes so that on lazy deopt the frame is wrapped (“continued”) with the builtin’s continuation frames. - Use counts / `AddDeoptUseToScopeData`
- every
ValueNodereferenced 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;AddDeoptUseToScopeDatarecords those uses for a frame’sFrameData. - `RecursivelyWrapDeoptFrameWithContinuations`
- walks a chain of
LazyDeoptFrameScopes, allocating a nestedDeoptFrameper 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.
AddDeoptUseToScopeData for each parent_scope->data() so those uses are counted.Attack Path
- Build nested builtin continuations
Craft JS that nests builtin calls whose lazy-deopt continuations stack, so
RecursivelyWrapDeoptFrameWithContinuationswalks multipleLazyDeoptFrameScopes (the PoC usesArray.prototype.forEachpassed as the callback toforEachwith a second array, under--turbolev). - Optimize the function
Tier the function up (
%OptimizeFunctionOnNextCallwith--turbolev) so Maglev builds the deopt frames and their continuation wrappers. - Undercount a value's deopt use
Because the pre-patch wrapper omits
AddDeoptUseToScopeDatafor the continuation scope data, aValueNodereferenced only by a nested continuation frame is left with too low a use count. - 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).
- 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/maglev/maglev-graph-builder.cc |
modified |
Files Changed
src/maglev/maglev-graph-builder.ccsrc/maglev/maglev-graph-builder.htest/mjsunit/turbolev/regress-484527367.js
Audit Directions
- Deopt use-count completenessAudit every path that constructs or wraps
DeoptFrames (especially continuation/inlining wrappers) to confirmAddDeoptUse/AddDeoptUseToScopeDatais invoked for all embedded values, not just the outermost frame. - Helpers that cannot record usesReview 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 continuationsExercise 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.
Patch
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();
Regression Test / PoC
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();
Original Bug Report
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.