CVE-2026-102299
Overview
Background
- Turboshaft late load elimination
- A V8 optimization pass (
LateLoadEliminationAnalyzer) that removes redundant memory loads by tracking, per program point, which values and object maps are known to be live in registers/snapshots. - Map snapshot
- A per-block record of the possible
Map(hidden class) of each object, tracked inobject_maps_and merged across predecessor edges when control flow joins. - Forward edge vs. backedge
- At a loop header the forward edge is the entry from before the loop and the backedge is the edge returning from the loop body;
kForwardEdgeOffsetidentifies the forward-edge predecessor in the merge inputs. - Loop revisit
- When a merge detects that state flowing back on the backedge contradicts the optimistic state assumed on the first pass, the analyzer must re-run (
loop_needs_revisit) so downstream optimizations use sound facts.
Root Cause Analysis
At a loop header, merge_maps computes the merged map set from each predecessor’s maps_snapshot, but it previously omitted the backedge predecessor’s map snapshot entirely (the removed NeighboringPredecessor/IsLoop/LastPredecessor filter) and never compared the merged result against the forward-edge snapshot to decide whether a revisit was needed. When an object’s map is mutated inside the loop — for example via an in-object property addition (o1.q = 5 in the regression test) or a call/allocation that wipes maps — the optimistic forward-edge map remained in effect during pass 1, so alias analysis treated a store through an aliased pointer (b.p = i after b = o1) as not affecting the load of o1.p. Because the stale map prevented that store from invalidating the load, the memory snapshots on both edges stayed identical, so merge_memory also saw no discrepancy and never requested a revisit, and the loop-carried load of o1.p was eliminated against a value that the mutated object no longer holds.
The fix restores the backedge map snapshot to the predecessor set and adds an explicit for_loop_revisit check that sets loop_needs_revisit = true whenever the merged map differs from predecessors[kForwardEdgeOffset], forcing re-analysis with the true (post-mutation) maps. It also converts the empty-predecessor case to break out with a fully unknown MapMaskAndOr{} so the discrepancy is still observed rather than short-circuited.
merge_maps optimistically kept the forward-edge map at a loop header and never checked whether the backedge disagreed, so a map change inside the loop went undetected and load elimination reused a stale, type-incorrect value; the fix adds the missing loop_needs_revisit comparison against the forward-edge snapshot (and stops dropping the backedge snapshot) so the map discrepancy triggers re-analysis.Attack Path
- Craft a loop that mutates an object's map
Author JS where a load (
o1.p) inside a loop is repeated while the same object is aliased and mutated mid-loop (b = o1; o1.q = 5; b.p = i), matching the shape inregress-556908674.js. - Trigger optimization
Warm the function and force Turboshaft compilation (
%OptimizeFunctionOnNextCall), soLateLoadEliminationAnalyzerruns over the loop. - Escape revisit detection
Because
merge_mapsignores the backedge map and never compares to the forward edge, alias analysis uses the stale map, the aliased store fails to invalidate the load, and the redundant load is eliminated without a loop revisit. - Induce type confusion The optimized code reuses the eliminated load value under an assumption about the object’s map that no longer holds after the in-loop map change, yielding a value/field interpreted as the wrong type. (inferred) A well-crafted map transition can turn this into reading a field at an offset valid for the old map but not the new one.
- Escalate Type confusion in V8 is a common primitive for constructing arbitrary read/write within the renderer sandbox. (inferred)
Impact Assessment
Map, producing a type confusion in V8. The advisory classifies this as type confusion in V8 with no exploited-in-the-wild status stated here. Beyond the miscompilation the patch demonstrates, exploitation into a memory read/write primitive and renderer compromise is plausible but not shown by the diff (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/compiler/turboshaft/late-load-elimination-reducer.cc |
modified | |
iftest/mjsunit/turboshaft/regress-556908674.js |
modified | |
fortest/mjsunit/turboshaft/regress-556908674.js |
modified |
Files Changed
test/mjsunit/turboshaft/regress-556908674.jssrc/compiler/turboshaft/late-load-elimination-reducer.cc
Audit Directions
- Optimistic loop-header state without a backedge reconciliationAudit every merge in Turboshaft/Turbofan analyses that seeds loop-header facts from the forward edge and confirm each has a discrepancy check that forces a revisit when the backedge contradicts the assumption.
- Map/alias couplingWhere alias analysis depends on tracked object maps, verify that in-loop map mutations (in-object property additions, transitions, map-wiping calls/allocations) invalidate dependent load-elimination snapshots.
- Short-circuit returns in merge routinesReview merge helpers that early-
returna “top”/unknown value and ensure they still propagate the information needed by later revisit checks rather than hiding a discrepancy.
Patch
From 1aaed11297edb3e3dea3c45e17f70b175de7d626 Mon Sep 17 00:00:00 2001
From: Darius Mercadier <dmercadier@chromium.org>
Date: Wed, 16 Sep 2026 14:57:57 +0200
Subject: [PATCH] [turboshaft] Fix load elimination when map changes in a loop
When an object's map changes inside a loop (e.g. via an in-object property
addition) or maps are wiped by a call/allocation,
LateLoadEliminationAnalyzer's merge_maps must detect the discrepancy
between the forward-edge snapshot and the backedge snapshot and trigger
a loop revisit.
Previously, merge_maps lacked a loop revisit check. Because alias
analysis in pass 1 used the optimistic (stale) forward-edge map, stores
through aliased pointers failed to invalidate loads, leaving memory
snapshots identical on both edges and preventing merge_memory from
detecting the need for a revisit.
This CL adds the missing loop_needs_revisit check in merge_maps and
cleans up the redundant predecessor map snapshot filtering.
TAG=agy
CONV=da17fd52-5094-4a27-b768-97cf84ff9eff
Fixed: 556908674
Change-Id: Ie7838faee3e3f99b2bced71a93e79319500a807e
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8352970
Commit-Queue: Jakob Kummerow <jkummerow@chromium.org>
Reviewed-by: Jakob Kummerow <jkummerow@chromium.org>
Auto-Submit: Darius Mercadier <dmercadier@chromium.org>
Cr-Commit-Position: refs/heads/main@{#109907}
---
diff --git a/src/compiler/turboshaft/late-load-elimination-reducer.cc b/src/compiler/turboshaft/late-load-elimination-reducer.cc
index d9e386f..1a3e7d7 100644
--- a/src/compiler/turboshaft/late-load-elimination-reducer.cc
+++ b/src/compiler/turboshaft/late-load-elimination-reducer.cc
@@ -800,13 +800,7 @@
// edge of the inner loop should reflect this restriction.
predecessor_alias_snapshots_.push_back(pred_snapshots->alias_snapshot);
predecessor_memory_snapshots_.push_back(pred_snapshots->memory_snapshot);
- if (p->NeighboringPredecessor() != nullptr || !block->IsLoop() ||
- block->LastPredecessor() != p) {
- // We only add a MapSnapshot predecessor for non-backedge predecessor.
- // This is because maps coming from inside of the loop may be wrong
- // until a specific check has been executed.
- predecessor_maps_snapshots_.push_back(pred_snapshots->maps_snapshot);
- }
+ predecessor_maps_snapshots_.push_back(pred_snapshots->maps_snapshot);
}
}
@@ -839,10 +833,14 @@
if (is_empty(pred)) {
// One of the predecessors doesn't have maps for this object, so we have
// to assume that this object could have any map.
- return MapMaskAndOr{};
+ minmax = MapMaskAndOr{};
+ break;
}
minmax = CombineMinMax(minmax, pred);
}
+ if (for_loop_revisit && minmax != predecessors[kForwardEdgeOffset]) {
+ loop_needs_revisit = true;
+ }
return minmax;
};
object_maps_.StartNewSnapshot(base::VectorOf(predecessor_maps_snapshots_),
diff --git a/test/mjsunit/turboshaft/regress-556908674.js b/test/mjsunit/turboshaft/regress-556908674.js
new file mode 100644
index 0000000..554fc54
--- /dev/null
+++ b/test/mjsunit/turboshaft/regress-556908674.js
@@ -0,0 +1,37 @@
+// 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 --turbofan --no-turboshaft-loop-unrolling
+
+function f(o1, o2, n) {
+ if (n) {
+ let w = o1;
+ w.p;
+ } else {
+ let w = o1;
+ w.p;
+ }
+
+ let x = o1.p;
+ let b = o2;
+ let total = 0;
+ for (let i = 0; i < 10; i++) {
+ b.p = i;
+ total += o1.p;
+ if (i === 2) {
+ o1.q = 5;
+ b = o1;
+ }
+ }
+ return total + x;
+}
+
+%PrepareFunctionForOptimization(f);
+f({p: 7}, {p: 7, r: 1}, 0);
+f({p: 7}, {p: 7, r: 1}, 1);
+%OptimizeFunctionOnNextCall(f);
+
+let res = f({p: 7}, {p: 7, r: 1}, 0);
+assertEquals(70, res);
+assertOptimized(f);
Regression Test / PoC
diff --git a/test/mjsunit/turboshaft/regress-556908674.js b/test/mjsunit/turboshaft/regress-556908674.js
new file mode 100644
index 0000000..554fc54
--- /dev/null
+++ b/test/mjsunit/turboshaft/regress-556908674.js
@@ -0,0 +1,37 @@
+// 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 --turbofan --no-turboshaft-loop-unrolling
+
+function f(o1, o2, n) {
+ if (n) {
+ let w = o1;
+ w.p;
+ } else {
+ let w = o1;
+ w.p;
+ }
+
+ let x = o1.p;
+ let b = o2;
+ let total = 0;
+ for (let i = 0; i < 10; i++) {
+ b.p = i;
+ total += o1.p;
+ if (i === 2) {
+ o1.q = 5;
+ b = o1;
+ }
+ }
+ return total + x;
+}
+
+%PrepareFunctionForOptimization(f);
+f({p: 7}, {p: 7, r: 1}, 0);
+f({p: 7}, {p: 7, r: 1}, 1);
+%OptimizeFunctionOnNextCall(f);
+
+let res = f({p: 7}, {p: 7, r: 1}, 0);
+assertEquals(70, res);
+assertOptimized(f);