Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in Chromecast
DescriptionInteger overflow in Chromecast
ComponentChromecast
Sub-componentStarboard
Bug ClassInteger Overflow
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
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.
Tracker517519352
Fix commit55122e96e412 (chromium/src) +74/-1
CWECWE-190 (Integer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-28 (per vendor advisory)
Fix landed2026-07-17 (public git)
Disclosed2026-08-25
Patch gap39 days — fix public in source before the advisory

Background

`StarboardSampleInfo`
A struct handed to Starboard’s SbPlayerWriteSample* APIs describing one media sample, whose buffer_size field is a signed int.
`DemuxerStreamReader`
The Chromecast/Starboard media component that pulls decoded/demuxed buffers off a DemuxerStream and forwards them into the Starboard player pipeline.
`DecoderBuffer::size()`
Returns the buffer’s length as a size_t (unsigned, 64-bit on the target), which can represent values far larger than INT_MAX.
`base::IsValueInRangeForNumericType<int>`
A base/numerics helper that returns whether a given value can be represented in the target type (int) without loss.

Root Cause Analysis

Per the commit message, DemuxerStreamReader::OnReadBuffer (the enclosing function is named only in the commit message, not in the @@ hunk-context header) assigned buffer->size(), a size_t, directly into StarboardSampleInfo::buffer_size, which is a signed int. For a DecoderBuffer of 2 GiB or more, this implicit narrowing conversion silently truncated the size, and because the low 32 bits of such a value have the sign bit set it became a negative int that was then passed to SbPlayerWriteSample*. The invariant violated is that the size forwarded to the Starboard player must faithfully represent the actual buffer length; truncation broke the correspondence between the reported buffer_size and the memory span.

The fix adds an explicit bound check with base::IsValueInRangeForNumericType<int>(buffer->size()) before building the sample_info; if the size does not fit, it logs an error, calls client_->OnError(::media::PIPELINE_ERROR_DECODE), and returns, so no sample is ever written. The remaining conversion is made explicit with static_cast<int>(buffer->size()), which is now safe because the range check guarantees it fits. This works because the oversized buffer is rejected at the pipeline layer before any truncated value can reach the Starboard sink.

Key insight
The single mistake is an unchecked implicit size_t-to-int narrowing of an attacker-influenceable buffer length that wraps to a negative value; the fix closes it by range-checking the size against int up front and failing the decode instead of forwarding a truncated size.

Attack Path

  1. Supply an oversized media buffer An attacker gets the demuxer to emit a DecoderBuffer whose size() is INT_MAX or larger (2 GiB+), for example through a crafted media stream, exactly as the regression test models with a MAP_NORESERVE reservation of INT_MAX + 2 bytes.
  2. Reach the vulnerable assignment DemuxerStreamReader::OnReadBuffer runs for that buffer and, pre-patch, assigns the size_t length straight into the int StarboardSampleInfo::buffer_size.
  3. Silent truncation to a negative value The narrowing conversion drops the high bits and produces a negative buffer_size, since the commit states such buffers “silently truncate to a negative value.”
  4. Hand the bad size to Starboard The malformed sample_info is passed to SbPlayerWriteSample* with a size that no longer matches the real buffer span, corrupting the length seen by the Starboard player. (inferred: any downstream out-of-bounds access, memory corruption, or further escalation — the patch does not show the SbPlayerWriteSample* implementation.)

Impact Assessment

An attacker able to drive an oversized (≥2 GiB) media buffer through the Chromecast Starboard media pipeline could cause an integer overflow/truncation that hands a negative, mismatched sample size to SbPlayerWriteSample* in the Cast media/renderer context. The concrete downstream consequence (out-of-bounds access or memory corruption) is not demonstrated by the diff and is (inferred); the advisory classifies this only as an integer overflow in Chromecast. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.3. The advisory does not state exploited-in-the-wild status or a severity rating.

Changed Functions

FunctionChangeNotes
TEST_F
chromecast/starboard/media/renderer/demuxer_stream_reader_test.cc
modified
if
chromecast/starboard/media/renderer/demuxer_stream_reader_test.cc
modified

Files Changed

  • chromecast/starboard/media/renderer/demuxer_stream_reader.cc
  • chromecast/starboard/media/renderer/demuxer_stream_reader_test.cc

Audit Directions

  • Unchecked `size_t` → `int` assignments into Starboard structs
    Audit every place that fills StarboardSampleInfo, StarboardVideoSampleInfo, side-data counts, or similar int fields from size_t/size() values, since the same silent truncation applies wherever a length crosses into a signed 32-bit field.
  • Attacker-influenced lengths reaching fixed-width APIs
    Look for buffer sizes, offsets, and counts derived from demuxed/decoded media that are passed to Sb* C APIs taking int, and verify a range check or base::checked_cast/base::IsValueInRangeForNumericType guards each narrowing.
  • Implicit narrowing conversions in media forwarding paths
    Search the chromecast/starboard/media renderer for implicit conversions and casts that lack an explicit safe-numerics check, especially on paths that forward externally sized DecoderBuffers.
From 55122e96e41286a8775dd1ae82eca0de37c92487 Mon Sep 17 00:00:00 2001
From: Richard Nichols <[email protected]>
Date: Fri, 17 Jul 2026 12:41:47 -0700
Subject: [PATCH] [starboard] Reject DecoderBuffers that exceed sample_info limits

DemuxerStreamReader::OnReadBuffer assigns DecoderBuffer::size() (a
size_t) directly into StarboardSampleInfo::buffer_size, which is an int.
For buffers of 2 GiB or more this silently truncates to a negative value
that is then handed to SbPlayerWriteSample*.

Bound-check the size with base::IsValueInRangeForNumericType and report
PIPELINE_ERROR_DECODE for anything that does not fit, and make the
remaining narrowing conversion explicit. Adds a unit test that backs a
DecoderBuffer with an mmap(MAP_NORESERVE) reservation larger than
INT_MAX and verifies that the buffer callback is not invoked and the
client receives an error.

Bug: 517519352
Change-Id: I0feac5e7ea3792708f4e8c1851a1ca5f05aafa60
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8117188
Reviewed-by: Shawn Quereshi <[email protected]>
Commit-Queue: Richard Nichols <[email protected]>
Reviewed-by: Simeon Anfinrud <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1664118}
---
diff --git a/chromecast/starboard/media/renderer/demuxer_stream_reader.cc b/chromecast/starboard/media/renderer/demuxer_stream_reader.cc
index 5334cc14..b388fd3 100644
--- a/chromecast/starboard/media/renderer/demuxer_stream_reader.cc
+++ b/chromecast/starboard/media/renderer/demuxer_stream_reader.cc
@@ -12,6 +12,7 @@
 #include "base/functional/bind.h"
 #include "base/hash/hash.h"
 #include "base/logging.h"
