Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in V8
DescriptionOut of bounds write in V8
ComponentV8
Sub-componentRAB/GSAB
Bug ClassOOB
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed
Reviewer notes: Diff clearly adds max_nof_indices bound, break, and CHECK_LE matching the claimed background-growth OOB write. Precondition (background-growable TypedArray) implies a shared/growable buffer with worker growth; otherwise accurate.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker487746373
Fix commita9509c5e815d (v8/v8) +28/-20
CWECWE-787 (Out-of-bounds write)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
Creditedqymag1c
Reported2026-02-26 (per vendor advisory)
Fix landed2026-03-03 (public git)
Disclosed2026-03-18
Patch gap15 days — fix public in source before the advisory

Background

`Object.keys` / element index collection
enumerating an object’s keys collects its element indices into a preallocated FixedArray (list/indices) before converting them to keys.
RAB/GSAB TypedArrays
TypedArrays backed by a resizable ArrayBuffer or growable shared ArrayBuffer, whose length can change; a shared (GSAB) buffer may be grown concurrently by another thread.
`DirectCollectElementIndicesImpl`
the per-elements-kind routine that iterates the backing store and writes present element indices into list at insertion_index, returning the count in *nof_indices.
`GetCapacityImpl` / `GetMaxNumberOfEntries` / `GetMaxIndex`
helpers giving the size used both to allocate the destination FixedArray and (via GetMaxIndex) to bound the collection loop; a TOCTOU gap opens if the length grows between allocation and collection.

Root Cause Analysis

The destination FixedArray was allocated with a capacity computed from the TypedArray’s size at one point in time (GetCapacityImpl(*object, *backing_store) or GetMaxNumberOfEntries(...)), but DirectCollectElementIndicesImpl then iterated up to Subclass::GetMaxIndex(*object, *backing_store) and wrote one entry per present element with no bound on insertion_index relative to the allocated size. As the new comment states, for a TypedArray “grown by a background thread” (a GSAB-backed TA) the length observed during the loop can exceed the length used to size the list, so the routine writes more indices than the FixedArray can hold — an out-of-bounds write past the allocated array.

The fix adds an explicit capacity parameter max_nof_indices (set to the allocated GetCapacityImpl/nof_elements size) and breaks out of the loop when insertion_index >= max_nof_indices, plus a CHECK_LE(*nof_indices, max_nof_indices) backstop, so no write ever exceeds the array actually allocated. The caller was updated to compute max_nof_indices once, allocate the FixedArray with it, and pass it down through the overloads (including the SloppyArgumentsElements delegation), keeping the write count and allocation size consistent.

Key insight
The single mistake is a time-of-check/time-of-use gap: the destination array was sized from an earlier length while the collection loop used a possibly-larger current length, so a concurrently grown TypedArray overflowed the array; the fix threads the allocated capacity (max_nof_indices) into the loop and stops writing at it.

Attack Path

  1. Create a growable-backed TypedArray Script allocates a TypedArray over a growable shared ArrayBuffer (GSAB) whose length can be increased from another thread.
  2. Enumerate keys The attacker calls an operation that collects element indices (e.g. Object.keys) so the code allocates the indices FixedArray sized to the current length.
  3. Grow concurrently A background thread grows the shared buffer/TypedArray after allocation but before/while DirectCollectElementIndicesImpl iterates, so GetMaxIndex now reports more present elements than the array can hold.
  4. Out-of-bounds write The loop writes indices past the end of the allocated FixedArray, corrupting adjacent heap memory. (inferred)

Impact Assessment

An attacker running crafted JavaScript with a background-growable (GSAB) TypedArray can drive an out-of-bounds write in the V8 heap during element-index collection in the renderer process, which the advisory classifies as an out-of-bounds write in V8. Preconditions are the ability to run script and to grow the backing buffer concurrently (a race). Any escalation of the OOB write into a controlled read/write primitive or code execution is (inferred), and the advisory does not report exploitation in the wild.

Changed Functions

FunctionChangeNotes
for
src/objects/elements.cc
modified
if
src/objects/elements.cc
modified

