Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType Confusion in V8
DescriptionType Confusion in V8
ComponentV8
Sub-componentTurboshaft
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
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker508811477
Fix commitdf948e672588 (v8/v8) +8/-0
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
CreditedMufeed VH from Winfunc Research (winfunc.com)
Reported2026-05-02 · bounty $500 (per vendor advisory)
Fix landed2026-05-06 (public git)
Disclosed2026-06-02
Patch gap27 days — fix public in source before the advisory

Background

`late-load-elimination-reducer.cc`
The Turboshaft compiler pass that removes redundant loads by tracking known memory and object state across the IR graph.
Load elimination
An optimization that reuses a previously-loaded value (including known object maps) instead of re-emitting a load or a map check.
`memory_.InvalidateMaybeAliasing()`
A call that discards cached load values for every object that could be aliased and mutated by an unknown callee.
`WipeAllMaps()`
A helper that clears all recorded map knowledge, forcing later map-dependent operations to re-check an object’s map.

Root Cause Analysis

When the reducer encountered a Call operation, it invoked memory_.InvalidateMaybeAliasing() to drop cached load values for objects the callee might mutate, but it never cleared its record of known object maps. A Call can transition an object to a different map (e.g. changing element kinds or adding properties), so the compiler’s assumption that a previously-observed map still holds after the call is unsound. Relying on this stale map knowledge, the optimizer would elide a map check that was actually required, allowing subsequent map-dependent accesses to treat an object as a type it no longer was.

The fix adds WipeAllMaps() immediately after InvalidateMaybeAliasing() on the Call path, so all map assumptions are discarded and the necessary map check is preserved.

Key insight
The single mistake was invalidating aliasing memory on a Call while leaving known maps untouched, ignoring that a callee can transition object maps; the fix closes the gap by wiping all recorded maps with WipeAllMaps() after the call.

Attack Path

  1. Prime the map Script creates an object and runs code so Turboshaft records a stable known map for it, enabling later map-check elimination.
  2. Interpose a transitioning call A Call is placed between the map observation and a later map-dependent access; the callee transitions the object to a different map.
  3. Elide the required check Because the reducer keeps the stale map after the Call, it removes the map check that would have re-validated the object’s type.
  4. Trigger type confusion The optimized code accesses the object under the old map assumption while the object now has an incompatible layout, producing a type confusion.
  5. Escalate The confused access is leveraged for out-of-bounds read/write and ultimately arbitrary memory corruption in the renderer.

Impact Assessment

An attacker who can run JavaScript in a renderer gains a type confusion in V8 that leads to memory corruption and likely arbitrary read/write within the sandboxed renderer process. Exploitation requires only that the victim load an attacker-controlled page whose script reaches the optimizing compiler and shapes the transitioning-call pattern. It provides a foundation for renderer-level code execution, typically chained with a sandbox escape for full compromise.

Files Changed

  • src/compiler/turboshaft/late-load-elimination-reducer.cc

Audit Directions

  • Incomplete invalidation on calls
    Review every site that handles Call or other opaque effects in load/store elimination to confirm it invalidates both cached values and known maps, not just aliasing memory.
  • Map-transitioning operations
    Audit reducers that cache maps for operations beyond calls (deopts, allocations, runtime/builtin calls) that can transition maps yet leave map state intact.
  • Stable-vs-unstable map assumptions
    As the TODO(dmercadier) notes, examine MapMaskAndOr map compaction where stable/unstable maps are indistinguishable, since coarse map tracking can hide cases where only unstable maps should be invalidated.
From df948e6725883d290998e99343a7ef093f781c13 Mon Sep 17 00:00:00 2001
From: Darius Mercadier <[email protected]>
Date: Wed, 06 May 2026 17:29:39 +0200
Subject: [PATCH] [turboshaft] Load elimination: invalidate known maps on Calls

Fixed: 508811477
Change-Id: I090877c8babd5102319d7b3f5b63fefd5af941a9
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7823059
Reviewed-by: Leszek Swirski <[email protected]>
Commit-Queue: Leszek Swirski <[email protected]>
Auto-Submit: Darius Mercadier <[email protected]>
Cr-Commit-Position: refs/heads/main@{#107101}
---

diff --git a/src/compiler/turboshaft/late-load-elimination-reducer.cc b/src/compiler/turboshaft/late-load-elimination-reducer.cc
index 8c7d9cf..79bc56b 100644
--- a/src/compiler/turboshaft/late-load-elimination-reducer.cc
+++ b/src/compiler/turboshaft/late-load-elimination-reducer.cc
@@ -546,6 +546,14 @@
   // The call could modify arbitrary memory, so we invalidate every
   // potentially-aliasing object.
   memory_.InvalidateMaybeAliasing();
+
+  // This call could transition objects, thus invalidating their maps.
+  // TODO(dmercadier): we should only wipe unstable maps here, except that we
+  // don't know which maps are stable or not because we compact maps in
+  // MapMaskAndOr. I'm really not sure how much benefits this MapMaskAndOr
+  // structure brings, so we could instead consider to record exact maps, in
+  // which case we'd be able to only invalidate unstable ones.
+  WipeAllMaps();
 }
 
 // The only time an Allocate should flow into a WordBinop is for Smi checks
