Medium patch-now ✔︎ verified CVSS 7.5 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
7.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactBuffer overflow in Blink
DescriptionBuffer overflow in Blink
ComponentBlink
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 caches keys->Length() once, pairing with UncheckedAppend as claimed -- an OOB write. Whether keys->Length() is actually attacker-mutable mid-loop is not shown in the hunk.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker536913431
Fix commitdf0f26494ab3 (chromium/src) +8/-3
CWECWE-122 (Heap buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedFound by XBOW and triaged by Andrés Luksenberg
Reported2026-07-20 · bounty $1,000 (per vendor advisory)
Fix landed2026-07-23 (public git)
Disclosed2026-08-25
Patch gap33 days — fix public in source before the advisory

Background

IDL `record<K, V>`
a WebIDL dictionary-like type that Blink materializes from a JavaScript object by enumerating its own keys and converting each entry.
`native_value_traits_impl.h`
the Blink bindings header that holds the templated conversion routines translating V8 values into Blink IDL C++ types.
`UncheckedAppend()`
a Blink container fast-path insertion that skips capacity/bounds checks, relying on the caller to have reserved sufficient space in advance.
`v8::Array::Length()`
a call into V8 that returns an array’s current length and can re-enter JavaScript, so its result may differ between successive calls.

Root Cause Analysis

The conversion routine for IDL record types (enclosing function not shown in the hunk; the @@ context only shows loop lines) obtains the keys array from V8, then calls keys->Length() in three separate places: once for the MaxCapacity() bounds check, once for ReserveInitialCapacity(), and once as the loop bound. Because each keys->Length() call can re-enter JavaScript (for example via a proxy or accessor that mutates the array), the length used to reserve capacity could differ from the length used to bound the append loop. If the array grew after ReserveInitialCapacity() was called with a smaller value, the loop — which uses UncheckedAppend() on the result container — could iterate past the reserved capacity and write out of bounds.

The fix caches the length once into a local uint32_t length immediately after obtaining keys, and uses that single snapshot for the MaxCapacity() check, the reservation, and the loop bound, so the reserved capacity and the iteration count can no longer diverge.

Key insight
The single mistake was re-reading a mutable, JavaScript-observable length (keys->Length()) multiple times while pairing it with the unchecked UncheckedAppend() fast path; caching the length once into length guarantees the reserved capacity and the loop iteration count stay consistent.

Attack Path

  1. Trigger a record conversion Cause Blink to convert a JavaScript object into an IDL record<K, V> argument for some Web API, so the vulnerable routine in native_value_traits_impl.h runs.
  2. Supply a mutable key source Provide an object whose key enumeration is observable to script (inferred), so that a callback fires between the capacity reservation and the append loop.
  3. Grow the array mid-conversion From that callback, increase the array’s length so a later keys->Length() returns more elements than were reserved by ReserveInitialCapacity() (inferred).
  4. Overflow via `UncheckedAppend()` The loop, bounded by the now-larger length, appends more entries than the reserved capacity, writing past the buffer since UncheckedAppend() performs no bounds check.

Impact Assessment

An attacker who runs JavaScript in the renderer can drive a heap buffer overflow in the Blink bindings layer, corrupting adjacent heap memory in the renderer process. The advisory classifies this as an out-of-bounds/buffer overflow in Blink; any further escalation to controlled read/write or code execution is (inferred) and not demonstrated by the patch. No exploited-in-the-wild status is stated in the provided material.

Changed Functions

FunctionChangeNotes
for
third_party/blink/renderer/bindings/core/v8/native_value_traits_impl.h
modified

Files Changed

  • third_party/blink/renderer/bindings/core/v8/native_value_traits_impl.h

Audit Directions

  • Re-read of JS-observable sizes
    Search bindings code for repeated calls to ->Length(), ->Get(), or other V8 accessors whose results are used both to size a buffer and to bound a fill loop; each such pair is a TOCTOU candidate.
  • `UncheckedAppend()` without cached bound
    Audit every UncheckedAppend() / unchecked-insertion call site to confirm the reserved capacity and the loop count derive from a single, immutable snapshot taken before any code that can re-enter script.
  • Reservation vs. iteration mismatch
    Look for patterns where ReserveInitialCapacity()/reserve() and the subsequent loop bound are computed from independent expressions rather than one shared local.
From df0f26494ab36387a14c257578be1e5987c5ea61 Mon Sep 17 00:00:00 2001
From: Dave Tapuska <[email protected]>
Date: Thu, 23 Jul 2026 09:15:52 -0700
Subject: [PATCH] Cache the length of keys when converting to IDL record types.

Store the length of the keys array in a local variable before iterating. This ensures the length does not change during the loop, preventing potential issues with UncheckedAppend() if the array is modified during iteration.

BUG=536913431

Change-Id: I7589d1d417c4e8dbcce2a5bcd8b5bd96596f8f8a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8138460
Reviewed-by: Jeremy Roman <[email protected]>
Commit-Queue: Dave Tapuska <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1667167}
---
diff --git a/third_party/blink/renderer/bindings/core/v8/native_value_traits_impl.h b/third_party/blink/renderer/bindings/core/v8/native_value_traits_impl.h
index bbc4252c..eafc6a5 100644
--- a/third_party/blink/renderer/bindings/core/v8/native_value_traits_impl.h
+++ b/third_party/blink/renderer/bindings/core/v8/native_value_traits_impl.h
@@ -1174,14 +1174,19 @@
              .ToLocal(&keys)) {
       return ImplType();
     }
