Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in V8
DescriptionOut of bounds write in V8
ComponentV8
Sub-componentTurboshaft
Bug ClassOOB
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: Stale per-alias map invalidation and the wipe-all fix match the diff. The claim that the transition changed object size so an access hits an invalid offset is not shown; the visible tests show only a wrongly eliminated reload.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker417169470
Fix commit37d6fa3f39e1 (v8/v8) +125/-4
CWECWE-787 (Out-of-bounds write)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
Credited[pwn2car]
Reported2025-05-12 (per vendor advisory)
Fix landed2025-05-19 (public git)
Disclosed2025-05-27
Patch gap8 days — fix public in source before the advisory

Background

Late Load Elimination (LLE)
a Turboshaft compiler pass that removes redundant memory loads by proving that no intervening store can have written to the same location.
Map-based alias analysis
LLE tracks each object’s known map(s) in object_maps_ to decide whether two accesses can refer to the same object; distinct maps imply the objects cannot alias.
Map store / transitioning store
a write to HeapObject::kMapOffset that changes an object’s map, as happens when adding a property triggers a map transition.
`MapMaskAndOr`
the summarized map-information value stored per object in object_maps_; setting it to MapMaskAndOr{} wipes (invalidates) that object’s known-map knowledge.

Root Cause Analysis

In src/compiler/turboshaft/late-load-elimination-reducer.cc, when LLE saw a store to HeapObject::kMapOffset (a transitioning store), the old code only invalidated the map entry for that exact store base: if (object_maps_.HasKeyFor(store.base())) object_maps_.Set(store.base(), MapMaskAndOr{}). But at runtime, other tracked OpIndex values can refer to the same object as store.base(), so after the transition their recorded maps in object_maps_ become stale while remaining marked as known. Using that stale map knowledge, the alias analysis could wrongly conclude that a later store (e.g. c.x = 17) does not alias an earlier loaded field (a.x), and eliminate the reload — even though a, b, and c are the same transitioned object — yielding an incorrect result and, because the transition changed the object’s layout/size, a store or load directed at an offset that is no longer valid for the new shape.

The fix conservatively wipes all entries by iterating object_maps_ and setting every key to MapMaskAndOr{}, so no stale map survives a map store; the added begin()/end() iterators on SparseOpIndexSnapshotTable (src/compiler/turboshaft/snapshot-table-opindex.h) make that iteration possible. The regression tests confirm the intended semantics: after b.y = 99 transitions the shared object, foo must return 17, not the stale 42.

Key insight
The single mistake was invalidating map knowledge only for the literal store base after a map transition, ignoring other tracked indices that alias the same object; the fix wipes the entire object_maps_ table on any map store so no stale, over-precise map can drive an unsound alias disambiguation.

Attack Path

  1. Shape a transition Script defines a function like foo(a,b,c){ a.x=42; b.y=99; c.x=17; return a.x; } where b.y = 99 is a transitioning store, then runs it so the same object is passed as all three arguments.
  2. Optimize %OptimizeFunctionOnNextCall drives Turboshaft to compile foo; LLE records maps for the accesses to a, b, and c.
  3. Stale map survives transition At the b.y = 99 map store, only b’s map entry is wiped, so the entries for the aliasing a/c retain pre-transition maps and LLE treats the later access as non-aliasing.
  4. Unsound elimination LLE eliminates the reload of a.x (or mis-disambiguates the c.x store), so the compiled code reads/writes a field at an offset valid only for the pre-transition shape (inferred as the OOB write mechanism the advisory names, since the diff shows the alias-analysis flaw and the regression tests, not the concrete faulting access).

Impact Assessment

An attacker with script execution in the renderer can cause the Turboshaft optimizer to emit code with an unsound alias assumption, producing an out-of-bounds write in V8 (per the advisory, “Out of bounds write in V8”), which is a standard foundation for memory corruption. Preconditions are only that the victim function be JIT-compiled with a transitioning store between aliasing accesses; this is reachable from ordinary optimized JavaScript. The advisory does not report in-the-wild exploitation, and escalation to a full renderer exploit beyond the OOB write is not demonstrated by this diff (inferred).

