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: Primitive overclaimed: diff shows only a deopt use-count underflow (bookkeeping); commit calls over-count 'not really a correctness issue' and does not show memory corruption, so corruption is inferred, not demonstrated.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker470566252
Fix commita0570afad500 (v8/v8) +27/-11
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-21 (per vendor advisory)
Fix landed2026-01-09 (public git)
Disclosed2026-04-07
Patch gap88 days — fix public in source before the advisory

Background

Maglev
V8’s mid-tier optimizing JIT compiler that builds a graph of ValueNodes from bytecode via MaglevGraphBuilder.
Deopt use counting
AddDeoptUse records that a ValueNode is 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 carries DeoptFrame::FrameData describing 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 the ValueNodes (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.

Key insight
The single core mistake was counting deopt uses at scope-data creation time instead of at deopt-frame creation time, so scopes reused for multiple frames were under-counted; moving the counting into AddDeoptUseToScopeData called per deopt frame restores an exact one-count-per-use accounting that cannot underflow.

Attack Path

  1. Reach Maglev compilation Trigger tiering-up of a JavaScript function into the Maglev compiler so MaglevGraphBuilder builds the graph and deopt metadata for it.
  2. Create a reusable deopt scope Cause a construct-invoke stub or builtin-continuation deopt scope to be established, whose context/receiver/parameters ValueNodes get their deopt use counted once at scope-data creation.
  3. 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.
  4. 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.
  5. 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

An attacker running crafted JavaScript in the renderer can corrupt Maglev’s deopt use / liveness bookkeeping via counter underflow, leading to incorrect optimized code generation and potential memory-safety corruption in the renderer process. The precondition is that the victim function tier up into Maglev with a deopt scope that is materialized into more than one deopt frame, so a use removal can drive the counter negative. Exploitation is confined to the sandboxed renderer process but the resulting bookkeeping corruption is a plausible primitive for further compromise.

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

Audit Directions

  • Creation-time vs consumption-time counting
    Audit 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 objects
    Review DeoptFrameScope and 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 underflow
    Search for AddDeoptUse/decrement pairs and other use counters in maglev-graph-builder that can be decremented below zero, where an under-count leads to underflow and corrupted liveness state.
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);
 
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=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.

View on issue tracker