Loading diff…

Original Bug Report

reported by [email protected]

V8 Turboshaft late load elimination reuses stale map load after aliased map transition

Security Bug

VULNERABILITY DETAILS

V8 Turboshaft Late Load Elimination can reuse a stale LoadMapField across an aliased map transition. The root cause is in LateLoadEliminationAnalyzer::ProcessStore():

v8/src/compiler/turboshaft/late-load-elimination-reducer.cc

// Updates known stored values first.
if (!invalidate_maybe_aliasing) memory_.Invalidate(store);
memory_.Insert(store);

// Clears map facts only after invalidation already used them.
if (store.offset == HeapObject::kMapOffset && !store.index().valid()) {
  WipeAllMaps();
}

memory_.Invalidate(store) calls InvalidateAtOffset(), which uses object_maps_ through BasesCouldAlias() to decide whether a cached load’s base can alias the store base. Separately, ProcessCall() invalidates memory but does not clear object_maps_:

// The call could modify arbitrary memory, so we invalidate every
// potentially-aliasing object.
memory_.InvalidateMaybeAliasing();

A JavaScript caller can therefore create this sequence in optimized code:

  1. x.x emits CheckMaps(x, A) / AssumeMap(x, A).
  2. An opaque callback transitions x from map A to map B. ProcessCall() wipes cached memory but leaves object_maps_[x] = A stale.
  3. y.x emits CheckMaps(y, B) / AssumeMap(y, B).
  4. x == sentinel lowers to a receiver check that performs a post-call LoadMapField(x) without AssumeMap(x, B), caching the B map load while object_maps_[x] is still A.
  5. y.z = 99 transitions y from B to C. During ProcessStore(), InvalidateAtOffset(kMapOffset, y) sees object_maps_[y] = B and stale object_maps_[x] = A, concludes the bases cannot alias, and does not invalidate the cached LoadMapField(x).
  6. A later x.y CheckMaps(x, B) reuses the stale cached map load. The Turboshaft load-elimination verifier confirms that the replacement differs from the actual runtime load when x === y.

This bypasses the WrongMap deoptimization that should guard optimized code after the map transition. The attached d8 harness deterministically aborts under --turboshaft-verify-load-elimination with:

abort: Turboshaft's load elimination wrongly eliminated a Load

Without the verifier, the same optimized aliased call completes without the WrongMap deopt that occurs in safe variants, demonstrating that the stale map check is accepted.

VERSION

Chrome Version: 149.0.7821.0, local V8/Chromium ASan d8 build from Chromium checkout

Operating System: Ubuntu 22.04.5 LTS x86_64

Build target: out/asan/d8

REPRODUCTION CASE

Attachments:

  • poc.html — Chrome browser PoC for direct local reproduction
  • chrome_parent_lle_browser.log — direct Chrome run showing browser console milestones, LLE trace, verifier abort, and renderer crash signal
  • poc_d8.js — deterministic d8 trigger for faster compiler triage
  • lle_verifier_crash.log — full d8 verifier crash and LateLoadElimination trace
  • lle_noverify.log — same d8 harness without verifier, showing no WrongMap deopt on the aliased call

Browser repro steps:

  1. Build local ASan Chrome from the Chromium checkout:
gn gen out/asan --args='is_asan=true is_debug=false symbol_level=1 use_sysroot=true treat_warnings_as_errors=false'
autoninja -C out/asan chrome
  1. Serve the attached poc.html locally:
python3 -m http.server 8123 --bind 127.0.0.1
  1. Run Chrome directly against the PoC. The --js-flags enable deterministic optimization and the Turboshaft load-elimination verifier; the source tree/binary are otherwise unmodified.
ASAN_OPTIONS='detect_leaks=0:abort_on_error=1:symbolize=1' \
  out/asan/chrome \
    --user-data-dir=/tmp/chrome-lle-poc \
    --no-first-run \
    --no-default-browser-check \
    --disable-background-networking \
    --disable-sync \
    --enable-logging=stderr \
    --js-flags='--allow-natives-syntax --turboshaft --no-maglev --turboshaft-load-elimination --turboshaft-verify-load-elimination --turboshaft-trace-load-elimination --trace-deopt' \
    http://127.0.0.1:8123/poc.html