-    if (keys->Length() > ImplType::MaxCapacity()) {
+
+    // Store the length because we use UncheckedAppend() below and we want to
+    // make sure that the length does not change during the loop.
+    uint32_t length = keys->Length();
+
+    if (length > ImplType::MaxCapacity()) {
       exception_state.ThrowRangeError("Array length exceeds supported limit.");
       return ImplType();
     }
 
     // "2. Let result be a new empty instance of record<K, V>."
     ImplType result;
-    result.ReserveInitialCapacity(keys->Length());
+    result.ReserveInitialCapacity(length);
 
     // The conversion algorithm needs a data structure with fast insertion at
     // the end while at the same time requiring fast checks for previous insert
@@ -1189,7 +1194,7 @@
     // the latter part.
     HashMap<String, uint32_t> seen_keys;
 
-    for (uint32_t i = 0; i < keys->Length(); ++i) {
+    for (uint32_t i = 0; i < length; ++i) {
       // "4. Repeat, for each element key of keys in List order:"
       v8::Local<v8::Value> key;
       if (!keys->Get(context, i).ToLocal(&key)) {
Loading diff…

Original Bug Report

reported by [email protected]

Blink IDLRecord conversion can outgrow its reserved native Vector during reentrancy

VULNERABILITY DETAILS

Blink’s generic Web IDL record<K,V> conversion reserves a native Vector from a V8 array’s length once, but re-reads the mutable V8 length on every loop iteration and appends with UncheckedAppend(). If an attacker who has compromised the V8 sandbox changes the live key array during a JavaScript callback, the loop can append more native elements than were reserved, causing an out-of-bounds write in PartitionAlloc memory.

The vulnerable conversion is generic Blink C++ with no operating-system or architecture guard, so the capacity violation itself is not Linux-specific. The attached PoC was validated on Linux x86-64. Its classification as a V8 sandbox bypass applies to supported 64-bit V8-sandbox configurations; V8’s sandbox is available only in 64-bit configurations. We did not test the PoC’s heap-layout assumptions on other targets.

The affected implementation is NativeValueTraits<IDLRecord<K,V>>::NativeValue(). The relevant sequence is:

v8::Local<v8::Array> keys;
v8_object->GetOwnPropertyNames(...).ToLocal(&keys);

ImplType result;
result.ReserveInitialCapacity(keys->Length());

for (uint32_t i = 0; i < keys->Length(); ++i) {
  // GetOwnPropertyDescriptor(), property reads, and K/V conversions can call JS.
  ...
  result.UncheckedAppend(
      std::make_pair(std::move(typed_key), std::move(typed_value)));
}

GetOwnPropertyDescriptor(), reads from the descriptor and input object, and key/value conversions are callback-capable. The copied keys array is not directly exposed to the page, but it is inside the V8 sandbox. The stated attacker precondition—arbitrary read/write within that sandbox—allows its elements and length to be changed during one of those callbacks.

The attached PoC uses the public URLSearchParams record overload:

  1. A Proxy’s ownKeys trap returns one key, so Blink reserves one pair<String, String>.
  2. During the first get callback, the PoC uses V8’s official sandbox-testing API to locate Blink’s copied JSArray and replace its elements and length with a prepared 256-key array.
  3. The native loop observes the new length, while the native Vector still has capacity one.
  4. Up to 96 distinct pairs are written through UncheckedAppend() into and beyond that one-element allocation.
  5. The PoC grooms adjacent one-pair URLSearchParams vectors and checks for changed native entries. If it survives the overwrite, it also performs a JavaScript method call that writes back through a corrupted adjacent native vector.

In an instrumented API-enabled Chromium 151.0.7882.0 build at revision 3ddb585438332a1ab87654d620c5e6a488fe7f1b, where this code remained vulnerable, the defining transition was:

initial_keys=1 capacity=1 size=0
after value callback: dynamic_keys=256
append: pos=0 capacity=1
append: pos=1 capacity=1   # first native OOB pair
append: pos=2 capacity=1

This report assumes a compromised V8 sandbox. The Sandbox object in the PoC supplies only that precondition; it is not the vulnerability and is not available in production builds. V8 documents this testing API as a way to emulate common sandbox memory-corruption primitives.

The vulnerability reported here begins when Blink trusts the mutable V8 object after reserving native capacity. Its security impact is a V8 sandbox bypass: controlled writes reach native Blink/PartitionAlloc memory outside the V8 cage.

VERSION

  • Validated Chrome configuration: Stable 150.0.7871.128, Linux x86-64
  • Code-level scope: generic Blink record conversion on all Blink targets; V8-sandbox-bypass impact applies to supported 64-bit sandbox configurations
  • Chromium revision: 81891e5ca708047763816c778216799ef14c66cb
  • V8 revision: 2b2f69158528fdd9d86b778cfcc2d0a1c4f8c59f
  • Chrome GNU build ID: 2f071daf2a0b4aa68bda1729031294d443a989ab
  • OS used for validation: Ubuntu 24.04 x86-64

REPRODUCTION CASE

Attachment: poc.html

The focused testcase intentionally uses V8’s testing API to provide the documented compromised-sandbox precondition while isolating the Blink bug. Build at the affected revision with the API enabled. This must be a non-ASan build because Blink changes the behavior of UncheckedAppend() in its Linux x86-64 ASan configuration, as explained under the crash information below:

gn gen out/idlrecord-poc --args='is_asan=false is_debug=false dcheck_always_on=false symbol_level=1 blink_symbol_level=1 v8_symbol_level=1 v8_enable_sandbox=true v8_enable_memory_corruption_api=true'
autoninja -C out/idlrecord-poc chrome

Then:

cd idlrecord
python3 -m http.server 8000

/path/to/chromium/src/out/idlrecord-poc/chrome \
  --user-data-dir=/tmp/chrome-idlrecord-poc \
  --js-flags=--sandbox-testing \
  http://127.0.0.1:8000/poc.html

Expected behavior is a renderer crash/hang after the first append beyond capacity or a JSON result with "ok": true, changed adjacent URLSearchParams entries, and a writeBack object showing a JavaScript-driven native slot update. The exact outcome depends on the adjacent PartitionAlloc layout.

FOR CRASHES, PLEASE INCLUDE THE FOLLOWING ADDITIONAL INFORMATION

Type of crash: renderer process (native Blink heap corruption).

Crash State: No ASan trace is available for the validated Linux x86-64 target because M150’s standard ASan configuration changes the vulnerable operation into a checked one. container_annotations.h defines ANNOTATE_CONTIGUOUS_CONTAINER for Linux/ChromeOS x86-64 ASan builds. Under that macro, the M150 Vector::UncheckedAppend() implementation calls push_back(), which grows the vector when it is full, instead of performing the unchecked placement used in release builds. Producing an ASan report would therefore require altering Blink’s ASan-specific implementation and would not reproduce the shipped vulnerable operation under Chrome’s standard ASan configuration.

In a non-ASan build, the exact first invalid write is deterministic at the source level: after capacity one is reserved, result.UncheckedAppend() at position one writes a second pair<String, String> immediately beyond the allocation. On the validated x86-64 build the pair is 16 bytes; its size is target-dependent. Subsequent positions extend the overwrite.

The final fault location is heap-layout-dependent because the unchecked loop can corrupt several adjacent native objects before one is dereferenced. For triage, the key diagnostic is the allocation stack for ReserveInitialCapacity(1) paired with the write stack at UncheckedAppend().

CREDIT INFORMATION

Reporter credit: Found by XBOW and triaged by Andrés Luksenberg

View on issue tracker