Changed Functions

FunctionChangeNotes
for
src/compiler/turboshaft/late-load-elimination-reducer.cc
modified

Files Changed

  • src/compiler/turboshaft/late-load-elimination-reducer.cc
  • src/compiler/turboshaft/snapshot-table-opindex.h
  • src/compiler/turboshaft/store-store-elimination-phase.cc
  • src/flags/flag-definitions.h
  • test/mjsunit/turboshaft/regress-417169470-1.js
  • test/mjsunit/turboshaft/regress-417169470-2.js
  • test/mjsunit/turboshaft/regress-417169470-3.js
  • test/mjsunit/turboshaft/regress-417169470-4.js

Audit Directions

  • Aliasing vs. keyed invalidation
    Review any analysis that invalidates cached facts by a single key (base/OpIndex) after an operation that can affect other objects aliasing that key, and check that all potentially-affected entries are invalidated.
  • Map-transition side effects
    Audit every optimization that caches object maps or layout to ensure map/transition stores conservatively drop cached shape knowledge for all tracked objects, not just the store target.
  • Snapshot-table consumers
    Now that SparseOpIndexSnapshotTable is iterable, review other passes for opportunities where per-key updates should have been whole-table updates on globally-invalidating events.
From 37d6fa3f39e17bf46d1cdf340e666cbd3ff976b3 Mon Sep 17 00:00:00 2001
From: Darius Mercadier <dmercadier@chromium.org>
Date: Mon, 19 May 2025 14:48:48 +0200
Subject: [PATCH] [turboshaft] Fix map-based alias analysis in Late Load Elimination

Fixed: 417169470
Change-Id: I589f6667ce3b26b07a4dffa707ee78f9d642d409
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/6564404
Reviewed-by: Marja Hölttä <marja@chromium.org>
Commit-Queue: Darius Mercadier <dmercadier@chromium.org>
Cr-Commit-Position: refs/heads/main@{#100349}
---

diff --git a/src/compiler/turboshaft/late-load-elimination-reducer.cc b/src/compiler/turboshaft/late-load-elimination-reducer.cc
index 7b21b84..e70e556 100644
--- a/src/compiler/turboshaft/late-load-elimination-reducer.cc
+++ b/src/compiler/turboshaft/late-load-elimination-reducer.cc
@@ -409,11 +409,17 @@
     non_aliasing_objects_.Set(value, false);
   }
 
-  // If we just stored a map, invalidate the maps for this base.
+  // If we just stored a map, invalidate all object_maps_.
   if (store.offset == HeapObject::kMapOffset && !store.index().valid()) {
-    if (object_maps_.HasKeyFor(store.base())) {
-      TRACE(">> Wiping map\n");
-      object_maps_.Set(store.base(), MapMaskAndOr{});
+    // TODO(dmercadier): can we only do this for objects that are potentially
+    // aliasing with the `base` (based on their maps and the maps of `base`)?
+    // Also, it might be worth to record a new map if this is actually a map
+    // store.
+    // TODO(dmercadier): do this only if `value` is a Constant with kind
+    // kHeapObject, since all map stores should store a known constant maps.
+    TRACE(">> Wiping all maps\n");
+    for (auto it : object_maps_) {
+      object_maps_.Set(it.second, MapMaskAndOr{});
     }
   }
 }
diff --git a/src/compiler/turboshaft/snapshot-table-opindex.h b/src/compiler/turboshaft/snapshot-table-opindex.h
index 733ef19..496065d 100644
--- a/src/compiler/turboshaft/snapshot-table-opindex.h
+++ b/src/compiler/turboshaft/snapshot-table-opindex.h
@@ -60,6 +60,9 @@
     return std::nullopt;
   }
 