Observed browser result:

[poc] starting warmup
[poc] optimizing victim on non-aliased call
[poc] non-aliased result: 1142
[poc] triggering aliased call; verifier should abort vulnerable renderer
[poc] target before: {"x":1337}

>>>> InvalidateAtOffset: not invalidating thanks for maps: MemoryAddress{base=0, index=<invalid OpIndex>, offset=0, elem_size_log2=0, size=4}
>> Found potential replacement at offset 72
>>> Confirming replacement
abort: Turboshaft's load elimination wrongly eliminated a Load
Received signal 4 ILL_ILLOPN

The attached chrome_parent_lle_browser.log contains the complete direct browser run.

D8 repro steps:

  1. Build d8 from the Chromium checkout:
gn gen out/asan --args='is_asan=true is_debug=false symbol_level=1 use_sysroot=true treat_warnings_as_errors=false'
autoninja -C out/asan d8
  1. Run the deterministic verifier repro:
ASAN_OPTIONS=detect_leaks=0:abort_on_error=1 \
  out/asan/d8 \
    --allow-natives-syntax \
    --turboshaft \
    --no-maglev \
    --turboshaft-load-elimination \
    --turboshaft-trace-load-elimination \
    --turboshaft-verify-load-elimination \
    --trace-deopt \
    poc_d8.js

Observed result:

>>>> InvalidateAtOffset: not invalidating thanks for maps: MemoryAddress{base=0, index=<invalid OpIndex>, offset=0, elem_size_log2=0, size=4}
>>>> InvalidateAtOffset: invalidating MemoryAddress{base=4, index=<invalid OpIndex>, offset=0, elem_size_log2=0, size=4}
>> Wiping all maps

> ProcessLoad(93)
>> Found potential replacement at offset 72
>>> Confirming replacement
...
[before] {"x":1337}
abort: Turboshaft's load elimination wrongly eliminated a Load
  1. Optional confirmation without the verifier:
out/asan/d8 \
  --allow-natives-syntax \
  --turboshaft \
  --no-maglev \
  --turboshaft-load-elimination \
  --trace-deopt \
  poc_d8.js

Observed result:

[test] 1142 status=41
[before] {"x":1337}
[attack] 2716 status=41
[after] {"x":1337,"y":42,"z":99}

There is no wrong map deopt for the aliased [attack] call in this run, whereas safe variants deopt at the post-transition access.

Type of crash: Renderer/V8 JIT verifier abort in optimized Turboshaft code.

Crash State:

abort: Turboshaft's load elimination wrongly eliminated a Load

==== JS stack trace =========================================

    0: ExitFrame
    1: victim [.../poc_d8.js:~6]
    2: /* anonymous */ [.../poc_d8.js:31]

Relevant LLE trace excerpt:

ProcessCall(40)
  InvalidateMaybeAliasing
  Invalidating MemoryAddress{base=0, offset=12}
  Invalidating MemoryAddress{base=0, offset=0}

ProcessLoad(59)
ProcessAssumeMap(63)        // y has B
ProcessLoad(72)             // bare post-call LoadMapField(x), no AssumeMap(x,B)

ProcessStore(89)            // y.z map transition B -> C
  InvalidateAtOffset: not invalidating thanks for maps: MemoryAddress{base=0, offset=0}
  InvalidateAtOffset: invalidating MemoryAddress{base=4, offset=0}
  Wiping all maps            // too late

ProcessLoad(93)
  Found potential replacement at offset 72
  Confirming replacement

SECURITY IMPACT

The PoC proves that optimized code can accept a stale object map after an aliased map transition and bypass the WrongMap deoptimization guard. The attached named-property harness uses a layout where the read remains benign, so the verifier is the deterministic signal. The same stale-CheckMaps primitive applied to elements-kind or layout-specialized accesses can produce type confusion (for example, double-elements code operating after an object-elements transition). The V8 sandbox does not make stale map-specialized optimized code semantically safe; it only constrains follow-on exploitation primitives.

RECOMMENDED FIX

For map-offset stores, clear map facts before any invalidation routine can consult them:

const bool is_map_store =
    store.offset == HeapObject::kMapOffset && !store.index().valid();
if (is_map_store) {
  WipeAllMaps();
}
if (!invalidate_maybe_aliasing) memory_.Invalidate(store);
memory_.Insert(store);

Also consider clearing map facts across arbitrary effectful calls:

WipeAllMaps();
memory_.InvalidateMaybeAliasing();

If the call-site precision cost is too high, add a dedicated map-store invalidation mode that never relies on object_maps_ to preserve active map-offset loads.

CREDIT INFORMATION

Reporter credit: Mufeed VH from Winfunc Research (winfunc.com)

View on issue tracker