+#include "base/numerics/safe_conversions.h"
 #include "base/task/bind_post_task.h"
 #include "base/task/sequenced_task_runner.h"
 #include "chromecast/base/metrics/cast_metrics_helper.h"
@@ -243,10 +244,19 @@
     buffer = convert_audio_fn_.Run(std::move(buffer));
   }
 
+  // StarboardSampleInfo::buffer_size is an int, so reject anything that does
+  // not fit rather than silently truncating the value.
+  if (!base::IsValueInRangeForNumericType<int>(buffer->size())) {
+    LOG(ERROR) << "DecoderBuffer size (" << buffer->size()
+               << ") exceeds the maximum supported sample size.";
+    client_->OnError(::media::PIPELINE_ERROR_DECODE);
+    return;
+  }
+
   StarboardSampleInfo sample_info = {};
   sample_info.type = type;
   sample_info.buffer = base::span(*buffer).data();
-  sample_info.buffer_size = buffer->size();
+  sample_info.buffer_size = static_cast<int>(buffer->size());
   sample_info.timestamp = buffer->timestamp().InMicroseconds();
   sample_info.side_data = base::span<const StarboardSampleSideData>();
 
diff --git a/chromecast/starboard/media/renderer/demuxer_stream_reader_test.cc b/chromecast/starboard/media/renderer/demuxer_stream_reader_test.cc
index 15437e3..239f1614 100644
--- a/chromecast/starboard/media/renderer/demuxer_stream_reader_test.cc
+++ b/chromecast/starboard/media/renderer/demuxer_stream_reader_test.cc
@@ -4,9 +4,12 @@
 
 #include "chromecast/starboard/media/renderer/demuxer_stream_reader.h"
 