+  auto begin() { return indices_to_keys_.begin(); }
+  auto end() { return indices_to_keys_.end(); }
+
  private:
   Key GetOrCreateKey(OpIndex idx) {
     auto it = indices_to_keys_.find(idx);
diff --git a/src/compiler/turboshaft/store-store-elimination-phase.cc b/src/compiler/turboshaft/store-store-elimination-phase.cc
index 1204683..ec00650 100644
--- a/src/compiler/turboshaft/store-store-elimination-phase.cc
+++ b/src/compiler/turboshaft/store-store-elimination-phase.cc
@@ -18,6 +18,9 @@
 namespace v8::internal::compiler::turboshaft {
 
 void StoreStoreEliminationPhase::Run(PipelineData* data, Zone* temp_zone) {
+  UnparkedScopeIfNeeded unparked_scope(
+      data->broker(), v8_flags.turboshaft_trace_load_elimination);
+
   turboshaft::CopyingPhase<
       LoopStackCheckElisionReducer, StoreStoreEliminationReducer,
       LateLoadEliminationReducer, MachineOptimizationReducer,
diff --git a/src/flags/flag-definitions.h b/src/flags/flag-definitions.h
index cf1e168..cdf3b2b 100644
--- a/src/flags/flag-definitions.h
+++ b/src/flags/flag-definitions.h
@@ -1644,6 +1644,8 @@
                      "trace emitted Turboshaft instructions")
 DEFINE_BOOL_READONLY(turboshaft_trace_intermediate_reductions, false,
                      "trace intermediate Turboshaft reduction steps")
+DEFINE_BOOL_READONLY(turboshaft_trace_load_elimination, false,
+                     "trace Turboshaft's late load elimination")
 #endif  // DEBUG
 
 DEFINE_BOOL(profile_guided_optimization, true, "profile guided optimization")
diff --git a/test/mjsunit/turboshaft/regress-417169470-1.js b/test/mjsunit/turboshaft/regress-417169470-1.js
new file mode 100644
index 0000000..ad0dd39
--- /dev/null
+++ b/test/mjsunit/turboshaft/regress-417169470-1.js
@@ -0,0 +1,24 @@
+// Copyright 2025 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 --turbofan
+
+function foo(a, b, c) {
+  a.x = 42;
+  b.y = 99; // Transitioning store
+  c.x = 17;
+  return a.x;
+}
+
+// Using a constructor so that we don't go out of object when creating the .y
+// property in foo.
+function MyObject() { this.x = 27; }
+let o1 = new MyObject();
+let o2 = new MyObject();
+
+%PrepareFunctionForOptimization(foo);
+assertEquals(17, foo(o1, o1, o1));
+
+%OptimizeFunctionOnNextCall(foo);
+assertEquals(17, foo(o2, o2, o2));
diff --git a/test/mjsunit/turboshaft/regress-417169470-2.js b/test/mjsunit/turboshaft/regress-417169470-2.js
new file mode 100644
index 0000000..f5ec903
--- /dev/null
+++ b/test/mjsunit/turboshaft/regress-417169470-2.js
@@ -0,0 +1,21 @@
+// Copyright 2025 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 --turbofan
+
+function foo(a, b, c) {
+  a.x = 42;
+  b.y = 99; // Transitioning store
+  c.x = 17;
+  return a.x;
+}
+
+let o1 = { x : 27 };
+let o2 = { x : 27 };
+
+%PrepareFunctionForOptimization(foo);
+assertEquals(17, foo(o1, o1, o1));
+
+%OptimizeFunctionOnNextCall(foo);
+assertEquals(17, foo(o2, o2, o2));
diff --git a/test/mjsunit/turboshaft/regress-417169470-3.js b/test/mjsunit/turboshaft/regress-417169470-3.js
new file mode 100644
index 0000000..064946c
--- /dev/null
+++ b/test/mjsunit/turboshaft/regress-417169470-3.js
@@ -0,0 +1,31 @@
+// Copyright 2025 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 --turbofan
+
+function foo(a, b, c) {
+  a.x = 42;
+  b.y = 99; // Transitioning store
+  c.x = 17;
+  return a.x;
+}
+
+let o1 = { x : 27 };
+// If we add additional properties, we need to add at least 2 so that `b.y = 99`
+// doesn't end up at offset 12, because that's also the offset of .x (which is
+// in-object rather than out-of-object), and because we don't have map
+// information for backing stores in Late load elimination, we'll assume that
+// the store at b.y can alias with the a.x that was previously loaded.
+o1.unused1 = 12;
+o1.unused2 = 12;
+
+let o2 = { x : 27 };
+o2.unused1 = 12;
+o2.unused2 = 12;
+
+%PrepareFunctionForOptimization(foo);
+assertEquals(17, foo(o1, o1, o1));
+
+%OptimizeFunctionOnNextCall(foo);
+assertEquals(17, foo(o2, o2, o2));
diff --git a/test/mjsunit/turboshaft/regress-417169470-4.js b/test/mjsunit/turboshaft/regress-417169470-4.js
new file mode 100644
index 0000000..d632a8f
--- /dev/null
+++ b/test/mjsunit/turboshaft/regress-417169470-4.js
@@ -0,0 +1,31 @@
+// Copyright 2025 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 --turbofan
+
+function opt() {
+  const nop = 0;
+  const empty = {};
+  const a = {p1: 42.42};
+
+  function foo(b) {
+    nop;
+
+    a.p4 = 42;
+    b.p2 = 42;
+    b.p5 = empty;
+    a.p6 = 41.414;
+  }
+
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/test/mjsunit/turboshaft/regress-417169470-1.js b/test/mjsunit/turboshaft/regress-417169470-1.js
new file mode 100644
index 0000000..ad0dd39
--- /dev/null
+++ b/test/mjsunit/turboshaft/regress-417169470-1.js
@@ -0,0 +1,24 @@
+// Copyright 2025 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 --turbofan
+
+function foo(a, b, c) {
+  a.x = 42;
+  b.y = 99; // Transitioning store
+  c.x = 17;
+  return a.x;
+}
+
+// Using a constructor so that we don't go out of object when creating the .y
+// property in foo.
+function MyObject() { this.x = 27; }
+let o1 = new MyObject();
+let o2 = new MyObject();
+
+%PrepareFunctionForOptimization(foo);
+assertEquals(17, foo(o1, o1, o1));
+
+%OptimizeFunctionOnNextCall(foo);
+assertEquals(17, foo(o2, o2, o2));
diff --git a/test/mjsunit/turboshaft/regress-417169470-2.js b/test/mjsunit/turboshaft/regress-417169470-2.js
new file mode 100644
index 0000000..f5ec903
--- /dev/null
+++ b/test/mjsunit/turboshaft/regress-417169470-2.js
@@ -0,0 +1,21 @@
+// Copyright 2025 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 --turbofan
+
+function foo(a, b, c) {
+  a.x = 42;
+  b.y = 99; // Transitioning store
+  c.x = 17;
+  return a.x;
+}
+
+let o1 = { x : 27 };
+let o2 = { x : 27 };
+
+%PrepareFunctionForOptimization(foo);
+assertEquals(17, foo(o1, o1, o1));
+
+%OptimizeFunctionOnNextCall(foo);
+assertEquals(17, foo(o2, o2, o2));
diff --git a/test/mjsunit/turboshaft/regress-417169470-3.js b/test/mjsunit/turboshaft/regress-417169470-3.js
new file mode 100644
index 0000000..064946c
--- /dev/null
+++ b/test/mjsunit/turboshaft/regress-417169470-3.js
@@ -0,0 +1,31 @@
+// Copyright 2025 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 --turbofan
+
+function foo(a, b, c) {
+  a.x = 42;
+  b.y = 99; // Transitioning store
+  c.x = 17;
+  return a.x;
+}
+
+let o1 = { x : 27 };
+// If we add additional properties, we need to add at least 2 so that `b.y = 99`
+// doesn't end up at offset 12, because that's also the offset of .x (which is
+// in-object rather than out-of-object), and because we don't have map
+// information for backing stores in Late load elimination, we'll assume that
+// the store at b.y can alias with the a.x that was previously loaded.
+o1.unused1 = 12;
+o1.unused2 = 12;
+
+let o2 = { x : 27 };
+o2.unused1 = 12;
+o2.unused2 = 12;
+
+%PrepareFunctionForOptimization(foo);
+assertEquals(17, foo(o1, o1, o1));
+
+%OptimizeFunctionOnNextCall(foo);
+assertEquals(17, foo(o2, o2, o2));
diff --git a/test/mjsunit/turboshaft/regress-417169470-4.js b/test/mjsunit/turboshaft/regress-417169470-4.js
new file mode 100644
index 0000000..d632a8f
--- /dev/null
+++ b/test/mjsunit/turboshaft/regress-417169470-4.js
@@ -0,0 +1,31 @@
+// Copyright 2025 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 --turbofan
+
+function opt() {
+  const nop = 0;
+  const empty = {};
+  const a = {p1: 42.42};
+
+  function foo(b) {
+    nop;
+
+    a.p4 = 42;
+    b.p2 = 42;
+    b.p5 = empty;
+    a.p6 = 41.414;
+  }
+
+  a.p3 = 42;
+  a.p4 = 42;
+
+  for (let i = 0; i < 300; i++) {
+    foo(a);
+  }
+}
+
+opt();
+opt();
+opt();
Loading diff…

Original Bug Report

reported by pw...@gmail.com

V8 Turboshaft Late Load Elimination Aliasing bug leads to Memory Corruption

Steps to reproduce the problem

  1. ./d8 –expose-externalize-string –omit-quit –allow-natives-syntax –fuzzing –jit-fuzzing –future –harmony –js-staging –wasm-staging –wasm-fast-api –expose-fast-api –turbolev –shared-string-table poc.js

Problem Description

I’ll analyze it and upload it soon.

poc.js

function opt(){
        function v3() {
                const v4 = {"B":42};
        }
        const v2 = {};
        const v6 = {"C":42.42};
        function v10(v11) {
                function v13() {
                        v6.e = 41.414;
                        for (let v19 = 0; v19 < 100; v19++) {
                                //const v4 = {"B":42};
                                v3();
                        }
                }
                v6.d = 42;
                v11.b = 42;
                v11.f = v2;
                for (let v26 = 0; v26 < 100; v26++) {
                        v13();
                }
        }
        v6.c = 42;
        v6.d = v2;
        for (let v36 = 0; v36 < 1000; v36++) {
                v10(v6);
        }
}
opt();
opt();
opt();

Summary

V8 Turbolev Memory Corruption

Custom Questions

Crash state:

./d8 –expose-externalize-string –omit-quit –allow-natives-syntax –fuzzing –jit-fuzzing –future –harmony –js-staging –wasm-staging –wasm-fast-api –expose-fast-api –turbolev –shared-string-table test3.js [COV] no shared memory bitmap available, skipping [COV] edge counters initialized. Shared memory: (null) with 1520960 edges V8 is running with experimental features enabled. Stability and security will suffer. Received signal 11 SEGV_ACCERR 7e8a00000014

==== C stack trace ===============================

./d8(___interceptor_backtrace+0x46)[0x5a98942fd856] ./d8(+0x26e5959)[0x5a98947a5959] /lib/x86_64-linux-gnu/libc.so.6(+0x42520)[0x78df3e842520] [0x5a98e0002849] [end of stack trace] Segmentation fault

Reporter credit:

un3xploitable

Additional Data

Category: Security
Chrome Channel: Not sure
Regression: N/A

View on issue tracker