CVE-2026-78939
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchromecast/starboard/media/renderer/demuxer_stream_reader.cc |
modified |
Files Changed
chromecast/starboard/media/renderer/demuxer_stream_reader.ccchromecast/starboard/media/renderer/starboard_player_manager_test.ccchromecast/starboard/media/renderer/starboard_renderer_test.cc
Patch
From 0bb350515768a179bc18af99960b48ebbfd10238 Mon Sep 17 00:00:00 2001 From: Richard Nichols <[email protected]> Date: Tue, 21 Jul 2026 06:48:16 -0700 Subject: [PATCH] [chromecast] Own audio extra_data in DemuxerStreamReader StarboardAudioSampleInfo::audio_specific_config is a non-owning pointer into an AudioDecoderConfig's extra_data. The DemuxerStreamReader already has a chromium_audio_config_ member intended to back this pointer, but it was only populated on kConfigChanged, not for the initial config supplied at construction time. As a result, audio_specific_config referenced the extra_data of a stack-local AudioDecoderConfig in StarboardPlayerManager::Create() that went out of scope before the first sample was written to starboard. Populate chromium_audio_config_ in the constructor and re-point audio_specific_config at the owned copy, matching what UpdateAudioConfig() already does for subsequent configs. Change-Id: I80ba0b17e7ec83d86fa3d5e27b92ff6788d910a2 Bug: 513261751 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8102143 Commit-Queue: Richard Nichols <[email protected]> Reviewed-by: Simeon Anfinrud <[email protected]> Reviewed-by: Shawn Quereshi <[email protected]> Cr-Commit-Position: refs/heads/main@{#1665437} --- diff --git a/chromecast/starboard/media/renderer/demuxer_stream_reader.cc b/chromecast/starboard/media/renderer/demuxer_stream_reader.cc index b388fd3..4e77b0c3 100644 --- a/chromecast/starboard/media/renderer/demuxer_stream_reader.cc +++ b/chromecast/starboard/media/renderer/demuxer_stream_reader.cc @@ -107,13 +107,23 @@ } if (audio_stream_) { - ::media::AudioDecoderConfig audio_config = - audio_stream_->audio_decoder_config(); + chromium_audio_config_ = audio_stream_->audio_decoder_config(); + if (audio_sample_info_) { + // audio_sample_info_'s audio_specific_config may currently point to the + // extra data of an AudioDecoderConfig owned by the caller. Re-point it + // at our own copy so that it remains valid for the lifetime of this + // object. + audio_sample_info_->audio_specific_config_size = + chromium_audio_config_.extra_data().size(); + audio_sample_info_->audio_specific_config = + chromium_audio_config_.extra_data().data(); + } - if (IsResamplingNecessary(audio_config)) { + if (IsResamplingNecessary(chromium_audio_config_)) { convert_audio_fn_ = base::BindRepeating( - &ConvertPcmAudioBufferToS16, audio_config.codec(), - audio_config.sample_format(), audio_config.channels()); + &ConvertPcmAudioBufferToS16, chromium_audio_config_.codec(), + chromium_audio_config_.sample_format(), + chromium_audio_config_.channels()); } else { convert_audio_fn_ = base::BindRepeating(&DoNotConvertBuffer); } diff --git a/chromecast/starboard/media/renderer/starboard_player_manager_test.cc b/chromecast/starboard/media/renderer/starboard_player_manager_test.cc index cc960f7..f827580 100644 --- a/chromecast/starboard/media/renderer/starboard_player_manager_test.cc +++ b/chromecast/starboard/media/renderer/starboard_player_manager_test.cc @@ -721,6 +721,83 @@ } TEST_F(StarboardPlayerManagerTest, + WritesInitialAudioSpecificConfigToStarboard) { + // Verify that when the initial audio config has extra data, the bytes that + // audio_specific_config points to are still readable and correct by the time + // the first sample is written to starboard. + constexpr auto kSeekTime = base::Seconds(10); + constexpr auto kAudioData = std::to_array<uint8_t>({9, 8, 7}); + const std::vector<uint8_t> kExtraData = {0x12, 0x10, 0x56, 0xE5, 0x00}; + + audio_stream_.set_audio_decoder_config(::media::AudioDecoderConfig( + ::media::AudioCodec::kAAC, ::media::SampleFormat::kSampleFormatS16, + ::media::ChannelLayoutConfig::Stereo(), 44100, kExtraData, + ::media::EncryptionScheme::kUnencrypted)); + + // This will be updated whenever the player manager seeks in starboard. + int seek_ticket = -1; + ON_CALL(starboard_, SeekTo(&sb_player_, _, _)) + .WillByDefault(SaveArg<2>(&seek_ticket)); + + // This will be set to the callbacks received by the mock Starboard. + const StarboardPlayerCallbackHandler* callbacks = nullptr; + EXPECT_CALL(starboard_, CreatePlayer(NotNull(), _)) + .WillOnce(DoAll(SaveArg<1>(&callbacks), Return(&sb_player_))); + + scoped_refptr<::media::DecoderBuffer> audio_buffer = + ::media::DecoderBuffer::CopyFrom(kAudioData); + EXPECT_CALL(audio_stream_, OnRead) + .WillOnce(RunOnceCallback<0>( + DemuxerStream::Status::kOk, + std::vector<scoped_refptr<::media::DecoderBuffer>>({audio_buffer}))); + + // Capture a copy of the bytes that starboard would read from + // audio_specific_config. + std::vector<uint8_t> captured_audio_specific_config; + EXPECT_CALL( + starboard_, + WriteSample(&sb_player_, StarboardMediaType::kStarboardMediaTypeAudio, _)) + .WillOnce(WithArg<2>( + [&captured_audio_specific_config]( + base::span<const StarboardSampleInfo> sample_infos) { + ASSERT_EQ(sample_infos.size(), 1u); + const StarboardAudioSampleInfo& audio_info = + sample_infos[0].audio_sample_info; + ASSERT_THAT(audio_info.audio_specific_config, NotNull()); + const uint8_t* config_bytes = + static_cast<const uint8_t*>(audio_info.audio_specific_config); + // SAFETY: audio_specific_config points to + // audio_specific_config_size bytes per the Starboard API contract. + UNSAFE_BUFFERS(captured_audio_specific_config.assign( + config_bytes, + config_bytes + audio_info.audio_specific_config_size)); + })); + + std::unique_ptr<StarboardPlayerManager> player_manager = + StarboardPlayerManager::Create( + &starboard_, &audio_stream_, /*video_stream=*/nullptr, + &renderer_client_, &metrics_helper_, + base::SequencedTaskRunner::GetCurrentDefault(), + /*enable_buffering=*/true); + ASSERT_THAT(player_manager, NotNull()); + + player_manager->StartPlayingFrom(kSeekTime); + + // Simulate Starboard requesting an audio buffer. + ASSERT_THAT(callbacks, NotNull()); + ASSERT_THAT(callbacks->decoder_status_fn, NotNull()); + ASSERT_THAT(callbacks->context, NotNull()); + callbacks->decoder_status_fn( + &sb_player_, callbacks->context, + StarboardMediaType::kStarboardMediaTypeAudio, + StarboardDecoderState::kStarboardDecoderStateNeedsData, seek_ticket); + + RunPendingTasks(); + + EXPECT_EQ(captured_audio_specific_config, kExtraData); +} + +TEST_F(StarboardPlayerManagerTest, CreatePlayerReturnsNullIfBothDemuxerStreamsAreNull) { EXPECT_THAT(StarboardPlayerManager::Create( &starboard_, /*audio_stream=*/nullptr, diff --git a/chromecast/starboard/media/renderer/starboard_renderer_test.cc b/chromecast/starboard/media/renderer/starboard_renderer_test.cc index 8f1ed6c..1683c07f 100644 --- a/chromecast/starboard/media/renderer/starboard_renderer_test.cc +++ b/chromecast/starboard/media/renderer/starboard_renderer_test.cc @@ -25,6 +25,7 @@ #include "base/task/sequenced_task_runner.h" #include "base/test/bind.h" #include "base/test/gmock_callback_support.h" +#include "base/test/gtest_util.h" #include "base/test/task_environment.h" #include "base/time/time.h" #include "base/unguessable_token.h"
Regression Test / PoC
diff --git a/chromecast/starboard/media/renderer/starboard_player_manager_test.cc b/chromecast/starboard/media/renderer/starboard_player_manager_test.cc
index cc960f7..f827580 100644
--- a/chromecast/starboard/media/renderer/starboard_player_manager_test.cc
+++ b/chromecast/starboard/media/renderer/starboard_player_manager_test.cc
@@ -721,6 +721,83 @@
}
TEST_F(StarboardPlayerManagerTest,
+ WritesInitialAudioSpecificConfigToStarboard) {
+ // Verify that when the initial audio config has extra data, the bytes that
+ // audio_specific_config points to are still readable and correct by the time
+ // the first sample is written to starboard.
+ constexpr auto kSeekTime = base::Seconds(10);
+ constexpr auto kAudioData = std::to_array<uint8_t>({9, 8, 7});
+ const std::vector<uint8_t> kExtraData = {0x12, 0x10, 0x56, 0xE5, 0x00};
+
+ audio_stream_.set_audio_decoder_config(::media::AudioDecoderConfig(
+ ::media::AudioCodec::kAAC, ::media::SampleFormat::kSampleFormatS16,
+ ::media::ChannelLayoutConfig::Stereo(), 44100, kExtraData,
+ ::media::EncryptionScheme::kUnencrypted));
+
+ // This will be updated whenever the player manager seeks in starboard.
+ int seek_ticket = -1;
+ ON_CALL(starboard_, SeekTo(&sb_player_, _, _))
+ .WillByDefault(SaveArg<2>(&seek_ticket));
+
+ // This will be set to the callbacks received by the mock Starboard.
+ const StarboardPlayerCallbackHandler* callbacks = nullptr;
+ EXPECT_CALL(starboard_, CreatePlayer(NotNull(), _))
+ .WillOnce(DoAll(SaveArg<1>(&callbacks), Return(&sb_player_)));
+
+ scoped_refptr<::media::DecoderBuffer> audio_buffer =
+ ::media::DecoderBuffer::CopyFrom(kAudioData);
+ EXPECT_CALL(audio_stream_, OnRead)
+ .WillOnce(RunOnceCallback<0>(
+ DemuxerStream::Status::kOk,
+ std::vector<scoped_refptr<::media::DecoderBuffer>>({audio_buffer})));
+
+ // Capture a copy of the bytes that starboard would read from
+ // audio_specific_config.
+ std::vector<uint8_t> captured_audio_specific_config;
+ EXPECT_CALL(
+ starboard_,
+ WriteSample(&sb_player_, StarboardMediaType::kStarboardMediaTypeAudio, _))
+ .WillOnce(WithArg<2>(
+ [&captured_audio_specific_config](
+ base::span<const StarboardSampleInfo> sample_infos) {
+ ASSERT_EQ(sample_infos.size(), 1u);
+ const StarboardAudioSampleInfo& audio_info =
+ sample_infos[0].audio_sample_info;
+ ASSERT_THAT(audio_info.audio_specific_config, NotNull());
+ const uint8_t* config_bytes =
+ static_cast<const uint8_t*>(audio_info.audio_specific_config);
+ // SAFETY: audio_specific_config points to
+ // audio_specific_config_size bytes per the Starboard API contract.
+ UNSAFE_BUFFERS(captured_audio_specific_config.assign(
+ config_bytes,
+ config_bytes + audio_info.audio_specific_config_size));
+ }));
+
+ std::unique_ptr<StarboardPlayerManager> player_manager =
+ StarboardPlayerManager::Create(
+ &starboard_, &audio_stream_, /*video_stream=*/nullptr,
+ &renderer_client_, &metrics_helper_,
+ base::SequencedTaskRunner::GetCurrentDefault(),
+ /*enable_buffering=*/true);
+ ASSERT_THAT(player_manager, NotNull());
+
+ player_manager->StartPlayingFrom(kSeekTime);
+
+ // Simulate Starboard requesting an audio buffer.
+ ASSERT_THAT(callbacks, NotNull());
+ ASSERT_THAT(callbacks->decoder_status_fn, NotNull());
+ ASSERT_THAT(callbacks->context, NotNull());
+ callbacks->decoder_status_fn(
+ &sb_player_, callbacks->context,
+ StarboardMediaType::kStarboardMediaTypeAudio,
+ StarboardDecoderState::kStarboardDecoderStateNeedsData, seek_ticket);
+
+ RunPendingTasks();
+
+ EXPECT_EQ(captured_audio_specific_config, kExtraData);
+}
+
+TEST_F(StarboardPlayerManagerTest,
CreatePlayerReturnsNullIfBothDemuxerStreamsAreNull) {
EXPECT_THAT(StarboardPlayerManager::Create(
&starboard_, /*audio_stream=*/nullptr,
diff --git a/chromecast/starboard/media/renderer/starboard_renderer_test.cc b/chromecast/starboard/media/renderer/starboard_renderer_test.cc
index 8f1ed6c..1683c07f 100644
--- a/chromecast/starboard/media/renderer/starboard_renderer_test.cc
+++ b/chromecast/starboard/media/renderer/starboard_renderer_test.cc
@@ -25,6 +25,7 @@
#include "base/task/sequenced_task_runner.h"
#include "base/test/bind.h"
#include "base/test/gmock_callback_support.h"
+#include "base/test/gtest_util.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "base/unguessable_token.h"
Original Bug Report
Potential Browser Process Use-After-Free in StarboardRenderer on Chromecast
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A Use-After-Free (UAF) vulnerability exists in the StarboardRenderer component on Chromecast due to improper lifetime management of audio configuration data. A pointer to a stack-local buffer is retained and subsequently used during media playback after the buffer has been freed. This could potentially allow a compromised renderer process to achieve code execution in the unsandboxed browser process.
Affected files:
chromecast/starboard/media/renderer/starboard_player_manager.ccchromecast/starboard/media/renderer/demuxer_stream_reader.ccchromecast/starboard/media/renderer/chromium_starboard_conversions.ccchromecast/starboard/media/renderer/demuxer_stream_reader.hchromecast/starboard/media/media/starboard_api_wrapper.h
Estimated timestamp from git blame: 2025-06-05
Summary
A potential Use-After-Free (UAF) vulnerability has been identified in the StarboardRenderer implementation used on Chromecast. The issue occurs because StarboardPlayerManager retains a dangling raw pointer to an AudioDecoderConfig buffer that is destroyed immediately after initialization. This dangling pointer is used for every audio sample written to the Starboard platform API, potentially leading to memory corruption in the unsandboxed browser process.
Root Cause Analysis
In StarboardPlayerManager::Create (chromecast/starboard/media/renderer/starboard_player_manager.cc), a stack-local ::media::AudioDecoderConfig object is created to store the initial audio configuration:
// line 35
::media::AudioDecoderConfig audio_config;
// ...
// line 48
audio_config = audio_stream->audio_decoder_config();
// line 49
audio_sample_info = ToStarboardAudioSampleInfo(audio_config);
The function ToStarboardAudioSampleInfo (chromecast/starboard/media/renderer/chromium_starboard_conversions.cc) converts this configuration into a StarboardAudioSampleInfo structure. During this conversion, it captures a raw pointer to the configuration’s internal extra_data buffer:
// line 304
out_config.audio_specific_config_size = in_config.extra_data().size();
// line 305
out_config.audio_specific_config = in_config.extra_data().data();
This structure, containing the pointer to the stack-local audio_config’s heap buffer, is stored in DemuxerStreamReader::audio_sample_info_. However, once StarboardPlayerManager::Create returns, the stack-local audio_config is destroyed, and its internal extra_data buffer is freed.
Crucially, DemuxerStreamReader does not save the AudioDecoderConfig during its initial construction to keep the buffer alive, even though it has a member chromium_audio_config_ intended for this purpose. This member is only populated during mid-stream configuration changes in UpdateAudioConfig.
Impact
Every subsequent audio sample processed by DemuxerStreamReader::OnReadBuffer copies this dangling pointer into the sample information passed to the Starboard API:
// chromecast/starboard/media/renderer/demuxer_stream_reader.cc:255
sample_info.audio_sample_info = *audio_sample_info_;
The Starboard platform implementation (e.g., SbPlayerWriteSample2) then dereferences this dangling pointer in the unsandboxed browser process. A compromised renderer can control the contents and size of the extra_data buffer, making this a potential path for a sandbox escape and remote code execution (RCE) in the browser process.
Potential Replication Steps
- Compromise a renderer process on a Chromecast device.
- Trigger media playback using
StarboardRenderer(e.g., by providing a specific audio configuration via Mojo). - Reallocate the freed
extra_databuffer in the browser process with attacker-controlled data (e.g., via heap spraying or other IPCs). - Observe the browser process crash or exhibit controlled behavior when
SbPlayerWriteSample2is called with the corrupted configuration data.
Note: These steps are based on static analysis of the code flow; a functional proof-of-concept has not been executed.
Suggested Fix
The DemuxerStreamReader should ensure the initial AudioDecoderConfig is preserved. In DemuxerStreamReader::DemuxerStreamReader (chromecast/starboard/media/renderer/demuxer_stream_reader.cc:85), the constructor should store the current configuration from the stream into chromium_audio_config_ if the stream exists.
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
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.
- https://source.chromium.org/chromium/chromium/src/+/main:chromecast/starboard/media/renderer/chromium_starboard_conversions.cc;l=248
- https://source.chromium.org/chromium/chromium/src/+/main:chromecast/starboard/media/renderer/demuxer_stream_reader.cc;l=85
- https://source.chromium.org/chromium/chromium/src/+/main:chromecast/starboard/media/renderer/starboard_player_manager.cc;l=19