Overview

Critical
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Chromecast
DescriptionUse after free in Chromecast
ComponentChromecast
Sub-componentChromecast
Bug ClassUAF
Tracker534994449
Fix commit991f7b234fef (chromium/src) +65/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
TestDecoderBuffer
chromecast/media/common/audio_decoder_software_wrapper_unittest.cc
modified
if
chromecast/media/common/audio_decoder_software_wrapper_unittest.cc
modified
AudioDecoderSoftwareWrapperTest
chromecast/media/common/audio_decoder_software_wrapper_unittest.cc
modified
TEST
chromecast/media/common/audio_decoder_software_wrapper_unittest.cc
modified

Files Changed

  • chromecast/media/common/audio_decoder_software_wrapper_unittest.cc
  • chromecast/media/common/audio_decoder_wrapper.cc
  • chromecast/media/common/audio_decoder_wrapper.h
From 991f7b234fef580899b601d61df58a44bfe453bd Mon Sep 17 00:00:00 2001
From: Frank Liberato <[email protected]>
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 <[email protected]>
Auto-Submit: Frank Liberato <[email protected]>
Commit-Queue: Frank Liberato <[email protected]>
Reviewed-by: Dale Curtis <[email protected]>
Reviewed-by: Simeon Anfinrud <[email protected]>
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
Loading diff…

Regression Test / PoC

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

Original Bug Report

reported by [email protected]

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.cc
  • chromecast/media/common/audio_decoder_wrapper.cc
  • chromecast/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

  1. Premature Reference Release: When a buffer is pushed, MediaPipelineBufferExtension::PushBuffer delegates downstream, eventually invoking the vendor’s asynchronous PushBuffer which returns kBufferPending. However, if the buffer extension’s queue is not full, it translates this status and returns kBufferSuccess to the upstream AvPipelineImpl (chromecast/media/cma/backend/proxy/media_pipeline_buffer_extension.cc):

    return IsBufferFull() ? BufferStatus::kBufferPending
                          : BufferStatus::kBufferSuccess;
    

    As a result, AvPipelineImpl immediately nulls its pushed_buffer_ reference (chromecast/media/cma/pipeline/av_pipeline_impl.cc), assuming the buffer push completed successfully.

  2. Sole Ownership: At this stage, the only scoped_refptr holding the DecoderBufferBase alive is ActiveAudioDecoderWrapper::pushed_buffer_ (chromecast/media/common/audio_decoder_wrapper.cc). Meanwhile, the vendor’s backend decoder holds a raw, unprotected pointer (CastDecoderBuffer*) to the buffer.

  3. 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 the DecoderBufferBase. Crucially, this happens before the active vendor-implemented backend is stopped or destroyed.

  4. Use-After-Free: Immediately after, MediaPipelineBackendWrapper::Revoke() queries backend_->GetCurrentPts(). During this call, or during subsequent destruction of the vendor backend/asynchronous decode threads, the vendor backend may dereference the dangling CastDecoderBuffer* pointer. Since CastDecoderBuffer is 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.

  1. Navigate to a page on a Cast Core device that initiates media playback on a multiroom/cast-renderer-enabled stream (Element A).
  2. Feed encoded audio such that the vendor backend returns kBufferPending, but MediaPipelineBufferExtension returns kBufferSuccess, leaving ActiveAudioDecoderWrapper as the sole owner of the buffer.
  3. Initiate playback of a second video element (Element B) to trigger video decoder resource reallocation.
  4. MediaPipelineBackendManager invokes Revoke() on Element A, destroying ActiveAudioDecoderWrapper and freeing the buffer.
  5. 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.

  1. Maintain Buffer Lifetime: Ensure that ActiveAudioDecoderWrapper does not destroy or release pushed_buffer_ during Revoke() until the underlying vendor backend’s AudioDecoder has been stopped, flushed, or fully destroyed.
  2. Correct Status Propagation: Avoid translating kBufferPending to kBufferSuccess upstream 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.

View on issue tracker