CVE-2026-10910
Overview
Files Changed
src/compiler/turboshaft/late-load-elimination-reducer.cc
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)