Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Chromecast
DescriptionUse after free in Chromecast
ComponentChromecast
Bug ClassUAF
Tracker513261751
Fix commit0bb350515768 (chromium/src) +93/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
if
chromecast/starboard/media/renderer/demuxer_stream_reader.cc
modified

Files Changed

  • chromecast/starboard/media/renderer/demuxer_stream_reader.cc
  • chromecast/starboard/media/renderer/starboard_player_manager_test.cc
  • chromecast/starboard/media/renderer/starboard_renderer_test.cc
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"
Loading diff…

Regression Test / PoC

shipped with the fix
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"
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • chromecast/starboard/media/renderer/demuxer_stream_reader.cc
  • chromecast/starboard/media/renderer/chromium_starboard_conversions.cc
  • chromecast/starboard/media/renderer/demuxer_stream_reader.h
  • chromecast/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

  1. Compromise a renderer process on a Chromecast device.
  2. Trigger media playback using StarboardRenderer (e.g., by providing a specific audio configuration via Mojo).
  3. Reallocate the freed extra_data buffer in the browser process with attacker-controlled data (e.g., via heap spraying or other IPCs).
  4. Observe the browser process crash or exhibit controlled behavior when SbPlayerWriteSample2 is 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.

View on issue tracker