High patch-now ✔︎ verified CVSS 8.8 chrome UAF 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in V8
DescriptionUse after free in V8
ComponentV8
Sub-componentTurbofan
Bug ClassUAF
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — upheld 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
Reviewer notes: Diff/commit confirm the UseCount>1 bailout skipping sibling renames, leaving a cloned FrameState with a dangling IR node. Commit cites verifier failures; the 'controlled memory corruption' escalation is inferred (advisory does classify it UAF).
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker558734727
Fix commit1926a0d990ff (v8/v8) +33/-2
CWECWE-416 (Use-after-free)
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-09-08 (per vendor advisory)
Fix landed2026-09-09 (public git)
Disclosed2026-09-15
Patch gap6 days — fix public in source before the advisory

Background

TurboFan
V8’s optimizing JIT compiler, which builds and mutates a sea-of-nodes IR graph where each node’s UseCount() tracks how many other nodes reference it.
`FrameState` / `StateValues`
IR nodes that capture the interpreter/deopt state; StateValues holds the live value inputs, and when a node exceeds 8 inputs it is split into a tree of nested StateValues children.
`JSInliningHeuristic::TryReuseDispatch`
the pass that specializes and duplicates frame states across the branches of an inlined polymorphic (megamorphic) call site, so each callee branch has its own renamed state.
`DuplicateStateValuesAndRename`
the recursive helper that walks a StateValues tree, either mutating in place (kChangeInPlace) or copy-on-writing via CloneNode, while renaming a callee Phi reference to the branch-specific value.

Root Cause Analysis

DuplicateStateValuesAndRename bailed out early with if (state_values->UseCount() > 1) return state_values;, treating any shared node as immutable and returning it unrenamed. For a large StateValues node (>8 inputs) that has been split into a tree, renaming an earlier child subtree causes the parent to be cloned via CloneNode, and cloning bumps the UseCount() of every sibling child subtree from 1 to 2 because the clone now also references them. When the recursion then descended into a sibling subtree, it observed UseCount() > 1 and returned immediately without renaming the callee Phi inside it. The cloned FrameState therefore retained a reference to the original callee Phi in those siblings instead of the branch-specific replacement. When that callee Phi was subsequently killed/replaced during inlining, the cloned FrameState was left pointing at a dead IR node — a dangling reference.

The fix drops the early bailout and instead computes Node* copy = (mode == kChangeInPlace && state_values->UseCount() <= 1) ? state_values : nullptr;, so a shared node is still traversed and renamed via copy-on-write rather than skipped.

Key insight
The UseCount() > 1 bailout conflated “shared, so don’t mutate in place” with “already handled, so skip entirely” — but a sibling’s UseCount became >1 as an artifact of cloning its own parent, meaning nodes that still needed renaming were silently skipped.

Attack Path

  1. Reach the heuristic Craft JavaScript with a polymorphic call site whose callee is inlined so that JSInliningHeuristic::TryReuseDispatch duplicates and specializes the frame states across branches.
  2. Force a StateValues tree Arrange enough live values (>8 inputs) at the call site that the StateValues node splits into a tree of nested children.
  3. Trigger the sibling skip Ensure an earlier child subtree is renamed first, so CloneNode on the parent inflates sibling UseCount() to 2, causing DuplicateStateValuesAndRename to bail on the sibling and leave its callee Phi unrenamed.
  4. Kill the retained node Let later inlining/reduction kill or replace that callee Phi, leaving the cloned FrameState holding a dangling reference to the freed IR node.
  5. Exploit downstream Use the corrupted deopt state / dangling node to drive memory corruption during optimization or at deopt.

Impact Assessment

This is a use-after-free / dangling-node defect in TurboFan: a cloned FrameState retains a reference to an IR node that is later killed, which surfaces at minimum as a graph verifier failure and, in a release build, as a dangling reference consumed during compilation or deoptimization. Because deopt frame state governs how the runtime reconstructs interpreter state, a corrupted or freed node reachable through it is a strong primitive for controlled memory corruption. Exploitation is reachable from untrusted JavaScript, making it a high-severity renderer-side V8 vulnerability.

Files Changed

  • test/mjsunit/compiler/regress-558734727.js
  • src/compiler/js-inlining-heuristic.cc

Audit Directions

  • UseCount-based bailouts during graph mutation
    Audit any pass that skips a node when UseCount() > 1; if the same pass clones parents via CloneNode, sibling use counts change mid-traversal and “shared” can no longer be treated as “already handled.”
  • Recursive StateValues/FrameState rewriting
    Review other recursive StateValues walkers (renaming, duplication, specialization) for the >8-input tree-split edge case where nested children are shared and must be copy-on-written rather than short-circuited.
  • Dangling references from cloned deopt state
    Look for spots where cloned FrameState nodes may retain references to nodes that are later killed or replaced, especially Phi nodes rewritten during inlining, as these produce dangling-node UAFs that only manifest under verifier or at deopt.
From 1926a0d990ffa5db0c27cfa1130eba651e1a9ffb Mon Sep 17 00:00:00 2001
From: Leszek Swirski <leszeks@chromium.org>
Date: Wed, 09 Sep 2026 16:58:35 +0200
Subject: [PATCH] [turbofan] Fix UseCount bailout in DuplicateStateValuesAndRename

In JSInliningHeuristic::TryReuseDispatch, when duplicating and
specializing frame states for inlined polymorphic call branches,
DuplicateStateValuesAndRename recursively traversed StateValues trees.
However, it bailed out early if state_values->UseCount() > 1.

