CVE-2026-79292
Overview
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
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.