High watch ✔︎ verified CVSS 8.8 chrome Integer Overflow 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in WTF
DescriptionInteger overflow in WTF
ComponentWTF
Sub-componentWTF
Bug ClassInteger Overflow
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable — factors set 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: Citations accurate; the size_t->wtf_size_t truncation and checked_cast fix are clearly shown. UninitializedCopy's copy-length impl is not in the material, so the full-extent overflow rests on that (attack path marks it inferred; commit message corroborates overflow risk).
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker514746176
Fix commitddf427ac6b17 (chromium/src) +4/-4
CWECWE-472
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-19 (per vendor advisory)
Fix landed2026-05-20 (public git)
Disclosed2026-05-27
Patch gap7 days — fix public in source before the advisory

Background

`WTF::Vector`
Blink’s vector container whose size/capacity are stored as wtf_size_t (third_party/blink/renderer/platform/wtf/vector.h).
`wtf_size_t`
a 32-bit unsigned size type, narrower than the 64-bit size_t/std::ranges::size inputs the constructors accept.
`base::checked_cast`
a narrowing cast that terminates the process instead of silently truncating when the value does not fit the target type (added by the fix).

Root Cause Analysis

The Vector(base::span<const U> other) constructor delegated the allocation with the untruncated size, : Base(other.size()), and then set size_ = other.size(); (third_party/blink/renderer/platform/wtf/vector.h:1878-1884). Here other.size() is a 64-bit size_t, but Base’s size parameter and size_ are 32-bit wtf_size_t, so a size exceeding 2^32 is silently truncated for the buffer allocation while the subsequent TypeOperations::UninitializedCopy(other, base::span(*this), ...) copies based on the span’s full extent — an undersized buffer with a full-length copy. The invariant that the allocation size equals the number of elements copied is therefore broken only when the source exceeds wtf_size_t range. The sibling range constructor had the same pattern: : Base(std::ranges::size(other)) with size_ = std::ranges::size(other); (vector.h:1890, 1901).

The fix wraps every such size with base::checked_cast<wtf_size_t>(...) in both constructors — Base(base::checked_cast<wtf_size_t>(other.size())) and size_ = base::checked_cast<wtf_size_t>(other.size()) (diff @@ -1876), and the analogous two sites in the range constructor (diff @@ -1887) — so an over-range size now crashes safely before any undersized allocation or oversized copy. The ANNOTATE_NEW_BUFFER calls (vector.h:1880, 1897) are left using the raw size; the security-relevant allocation and size_ are the ones hardened.

Key insight
The constructors truncated a 64-bit element count to 32-bit when sizing the buffer but copied the full source, so the fix adds base::checked_cast<wtf_size_t> to force a safe crash instead of a silent narrowing.

Attack Path

  1. Allocate oversize data Produce a source buffer/range larger than 2^32 elements on a 64-bit build. (inferred: the >4 GiB ArrayBuffer via IndexedDB is the reporter’s claim)
  2. Construct the vector Reach Vector(base::span<const U>) with that span, where Base(other.size()) truncates the allocation size (third_party/blink/renderer/platform/wtf/vector.h:1879). (inferred: the v8_binding_for_modules.cc IndexedDB key path is not in the material)
  3. Undersized buffer The truncated 32-bit value yields a too-small allocation while size_ and the copy reference the full length (vector.h:1880-1882).
  4. Linear overflow UninitializedCopy writes the full source past the small buffer, a heap buffer overflow in the renderer. (inferred: the memcpy/PartitionAlloc guard-page details are reporter claims)

Impact Assessment

Constructing a WTF::Vector from a span/range whose size exceeds 32 bits silently allocated a truncated buffer but copied the full source, giving a linear heap buffer overflow in the renderer process. The advisory rates it high severity with no in-the-wild exploitation; JavaScript reachability via IndexedDB and the exact overflow extent are reporter claims not shown in the material (inferred).

Files Changed

  • third_party/blink/renderer/platform/wtf/vector.h