When a StateValues node has more than 8 inputs, it is split into a tree
of nested StateValues nodes. When renaming in an earlier child subtree,
the parent StateValues is cloned via CloneNode, which immediately
increments the UseCount of all sibling child subtrees from 1 to 2.
Subsequent recursive calls encountered UseCount() > 1 and bailed out
without renaming the callee Phi in the sibling subtrees. When the callee
Phi was later killed, the cloned FrameState retained dangling references
to the dead node, causing verifier check failures.

This CL removes the premature UseCount() > 1 early return from
DuplicateStateValuesAndRename and ensures that kChangeInPlace only
mutates when UseCount() <= 1, falling back to cloning (copy-on-write)
otherwise.

Note that removing this early return does not cause unnecessary copying
for StateValues subtrees that already had UseCount() > 1 initially (e.g.
shared with other frame states): if such a subtree contained the callee,
CollectStateValuesOwnedUses would have skipped it, causing subsequent
validation in TryReuseDispatch to abort before cloning began. Therefore,
any subtree with UseCount() > 1 initially is guaranteed not to contain
the callee, so DuplicateStateValuesAndRename visits its inputs, finds no
matches, and returns the original node without copying.

Fixed: 558734727
TAG=agy
CONV=cc6d3524-1bd6-4a59-892f-3bae5a2cc132

Change-Id: I09116bd7eed39def19a5d15f9919db2f302c13c4
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8378907
Auto-Submit: Leszek Swirski <leszeks@chromium.org>
Commit-Queue: Leszek Swirski <leszeks@chromium.org>
Reviewed-by: Patrick Thier <pthier@chromium.org>
Commit-Queue: Patrick Thier <pthier@chromium.org>
Cr-Commit-Position: refs/heads/main@{#109756}
---
diff --git a/src/compiler/js-inlining-heuristic.cc b/src/compiler/js-inlining-heuristic.cc
index fd43d7a..b6d9a3f 100644
--- a/src/compiler/js-inlining-heuristic.cc
+++ b/src/compiler/js-inlining-heuristic.cc
@@ -423,8 +423,9 @@
                                                          StateCloneMode mode) {
   // Only rename in states that are not shared with other users. This needs to
   // be in sync with the condition in {CollectStateValuesOwnedUses}.
-  if (state_values->UseCount() > 1) return state_values;
-  Node* copy = mode == kChangeInPlace ? state_values : nullptr;
+  Node* copy = (mode == kChangeInPlace && state_values->UseCount() <= 1)
+                   ? state_values
+                   : nullptr;
   for (int i = 0; i < state_values->InputCount(); i++) {
     Node* input = state_values->InputAt(i);
     Node* processed;
diff --git a/test/mjsunit/compiler/regress-558734727.js b/test/mjsunit/compiler/regress-558734727.js
new file mode 100644
index 0000000..4642ee5
--- /dev/null
+++ b/test/mjsunit/compiler/regress-558734727.js
@@ -0,0 +1,30 @@
+// 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: --turbofan --allow-natives-syntax
+
+function f1(a, b) { return a + b + 1; }
+function f2(a, b) { return a + b + 2; }
+
+function test(cond) {
+  var f = cond ? f1 : f2;
+  var v0 = f;
+  var v1 = 1, v2 = 2, v3 = 3, v4 = 4, v5 = 5, v6 = 6, v7 = 7;
+  var v8 = (v1 ? f : f);
+  var v9 = 9, v10 = 10, v11 = 11, v12 = 12, v13 = 13, v14 = 14, v15 = 15;
+  var res = f(v1, v15);
+  var temp0 = v0;
+  temp0 = 0;
+  var temp8 = v8;
+  return res + v1 + v2 + v3 + v4 + v5 + v6 + v7 + v9 + v10 + v11 + v12 + v13 + v14 + v15;
+}
+
+%PrepareFunctionForOptimization(f1);
+%PrepareFunctionForOptimization(f2);
+%PrepareFunctionForOptimization(test);
+assertEquals(129, test(true));
+assertEquals(130, test(false));
+%OptimizeFunctionOnNextCall(test);
+assertEquals(129, test(true));
+assertEquals(130, test(false));
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/test/mjsunit/compiler/regress-558734727.js b/test/mjsunit/compiler/regress-558734727.js
new file mode 100644
index 0000000..4642ee5
--- /dev/null
+++ b/test/mjsunit/compiler/regress-558734727.js
@@ -0,0 +1,30 @@
+// 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: --turbofan --allow-natives-syntax
+
+function f1(a, b) { return a + b + 1; }
+function f2(a, b) { return a + b + 2; }
+
+function test(cond) {
+  var f = cond ? f1 : f2;
+  var v0 = f;
+  var v1 = 1, v2 = 2, v3 = 3, v4 = 4, v5 = 5, v6 = 6, v7 = 7;
+  var v8 = (v1 ? f : f);
+  var v9 = 9, v10 = 10, v11 = 11, v12 = 12, v13 = 13, v14 = 14, v15 = 15;
+  var res = f(v1, v15);
+  var temp0 = v0;
+  temp0 = 0;
+  var temp8 = v8;
+  return res + v1 + v2 + v3 + v4 + v5 + v6 + v7 + v9 + v10 + v11 + v12 + v13 + v14 + v15;
+}
+
+%PrepareFunctionForOptimization(f1);
+%PrepareFunctionForOptimization(f2);
+%PrepareFunctionForOptimization(test);
+assertEquals(129, test(true));
+assertEquals(130, test(false));
+%OptimizeFunctionOnNextCall(test);
+assertEquals(129, test(true));
+assertEquals(130, test(false));
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.