CVE-2026-5862
Overview
Background
- Maglev
- V8’s mid-tier optimizing JIT compiler that builds a graph of
ValueNodes from bytecode viaMaglevGraphBuilder. - Deopt use counting
AddDeoptUserecords that aValueNodeis referenced by a deopt frame, incrementing a per-node use counter that keeps the value alive and correctly tracked for register allocation and liveness.- `DeoptFrameScope`
- a scoped object (
EagerDeoptFrameScope/LazyDeoptFrameScope) that carriesDeoptFrame::FrameDatadescribing the deopt state to reconstruct when a subsequent deopt frame is materialized from it. - `DeoptFrame::FrameData`
- a tagged union of frame variants (
kConstructInvokeStubFrame,kBuiltinContinuationFrame, etc.) each holding theValueNodes (context, receiver, parameters) that must be counted as deopt uses.
Root Cause Analysis
The vulnerable path was the construction of deopt scope data in DeoptFrameScopeBase, where AddDeoptUse was called eagerly on the scope’s context, receiver, and parameters at the moment the scope data was created rather than when an actual DeoptFrame was built from it. The invariant AddDeoptUse must be paired one-to-one with the deopt frame that actually consumes the node was therefore violated: a single scope could be used to materialize a deopt frame zero times (over-counting the uses) or, critically, more than once (under-counting), because counting was tied to scope creation instead of per-frame creation. When a scope was used twice, its nodes’ deopt uses were counted once but consumed twice, so a later RemoveDeoptUse-style decrement could drive the counter below zero and underflow it, corrupting the liveness/use bookkeeping the compiler relies on.
The fix removes the eager counting from the scope-data constructors and introduces AddDeoptUseToScopeData, which is invoked exactly at each deopt-frame creation site (GetLatestCheckpointedFrame for eager frames and GetDeoptFrameForLazyDeoptHelper for lazy ones), so uses are counted once per frame actually created.
AddDeoptUseToScopeData called per deopt frame restores an exact one-count-per-use accounting that cannot underflow.Attack Path
- Reach Maglev compilation
Trigger tiering-up of a JavaScript function into the Maglev compiler so
MaglevGraphBuilderbuilds the graph and deopt metadata for it. - Create a reusable deopt scope
Cause a construct-invoke stub or builtin-continuation deopt scope to be established, whose
context/receiver/parametersValueNodes get their deopt use counted once at scope-data creation. - Materialize the scope twice Shape the code so the same deopt scope is consumed to build two deopt frames, meaning the nodes are used twice but counted only once.
- Underflow the use counter When a later step removes a use of one of those nodes, the under-counted counter is decremented past zero and underflows, corrupting the node’s liveness/deopt-use bookkeeping.
- Corrupt downstream compilation The mis-tracked node state feeds register allocation and deopt-frame emission, producing incorrect generated code or freed/stale value tracking exploitable during optimized execution.
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.h
Audit Directions
- Creation-time vs consumption-time countingAudit any reference-counting or liveness accounting where a
AddUse/increment happens at object/scope construction rather than at the point the object is actually consumed, since reuse or non-use breaks the pairing. - Reusable scope objectsReview
DeoptFrameScopeand similar scoped state that can be materialized into zero, one, or many frames, checking that per-node bookkeeping scales with the number of frames created, not with the scope. - Unsigned counter underflowSearch for
AddDeoptUse/decrement pairs and other use counters inmaglev-graph-builderthat can be decremented below zero, where an under-count leads to underflow and corrupted liveness state.
Patch
From a0570afad500d7882b2e4c5981ccb6cf00704df5 Mon Sep 17 00:00:00 2001
From: Victor Gomes <victorgomes@chromium.org>
Date: Fri, 09 Jan 2026 15:13:28 +0100
Subject: [PATCH] [maglev] Fix deopt use counting for deopt scope data
We shouldn't count the deopt use when creating the scope data,
but only when creating the deopt frame.
If the scope was unused, then we were over counting,
not really a correctness issue though.
If the scope was used twice, we were under counting and
we can underflow the counting when removing the use of
a node.
Fixed: 470566252
Change-Id: I7b6f5647d1ed1f9a7c915d3682f10cfc71a5924c
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7415271
Commit-Queue: Toon Verwaest <verwaest@chromium.org>
Reviewed-by: Toon Verwaest <verwaest@chromium.org>
Commit-Queue: Victor Gomes <victorgomes@chromium.org>
Auto-Submit: Victor Gomes <victorgomes@chromium.org>
Cr-Commit-Position: refs/heads/main@{#104601}
---
diff --git a/src/maglev/maglev-graph-builder.cc b/src/maglev/maglev-graph-builder.cc
index 1afdb79..c3e2247 100644
--- a/src/maglev/maglev-graph-builder.cc
+++ b/src/maglev/maglev-graph-builder.cc
@@ -427,8 +427,6 @@
: builder->zone()->CloneVector(parameters),
builder->GetContext(), maybe_js_target}) {
builder->current_interpreter_frame().virtual_objects().Snapshot();
- builder->AddDeoptUse(
- data_.get<DeoptFrame::BuiltinContinuationFrameData>().context);
if (parameters.size() > 0) {
if (InlinedAllocation* receiver =
parameters[0]->TryCast<InlinedAllocation>()) {
@@ -437,10 +435,6 @@
// meterialized object.
receiver->ForceEscaping();
}
- for (ValueNode* node :
- data_.get<DeoptFrame::BuiltinContinuationFrameData>().parameters) {
- builder->AddDeoptUse(node);
- }
} else {
DCHECK(data_.get<DeoptFrame::BuiltinContinuationFrameData>()
.parameters.empty());
@@ -453,10 +447,6 @@
*builder->compilation_unit(), builder->GetCurrentSourcePosition(),
receiver, builder->GetContext()}) {
builder_->current_interpreter_frame().virtual_objects().Snapshot();
- builder_->AddDeoptUse(
- data_.get<DeoptFrame::ConstructInvokeStubFrameData>().receiver);
- builder_->AddDeoptUse(
- data_.get<DeoptFrame::ConstructInvokeStubFrameData>().context);
}
~DeoptFrameScopeBase() {
@@ -1562,8 +1552,9 @@
[&](ValueNode* node, interpreter::Register) { AddDeoptUse(node); });
AddDeoptUse(latest_checkpointed_frame_->as_interpreted().closure());
- const EagerDeoptFrameScope* deopt_scope = current_eager_deopt_scope_;
+ EagerDeoptFrameScope* deopt_scope = current_eager_deopt_scope_;
if (deopt_scope != nullptr) {
+ AddDeoptUseToScopeData(deopt_scope->data());
latest_checkpointed_frame_ = zone()->New<DeoptFrame>(
deopt_scope->data(),
RecursivelyWrapDeoptFrameWithContinuations(
@@ -1589,6 +1580,27 @@
result_location, result_size);
}
+void MaglevGraphBuilder::AddDeoptUseToScopeData(DeoptFrame::FrameData& data) {
+ switch (data.tag()) {
+ case DeoptFrame::FrameType::kInterpretedFrame:
+ case DeoptFrame::FrameType::kInlinedArgumentsFrame:
+ // These frames are never created as deopt scope.
+ UNREACHABLE();
+ case DeoptFrame::FrameType::kConstructInvokeStubFrame:
+ AddDeoptUse(
+ data.get<DeoptFrame::ConstructInvokeStubFrameData>().receiver);
+ AddDeoptUse(data.get<DeoptFrame::ConstructInvokeStubFrameData>().context);
+ break;
+ case DeoptFrame::FrameType::kBuiltinContinuationFrame:
+ AddDeoptUse(data.get<DeoptFrame::BuiltinContinuationFrameData>().context);
+ for (ValueNode* node :
+ data.get<DeoptFrame::BuiltinContinuationFrameData>().parameters) {
+ AddDeoptUse(node);
+ }
+ break;
+ }
+}
+
DeoptFrame* MaglevGraphBuilder::GetDeoptFrameForLazyDeoptHelper(
interpreter::Register result_location, int result_size,
LazyDeoptFrameScope* scope, bool mark_accumulator_dead, bool can_throw) {
@@ -1665,6 +1677,8 @@
DCHECK(interpreter::Bytecodes::WritesOrClobbersAccumulator(
iterator_.current_bytecode()));
+ AddDeoptUseToScopeData(scope->data());
+
// Mark the accumulator dead in parent frames since we know that the
// continuation will write it.
return zone()->New<DeoptFrame>(
diff --git a/src/maglev/maglev-graph-builder.h b/src/maglev/maglev-graph-builder.h
index bae2273..cae3658 100644
--- a/src/maglev/maglev-graph-builder.h
+++ b/src/maglev/maglev-graph-builder.h
@@ -1583,6 +1583,8 @@
void AddDeoptUse(VirtualObject* alloc);
void AddNonEscapingUses(InlinedAllocation* allocation, int use_count);
+ void AddDeoptUseToScopeData(DeoptFrame::FrameData& data);
+
std::optional<VirtualObject*> TryGetNonEscapingArgumentsObject(
ValueNode* value);
Original Bug Report
DCHECK failure in use_count_ > 0 in maglev-ir.h
Detailed Report: https://clusterfuzz.com/testcase?key=6688632614092800
Fuzzer: ochang_js_fuzzer Job Type: linux32_asan_d8_dbg Platform Id: linux
Crash Type: DCHECK failure Crash Address: Crash State: use_count_ > 0 in maglev-ir.h V8_Dcheck v8::internal::maglev::ValueNode::remove_use
Sanitizer: address (ASAN)
Regressed: https://clusterfuzz.com/revisions?job=linux32_asan_d8_dbg&range=104326:104327
Reproducer Testcase: https://clusterfuzz.com/download?testcase_id=6688632614092800
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.