CVE-2026-78939
Overview
Background
- `DemuxerStreamReader`
- a Chromecast Starboard-renderer helper (
chromecast/starboard/media/renderer/demuxer_stream_reader.cc) that pullsDecoderBuffers from a ChromiumDemuxerStreamand hands them to the Starboard player asStarboardSampleInforecords. - `StarboardAudioSampleInfo::audio_specific_config`
- a raw, non-owning pointer (with an
audio_specific_config_size) to codec setup bytes (e.g. the AAC AudioSpecificConfig) that Starboard reads when a sample is written; whoever fills it must keep the bytes alive. - `AudioDecoderConfig::extra_data()`
- the codec “extra data” held by value inside a
::media::AudioDecoderConfig; its backing storage is freed when that config object is destroyed. - `chromium_audio_config_`
- a member of
DemuxerStreamReadermeant to own a copy of the current audio config so thataudio_specific_configcan safely point into it.
Root Cause Analysis
Before the patch, the DemuxerStreamReader constructor (the hunk body is the if (audio_stream_) block the commit message describes as constructor code; the function signature itself is not in the hunk) copied the stream’s config into a local ::media::AudioDecoderConfig audio_config used only to choose convert_audio_fn_, and never populated chromium_audio_config_. That member was only filled on the kConfigChanged path (via UpdateAudioConfig(), per the commit message), so for the initial config audio_sample_info_->audio_specific_config kept pointing at the extra_data() of a stack-local AudioDecoderConfig in StarboardPlayerManager::Create(). When Create() returned, that config was destroyed and its extra_data storage freed, yet the dangling pointer was later dereferenced when the first audio sample was written to Starboard — a use-after-free read. The violated invariant is that audio_specific_config must reference memory owned for the lifetime of the reader.
The fix assigns chromium_audio_config_ = audio_stream_->audio_decoder_config() in the constructor and, if audio_sample_info_ is set, re-points audio_specific_config/audio_specific_config_size at chromium_audio_config_.extra_data(), matching what the config-change path already did; the resampling decision now also reads from the owned copy.
AudioDecoderConfig was retained past that object’s lifetime because the owning member chromium_audio_config_ was populated only on config changes, not at construction. The fix makes the reader own the initial config and re-anchors the pointer to that owned copy.Attack Path
- Cast receiver plays audio on a Starboard backend
The bug only exists in the
chromecast/starboardrenderer, so it requires a Cast device/build using the Starboard media pipeline, with a stream that has an audio track. - Initial config carries extra data
The audio stream’s initial
AudioDecoderConfighas non-emptyextra_data(e.g. AAC with an AudioSpecificConfig, as in the regression test), soaudio_specific_configis non-null and points into the caller’s config. - Creating config goes out of scope
StarboardPlayerManager::Create()returns and its stack-localAudioDecoderConfigis destroyed, freeing theextra_databuffer whileaudio_sample_info_still references it. - First sample write reads freed memory
When Starboard signals
kStarboardDecoderStateNeedsDataand the reader writes the first audio sample, Starboard readsaudio_specific_config_sizebytes from the freed buffer; if that heap slot has been reallocated with content-influenced data, the decoder receives attacker-shaped codec configuration (inferred).
Impact Assessment
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_renderer_test.ccchromecast/starboard/media/renderer/starboard_player_manager_test.cc
Audit Directions
- Non-owning pointers in platform-ABI structsAudit other code that fills Starboard/C-ABI structs (
StarboardVideoSampleInfo, DRM sample info, color metadata) with.data()pointers and confirm the backing container is a member, not a local or a caller-owned temporary. - Initial-vs-update path asymmetryWhere an “on config changed” handler stores an owned copy, check that the constructor/initialization path does the same; the original config is frequently handled differently from subsequent ones.
- Factory functions with stack-local configsReview
Create()-style factories inchromecast/starboard/mediathat build configs on the stack and pass them (or pointers into them) to objects that outlive the factory call.
Patch
From 0bb350515768a179bc18af99960b48ebbfd10238 Mon Sep 17 00:00:00 2001
From: Richard Nichols <rknichols@google.com>
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 <rknichols@google.com>
Reviewed-by: Simeon Anfinrud <sanfin@chromium.org>
Reviewed-by: Shawn Quereshi <shawnq@google.com>
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