CVE-2026-79292
Overview
Background
- `StarboardSampleInfo`
- A struct handed to Starboard’s
SbPlayerWriteSample*APIs describing one media sample, whosebuffer_sizefield is a signedint. - `DemuxerStreamReader`
- The Chromecast/Starboard media component that pulls decoded/demuxed buffers off a
DemuxerStreamand 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 thanINT_MAX. - `base::IsValueInRangeForNumericType<int>`
- A
base/numericshelper 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.
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
- Supply an oversized media buffer
An attacker gets the demuxer to emit a
DecoderBufferwhosesize()isINT_MAXor larger (2 GiB+), for example through a crafted media stream, exactly as the regression test models with aMAP_NORESERVEreservation ofINT_MAX + 2bytes. - Reach the vulnerable assignment
DemuxerStreamReader::OnReadBufferruns for that buffer and, pre-patch, assigns thesize_tlength straight into theintStarboardSampleInfo::buffer_size. - 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.” - Hand the bad size to Starboard
The malformed
sample_infois passed toSbPlayerWriteSample*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 theSbPlayerWriteSample*implementation.)
Impact Assessment
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
| Function | Change | Notes |
|---|---|---|
TEST_Fchromecast/starboard/media/renderer/demuxer_stream_reader_test.cc |
modified | |
ifchromecast/starboard/media/renderer/demuxer_stream_reader_test.cc |
modified |
Files Changed
chromecast/starboard/media/renderer/demuxer_stream_reader.ccchromecast/starboard/media/renderer/demuxer_stream_reader_test.cc
Audit Directions
- Unchecked `size_t` → `int` assignments into Starboard structsAudit every place that fills
StarboardSampleInfo,StarboardVideoSampleInfo, side-data counts, or similarintfields fromsize_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 APIsLook for buffer sizes, offsets, and counts derived from demuxed/decoded media that are passed to
Sb*C APIs takingint, and verify a range check orbase::checked_cast/base::IsValueInRangeForNumericTypeguards each narrowing. - Implicit narrowing conversions in media forwarding pathsSearch the
chromecast/starboard/mediarenderer for implicit conversions and casts that lack an explicit safe-numerics check, especially on paths that forward externally sizedDecoderBuffers.
Patch
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 = {};
Regression Test / PoC
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 = {};
Original Bug Report
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.ccchromecast/starboard/media/media/starboard_api_wrapper.hchromecast/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
- From a compromised renderer, bind the browser-side
media.mojom.Renderer(StarboardRenderer). - Supply a renderer-hosted
mojom::DemuxerStreamduring initialization. - Reply to a
DemuxerStream::Readrequest with amojom::DecoderBufferwhoseDataDecoderBuffer.data_sizeis in the range[0x80000000, 0xFFFFFFFF]. - Stream the data through the data pipe to complete the read in the browser.
DemuxerStreamReader::OnReadBuffernarrows thesize_tto a negativeint, 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>::Uniniton 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.