Audit Directions

  • 64-bit to `wtf_size_t` narrowing
    Flag any assignment of size_t/std::ranges::size into wtf_size_t allocation parameters that lacks a checked cast.
  • Alloc-size vs. copy-length mismatch
    Verify the value used to size an allocation is the same value (and width) used to bound the subsequent copy/fill.
  • Container constructors from external data
    Audit container constructors that accept attacker-influenced sizes (spans, ranges, serialized buffers) for silent truncation.
From ddf427ac6b17eb7fc8ffb41adbac960acbe4d73d Mon Sep 17 00:00:00 2001
From: Kent Tamura <tkent@chromium.org>
Date: Tue, 19 May 2026 20:03:50 -0700
Subject: [PATCH] WTF: Use base::checked_cast for size casting in Vector constructors

Initializing a blink::Vector from a base::span or a range uses size_t
for the input size, but blink::Vector internally uses wtf_size_t
(32-bit) for its size representation.

This change introduces base::checked_cast<wtf_size_t> during
construction to ensure that if the input size exceeds the 32-bit limit,
the process crashes safely. This prevents silent integer truncation,
which could otherwise lead to memory safety issues such as buffer
overflows.

No unit tests are added because it is impractical to allocate a 4GB+
buffer to trigger this condition in a standard test environment.

Bug: 514746176
Change-Id: Iff0edf2a1ce40da294c77f9c5eeb5898ac56611b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7861980
Reviewed-by: Kentaro Hara <haraken@chromium.org>
Commit-Queue: Kent Tamura <tkent@chromium.org>
Auto-Submit: Kent Tamura <tkent@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1633311}
---

diff --git a/third_party/blink/renderer/platform/wtf/vector.h b/third_party/blink/renderer/platform/wtf/vector.h
index e5ae4f4..b1274e3f 100644
--- a/third_party/blink/renderer/platform/wtf/vector.h
+++ b/third_party/blink/renderer/platform/wtf/vector.h
@@ -1876,9 +1876,9 @@
 template <typename T, wtf_size_t InlineCapacity, typename Allocator>
 template <typename U>
 Vector<T, InlineCapacity, Allocator>::Vector(base::span<const U> other)
