CVE-2026-79121
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fchromecast/starboard/media/media/starboard_audio_decoder_test.cc |
modified |
Files Changed
chromecast/starboard/media/media/drm_util.ccchromecast/starboard/media/media/drm_util_test.ccchromecast/starboard/media/media/starboard_audio_decoder.ccchromecast/starboard/media/media/starboard_audio_decoder_test.ccchromecast/starboard/media/media/starboard_video_decoder_test.cc
Patch
From cecd12757600de8eaf3932b6695c0d748b1c389c Mon Sep 17 00:00:00 2001
From: Richard Nichols <rknichols@google.com>
Date: Tue, 07 Jul 2026 09:47:38 -0700
Subject: [PATCH] Validate AudioConfig before updating StarboardAudioDecoder state
StarboardAudioDecoder::SetConfig stored the supplied config into config_
and audio_sample_info_ before calling IsValidConfig(). When the config
was rejected the decoder state had already been updated, so the next
PushBuffer forwarded the rejected sample info (including an out-of-range
channel count) to SbPlayerWriteSample.
Validate the config first and leave the decoder state untouched when it
is rejected.
Also fixes some compile errors in starboard_media_unittests that
accumulated since crrev.com/c/7728010 made the two-arg EncryptionPattern
constructor private.
Bug: 516777082
Change-Id: Iff88f3ab296ee2b67da00de523a805cf463728bb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8009795
Commit-Queue: Richard Nichols <rknichols@google.com>
Reviewed-by: Shawn Quereshi <shawnq@google.com>
Reviewed-by: Sandeep Vijayasekar <sandv@google.com>
Cr-Commit-Position: refs/heads/main@{#1658020}
---
diff --git a/chromecast/starboard/media/media/drm_util.cc b/chromecast/starboard/media/media/drm_util.cc
index 48449fd7..d259bf1b 100644
--- a/chromecast/starboard/media/media/drm_util.cc
+++ b/chromecast/starboard/media/media/drm_util.cc
@@ -59,7 +59,7 @@
if (!total_size.IsValid() || total_size.ValueOrDie() != buffer.data_size()) {
LOG(ERROR) << "Subsample sizes do not equal input size. Total size: "
- << total_size.ValueOrDefault(0)
+ << size_t{total_size.ValueOrDefault(0)}
<< ", expected size: " << buffer.data_size();
return false;
}
diff --git a/chromecast/starboard/media/media/drm_util_test.cc b/chromecast/starboard/media/media/drm_util_test.cc
index 70c75872..7de1adc3 100644
--- a/chromecast/starboard/media/media/drm_util_test.cc
+++ b/chromecast/starboard/media/media/drm_util_test.cc
@@ -131,9 +131,9 @@
expected_drm_info.encryption_scheme =
StarboardDrmEncryptionScheme::kStarboardDrmEncryptionSchemeAesCbc;
expected_drm_info.encryption_pattern.crypt_byte_block =
- encryption_pattern.crypt_byte_block();
+ encryption_pattern->crypt_byte_block();
expected_drm_info.encryption_pattern.skip_byte_block =
- encryption_pattern.skip_byte_block();
+ encryption_pattern->skip_byte_block();
base::span<uint8_t>(expected_drm_info.initialization_vector)
.copy_from_nonoverlapping(base::as_byte_span(kIv));
expected_drm_info.initialization_vector_size = kIv.size();
diff --git a/chromecast/starboard/media/media/starboard_audio_decoder.cc b/chromecast/starboard/media/media/starboard_audio_decoder.cc
index dfd818b81..bc4c4fa4 100644
--- a/chromecast/starboard/media/media/starboard_audio_decoder.cc
+++ b/chromecast/starboard/media/media/starboard_audio_decoder.cc
@@ -124,6 +124,10 @@
bool StarboardAudioDecoder::SetConfig(const AudioConfig& config) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+ if (!IsValidConfig(config)) {
+ return false;
+ }
+
if ((config.codec == kCodecPCM || config.codec == kCodecPCM_S16BE) &&
config.channel_number > 8) {
LOG(ERROR) << "Config channels exceeds 8, which is not supported.";
@@ -132,7 +136,7 @@
config_ = config;
audio_sample_info_.emplace(ToAudioSampleInfo(config_));
- return IsValidConfig(config_);
+ return true;
}
bool StarboardAudioDecoder::SetVolume(float multiplier) {
diff --git a/chromecast/starboard/media/media/starboard_audio_decoder_test.cc b/chromecast/starboard/media/media/starboard_audio_decoder_test.cc
index 07bd76f5..4bbe1509 100644
--- a/chromecast/starboard/media/media/starboard_audio_decoder_test.cc
+++ b/chromecast/starboard/media/media/starboard_audio_decoder_test.cc
@@ -315,7 +315,7 @@
// unencrypted even for encrypted content.
config.encryption_scheme = EncryptionScheme::kUnencrypted;
- const ::media::EncryptionPattern encryption_pattern(5, 6);
+ const auto encryption_pattern = ::media::EncryptionPattern::Create(5, 6);
std::unique_ptr<::media::DecryptConfig> decrypt_config =
::media::DecryptConfig::CreateCbcsConfig(kKeyId, kIv, subsamples,
encryption_pattern);
@@ -406,7 +406,7 @@
AudioConfig config = GetBasicConfig();
config.encryption_scheme = EncryptionScheme::kAesCtr;
- const ::media::EncryptionPattern encryption_pattern(5, 6);
+ const auto encryption_pattern = ::media::EncryptionPattern::Create(5, 6);
std::unique_ptr<::media::DecryptConfig> decrypt_config =
::media::DecryptConfig::CreateCbcsConfig(kKeyId, kIv, subsamples,
encryption_pattern);
@@ -449,7 +449,7 @@
AudioConfig config = GetBasicConfig();
config.encryption_scheme = EncryptionScheme::kAesCbc;
- const ::media::EncryptionPattern encryption_pattern(5, 6);
+ const auto encryption_pattern = ::media::EncryptionPattern::Create(5, 6);
std::unique_ptr<::media::DecryptConfig> decrypt_config =
::media::DecryptConfig::CreateCbcsConfig(kKeyId, kIv, subsamples,
encryption_pattern);
@@ -554,6 +554,41 @@
kStarboardAudioCodecAac)));
}
+TEST_F(StarboardAudioDecoderTest, RejectedConfigDoesNotUpdateSampleInfo) {
+ const AudioConfig good_config = GetBasicConfig();
+
+ AudioConfig bad_config = GetBasicConfig();
+ bad_config.codec = AudioCodec::kCodecAAC;
+ bad_config.channel_number = 64;
+
+ const std::vector<uint8_t> buffer_data = {1, 2, 3, 4, 5};
+ auto buffer = base::MakeRefCounted<DecoderBufferAdapter>(
+ ::media::DecoderBuffer::CopyFrom(buffer_data));
+
+ // The buffer pushed after the rejected config change should still use the
+ // last accepted config.
+ EXPECT_CALL(*starboard_,
+ WriteSample(&fake_player_, kStarboardMediaTypeAudio,
+ ElementsAre(MatchesAudioConfigAndBuffer(good_config,
+ buffer))))
+ .Times(1);
+
+ StarboardAudioDecoder decoder(starboard_.get());
+ MockDelegate delegate;
+
+ decoder.Initialize(&fake_player_);
+ EXPECT_TRUE(decoder.SetConfig(good_config));
+ decoder.SetDelegate(&delegate);
+
+ EXPECT_FALSE(decoder.SetConfig(bad_config));
+ EXPECT_THAT(decoder.GetAudioSampleInfo(),
+ Optional(Field(&StarboardAudioSampleInfo::number_of_channels,
+ good_config.channel_number)));
+
+ EXPECT_EQ(decoder.PushBuffer(buffer.get()),
+ MediaPipelineBackend::BufferStatus::kBufferPending);
+}
+
TEST_F(StarboardAudioDecoderTest,
HandlesMultiplePushBuffersBeforeInitialization) {
const std::vector<uint8_t> buffer_data_1 = {1, 2, 3, 4, 5};
diff --git a/chromecast/starboard/media/media/starboard_video_decoder_test.cc b/chromecast/starboard/media/media/starboard_video_decoder_test.cc
index e7dabc0..124e67f3 100644
--- a/chromecast/starboard/media/media/starboard_video_decoder_test.cc
+++ b/chromecast/starboard/media/media/starboard_video_decoder_test.cc
@@ -246,7 +246,7 @@
VideoConfig config = GetBasicConfig();
config.encryption_scheme = EncryptionScheme::kAesCbc;
- const ::media::EncryptionPattern encryption_pattern(5, 6);
+ const auto encryption_pattern = ::media::EncryptionPattern::Create(5, 6);
std::unique_ptr<::media::DecryptConfig> decrypt_config =
::media::DecryptConfig::CreateCbcsConfig(kKeyId, kIv, subsamples,
encryption_pattern);
@@ -336,7 +336,7 @@
VideoConfig config = GetBasicConfig();
config.encryption_scheme = EncryptionScheme::kAesCtr;
- const ::media::EncryptionPattern encryption_pattern(5, 6);
+ const auto encryption_pattern = ::media::EncryptionPattern::Create(5, 6);
std::unique_ptr<::media::DecryptConfig> decrypt_config =
::media::DecryptConfig::CreateCbcsConfig(kKeyId, kIv, subsamples,
encryption_pattern);
@@ -379,7 +379,7 @@
VideoConfig config = GetBasicConfig();
config.encryption_scheme = EncryptionScheme::kAesCbc;
- const ::media::EncryptionPattern encryption_pattern(5, 6);
+ const auto encryption_pattern = ::media::EncryptionPattern::Create(5, 6);
std::unique_ptr<::media::DecryptConfig> decrypt_config =
::media::DecryptConfig::CreateCbcsConfig(kKeyId, kIv, subsamples,
encryption_pattern);
Regression Test / PoC
diff --git a/chromecast/starboard/media/media/drm_util_test.cc b/chromecast/starboard/media/media/drm_util_test.cc
index 70c75872..7de1adc3 100644
--- a/chromecast/starboard/media/media/drm_util_test.cc
+++ b/chromecast/starboard/media/media/drm_util_test.cc
@@ -131,9 +131,9 @@
expected_drm_info.encryption_scheme =
StarboardDrmEncryptionScheme::kStarboardDrmEncryptionSchemeAesCbc;
expected_drm_info.encryption_pattern.crypt_byte_block =
- encryption_pattern.crypt_byte_block();
+ encryption_pattern->crypt_byte_block();
expected_drm_info.encryption_pattern.skip_byte_block =
- encryption_pattern.skip_byte_block();
+ encryption_pattern->skip_byte_block();
base::span<uint8_t>(expected_drm_info.initialization_vector)
.copy_from_nonoverlapping(base::as_byte_span(kIv));
expected_drm_info.initialization_vector_size = kIv.size();
diff --git a/chromecast/starboard/media/media/starboard_audio_decoder_test.cc b/chromecast/starboard/media/media/starboard_audio_decoder_test.cc
index 07bd76f5..4bbe1509 100644
--- a/chromecast/starboard/media/media/starboard_audio_decoder_test.cc
+++ b/chromecast/starboard/media/media/starboard_audio_decoder_test.cc
@@ -315,7 +315,7 @@
// unencrypted even for encrypted content.
config.encryption_scheme = EncryptionScheme::kUnencrypted;
- const ::media::EncryptionPattern encryption_pattern(5, 6);
+ const auto encryption_pattern = ::media::EncryptionPattern::Create(5, 6);
std::unique_ptr<::media::DecryptConfig> decrypt_config =
::media::DecryptConfig::CreateCbcsConfig(kKeyId, kIv, subsamples,
encryption_pattern);
@@ -406,7 +406,7 @@
AudioConfig config = GetBasicConfig();
config.encryption_scheme = EncryptionScheme::kAesCtr;
- const ::media::EncryptionPattern encryption_pattern(5, 6);
+ const auto encryption_pattern = ::media::EncryptionPattern::Create(5, 6);
std::unique_ptr<::media::DecryptConfig> decrypt_config =
::media::DecryptConfig::CreateCbcsConfig(kKeyId, kIv, subsamples,
encryption_pattern);
@@ -449,7 +449,7 @@
AudioConfig config = GetBasicConfig();
config.encryption_scheme = EncryptionScheme::kAesCbc;
- const ::media::EncryptionPattern encryption_pattern(5, 6);
+ const auto encryption_pattern = ::media::EncryptionPattern::Create(5, 6);
std::unique_ptr<::media::DecryptConfig> decrypt_config =
::media::DecryptConfig::CreateCbcsConfig(kKeyId, kIv, subsamples,
encryption_pattern);
@@ -554,6 +554,41 @@
kStarboardAudioCodecAac)));
}
+TEST_F(StarboardAudioDecoderTest, RejectedConfigDoesNotUpdateSampleInfo) {
+ const AudioConfig good_config = GetBasicConfig();
+
+ AudioConfig bad_config = GetBasicConfig();
+ bad_config.codec = AudioCodec::kCodecAAC;
+ bad_config.channel_number = 64;
+
+ const std::vector<uint8_t> buffer_data = {1, 2, 3, 4, 5};
+ auto buffer = base::MakeRefCounted<DecoderBufferAdapter>(
+ ::media::DecoderBuffer::CopyFrom(buffer_data));
+
+ // The buffer pushed after the rejected config change should still use the
+ // last accepted config.
+ EXPECT_CALL(*starboard_,
+ WriteSample(&fake_player_, kStarboardMediaTypeAudio,
+ ElementsAre(MatchesAudioConfigAndBuffer(good_config,
+ buffer))))
+ .Times(1);
+
+ StarboardAudioDecoder decoder(starboard_.get());
+ MockDelegate delegate;
+
+ decoder.Initialize(&fake_player_);
+ EXPECT_TRUE(decoder.SetConfig(good_config));
+ decoder.SetDelegate(&delegate);
+
+ EXPECT_FALSE(decoder.SetConfig(bad_config));
+ EXPECT_THAT(decoder.GetAudioSampleInfo(),
+ Optional(Field(&StarboardAudioSampleInfo::number_of_channels,
+ good_config.channel_number)));
+
+ EXPECT_EQ(decoder.PushBuffer(buffer.get()),
+ MediaPipelineBackend::BufferStatus::kBufferPending);
+}
+
TEST_F(StarboardAudioDecoderTest,
HandlesMultiplePushBuffersBeforeInitialization) {
const std::vector<uint8_t> buffer_data_1 = {1, 2, 3, 4, 5};
diff --git a/chromecast/starboard/media/media/starboard_video_decoder_test.cc b/chromecast/starboard/media/media/starboard_video_decoder_test.cc
index e7dabc0..124e67f3 100644
--- a/chromecast/starboard/media/media/starboard_video_decoder_test.cc
+++ b/chromecast/starboard/media/media/starboard_video_decoder_test.cc
@@ -246,7 +246,7 @@
VideoConfig config = GetBasicConfig();
config.encryption_scheme = EncryptionScheme::kAesCbc;
- const ::media::EncryptionPattern encryption_pattern(5, 6);
+ const auto encryption_pattern = ::media::EncryptionPattern::Create(5, 6);
std::unique_ptr<::media::DecryptConfig> decrypt_config =
::media::DecryptConfig::CreateCbcsConfig(kKeyId, kIv, subsamples,
encryption_pattern);
@@ -336,7 +336,7 @@
VideoConfig config = GetBasicConfig();
config.encryption_scheme = EncryptionScheme::kAesCtr;
- const ::media::EncryptionPattern encryption_pattern(5, 6);
+ const auto encryption_pattern = ::media::EncryptionPattern::Create(5, 6);
std::unique_ptr<::media::DecryptConfig> decrypt_config =
::media::DecryptConfig::CreateCbcsConfig(kKeyId, kIv, subsamples,
encryption_pattern);
@@ -379,7 +379,7 @@
VideoConfig config = GetBasicConfig();
config.encryption_scheme = EncryptionScheme::kAesCbc;
- const ::media::EncryptionPattern encryption_pattern(5, 6);
+ const auto encryption_pattern = ::media::EncryptionPattern::Create(5, 6);
std::unique_ptr<::media::DecryptConfig> decrypt_config =
::media::DecryptConfig::CreateCbcsConfig(kKeyId, kIv, subsamples,
encryption_pattern);
Original Bug Report
Potential State Commitment Before Validation in StarboardAudioDecoder
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: A potential state-tracking vulnerability exists where StarboardAudioDecoder::SetConfig commits configuration data to internal state variables before executing validation checks. If validation fails but the media pipeline continues to process buffers, this unvalidated state may be accessed, potentially leading to downstream memory safety issues.
Affected files:
chromecast/starboard/media/media/starboard_audio_decoder.ccchromecast/media/common/audio_decoder_software_wrapper.ccchromecast/media/cma/pipeline/audio_pipeline_impl.ccchromecast/media/cma/pipeline/av_pipeline_impl.cc
Estimated timestamp from git blame: 2024-04-22
Description
In StarboardAudioDecoder::SetConfig (located in chromecast/starboard/media/media/starboard_audio_decoder.cc), the decoder’s internal configuration state is updated before the validation check IsValidConfig is evaluated.
bool StarboardAudioDecoder::SetConfig(const AudioConfig& config) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if ((config.codec == kCodecPCM || config.codec == kCodecPCM_S16BE) &&
config.channel_number > 8) {
LOG(ERROR) << "Config channels exceeds 8, which is not supported.";
return false;
}
config_ = config; // Potential state modification
audio_sample_info_.emplace(ToAudioSampleInfo(config_)); // Potential state modification
return IsValidConfig(config_); // Validation executed LAST
}
If IsValidConfig(config_) returns false (for example, if the channel count exceeds 32), SetConfig correctly returns false to notify the caller of the failure. However, the unvalidated configuration has already been persistently written to config_ and audio_sample_info_.
Potential Impact & Propagation
Under normal circumstances, a failure in SetConfig is expected to halt the pipeline. However, if the error handling path does not immediately tear down or halt the feeding loop (such as when AvPipelineImpl::enable_feeding_ remains active during asynchronous error propagation), a subsequent buffer might still be pushed to the decoder.
Because software fallback decoders (e.g., in AudioDecoderSoftwareWrapper) would fail to initialize on an invalid configuration, the wrapper may fall back to forwarding raw buffers directly to the backend decoder via backend_decoder_->PushBuffer(buffer). When StarboardAudioDecoder::PushBuffer is invoked, it retrieves the previously cached, invalid audio_sample_info_ state and passes it to the underlying platform’s audio API (such as the Starboard API wrapper).
On specific platforms or implementations, receiving an audio configuration with anomalous parameters (e.g., extremely high channel counts) could lead to integer overflows during internal buffer sizing calculations, potentially resulting in memory corruption (such as a heap out-of-bounds write).
Suggested Steps to Trigger (Potential)
Note: These steps are theoretical as a working proof-of-concept has not been executed.
- A compromised renderer sends a serialized
AudioDecoderConfigcontaining an exceptionally high channel count (e.g., utilizingCHANNEL_LAYOUT_DISCRETE). - The media pipeline processes a mid-stream configuration update, invoking
StarboardAudioDecoder::SetConfigwith the anomalous configuration. SetConfigupdates its internal state with the high channel count, then returnsfalsewhen validation fails.- Due to the failure, software decoder wrapper initialization fails, and the pipeline triggers an asynchronous decode error.
- If the feeding loop continues to execute before pipeline teardown completes, a subsequent buffer is pushed.
- The wrapper forwards the buffer to
StarboardAudioDecoder::PushBuffer, which dispatches the sample along with the poisoned cachedaudio_sample_info_to the low-level platform playback API.
Remediation
To resolve this issue, validation checks must be performed on the configuration parameters before any permanent changes are written to the decoder’s member variables.
Modify StarboardAudioDecoder::SetConfig to validate the configuration first, and only commit to internal state if validation succeeds:
bool StarboardAudioDecoder::SetConfig(const AudioConfig& config) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!IsValidConfig(config)) {
return false;
}
if ((config.codec == kCodecPCM || config.codec == kCodecPCM_S16BE) &&
config.channel_number > 8) {
LOG(ERROR) << "Config channels exceeds 8, which is not supported.";
return false;
}
config_ = config;
audio_sample_info_.emplace(ToAudioSampleInfo(config_));
return true;
}
Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8
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.