CVE-2026-4450
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forsrc/objects/elements.cc |
modified | |
ifsrc/objects/elements.cc |
modified |
Files Changed
src/objects/elements.cc
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 \