CVE-2026-10910
Overview
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.
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
- Prime the map Script creates an object and runs code so Turboshaft records a stable known map for it, enabling later map-check elimination.
- Interpose a transitioning call
A
Callis placed between the map observation and a later map-dependent access; the callee transitions the object to a different map. - 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. - 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.
- Escalate The confused access is leveraged for out-of-bounds read/write and ultimately arbitrary memory corruption in the renderer.
Impact Assessment
Files Changed
src/compiler/turboshaft/late-load-elimination-reducer.cc
Audit Directions
- Incomplete invalidation on callsReview every site that handles
Callor other opaque effects in load/store elimination to confirm it invalidates both cached values and known maps, not just aliasing memory. - Map-transitioning operationsAudit 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 assumptionsAs the
TODO(dmercadier)notes, examineMapMaskAndOrmap compaction where stable/unstable maps are indistinguishable, since coarse map tracking can hide cases where only unstable maps should be invalidated.
Patch
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
Original Bug Report
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:
x.xemitsCheckMaps(x, A)/AssumeMap(x, A).- An opaque callback transitions
xfrom map A to map B.ProcessCall()wipes cached memory but leavesobject_maps_[x] = Astale. y.xemitsCheckMaps(y, B)/AssumeMap(y, B).x == sentinellowers to a receiver check that performs a post-callLoadMapField(x)withoutAssumeMap(x, B), caching the B map load whileobject_maps_[x]is still A.y.z = 99transitionsyfrom B to C. DuringProcessStore(),InvalidateAtOffset(kMapOffset, y)seesobject_maps_[y] = Band staleobject_maps_[x] = A, concludes the bases cannot alias, and does not invalidate the cachedLoadMapField(x).- A later
x.yCheckMaps(x, B)reuses the stale cached map load. The Turboshaft load-elimination verifier confirms that the replacement differs from the actual runtime load whenx === 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 reproductionchrome_parent_lle_browser.log— direct Chrome run showing browser console milestones, LLE trace, verifier abort, and renderer crash signalpoc_d8.js— deterministic d8 trigger for faster compiler triagelle_verifier_crash.log— full d8 verifier crash and LateLoadElimination tracelle_noverify.log— same d8 harness without verifier, showing noWrongMapdeopt on the aliased call
Browser repro steps:
- 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
- Serve the attached
poc.htmllocally:
python3 -m http.server 8123 --bind 127.0.0.1
- Run Chrome directly against the PoC. The
--js-flagsenable 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:
- 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
- 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
- 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)
chrome_missing_property_noverify.logchrome_missing_property_verify.logchrome_parent_lle_browser.loglle_missing_property_interp.loglle_missing_property_noverify.loglle_missing_property_verify.loglle_noverify.loglle_verifier_crash.logpoc.htmlpoc_d8.jspoc_missing_property.htmlpoc_missing_property_wrong_result.js