Overview

Medium
Severity
6.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactRace in V8
DescriptionRace in V8
ComponentV8
Sub-componentRAB/GSAB
Bug ClassRace
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 confirms max_nof_items clamp preventing OOB write past values_or_entries. Triggering needs winning the grow-race via worker + GSAB-backed TypedArray; advisory rates only medium (6.8).
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker487768771
Fix commit02cd73dfc58d (v8/v8) +26/-15
CWECWE-362 (Race condition)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N
CISA KEVNot listed
CreditedQYmag1c
Reported2026-02-26 (per vendor advisory)
Fix landed2026-03-03 (public git)
Disclosed2026-04-07
Patch gap35 days — fix public in source before the advisory

Background

`Object.values` / `Object.entries`
built-ins that snapshot an object’s own enumerable properties into a pre-sized FixedArray, driven in V8 by CollectValuesOrEntries.
RAB/GSAB TypedArray
a TypedArray backed by a ResizableArrayBuffer or GrowableSharedArrayBuffer, whose byte length (and thus element capacity) can change at runtime, including from another thread for a GSAB.
`ElementsAccessor::CollectValuesOrEntriesImpl`
the per-elements-kind routine that iterates a receiver’s indexed elements and writes each value/entry into the caller-provided values_or_entries array, reporting the written count in *nof_items.
`GetCapacityImpl`
returns the current element capacity of a TypedArray’s backing store, re-read live at collection time rather than being frozen when the destination array was sized.

Root Cause Analysis

The caller in src/objects/js-objects.cc first computes number_of_own_elements and allocates a values_or_entries FixedArray sized to it, then calls CollectValuesOrEntries to fill it. For the TypedArray elements kind, CollectValuesOrEntriesImpl recomputed the iteration bound live via AccessorClass::GetCapacityImpl(*object, *elements) and looped for (index = 0; index < length; ++index), writing one entry per index. Because a GSAB-backed TypedArray can be grown by a background thread after the destination array was sized but before/while the loop runs, length could exceed the capacity that values_or_entries was allocated for, and the loop then wrote past the end of that FixedArray.

The fix threads a max_nof_items argument (set to the original number_of_own_elements) down to the impl and clamps if (length > max_nof_items) length = max_nof_items;, plus a CHECK_LE(*nof_items, max_nof_items) in the wrapper to enforce the invariant. This restores the guarantee that no more elements are written than the destination array was sized to hold.

Key insight
The destination array’s size was fixed from an element count sampled at one instant, while the collection loop trusted a length re-read live from a concurrently growable backing store; the fix passes the original count down as an explicit upper bound and clamps the iteration to it.

Attack Path

  1. Create a growable typed array Allocate a TypedArray over a GrowableSharedArrayBuffer shared with a worker, and invoke Object.values/Object.entries on it so V8 samples number_of_own_elements and allocates a matching values_or_entries FixedArray.
  2. Grow it from a background thread Race a worker thread that grows the GSAB so that GetCapacityImpl returns a larger length than the count used to size the destination array.
  3. Overrun the destination array CollectValuesOrEntriesImpl loops up to the enlarged length and writes entries beyond the bounds of values_or_entries, an out-of-bounds heap write into adjacent objects (inferred as the exploitable primitive from the bug class Race and the clamp added by the patch).

Impact Assessment

An attacker running script in the renderer can, by winning the race, cause a heap out-of-bounds write past a FixedArray in the V8 heap, corrupting adjacent objects. Preconditions are the ability to spawn a worker and share a GSAB-backed TypedArray and to hit the timing window between sizing and collection. The advisory classifies this as a race in V8; it does not report exploited-in-the-wild status, and any escalation to a full read/write primitive or code execution is (inferred).

Changed Functions

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

Files Changed

  • src/objects/elements.cc
  • src/objects/elements.h
  • src/objects/js-objects.cc

Audit Directions

  • Live length re-reads after sizing
    Audit any collection/copy loop whose destination buffer is sized from one snapshot but whose iteration bound is re-read from a TypedArray/RAB/GSAB backing store at loop time.
  • Concurrently resizable backing stores
    Flag element-accessor code paths reachable on GSAB-backed arrays where growth from another thread can invalidate a previously sampled capacity.
  • Count-then-fill idioms
    Review built-ins that allocate a result of size N then fill it in a second pass, checking that N is passed down and enforced as a hard cap rather than recomputed.
From 02cd73dfc58dbe73054382c85f9f126af9ce597e Mon Sep 17 00:00:00 2001
From: Marja Hölttä <[email protected]>
Date: Mon, 02 Mar 2026 20:05:25 +0100
Subject: [PATCH] [RAB/GSAB] Object.values: Handle a TA grown by a background thread gracefully