-    : Base(other.size()) {
+    : Base(base::checked_cast<wtf_size_t>(other.size())) {
   UNSAFE_TODO(ANNOTATE_NEW_BUFFER(data(), capacity(), other.size()));
-  size_ = other.size();
+  size_ = base::checked_cast<wtf_size_t>(other.size());
   TypeOperations::UninitializedCopy(other, base::span(*this),
                                     VectorOperationOrigin::kConstruction);
 }
@@ -1887,7 +1887,7 @@
 template <typename Range, typename Proj>
   requires VectorCanAssignFromRange<T, InlineCapacity, Allocator, Range, Proj>
 Vector<T, InlineCapacity, Allocator>::Vector(Range&& other, Proj proj)
-    : Base(std::ranges::size(other)) {
+    : Base(base::checked_cast<wtf_size_t>(std::ranges::size(other))) {
   // Note that `size(other)` may become smaller if `other` is a hash table
   // with WeakMember keys and `Base(size(other))` above caused GC which
   // removed some entries from `other`, see crbug.com/40448463. This won't
@@ -1898,7 +1898,7 @@
   TypeOperations::UninitializedTransform(
       std::ranges::begin(other), std::ranges::end(other), data(),
       VectorOperationOrigin::kConstruction, std::move(proj));
-  size_ = std::ranges::size(other);
+  size_ = base::checked_cast<wtf_size_t>(std::ranges::size(other));
 }
 
 template <typename T, wtf_size_t InlineCapacity, typename Allocator>
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential heap overflow in WTF::Vector constructor due to integer truncation

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A 64-to-32-bit integer truncation in the WTF::Vector constructor when initialized from a base::span results in an undersized buffer allocation. This leads to a potential linear heap buffer overflow of attacker-controlled data during the subsequent copy operation. The issue is reachable from JavaScript via the IndexedDB API using large ArrayBuffers.

Affected files:

  • third_party/blink/renderer/platform/wtf/vector.h
  • third_party/blink/renderer/bindings/modules/v8/v8_binding_for_modules.cc

Estimated timestamp from git blame: 2025-10-24

Description

A potential integer truncation vulnerability exists in the WTF::Vector constructor on 64-bit platforms. When a WTF::Vector is constructed from a base::span, the size of the span (a 64-bit size_t) is implicitly truncated to a 32-bit wtf_size_t (uint32_t) during buffer allocation and when setting the vector’s size_ member. This narrowing occurs without a base::checked_cast or any bounds validation.

The vulnerable code in third_party/blink/renderer/platform/wtf/vector.h is:

template <typename T, wtf_size_t InlineCapacity, typename Allocator>
template <typename U>
Vector<T, InlineCapacity, Allocator>::Vector(base::span<const U> other)
    : Base(other.size()) {
  UNSAFE_TODO(ANNOTATE_NEW_BUFFER(data(), capacity(), other.size()));
  size_ = other.size();
  TypeOperations::UninitializedCopy(other, base::span(*this),
                                    VectorOperationOrigin::kConstruction);
}

When other.size() exceeds 2^32 (e.g., 0x100000100 bytes), the Base (VectorBuffer) constructor receives the truncated 32-bit value (e.g., 0x100). This results in an undersized buffer allocation (e.g., 256 bytes).

Subsequently, TypeOperations::UninitializedCopy is called. For types that use memcpy (like char), it calculates the copy length based on the source span’s original 64-bit extent. This results in a memcpy of the full >4 GiB source data into the truncated 256-byte destination buffer, causing a linear heap buffer overflow.

Reachability and Impact

This vulnerability is potentially reachable from JavaScript without user interaction via the IndexedDB API. In third_party/blink/renderer/bindings/modules/v8/v8_binding_for_modules.cc, using an ArrayBuffer as an IndexedDB key triggers the vulnerable WTF::Vector<char> constructor:

return IDBKey::CreateBinary(
    base::MakeRefCounted<base::RefCountedData<Vector<char>>>(
        Vector<char>(base::as_chars(buffer->ByteSpan()))));

On 64-bit systems, ArrayBuffer objects can be as large as 32 GiB. An attacker can allocate an ArrayBuffer larger than 4 GiB and use it as a key in an operation like IDBObjectStore.put().

While a massive linear overflow will eventually hit a PartitionAlloc guard page (located every 2MB) and cause a crash, it can corrupt adjacent heap objects before the crash occurs. Since the destination vector is allocated on the renderer heap outside the V8 sandbox, this provides a primitive for memory corruption within the renderer process.

Suggested Potential Reproduction Steps

  1. On a 64-bit system with sufficient RAM, open a website and initialize an IndexedDB database.
  2. In JavaScript, allocate an ArrayBuffer larger than 4 GiB (e.g., new ArrayBuffer(0x100000100)).
  3. Perform an IndexedDB operation using this buffer as a key:
    let largeKey = new ArrayBuffer(0x100000100);
    db.transaction('store', 'readwrite').objectStore('store').put('value', largeKey);
    
  4. The renderer is expected to crash due to a heap buffer overflow in memcpy within the WTF::Vector constructor.

Note: These steps are suggested based on code analysis; the behavior hasn’t been verified with a running PoC.

Apply base::checked_cast<wtf_size_t> to the span size in the constructor to ensure that any size exceeding the maximum wtf_size_t results in a safe crash/exception rather than a truncation.

template <typename U>
Vector<T, InlineCapacity, Allocator>::Vector(base::span<const U> other)
    : Base(base::checked_cast<wtf_size_t>(other.size())) {
  ...
  size_ = base::checked_cast<wtf_size_t>(other.size());
  ...
}

Evaluated with Chrome root at commit: 29093e11cf509e3593f6229e4b1b075cca356049


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker