CVE-2026-4450
Overview
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
ArrayBufferor growable sharedArrayBuffer, 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
listatinsertion_index, returning the count in*nof_indices. - `GetCapacityImpl` / `GetMaxNumberOfEntries` / `GetMaxIndex`
- helpers giving the size used both to allocate the destination
FixedArrayand (viaGetMaxIndex) 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.
max_nof_indices) into the loop and stops writing at it.Attack Path
- Create a growable-backed TypedArray
Script allocates a TypedArray over a growable shared
ArrayBuffer(GSAB) whose length can be increased from another thread. - Enumerate keys
The attacker calls an operation that collects element indices (e.g.
Object.keys) so the code allocates theindicesFixedArraysized to the current length. - Grow concurrently
A background thread grows the shared buffer/TypedArray after allocation but before/while
DirectCollectElementIndicesImpliterates, soGetMaxIndexnow reports more present elements than the array can hold. - Out-of-bounds write
The loop writes indices past the end of the allocated
FixedArray, corrupting adjacent heap memory. (inferred)
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
forsrc/objects/elements.cc |
modified | |
ifsrc/objects/elements.cc |
modified |
Files Changed
src/objects/elements.cc
Audit Directions
- Allocate-then-fill with a re-read lengthLook 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 TOCTOUAudit 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.
Patch
From a9509c5e815de90dcbbf6b03af50fc069fd87b8f Mon Sep 17 00:00:00 2001 From: Marja Hölttä <[email protected]> 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ä <[email protected]> Reviewed-by: Igor Sheludko <[email protected]> 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,
Original Bug Report
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.
PrependElementIndicesImplsize estimation uses snapshot ADirectCollectElementIndicesImpliteration uses snapshot B- If B > A,
list->set(insertion_index, ...)can exceed list bounds
Additional risk in this path:
nof_indicescan consume space reserved fornof_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:
Object.keys(ta)/for-in/JSON.stringify(ta)enters key collection.PrependElementIndicesImplestimates size and allocates using one length snapshot:src/objects/elements.cc:1426src/objects/elements.cc:1438/src/objects/elements.cc:1455
DirectCollectElementIndicesImplreads length again and writes:- Length read:
src/objects/elements.cc:1389 - Write sites:
src/objects/elements.cc:1399/src/objects/elements.cc:1403
- Length read:
- 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 \