Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Media
DescriptionUse after free in Media
ComponentMedia
Bug ClassUAF
Tracker501676175
Fix commit936ec5f355c9 (chromium/src) +49/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
TEST_F
third_party/blink/renderer/platform/media/multi_buffer_unittest.cc
modified
MockDataProvider
third_party/blink/renderer/platform/media/multi_buffer_unittest.cc
modified

Files Changed

  • third_party/blink/renderer/platform/media/multi_buffer.cc
  • third_party/blink/renderer/platform/media/multi_buffer_reader.h
  • third_party/blink/renderer/platform/media/multi_buffer_unittest.cc
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
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by vm...@google.com

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.h
  • third_party/blink/renderer/platform/media/multi_buffer.cc
  • third_party/blink/renderer/platform/media/multi_buffer.h
  • third_party/blink/renderer/platform/wtf/hash_table.h
  • third_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

  1. 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 to MultiBufferBlockId (int32_t). If the byte offset is near 128TB, specifically 140737488289792 (128TB - 64KB), the shifted value is 0xFFFFFFFE. Cast to int32_t, this truncates to -2.
  2. Bounds Check Bypass: The seek logic checks if (preload_pos_ < block_ceil(end_)). For a file size of 128TB, block_ceil calculates an offset that overflows to 0. Since -2 < 0 is true, the check passes.
  3. Network Request: The reader requests the data provider to fetch block -2. The provider calculates the byte offset as -2 << 15 = -65536 and constructs an HTTP request with the malformed header Range: bytes=-65536-.
  4. 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 EOS media::DataBuffer to MultiBuffer::OnDataProviderEvent with pos = -2.
  5. Sentinel Collision: The event handler calls data_.Set(-2, eos_buffer). data_ is a WTF::HashMap configured with IntHashTraits<BlockId, -1, -2>, making -2 the map’s reserved “deleted” sentinel (tombstone). Because kSafeToCompareToEmptyOrDeleted is true for integers, WTF::HashTable::insert checks 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.
  6. Use-After-Free: HashMap::Set attempts to overwrite the mapped value of this tombstone. However, when the bucket was originally deleted, its scoped_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 underlying media::DataBuffer reference count when the temporary scoped_refptr is 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.)

  1. An attacker hosts a malicious website with an embedded <video> element pointing to a crafted MP4 file.
  2. The server responds with an HTTP header advertising a file size of exactly 128TB (140737488355328 bytes).
  3. The user plays the media normally for a short period to exceed the cache budget, forcing a MultiBuffer LRU eviction. This creates at least one tombstone bucket in the data_ hash map.
  4. The MP4’s index specifies a seek offset of 140737488289792.
  5. The browser issues the Range: bytes=-65536- request. The server immediately replies with a 416 status code.
  6. The UAF is triggered during the EOS processing.

Suggested Fix

  • Prevent Overflow: Modify MultiBufferReader::block and MultiBufferReader::block_ceil to use base::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 existing DCHECK_GE(pos, 0) to a CHECK_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.

View on issue tracker