Fixed: 487768771
Change-Id: I49dada228d49f3a36d083d0b1e009112bfeea89a
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7623620
Reviewed-by: Igor Sheludko <[email protected]>
Commit-Queue: Marja Hölttä <[email protected]>
Cr-Commit-Position: refs/heads/main@{#105548}
---

diff --git a/src/objects/elements.cc b/src/objects/elements.cc
index 3be69a6..351a76c 100644
--- a/src/objects/elements.cc
+++ b/src/objects/elements.cc
@@ -1281,16 +1281,20 @@
   Maybe<bool> CollectValuesOrEntries(Isolate* isolate,
                                      DirectHandle<JSObject> object,
                                      DirectHandle<FixedArray> values_or_entries,
-                                     bool get_entries, uint32_t* nof_items,
+                                     uint32_t max_nof_items, bool get_entries,
+                                     uint32_t* nof_items,
                                      PropertyFilter filter) override {
-    return Subclass::CollectValuesOrEntriesImpl(
-        isolate, object, values_or_entries, get_entries, nof_items, filter);
+    auto to_return = Subclass::CollectValuesOrEntriesImpl(
+        isolate, object, values_or_entries, max_nof_items, get_entries,
+        nof_items, filter);
+    CHECK_LE(*nof_items, max_nof_items);
+    return to_return;
   }
 
   static Maybe<bool> CollectValuesOrEntriesImpl(
       Isolate* isolate, DirectHandle<JSObject> object,
-      DirectHandle<FixedArray> values_or_entries, bool get_entries,
-      uint32_t* nof_items, PropertyFilter filter) {
+      DirectHandle<FixedArray> values_or_entries, uint32_t max_nof_items,
+      bool get_entries, uint32_t* nof_items, PropertyFilter filter) {
     DCHECK_EQ(*nof_items, 0);
     KeyAccumulator accumulator(isolate, KeyCollectionMode::kOwnOnly,
                                ALL_PROPERTIES);
@@ -2881,8 +2885,8 @@
 
   static Maybe<bool> CollectValuesOrEntriesImpl(
       Isolate* isolate, DirectHandle<JSObject> object,
-      DirectHandle<FixedArray> values_or_entries, bool get_entries,
-      uint32_t* nof_items, PropertyFilter filter) {
+      DirectHandle<FixedArray> values_or_entries, uint32_t max_nof_items,
+      bool get_entries, uint32_t* nof_items, PropertyFilter filter) {
     uint32_t count = 0;
     if (get_entries) {
       // Collecting entries needs to allocate, so this code must be handlified.
@@ -3377,8 +3381,8 @@
 
   static Maybe<bool> CollectValuesOrEntriesImpl(
       Isolate* isolate, DirectHandle<JSObject> object,
-      DirectHandle<FixedArray> values_or_entries, bool get_entries,
-      uint32_t* nof_items, PropertyFilter filter) {
+      DirectHandle<FixedArray> values_or_entries, uint32_t max_nof_items,
+      bool get_entries, uint32_t* nof_items, PropertyFilter filter) {
     DirectHandle<FixedDoubleArray> elements(
         Cast<FixedDoubleArray>(object->elements()), isolate);
     uint32_t count = 0;
@@ -3717,12 +3721,17 @@
 
   static Maybe<bool> CollectValuesOrEntriesImpl(
       Isolate* isolate, DirectHandle<JSObject> object,
-      DirectHandle<FixedArray> values_or_entries, bool get_entries,
-      uint32_t* nof_items, PropertyFilter filter) {
+      DirectHandle<FixedArray> values_or_entries, uint32_t max_nof_items,
+      bool get_entries, uint32_t* nof_items, PropertyFilter filter) {
     uint32_t count = 0;
     if ((filter & ONLY_CONFIGURABLE) == 0) {
       DirectHandle<FixedArrayBase> elements(object->elements(), isolate);
       size_t length = AccessorClass::GetCapacityImpl(*object, *elements);
+      // The TypedArray might have been grown by a background thread. Handle it
+      // gracefully.
+      if (length > max_nof_items) {
+        length = max_nof_items;
+      }
       for (size_t index = 0; index < length; ++index) {
         DirectHandle<Object> value = AccessorClass::GetInternalImpl(
             isolate, object, InternalIndex(index));
diff --git a/src/objects/elements.h b/src/objects/elements.h
index f763961..c11363e 100644
--- a/src/objects/elements.h
+++ b/src/objects/elements.h
@@ -92,8 +92,9 @@
 
   virtual Maybe<bool> CollectValuesOrEntries(
       Isolate* isolate, DirectHandle<JSObject> object,
-      DirectHandle<FixedArray> values_or_entries, bool get_entries,
-      uint32_t* nof_items, PropertyFilter filter = ALL_PROPERTIES) = 0;
+      DirectHandle<FixedArray> values_or_entries, uint32_t max_nof_items,
+      bool get_entries, uint32_t* nof_items,
+      PropertyFilter filter = ALL_PROPERTIES) = 0;
 
   virtual MaybeHandle<FixedArray> PrependElementIndices(
       Isolate* isolate, DirectHandle<JSObject> object,
diff --git a/src/objects/js-objects.cc b/src/objects/js-objects.cc
index aa69662..8f1ac7e 100644
--- a/src/objects/js-objects.cc
+++ b/src/objects/js-objects.cc
@@ -2257,8 +2257,9 @@
 
   if (object->elements() != ReadOnlyRoots(isolate).empty_fixed_array()) {
     MAYBE_RETURN(object->GetElementsAccessor()->CollectValuesOrEntries(
-                     isolate, object, values_or_entries, get_entries, &count,
-                     ENUMERABLE_STRINGS),
+                     isolate, object, values_or_entries,
+                     static_cast<uint32_t>(number_of_own_elements), get_entries,
+                     &count, ENUMERABLE_STRINGS),
                  Nothing<bool>());
   }
 
Loading diff…

Original Bug Report

reported by [email protected]

Debug check failed: IsInBounds(index).

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

Root Cause

The root cause is inconsistent length snapshots within one logical operation, plus missing write-side capacity bounds.

  • Allocation uses old length (snapshot A)
  • Writing re-reads a new length (snapshot B, possibly larger)
  • When snapshot B > snapshot A, values_or_entries->set(count++, ...) writes out of bounds

This is a classic shared-memory TOCTOU problem under GSAB + length-tracking TypedArray concurrent growth.

Trigger Path and Call Chain

Key call chain:

  1. Object.values(ta) / Object.entries(ta) enters the fast path.
  2. FastGetOwnValuesOrEntries allocates the result array using one length snapshot:
    • src/objects/js-objects.cc:2240
    • src/objects/js-objects.cc:2249
  3. Then it enters TypedArray element collection:
    • src/objects/js-objects.cc:2254
    • src/objects/elements.cc:3719
  4. TypedElementsAccessor::CollectValuesOrEntriesImpl reads the current length again and writes in a loop:
    • Length read: src/objects/elements.cc:3726
    • Write site: src/objects/elements.cc:3733

In the PoC, a worker concurrently executes sab.grow(...), making step-2 allocation length smaller than step-4 write length, which causes 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

Summary

Debug check failed: IsInBounds(index).

Custom Questions

Type of crash:

tab

Crash state:

# Fatal error in ../../src/objects/fixed-array-inl.h, line 160
# Debug check failed: IsInBounds(index).
#
#
#
#FailureMessage Object: 0x7ffcc5f29308
==== C stack trace ===============================

    /home/qy/new2/v8/out/x64.debug/libv8_libbase.so(v8::base::debug::StackTrace::StackTrace()+0x29) [0x7d3f31b090e9]
    /home/qy/new2/v8/out/x64.debug/libv8_libplatform.so(+0x4e29d) [0x7d3f31a6a29d]
    /home/qy/new2/v8/out/x64.debug/libv8_libbase.so(V8_Fatal(char const*, int, char const*, ...)+0x205) [0x7d3f31add2f5]
    /home/qy/new2/v8/out/x64.debug/libv8_libbase.so(+0x53b8c) [0x7d3f31adcb8c]
    /home/qy/new2/v8/out/x64.debug/libv8_libbase.so(V8_Dcheck(char const*, int, char const*)+0x4d) [0x7d3f31add3ed]
    /home/qy/new2/v8/out/x64.debug/libv8.so(v8::internal::TaggedArrayBase<v8::internal::FixedArray, v8::internal::TaggedArrayShape, v8::internal::HeapObjectLayout>::set(unsigned int, v8::internal::Tagged<v8::internal::Object>, v8::internal::WriteBarrierMode)+0x81) [0x7d3f2bbb9fe1]
    /home/qy/new2/v8/out/x64.debug/libv8.so(+0xa852a36) [0x7d3f2cc52a36]
    /home/qy/new2/v8/out/x64.debug/libv8.so(+0xa8508cb) [0x7d3f2cc508cb]
    /home/qy/new2/v8/out/x64.debug/libv8.so(v8::internal::FastGetOwnValuesOrEntries(v8::internal::Isolate*, v8::internal::DirectHandle<v8::internal::JSReceiver>, bool, v8::internal::Handle<v8::internal::FixedArray>*)+0x5f1) [0x7d3f2cdcc7f1]
    /home/qy/new2/v8/out/x64.debug/libv8.so(v8::internal::GetOwnValuesOrEntries(v8::internal::Isolate*, v8::internal::DirectHandle<v8::internal::JSReceiver>, v8::internal::PropertyFilter, bool, bool)+0x8b) [0x7d3f2cdcd89b]
    /home/qy/new2/v8/out/x64.debug/libv8.so(v8::internal::JSReceiver::GetOwnValues(v8::internal::Isolate*, v8::internal::DirectHandle<v8::internal::JSReceiver>, v8::internal::PropertyFilter, bool)+0x4a) [0x7d3f2cdce0ba]
    /home/qy/new2/v8/out/x64.debug/libv8.so(+0xaf3b29d) [0x7d3f2d33b29d]
    /home/qy/new2/v8/out/x64.debug/libv8.so(v8::internal::Runtime_ObjectValues(int, unsigned long*, v8::internal::Isolate*)+0x151) [0x7d3f2d33af21]
    /home/qy/new2/v8/out/x64.debug/libv8.so(+0x8c289bd) [0x7d3f2b0289bd]
Trace/breakpoint trap (core dumped)

Reporter credit:

QYmag1c

Additional Data

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

View on issue tracker