Files Changed

  • src/objects/elements.cc

Audit Directions

  • Allocate-then-fill with a re-read length
    Look for code that sizes a buffer from one length query and then fills it using a separately re-read length or GetMaxIndex, especially where the underlying object can change size in between.
  • RAB/GSAB TypedArray length TOCTOU
    Audit all iteration over resizable/growable-backed TypedArrays for assumptions that length is stable across an allocation, since GSAB buffers can be grown by another thread mid-operation.
  • Loops writing to preallocated `FixedArray`
    Review element/key/index collectors that write per-element into a fixed-capacity array without re-checking the insertion index against the allocated capacity.
From a9509c5e815de90dcbbf6b03af50fc069fd87b8f Mon Sep 17 00:00:00 2001
From: Marja Hölttä <marja@chromium.org>
Date: Tue, 03 Mar 2026 12:11:41 +0100
Subject: [PATCH] [RAB/GSAB] Object.keys: Handle a TA grown by a background thread gracefully

Fixed: 487746373
Change-Id: I24ddb75f34f4ea888e7679e9c250fcc2d2c23358
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7623640
Commit-Queue: Marja Hölttä <marja@chromium.org>
Reviewed-by: Igor Sheludko <ishell@chromium.org>
Cr-Commit-Position: refs/heads/main@{#105573}
---

diff --git a/src/objects/elements.cc b/src/objects/elements.cc
index 351a76c..2a7a791 100644
--- a/src/objects/elements.cc
+++ b/src/objects/elements.cc
@@ -1387,12 +1387,17 @@
   static Handle<FixedArray> DirectCollectElementIndicesImpl(
       Isolate* isolate, DirectHandle<JSObject> object,
       DirectHandle<FixedArrayBase> backing_store, GetKeysConversion convert,
-      PropertyFilter filter, Handle<FixedArray> list, uint32_t* nof_indices,
-      uint32_t insertion_index = 0) {
+      PropertyFilter filter, Handle<FixedArray> list, uint32_t max_nof_indices,
+      uint32_t* nof_indices, uint32_t insertion_index = 0) {
     size_t length = Subclass::GetMaxIndex(*object, *backing_store);
     for (size_t i = 0; i < length; i++) {
       if (Subclass::HasElementImpl(isolate, *object, i, *backing_store,
                                    filter)) {
+        if (insertion_index >= max_nof_indices) {
+          // This might happen when the object is a TypedArray which was grown
+          // by a background thread.
+          break;
+        }
         if (convert == GetKeysConversion::kConvertToString) {
           // Avoid trashing the number to string cache with numbers that
           // are not likely to be needed.
@@ -1409,6 +1414,7 @@
       }
     }
     *nof_indices = insertion_index;
+    CHECK_LE(*nof_indices, max_nof_indices);
     return list;
   }
 
@@ -1425,15 +1431,15 @@
       DirectHandle<FixedArrayBase> backing_store, DirectHandle<FixedArray> keys,
       GetKeysConversion convert, PropertyFilter filter) {
     uint32_t nof_property_keys = keys->ulength().value();
-    size_t initial_list_length_szt =
+    size_t nof_elements_szt =
         Subclass::GetMaxNumberOfEntries(isolate, *object, *backing_store);
 
-    if (initial_list_length_szt > FixedArray::kMaxLength - nof_property_keys) {
+    if (nof_elements_szt > FixedArray::kMaxLength - nof_property_keys) {
       THROW_NEW_ERROR(isolate,
                       NewRangeError(MessageTemplate::kInvalidArrayLength));
     }
-    uint32_t initial_list_length =
-        static_cast<uint32_t>(initial_list_length_szt) + nof_property_keys;
+    uint32_t nof_elements = static_cast<uint32_t>(nof_elements_szt);
+    uint32_t initial_list_length = nof_elements + nof_property_keys;
 
     // Collect the element indices into a new list.
     DCHECK_LE(initial_list_length, std::numeric_limits<int>::max());
@@ -1450,9 +1456,9 @@
         // large-object space which doesn't free memory on shrinking the list.
         // Hence we try to estimate the final size for holey backing stores more
         // precisely here.
-        initial_list_length =
+        nof_elements =
             Subclass::NumberOfElementsImpl(isolate, *object, *backing_store);
-        initial_list_length += nof_property_keys;
+        initial_list_length = nof_elements + nof_property_keys;
       }
       DCHECK_LE(initial_list_length, std::numeric_limits<int>::max());
       combined_keys = isolate->factory()->NewFixedArray(initial_list_length);
@@ -1464,7 +1470,7 @@
     combined_keys = Subclass::DirectCollectElementIndicesImpl(
         isolate, object, backing_store,
         needs_sorting ? GetKeysConversion::kKeepNumbers : convert, filter,
-        combined_keys, &nof_indices);
+        combined_keys, nof_elements, &nof_indices);
 
     if (needs_sorting) {
       SortIndices(isolate, combined_keys, nof_indices);
@@ -1922,8 +1928,8 @@
   static Handle<FixedArray> DirectCollectElementIndicesImpl(
       Isolate* isolate, DirectHandle<JSObject> object,
       DirectHandle<FixedArrayBase> backing_store, GetKeysConversion convert,
-      PropertyFilter filter, Handle<FixedArray> list, uint32_t* nof_indices,
-      uint32_t insertion_index = 0) {
+      PropertyFilter filter, Handle<FixedArray> list, uint32_t max_nof_indices,
+      uint32_t* nof_indices, uint32_t insertion_index = 0) {
     if (filter & SKIP_STRINGS) return list;
 
     auto dictionary = Cast<NumberDictionary>(backing_store);
@@ -1935,6 +1941,7 @@
       insertion_index++;
     }
     *nof_indices = insertion_index;
+    CHECK_LE(*nof_indices, max_nof_indices);
     return list;
   }
 
@@ -5236,12 +5243,13 @@
       DirectHandle<JSObject> object, DirectHandle<FixedArrayBase> backing_store,
       KeyAccumulator* keys) {
     Isolate* isolate = keys->isolate();
+    uint32_t max_nof_indices = GetCapacityImpl(*object, *backing_store);
     uint32_t nof_indices = 0;
-    Handle<FixedArray> indices = isolate->factory()->NewFixedArray(
-        GetCapacityImpl(*object, *backing_store));
-    DirectCollectElementIndicesImpl(isolate, object, backing_store,
-                                    GetKeysConversion::kKeepNumbers,
-                                    ENUMERABLE_STRINGS, indices, &nof_indices);
+    Handle<FixedArray> indices =
+        isolate->factory()->NewFixedArray(max_nof_indices);
+    DirectCollectElementIndicesImpl(
+        isolate, object, backing_store, GetKeysConversion::kKeepNumbers,
+        ENUMERABLE_STRINGS, indices, max_nof_indices, &nof_indices);
     SortIndices(isolate, indices, nof_indices);
     for (uint32_t i = 0; i < nof_indices; i++) {
       RETURN_FAILURE_IF_NOT_SUCCESSFUL(keys->AddKey(indices->get(i)));
@@ -5252,8 +5260,8 @@
   static Handle<FixedArray> DirectCollectElementIndicesImpl(
       Isolate* isolate, DirectHandle<JSObject> object,
       DirectHandle<FixedArrayBase> backing_store, GetKeysConversion convert,
-      PropertyFilter filter, Handle<FixedArray> list, uint32_t* nof_indices,
-      uint32_t insertion_index = 0) {
+      PropertyFilter filter, Handle<FixedArray> list, uint32_t max_nof_indices,
+      uint32_t* nof_indices, uint32_t insertion_index = 0) {
     auto elements = Cast<SloppyArgumentsElements>(backing_store);
     uint32_t length = elements->ulength().value();
 
@@ -5272,8 +5280,8 @@
 
     DirectHandle<FixedArray> store(elements->arguments(), isolate);
     return ArgumentsAccessor::DirectCollectElementIndicesImpl(
-        isolate, object, store, convert, filter, list, nof_indices,
-        insertion_index);
+        isolate, object, store, convert, filter, list, max_nof_indices,
+        nof_indices, insertion_index);
   }
 
   static Maybe<bool> IncludesValueImpl(Isolate* isolate,
Loading diff…

Original Bug Report

reported by qy...@gmail.com

Concurrent GSAB Growth Causes OOB Write in Key Enumeration

Steps to reproduce the problem

run with: Download worker.js and poc.js and put them in the same directory. d8 poc.js

Problem Description

Foreword

Since this vulnerability and the previous report https://issues.chromium.org/issues/487768771 were introduced in the same commit, I suspect they will most likely be assigned to the same Google developer.

To prevent unnecessary issue merging, please allow me to clarify here that these two vulnerabilities are not caused by the same code. This issue is in the key enumeration path, not the values/entries path.

Thank you for reading.

Root Cause

TOCTOU length mismatch in the key-enumeration pipeline, compounded by missing tail-reservation enforcement, leading to OOB writes.

  • PrependElementIndicesImpl size estimation uses snapshot A
  • DirectCollectElementIndicesImpl iteration uses snapshot B
  • If B > A, list->set(insertion_index, ...) can exceed list bounds

Additional risk in this path:

  • nof_indices can consume space reserved for nof_property_keys
  • later CopyObjectToObjectElements(... nof_indices, nof_property_keys) can become OOB copy as well

Trigger Path and Call Chain

This issue is in the key enumeration path, not the values/entries path.

Key chain:

  1. Object.keys(ta) / for-in / JSON.stringify(ta) enters key collection.
  2. PrependElementIndicesImpl estimates size and allocates using one length snapshot:
    • src/objects/elements.cc:1426
    • src/objects/elements.cc:1438 / src/objects/elements.cc:1455
  3. DirectCollectElementIndicesImpl reads length again and writes:
    • Length read: src/objects/elements.cc:1389
    • Write sites: src/objects/elements.cc:1399 / src/objects/elements.cc:1403
  4. Concurrent sab.grow(...) makes second length exceed first allocation, causing OOB writes.

Additional Comments

Introduced by commit

commit  3160edf011b11347dd741c1c09a7fcb57bb479c4
[rab/gsab] ResizableArrayBuffer / GrowableSharedArrayBuffer part 1

Detailed list of changes:
https://docs.google.com/document/d/15i4-SZDzFDW7FfclIYuZEhFn-q-KpobCBy23x9zZZLc/edit?usp=sharing

Bug: v8:11111
Change-Id: I931003bd4552cf91d57de95af04a427a9e6d6ac9

I will provide poc_debug.js in the comments section. If you encounter issues with ASAN version verification, please use the poc_debug.js.

Summary

Concurrent GSAB Growth Causes OOB Write in Key Enumeration

Custom Questions

Type of crash:

tab

Crash state:

To differentiate this from the report at https://issues.chromium.org/issues/487768771, a pocjs script that can cause the release version to crash is constructed here. This out-of-bounds write might trigger a check, and running it multiple times can trigger crashes in different locations.

Received signal 11 SEGV_ACCERR 6b3b69241848

==== C stack trace ===============================

out/x64.asan/d8(__interceptor_backtrace+0x46)[0x5fe982ffeb36]
out/x64.asan/d8(+0x17b06e0)[0x5fe9834416e0]
/lib/x86_64-linux-gnu/libc.so.6(+0x45330)[0x707cbe445330]
out/x64.asan/d8(+0x278bd26)[0x5fe98441cd26]
out/x64.asan/d8(+0x278a5c6)[0x5fe98441b5c6]
out/x64.asan/d8(+0x2684995)[0x5fe984315995]
out/x64.asan/d8(+0x26a936f)[0x5fe98433a36f]
out/x64.asan/d8(+0x6610fb6)[0x5fe9882a1fb6]
[end of stack trace]
Segmentation fault

Reporter credit:

QYmag1c

Additional Data

Category: Security
Chrome Channel: Not sure
Regression: N/A \

View on issue tracker