+#include <sys/mman.h>
+
 #include <array>
 #include <cstdint>
 #include <functional>
+#include <limits>
 #include <string>
 #include <string_view>
 #include <tuple>
@@ -27,6 +30,7 @@
 #include "media/base/encryption_scheme.h"
 #include "media/base/mock_filters.h"
 #include "media/base/sample_format.h"
+#include "media/base/test_helpers.h"
 #include "media/base/video_transformation.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -180,6 +184,65 @@
   RunPendingTasks();
 }
 
+TEST_F(DemuxerStreamReaderTest, RejectsVideoBufferLargerThanIntMax) {
+  // StarboardSampleInfo::buffer_size is an int, so a DecoderBuffer whose size
+  // does not fit in an int must be reported as an error rather than being
+  // forwarded with a truncated size.
+  constexpr int kSeekTicket = 7;
+  constexpr size_t kBufferSize =
+      static_cast<size_t>(std::numeric_limits<int>::max()) + 2;
+
+  // Reserve address space without committing physical pages so that the test
+  // does not require gigabytes of RAM.
+  void* mapping = mmap(nullptr, kBufferSize, PROT_READ,
+                       MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
+  if (mapping == MAP_FAILED) {
+    GTEST_SKIP() << "Unable to reserve " << kBufferSize
+                 << " bytes of address space.";
+  }
+
+  auto external_memory = std::make_unique<DecoderBuffer::UnownedExternalMemory>(
+      // SAFETY: `mapping` refers to `kBufferSize` bytes returned by mmap above
+      // and remains valid until the matching munmap below.
+      UNSAFE_BUFFERS(base::span<const uint8_t>(
+          static_cast<const uint8_t*>(mapping), kBufferSize)));
+  scoped_refptr<DecoderBuffer> buffer =
+      DecoderBuffer::FromExternalMemory(std::move(external_memory));
+  ASSERT_EQ(buffer->size(), kBufferSize);
+
+  StarboardVideoSampleInfo video_sample_info =
+      CreateVideoSample(kBufferData).video_sample_info;
+
+  EXPECT_CALL(handle_eos_cb_, Call).Times(0);
+  EXPECT_CALL(handle_buffer_cb_, Call).Times(0);
+  EXPECT_CALL(renderer_client_,
+              OnError(HasStatusCode(::media::PIPELINE_ERROR_DECODE)))
+      .Times(1);
+  base::OnceCallback<void(
+      DemuxerStream::Status status,
+      std::vector<scoped_refptr<::media::DecoderBuffer>> buffers)>
+      read_cb;
+  EXPECT_CALL(video_stream_, OnRead).WillOnce(SaveArgByMove<0>(&read_cb));
+
+  DemuxerStreamReader stream_reader(
+      /*audio_stream=*/nullptr, &video_stream_,
+      /*audio_sample_info=*/std::nullopt, video_sample_info,
+      base::BindLambdaForTesting(handle_buffer_cb_.AsStdFunction()),
+      base::BindLambdaForTesting(handle_eos_cb_.AsStdFunction()),
+      &renderer_client_, &metrics_helper_);
+  stream_reader.ReadBuffer(kSeekTicket,
+                           StarboardMediaType::kStarboardMediaTypeVideo);
+
+  // Simulate the DemuxerStream providing the oversized buffer.
+  ASSERT_FALSE(read_cb.is_null());
+  std::move(read_cb).Run(DemuxerStream::Status::kOk, {buffer});
+
+  RunPendingTasks();
+
+  buffer.reset();
+  munmap(mapping, kBufferSize);
+}
+
 TEST_F(DemuxerStreamReaderTest, ReadsVideoBufferAndCallsEosCb) {
   constexpr int kSeekTicket = 7;
   StarboardVideoSampleInfo video_sample_info = {};
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chromecast/starboard/media/renderer/demuxer_stream_reader_test.cc b/chromecast/starboard/media/renderer/demuxer_stream_reader_test.cc
index 15437e3..239f1614 100644
--- a/chromecast/starboard/media/renderer/demuxer_stream_reader_test.cc
+++ b/chromecast/starboard/media/renderer/demuxer_stream_reader_test.cc
@@ -4,9 +4,12 @@
 
 #include "chromecast/starboard/media/renderer/demuxer_stream_reader.h"
 
+#include <sys/mman.h>
+
 #include <array>
 #include <cstdint>
 #include <functional>
+#include <limits>
 #include <string>
 #include <string_view>
 #include <tuple>
@@ -27,6 +30,7 @@
 #include "media/base/encryption_scheme.h"
 #include "media/base/mock_filters.h"
 #include "media/base/sample_format.h"
+#include "media/base/test_helpers.h"
 #include "media/base/video_transformation.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -180,6 +184,65 @@
   RunPendingTasks();
 }
 
+TEST_F(DemuxerStreamReaderTest, RejectsVideoBufferLargerThanIntMax) {
+  // StarboardSampleInfo::buffer_size is an int, so a DecoderBuffer whose size
+  // does not fit in an int must be reported as an error rather than being
+  // forwarded with a truncated size.
+  constexpr int kSeekTicket = 7;
+  constexpr size_t kBufferSize =
+      static_cast<size_t>(std::numeric_limits<int>::max()) + 2;
+
+  // Reserve address space without committing physical pages so that the test
+  // does not require gigabytes of RAM.
+  void* mapping = mmap(nullptr, kBufferSize, PROT_READ,
+                       MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
+  if (mapping == MAP_FAILED) {
+    GTEST_SKIP() << "Unable to reserve " << kBufferSize
+                 << " bytes of address space.";
+  }
+
+  auto external_memory = std::make_unique<DecoderBuffer::UnownedExternalMemory>(
+      // SAFETY: `mapping` refers to `kBufferSize` bytes returned by mmap above
+      // and remains valid until the matching munmap below.
+      UNSAFE_BUFFERS(base::span<const uint8_t>(
+          static_cast<const uint8_t*>(mapping), kBufferSize)));
+  scoped_refptr<DecoderBuffer> buffer =
+      DecoderBuffer::FromExternalMemory(std::move(external_memory));
+  ASSERT_EQ(buffer->size(), kBufferSize);
+
+  StarboardVideoSampleInfo video_sample_info =
+      CreateVideoSample(kBufferData).video_sample_info;
+
+  EXPECT_CALL(handle_eos_cb_, Call).Times(0);
+  EXPECT_CALL(handle_buffer_cb_, Call).Times(0);
+  EXPECT_CALL(renderer_client_,
+              OnError(HasStatusCode(::media::PIPELINE_ERROR_DECODE)))
+      .Times(1);
+  base::OnceCallback<void(
+      DemuxerStream::Status status,
+      std::vector<scoped_refptr<::media::DecoderBuffer>> buffers)>
+      read_cb;
+  EXPECT_CALL(video_stream_, OnRead).WillOnce(SaveArgByMove<0>(&read_cb));
+
+  DemuxerStreamReader stream_reader(
+      /*audio_stream=*/nullptr, &video_stream_,
+      /*audio_sample_info=*/std::nullopt, video_sample_info,
+      base::BindLambdaForTesting(handle_buffer_cb_.AsStdFunction()),
+      base::BindLambdaForTesting(handle_eos_cb_.AsStdFunction()),
+      &renderer_client_, &metrics_helper_);
+  stream_reader.ReadBuffer(kSeekTicket,
+                           StarboardMediaType::kStarboardMediaTypeVideo);
+
+  // Simulate the DemuxerStream providing the oversized buffer.
+  ASSERT_FALSE(read_cb.is_null());
+  std::move(read_cb).Run(DemuxerStream::Status::kOk, {buffer});
+
+  RunPendingTasks();
+
+  buffer.reset();
+  munmap(mapping, kBufferSize);
+}
+
 TEST_F(DemuxerStreamReaderTest, ReadsVideoBufferAndCallsEosCb) {
   constexpr int kSeekTicket = 7;
   StarboardVideoSampleInfo video_sample_info = {};
Loading diff…

Original Bug Report

reported by [email protected]

Potential integer narrowing of DecoderBuffer size in Starboard DemuxerStreamReader

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: In Chromecast/Starboard builds, DemuxerStreamReader assigns a size_t buffer size to a signed 32-bit int. If a compromised renderer provides a buffer size exceeding 2 GiB, this value narrows to a negative integer. The negative size is then passed with a valid heap pointer to proprietary Starboard player APIs in the browser process.

Affected files:

  • chromecast/starboard/media/renderer/demuxer_stream_reader.cc
  • chromecast/starboard/media/media/starboard_api_wrapper.h
  • chromecast/starboard/media/media/starboard_api_wrapper_base.cc

Estimated timestamp from git blame: 2025-06-05

Potential Vulnerability Details

In Chromecast/Starboard media playback configurations, the StarboardRenderer runs inside the browser process’s in-process MediaService. A potential integer narrowing defect exists in DemuxerStreamReader::OnReadBuffer where a size_t value is implicitly cast into a signed int when copying to StarboardSampleInfo.

In chromecast/starboard/media/renderer/demuxer_stream_reader.cc:

StarboardSampleInfo sample_info = {};
sample_info.type = type;
sample_info.buffer = base::span(*buffer).data();
sample_info.buffer_size = buffer->size();  // Narrowing from size_t to int!

In chromecast/starboard/media/media/starboard_api_wrapper.h, the StarboardSampleInfo struct is defined as:

struct StarboardSampleInfo {
  int type;
  const void* buffer;
  int buffer_size; // Signed int
  ...
};

If a compromised renderer sends a Mojo DataDecoderBuffer with a data_size of 0x80000001 (approx. 2 GiB) or larger, this size is successfully promoted to size_t during conversion/deserialization. However, when assigned to sample_info.buffer_size, the implicit narrowing to a signed int results in a negative value (e.g., -2147483647). This negative value is then forwarded verbatim to SbPlayerWriteSample2 or SbPlayerWriteSamples in the closed-source proprietary platform/vendor library (libcast_starboard_api.so).

Potential Attack Steps

  1. From a compromised renderer, bind the browser-side media.mojom.Renderer (StarboardRenderer).
  2. Supply a renderer-hosted mojom::DemuxerStream during initialization.
  3. Reply to a DemuxerStream::Read request with a mojom::DecoderBuffer whose DataDecoderBuffer.data_size is in the range [0x80000000, 0xFFFFFFFF].
  4. Stream the data through the data pipe to complete the read in the browser.
  5. DemuxerStreamReader::OnReadBuffer narrows the size_t to a negative int, which is then sent to the vendor Starboard APIs.

Real-world Mitigations & Practical Exploitability

Note: Our security analysis tools do not have the capability to run code or execute a live proof of concept on physical hardware. These steps are theoretical.

In practice, this bug is highly unlikely to be exploitable due to physical memory constraints:

  • Allocating a contiguous 2 GiB buffer via base::HeapArray<uint8_t>::Uninit on typical 32-bit or memory-constrained embedded Chromecast/Starboard architectures will fail and trigger an immediate out-of-memory (OOM) crash.
  • Even if virtual memory allocation succeeds, streaming 2 GiB of data through a Mojo data pipe will consume physical memory/swap resources, triggering the OS Out-of-Memory (OOM) killer before the read callback completes. This limits the real-world impact of this code flaw to a Denial of Service (DoS) stability crash.

Suggested Remediation

To prevent potential integer narrowing, use base::checked_cast or validate the size of incoming buffers to enforce a reasonable upper boundary on DecoderBuffer sizes before processing:

sample_info.buffer_size = base::checked_cast<int>(buffer->size());

Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379


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