CVE-2026-11144
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fthird_party/blink/renderer/platform/media/multi_buffer_unittest.cc |
modified | |
MockDataProviderthird_party/blink/renderer/platform/media/multi_buffer_unittest.cc |
modified |
Files Changed
third_party/blink/renderer/platform/media/multi_buffer.ccthird_party/blink/renderer/platform/media/multi_buffer_reader.hthird_party/blink/renderer/platform/media/multi_buffer_unittest.cc
Patch
From 936ec5f355c9845ef70495d1e81d37911e7500d1 Mon Sep 17 00:00:00 2001
From: Dale Curtis <dalecurtis@chromium.org>
Date: Mon, 13 Apr 2026 19:43:31 -0700
Subject: [PATCH] Turn huge MultiBuffer requests into crashes
These should not happen in practice, though may be theoretically
possible. For safety, promote some DCHECKs to CHECKs.
R=tmathmeyer
Fixed: 501676175
Change-Id: Ia3180e24226a1a98b5edcf29fe9a9e0c710b50c7
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7759264
Commit-Queue: Dale Curtis <dalecurtis@chromium.org>
Commit-Queue: Ted (Chromium) Meyer <tmathmeyer@chromium.org>
Reviewed-by: Ted (Chromium) Meyer <tmathmeyer@chromium.org>
Auto-Submit: Dale Curtis <dalecurtis@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1614159}
---
diff --git a/third_party/blink/renderer/platform/media/multi_buffer.cc b/third_party/blink/renderer/platform/media/multi_buffer.cc
index a44fe6b..c5cf4b5 100644
--- a/third_party/blink/renderer/platform/media/multi_buffer.cc
+++ b/third_party/blink/renderer/platform/media/multi_buffer.cc
@@ -409,7 +409,7 @@
AddProvider(std::move(provider));
break;
}
- DCHECK_GE(pos, 0);
+ CHECK_GE(pos, 0);
scoped_refptr<media::DataBuffer> data = provider->Read();
data_.Set(pos, data);
eof = data->end_of_stream();
diff --git a/third_party/blink/renderer/platform/media/multi_buffer_reader.h b/third_party/blink/renderer/platform/media/multi_buffer_reader.h
index d4ea246..8c86465 100644
--- a/third_party/blink/renderer/platform/media/multi_buffer_reader.h
+++ b/third_party/blink/renderer/platform/media/multi_buffer_reader.h
@@ -12,6 +12,7 @@
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
+#include "base/numerics/safe_conversions.h"
#include "third_party/blink/renderer/platform/media/multi_buffer.h"
#include "third_party/blink/renderer/platform/platform_export.h"
@@ -113,8 +114,8 @@
// Returns the block for a particular byte position.
MultiBufferBlockId block(int64_t byte_pos) const {
- return static_cast<MultiBufferBlockId>(byte_pos >>
- multibuffer_->block_size_shift());
+ return base::checked_cast<MultiBufferBlockId>(
+ byte_pos >> multibuffer_->block_size_shift());
}
// Returns the block for a particular byte position, rounding up.
diff --git a/third_party/blink/renderer/platform/media/multi_buffer_unittest.cc b/third_party/blink/renderer/platform/media/multi_buffer_unittest.cc
index eed1065..b85c64b 100644
--- a/third_party/blink/renderer/platform/media/multi_buffer_unittest.cc
+++ b/third_party/blink/renderer/platform/media/multi_buffer_unittest.cc
@@ -613,4 +613,49 @@
multibuffer_.CheckPresentState();
}
+TEST_F(MultiBufferTest, BlockIdOverflowCrashes) {
+ // Mock a DataProvider that reports its position as -2.
+ class MockDataProvider : public MultiBuffer::DataProvider {
+ public:
+ explicit MockDataProvider(MultiBufferBlockId pos) : pos_(pos) {}
+ MultiBufferBlockId Tell() const override { return pos_; }
+ bool Available() const override { return available_; }
+ int64_t AvailableBytes() const override { return 0; }
+ scoped_refptr<media::DataBuffer> Read() override {
+ available_ = false;
+ return media::DataBuffer::CreateEOSBuffer();
+ }
+ void SetDeferred(bool deferred) override {}
+ void set_available(bool available) { available_ = available; }
+
+ private:
+ MultiBufferBlockId pos_;
+ bool available_ = false;
+ };
+
+ // We use -2 because it is the deleted sentinel for the HashMap.
+ MultiBufferBlockId sentinel_pos = -2;
+ auto mock_provider = std::make_unique<MockDataProvider>(sentinel_pos);
+ MockDataProvider* provider_ptr = mock_provider.get();
+ multibuffer_.AddProvider(std::move(mock_provider));
+
+ // Set available to true so OnDataProviderEvent can proceed.
+ provider_ptr->set_available(true);
+
+ // This call is expected to hit a CHECK_GE(pos, 0) in multi_buffer.cc.
+ EXPECT_DEATH(multibuffer_.OnDataProviderEvent(provider_ptr), "");
+}
+
+TEST_F(MultiBufferTest, BlockIdOverflow) {
+ // This test verifies that extremely large byte offsets that would cause
+ // MultiBufferBlockId (int32_t) overflow are caught by base::checked_cast.
+ int64_t huge_pos = 1LL << 45; // 32TB, overflows when shifted by 8.
+
+ MultiBufferReader reader(&multibuffer_, 0, huge_pos + 1024, false,
+ base::NullCallback(), task_runner_);
+
+ // This should crash due to base::checked_cast in MultiBufferReader::block.
+ EXPECT_DEATH(reader.Seek(huge_pos), "");
+}
+
} // namespace blink
Regression Test / PoC
diff --git a/third_party/blink/renderer/platform/media/multi_buffer_unittest.cc b/third_party/blink/renderer/platform/media/multi_buffer_unittest.cc
index eed1065..b85c64b 100644
--- a/third_party/blink/renderer/platform/media/multi_buffer_unittest.cc
+++ b/third_party/blink/renderer/platform/media/multi_buffer_unittest.cc
@@ -613,4 +613,49 @@
multibuffer_.CheckPresentState();
}
+TEST_F(MultiBufferTest, BlockIdOverflowCrashes) {
+ // Mock a DataProvider that reports its position as -2.
+ class MockDataProvider : public MultiBuffer::DataProvider {
+ public:
+ explicit MockDataProvider(MultiBufferBlockId pos) : pos_(pos) {}
+ MultiBufferBlockId Tell() const override { return pos_; }
+ bool Available() const override { return available_; }
+ int64_t AvailableBytes() const override { return 0; }
+ scoped_refptr<media::DataBuffer> Read() override {
+ available_ = false;
+ return media::DataBuffer::CreateEOSBuffer();
+ }
+ void SetDeferred(bool deferred) override {}
+ void set_available(bool available) { available_ = available; }
+
+ private:
+ MultiBufferBlockId pos_;
+ bool available_ = false;
+ };
+
+ // We use -2 because it is the deleted sentinel for the HashMap.
+ MultiBufferBlockId sentinel_pos = -2;
+ auto mock_provider = std::make_unique<MockDataProvider>(sentinel_pos);
+ MockDataProvider* provider_ptr = mock_provider.get();
+ multibuffer_.AddProvider(std::move(mock_provider));
+
+ // Set available to true so OnDataProviderEvent can proceed.
+ provider_ptr->set_available(true);
+
+ // This call is expected to hit a CHECK_GE(pos, 0) in multi_buffer.cc.
+ EXPECT_DEATH(multibuffer_.OnDataProviderEvent(provider_ptr), "");
+}
+
+TEST_F(MultiBufferTest, BlockIdOverflow) {
+ // This test verifies that extremely large byte offsets that would cause
+ // MultiBufferBlockId (int32_t) overflow are caught by base::checked_cast.
+ int64_t huge_pos = 1LL << 45; // 32TB, overflows when shifted by 8.
+
+ MultiBufferReader reader(&multibuffer_, 0, huge_pos + 1024, false,
+ base::NullCallback(), task_runner_);
+
+ // This should crash due to base::checked_cast in MultiBufferReader::block.
+ EXPECT_DEATH(reader.Seek(huge_pos), "");
+}
+
} // namespace blink
Original Bug Report
Potential Renderer UAF via integer truncation and sentinel key collision in WTF::HashMap
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 without the Chrome Security team.
Overview: An unchecked integer truncation in MultiBufferReader::block() can produce a block ID of -2, which is a reserved deleted sentinel in WTF::HashMap. Inserting this key causes a collision with a tombstone, leading to an assignment on an already-destructed scoped_refptr. This results in a potential double-release and Use-After-Free of a media::DataBuffer in the renderer process.
Affected files:
third_party/blink/renderer/platform/media/multi_buffer_reader.hthird_party/blink/renderer/platform/media/multi_buffer.ccthird_party/blink/renderer/platform/media/multi_buffer.hthird_party/blink/renderer/platform/wtf/hash_table.hthird_party/blink/renderer/platform/media/resource_multi_buffer_data_provider.cc
Estimated timestamp from git blame: 2025-08-11
Overview
A potential Use-After-Free (UAF) vulnerability exists in the Blink media MultiBuffer component. It is triggered by an unchecked 64-bit to 32-bit integer truncation that allows a reserved WTF::HashMap sentinel key to be inserted into the map.
Vulnerability Details
- Integer Truncation:
MultiBufferReader::block(int64_t byte_pos)calculates a block ID by shifting a 64-bit byte offset by 15 (for 32KB blocks) and casting it toMultiBufferBlockId(int32_t). If the byte offset is near 128TB, specifically140737488289792(128TB - 64KB), the shifted value is0xFFFFFFFE. Cast toint32_t, this truncates to-2. - Bounds Check Bypass: The seek logic checks
if (preload_pos_ < block_ceil(end_)). For a file size of 128TB,block_ceilcalculates an offset that overflows to0. Since-2 < 0is true, the check passes. - Network Request: The reader requests the data provider to fetch block
-2. The provider calculates the byte offset as-2 << 15 = -65536and constructs an HTTP request with the malformed headerRange: bytes=-65536-. - Error Handling: The server responds with
416 Range Not Satisfiable. The provider handles this by setting an End-Of-Stream (EOS) flag and passing an EOSmedia::DataBuffertoMultiBuffer::OnDataProviderEventwithpos = -2. - Sentinel Collision: The event handler calls
data_.Set(-2, eos_buffer).data_is aWTF::HashMapconfigured withIntHashTraits<BlockId, -1, -2>, making-2the map’s reserved “deleted” sentinel (tombstone). BecausekSafeToCompareToEmptyOrDeletedis true for integers,WTF::HashTable::insertchecks key equality before checking if a bucket is deleted. If the cache previously evicted a block, it will falsely match an existing tombstone bucket containing-2. - Use-After-Free:
HashMap::Setattempts to overwrite the mapped value of this tombstone. However, when the bucket was originally deleted, itsscoped_refptr<media::DataBuffer>value was explicitly destructed. The assignment operator performs a copy-and-swap using the tombstone’s stale memory pointer, causing a double-decrement of the underlyingmedia::DataBufferreference count when the temporaryscoped_refptris destroyed.
Because scoped_refptr explicitly uses RAW_PTR_EXCLUSION for its internal pointer, this vulnerability bypasses MiraclePtr protections and can lead to arbitrary code execution.
Potential Steps to Trigger
(Note: These are suggested/potential steps based on code analysis, as our tooling agent does not currently have the ability to run code or build a working proof-of-concept.)
- An attacker hosts a malicious website with an embedded
<video>element pointing to a crafted MP4 file. - The server responds with an HTTP header advertising a file size of exactly 128TB (
140737488355328bytes). - The user plays the media normally for a short period to exceed the cache budget, forcing a
MultiBufferLRU eviction. This creates at least one tombstone bucket in thedata_hash map. - The MP4’s index specifies a seek offset of
140737488289792. - The browser issues the
Range: bytes=-65536-request. The server immediately replies with a416status code. - The UAF is triggered during the EOS processing.
Suggested Fix
- Prevent Overflow: Modify
MultiBufferReader::blockandMultiBufferReader::block_ceilto usebase::checked_cast<int32_t>to safely handle excessively large byte offsets, or explicitly clamp the byte offsets to the supported 64TB limit. - Enforce Non-Negative IDs: In
MultiBuffer::OnDataProviderEvent, upgrade the existingDCHECK_GE(pos, 0)to aCHECK_GE(pos, 0)so that negative block IDs cause a safe process termination in Release builds before they can interact with the hash map.
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.