CVE-2026-106382
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TestDecoderBufferchromecast/media/common/audio_decoder_software_wrapper_unittest.cc |
modified | |
ifchromecast/media/common/audio_decoder_software_wrapper_unittest.cc |
modified | |
AudioDecoderSoftwareWrapperTestchromecast/media/common/audio_decoder_software_wrapper_unittest.cc |
modified | |
TESTchromecast/media/common/audio_decoder_software_wrapper_unittest.cc |
modified |
Files Changed
chromecast/media/common/audio_decoder_software_wrapper_unittest.ccchromecast/media/common/audio_decoder_wrapper.ccchromecast/media/common/audio_decoder_wrapper.h
Patch
From 991f7b234fef580899b601d61df58a44bfe453bd Mon Sep 17 00:00:00 2001
From: Frank Liberato <liberato@chromium.org>
Date: Wed, 09 Sep 2026 15:49:33 -0700
Subject: [PATCH] [chromecast] Fix potential UAF during audio decoder revocation
In MediaPipelineBackendWrapper::Revoke(), revoking the
AudioDecoderWrapper previously destructed the underlying
ActiveAudioDecoderWrapper and its pushed_buffer_ before stopping or
destroying the active backend. If the vendor backend was asynchronously
processing a pending buffer, this dropped the buffer reference while the
backend still held a raw pointer to it.
This change: 1. Retains pushed_buffer_ in AudioDecoderWrapper so buffer
lifetime is
preserved across decoder revocation until AudioDecoderWrapper is
destroyed or a new buffer is pushed.
2. Updates MediaPipelineBackendWrapper::Revoke() to query
GetCurrentPts()
and call Stop() on the backend before revoking the decoders, adhering
to the MediaPipelineBackend lifecycle contract.
3. Adds a unit test verifying buffer lifetime retention across
revocation.
TAG=agy
CONV=0d535dd2-16e6-431d-9310-cea649474cf8
Bug: 534994449
Change-Id: I26d3125ec4c17b8410cadee8a1fab8b2248ee0d4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8327968
Commit-Queue: Dale Curtis <dalecurtis@chromium.org>
Auto-Submit: Frank Liberato <liberato@chromium.org>
Commit-Queue: Frank Liberato <liberato@chromium.org>
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
Reviewed-by: Simeon Anfinrud <sanfin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1694950}
---
diff --git a/chromecast/media/common/audio_decoder_wrapper.cc b/chromecast/media/common/audio_decoder_wrapper.cc
index 3d950010..8ffe9ff 100644
--- a/chromecast/media/common/audio_decoder_wrapper.cc
+++ b/chromecast/media/common/audio_decoder_wrapper.cc
@@ -212,7 +212,8 @@
AudioDecoderWrapper::BufferStatus AudioDecoderWrapper::PushBuffer(
scoped_refptr<DecoderBufferBase> buffer) {
- return audio_decoder_->PushBuffer(buffer);
+ pushed_buffer_ = std::move(buffer);
+ return audio_decoder_->PushBuffer(pushed_buffer_);
}
bool AudioDecoderWrapper::SetConfig(const AudioConfig& config) {
diff --git a/chromecast/media/common/audio_decoder_wrapper.h b/chromecast/media/common/audio_decoder_wrapper.h
index 61ff6852..eb7542c 100644
--- a/chromecast/media/common/audio_decoder_wrapper.h
+++ b/chromecast/media/common/audio_decoder_wrapper.h
@@ -84,7 +84,6 @@
void OnInitialized();
void Revoke();
- private:
// CmaBackend::AudioDecoder implementation:
void SetDelegate(Delegate* delegate) override;
BufferStatus PushBuffer(scoped_refptr<DecoderBufferBase> buffer) override;
@@ -96,6 +95,8 @@
int GetStartThresholdInFrames() override;
bool RequiresDecryption() override;
+ private:
+ scoped_refptr<DecoderBufferBase> pushed_buffer_;
bool decoder_revoked_;
std::unique_ptr<DestructableAudioDecoder> audio_decoder_;
diff --git a/chromecast/media/common/audio_decoder_software_wrapper_unittest.cc b/chromecast/media/common/audio_decoder_software_wrapper_unittest.cc
index 7414cafc..3b4a7e8 100644
--- a/chromecast/media/common/audio_decoder_software_wrapper_unittest.cc
+++ b/chromecast/media/common/audio_decoder_software_wrapper_unittest.cc
@@ -4,7 +4,12 @@
#include "chromecast/media/common/audio_decoder_software_wrapper.h"
+#include <memory>
+
+#include "base/memory/scoped_refptr.h"
#include "base/test/task_environment.h"
+#include "chromecast/media/api/decoder_buffer_base.h"
+#include "chromecast/media/common/audio_decoder_wrapper.h"
#include "chromecast/public/media/cast_decoder_buffer.h"
#include "chromecast/public/media/decoder_config.h"
#include "chromecast/public/media/media_pipeline_backend.h"
@@ -34,6 +39,33 @@
MOCK_METHOD0(GetStartThresholdInFrames, int());
};
+class TestDecoderBuffer : public DecoderBufferBase {
+ public:
+ explicit TestDecoderBuffer(bool* destroyed_flag)
+ : destroyed_flag_(destroyed_flag) {
+ *destroyed_flag_ = false;
+ }
+
+ StreamId stream_id() const override { return StreamId::kPrimary; }
+ int64_t timestamp() const override { return 0; }
+ void set_timestamp(base::TimeDelta timestamp) override {}
+ const uint8_t* data() const override { return nullptr; }
+ uint8_t* writable_data() const override { return nullptr; }
+ size_t data_size() const override { return 0; }
+ const CastDecryptConfig* decrypt_config() const override { return nullptr; }
+ bool end_of_stream() const override { return false; }
+ bool is_key_frame() const override { return false; }
+
+ private:
+ ~TestDecoderBuffer() override {
+ if (destroyed_flag_) {
+ *destroyed_flag_ = true;
+ }
+ }
+
+ bool* const destroyed_flag_;
+};
+
} // namespace
class AudioDecoderSoftwareWrapperTest : public ::testing::Test {
@@ -68,5 +100,34 @@
EXPECT_TRUE(audio_decoder_software_wrapper_.IsUsingSoftwareDecoder());
}
+TEST(AudioDecoderWrapperTest, BufferRetainedAcrossRevocation) {
+ MockAudioDecoder backend_decoder;
+ EXPECT_CALL(backend_decoder, PushBuffer(_))
+ .WillOnce(Return(MediaPipelineBackend::kBufferPending));
+
+ auto audio_decoder_wrapper = std::make_unique<AudioDecoderWrapper>(
+ &backend_decoder, AudioContentType::kMedia, nullptr);
+
+ bool buffer_destroyed = false;
+ auto buffer = base::MakeRefCounted<TestDecoderBuffer>(&buffer_destroyed);
+
+ EXPECT_EQ(audio_decoder_wrapper->PushBuffer(buffer),
+ MediaPipelineBackend::kBufferPending);
+
+ // Upstream pipeline drops its reference.
+ buffer.reset();
+ EXPECT_FALSE(buffer_destroyed);
+
+ // Revoke the audio decoder wrapper.
+ audio_decoder_wrapper->Revoke();
+
+ // Buffer must still be alive in AudioDecoderWrapper to protect the backend.
+ EXPECT_FALSE(buffer_destroyed);
+
+ // Destroying AudioDecoderWrapper releases the buffer.
+ audio_decoder_wrapper.reset();
+ EXPECT_TRUE(buffer_destroyed);
+}
+
} // namespace media
} // namespace chromecast
Regression Test / PoC
diff --git a/chromecast/media/common/audio_decoder_software_wrapper_unittest.cc b/chromecast/media/common/audio_decoder_software_wrapper_unittest.cc
index 7414cafc..3b4a7e8 100644
--- a/chromecast/media/common/audio_decoder_software_wrapper_unittest.cc
+++ b/chromecast/media/common/audio_decoder_software_wrapper_unittest.cc
@@ -4,7 +4,12 @@
#include "chromecast/media/common/audio_decoder_software_wrapper.h"
+#include <memory>
+
+#include "base/memory/scoped_refptr.h"
#include "base/test/task_environment.h"
+#include "chromecast/media/api/decoder_buffer_base.h"
+#include "chromecast/media/common/audio_decoder_wrapper.h"
#include "chromecast/public/media/cast_decoder_buffer.h"
#include "chromecast/public/media/decoder_config.h"
#include "chromecast/public/media/media_pipeline_backend.h"
@@ -34,6 +39,33 @@
MOCK_METHOD0(GetStartThresholdInFrames, int());
};
+class TestDecoderBuffer : public DecoderBufferBase {
+ public:
+ explicit TestDecoderBuffer(bool* destroyed_flag)
+ : destroyed_flag_(destroyed_flag) {
+ *destroyed_flag_ = false;
+ }
+
+ StreamId stream_id() const override { return StreamId::kPrimary; }
+ int64_t timestamp() const override { return 0; }
+ void set_timestamp(base::TimeDelta timestamp) override {}
+ const uint8_t* data() const override { return nullptr; }
+ uint8_t* writable_data() const override { return nullptr; }
+ size_t data_size() const override { return 0; }
+ const CastDecryptConfig* decrypt_config() const override { return nullptr; }
+ bool end_of_stream() const override { return false; }
+ bool is_key_frame() const override { return false; }
+
+ private:
+ ~TestDecoderBuffer() override {
+ if (destroyed_flag_) {
+ *destroyed_flag_ = true;
+ }
+ }
+
+ bool* const destroyed_flag_;
+};
+
} // namespace
class AudioDecoderSoftwareWrapperTest : public ::testing::Test {
@@ -68,5 +100,34 @@
EXPECT_TRUE(audio_decoder_software_wrapper_.IsUsingSoftwareDecoder());
}
+TEST(AudioDecoderWrapperTest, BufferRetainedAcrossRevocation) {
+ MockAudioDecoder backend_decoder;
+ EXPECT_CALL(backend_decoder, PushBuffer(_))
+ .WillOnce(Return(MediaPipelineBackend::kBufferPending));
+
+ auto audio_decoder_wrapper = std::make_unique<AudioDecoderWrapper>(
+ &backend_decoder, AudioContentType::kMedia, nullptr);
+
+ bool buffer_destroyed = false;
+ auto buffer = base::MakeRefCounted<TestDecoderBuffer>(&buffer_destroyed);
+
+ EXPECT_EQ(audio_decoder_wrapper->PushBuffer(buffer),
+ MediaPipelineBackend::kBufferPending);
+
+ // Upstream pipeline drops its reference.
+ buffer.reset();
+ EXPECT_FALSE(buffer_destroyed);
+
+ // Revoke the audio decoder wrapper.
+ audio_decoder_wrapper->Revoke();
+
+ // Buffer must still be alive in AudioDecoderWrapper to protect the backend.
+ EXPECT_FALSE(buffer_destroyed);
+
+ // Destroying AudioDecoderWrapper releases the buffer.
+ audio_decoder_wrapper.reset();
+ EXPECT_TRUE(buffer_destroyed);
+}
+
} // namespace media
} // namespace chromecast
Original Bug Report
Use-after-free in Cast Media Service during Revocation of ActiveAudioDecoderWrapper
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: Under ENABLE_CHROMIUM_RUNTIME_CAST_RENDERER builds, MediaPipelineBufferExtension can prematurely return kBufferSuccess to the pipeline for a pending buffer. This causes the upstream pipeline to drop its reference, leaving ActiveAudioDecoderWrapper as the sole owner. When a subsequent revocation event destroys ActiveAudioDecoderWrapper, the buffer is freed while the vendor backend still holds a dangling raw pointer to it.
Affected files:
chromecast/media/cma/backend/proxy/media_pipeline_buffer_extension.ccchromecast/media/common/audio_decoder_wrapper.ccchromecast/media/common/media_pipeline_backend_wrapper.cc
Estimated timestamp from git blame: 2021-03-23
Description
A potential Use-After-Free (UAF) vulnerability exists in the Chromecast CMA (Cast Media Arena) backend proxy code under ENABLE_CHROMIUM_RUNTIME_CAST_RENDERER builds. This build configuration inserts a CmaBackendProxy between MediaPipelineImpl and the inner MediaPipelineBackendWrapper (chromecast/media/cma/backend/cma_backend_factory_impl.cc).
Root Cause Analysis
-
Premature Reference Release: When a buffer is pushed,
MediaPipelineBufferExtension::PushBufferdelegates downstream, eventually invoking the vendor’s asynchronousPushBufferwhich returnskBufferPending. However, if the buffer extension’s queue is not full, it translates this status and returnskBufferSuccessto the upstreamAvPipelineImpl(chromecast/media/cma/backend/proxy/media_pipeline_buffer_extension.cc):return IsBufferFull() ? BufferStatus::kBufferPending : BufferStatus::kBufferSuccess;As a result,
AvPipelineImplimmediately nulls itspushed_buffer_reference (chromecast/media/cma/pipeline/av_pipeline_impl.cc), assuming the buffer push completed successfully. -
Sole Ownership: At this stage, the only
scoped_refptrholding theDecoderBufferBasealive isActiveAudioDecoderWrapper::pushed_buffer_(chromecast/media/common/audio_decoder_wrapper.cc). Meanwhile, the vendor’s backend decoder holds a raw, unprotected pointer (CastDecoderBuffer*) to the buffer. -
Premature Destruction during Revocation: When another stream requires resources,
MediaPipelineBackendWrapper::Revoke()is called to revoke the active pipeline. This executes:if (audio_decoder_) audio_decoder_->Revoke();This reassignment destroys
ActiveAudioDecoderWrapper, which immediately deallocates theDecoderBufferBase. Crucially, this happens before the active vendor-implemented backend is stopped or destroyed. -
Use-After-Free: Immediately after,
MediaPipelineBackendWrapper::Revoke()queriesbackend_->GetCurrentPts(). During this call, or during subsequent destruction of the vendor backend/asynchronous decode threads, the vendor backend may dereference the danglingCastDecoderBuffer*pointer. SinceCastDecoderBufferis an abstract class with pure virtual methods, any method call leads to a virtual method call on a freed object, enabling potential flow hijack.
Potential Steps to Reproduce
Note: These are potential steps modeled from code analysis, as our tooling environment does not currently have the capability to execute code or run a live proof of concept.
- Navigate to a page on a Cast Core device that initiates media playback on a multiroom/cast-renderer-enabled stream (Element A).
- Feed encoded audio such that the vendor backend returns
kBufferPending, butMediaPipelineBufferExtensionreturnskBufferSuccess, leavingActiveAudioDecoderWrapperas the sole owner of the buffer. - Initiate playback of a second video element (Element B) to trigger video decoder resource reallocation.
MediaPipelineBackendManagerinvokesRevoke()on Element A, destroyingActiveAudioDecoderWrapperand freeing the buffer.- The vendor backend accesses the freed buffer during
GetCurrentPts()or destructor teardown, triggering a Use-After-Free.
Suggested Fix
To remediate this issue, the lifetime of the DecoderBufferBase must be guaranteed to span the duration expected by the vendor backend contract.
- Maintain Buffer Lifetime: Ensure that
ActiveAudioDecoderWrapperdoes not destroy or releasepushed_buffer_duringRevoke()until the underlying vendor backend’sAudioDecoderhas been stopped, flushed, or fully destroyed. - Correct Status Propagation: Avoid translating
kBufferPendingtokBufferSuccessupstream if doing so causes the pipeline to release its reference before the vendor backend has completed processing the buffer.
Evaluated with Chrome root at commit: b5b015ea5f690560237d1f0cff1405844cd12b8d
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.