Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Chromecast
DescriptionUse after free in Chromecast
ComponentChromecast
Bug ClassUAF
Tracker516764384
Fix commit5d15272dcd43 (chromium/src) +111/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
if
chromecast/media/cma/pipeline/media_pipeline_impl.cc
modified
StallingFrameProvider
chromecast/media/cma/pipeline/media_pipeline_impl_unittest.cc
modified
if
chromecast/media/cma/pipeline/media_pipeline_impl_unittest.cc
modified
TEST
chromecast/media/cma/pipeline/media_pipeline_impl_unittest.cc
modified
BindOnce
chromecast/media/cma/pipeline/media_pipeline_impl_unittest.cc
modified

Files Changed

  • chromecast/media/cma/pipeline/media_pipeline_impl.cc
  • chromecast/media/cma/pipeline/media_pipeline_impl_unittest.cc
From 5d15272dcd4391a7ac06f596f88518bbb5393865 Mon Sep 17 00:00:00 2001
From: Richard Nichols <rknichols@google.com>
Date: Tue, 07 Jul 2026 10:24:47 -0700
Subject: [PATCH] [Chromecast] Reject StartPlayingFrom while a flush is pending

MediaPipelineImpl only stops the backend once every per-stream flush has
completed. A StartPlayingFrom that arrives while one stream's flush is
still outstanding can therefore restart an AV pipeline whose previously
pushed buffer is still in use by the backend.

that reports PIPELINE_ERROR_ABORT and returns early, and add a
regression test that drives an asymmetric audio/video flush.

Bug: 516764384
Change-Id: I37dd5e1a230c675c7e624436e183114695f18148
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8012487
Reviewed-by: Shawn Quereshi <shawnq@google.com>
Reviewed-by: Sandeep Vijayasekar <sandv@google.com>
Commit-Queue: Richard Nichols <rknichols@google.com>
Cr-Commit-Position: refs/heads/main@{#1658062}
---

diff --git a/chromecast/media/cma/pipeline/media_pipeline_impl.cc b/chromecast/media/cma/pipeline/media_pipeline_impl.cc
index 999afb6..3e86da8 100644
--- a/chromecast/media/cma/pipeline/media_pipeline_impl.cc
+++ b/chromecast/media/cma/pipeline/media_pipeline_impl.cc
@@ -209,7 +209,17 @@
   LOG(INFO) << __FUNCTION__ << " t0=" << time.InMilliseconds();
   DCHECK(thread_checker_.CalledOnValidThread());
   DCHECK(audio_pipeline_ || video_pipeline_);
-  DCHECK(!pending_flush_task_);
+
+  // The backend is only stopped once every per-stream flush has completed,
+  // so a flush that is still pending means the backend may still be using a
+  // previously pushed buffer (see AvPipelineImpl::Flush). Restarting any AV
+  // pipeline before the backend has been stopped would release that buffer
+  // while it is still in use.
+  if (pending_flush_task_) {
+    LOG(ERROR) << __FUNCTION__ << " called while a flush is still pending";
+    OnError(::media::PIPELINE_ERROR_ABORT);
+    return;
+  }
 
   // Lazy initialize.
   if (backend_state_ == BACKEND_STATE_UNINITIALIZED) {
diff --git a/chromecast/media/cma/pipeline/media_pipeline_impl_unittest.cc b/chromecast/media/cma/pipeline/media_pipeline_impl_unittest.cc
index 88acfe8..bdc5ae0 100644
--- a/chromecast/media/cma/pipeline/media_pipeline_impl_unittest.cc
+++ b/chromecast/media/cma/pipeline/media_pipeline_impl_unittest.cc
@@ -10,10 +10,15 @@
 #include "base/functional/callback_helpers.h"
 #include "base/test/task_environment.h"
 #include "chromecast/media/api/test/mock_cma_backend.h"
+#include "chromecast/media/cma/base/coded_frame_provider.h"
+#include "chromecast/media/cma/pipeline/av_pipeline_client.h"
 #include "chromecast/media/cma/pipeline/load_type.h"
 #include "chromecast/media/cma/pipeline/video_pipeline_client.h"
 #include "chromecast/media/cma/test/mock_frame_provider.h"
+#include "media/base/audio_decoder_config.h"
+#include "media/base/channel_layout.h"
 #include "media/base/encryption_scheme.h"
+#include "media/base/media_util.h"
 #include "media/base/video_codecs.h"
 #include "media/base/video_color_space.h"
 #include "media/base/video_decoder_config.h"
@@ -26,9 +31,40 @@
 namespace media {
 namespace {
 
+using ::testing::_;
 using ::testing::AtLeast;
+using ::testing::NiceMock;
 using ::testing::Return;
 
+// A frame provider that never delivers any frames and whose Flush completion
+// can optionally be deferred indefinitely.
+class StallingFrameProvider : public CodedFrameProvider {
+ public:
+  explicit StallingFrameProvider(bool defer_flush)
+      : defer_flush_(defer_flush) {}
+
+  StallingFrameProvider(const StallingFrameProvider&) = delete;
+  StallingFrameProvider& operator=(const StallingFrameProvider&) = delete;
+
+  ~StallingFrameProvider() override = default;
+
+  void Read(ReadCB read_cb) override { pending_read_cb_ = std::move(read_cb); }
+
+  void Flush(base::OnceClosure flush_cb) override {
+    pending_read_cb_.Reset();
+    if (defer_flush_) {
+      pending_flush_cb_ = std::move(flush_cb);
+    } else {
+      std::move(flush_cb).Run();
+    }
+  }
+
+ private:
+  const bool defer_flush_;
+  ReadCB pending_read_cb_;
+  base::OnceClosure pending_flush_cb_;
+};
+
 TEST(MediaPipelineImplTest, DoesNotCrashOnFlushWhenBufferingIsDisabled) {
   base::test::TaskEnvironment task_environment;
 
@@ -60,6 +96,70 @@
   media_pipeline.Flush(base::DoNothing());
 }
 
+// When both an audio and a video stream are present, the per-stream flushes
+// can complete independently. StartPlayingFrom must not restart the backend
+// or any AV pipeline until both have completed and the backend has been
+// stopped.
+TEST(MediaPipelineImplTest, StartPlayingFromIgnoredWhileFlushPending) {
+  base::test::TaskEnvironment task_environment;
+
+  NiceMock<MockCmaBackend::AudioDecoder> audio_decoder;
+  NiceMock<MockCmaBackend::VideoDecoder> video_decoder;
+  ON_CALL(audio_decoder, SetConfig).WillByDefault(Return(true));
+  ON_CALL(video_decoder, SetConfig).WillByDefault(Return(true));
+
+  auto backend = std::make_unique<NiceMock<MockCmaBackend>>();
+  ON_CALL(*backend, Initialize).WillByDefault(Return(true));
+  ON_CALL(*backend, Start).WillByDefault(Return(true));
+  EXPECT_CALL(*backend, CreateAudioDecoder).WillOnce(Return(&audio_decoder));
+  EXPECT_CALL(*backend, CreateVideoDecoder).WillOnce(Return(&video_decoder));
+  EXPECT_CALL(*backend, Start).Times(1);
+  EXPECT_CALL(*backend, Stop).Times(0);
+
+  EXPECT_CALL(audio_decoder, PushBuffer(_)).Times(0);
+
+  MediaPipelineImpl media_pipeline;
+  media_pipeline.Initialize(LoadType::kLoadTypeMediaStream, std::move(backend),
+                            /*is_buffering_enabled=*/false);
+
+  ::media::AudioDecoderConfig audio_config(
+      ::media::AudioCodec::kMP3, ::media::kSampleFormatS16,
+      ::media::ChannelLayoutConfig::Stereo(), 44100, ::media::EmptyExtraData(),
+      ::media::EncryptionScheme::kUnencrypted);
+  ASSERT_EQ(::media::PIPELINE_OK,
+            media_pipeline.InitializeAudio(
+                audio_config, AvPipelineClient(),
+                std::make_unique<StallingFrameProvider>(
+                    /*defer_flush=*/false)));
+  ASSERT_EQ(
+      ::media::PIPELINE_OK,
+      media_pipeline.InitializeVideo(
+          {::media::VideoDecoderConfig(
+              ::media::VideoCodec::kH264, ::media::H264PROFILE_MAIN,
+              ::media::VideoDecoderConfig::AlphaMode::kIsOpaque,
+              ::media::VideoColorSpace(), ::media::kNoTransformation,
+              gfx::Size(640, 480), gfx::Rect(0, 0, 640, 480),
+              gfx::Size(640, 480), ::media::EmptyExtraData(),
+              ::media::EncryptionScheme::kUnencrypted)},
+          VideoPipelineClient(),
+          std::make_unique<StallingFrameProvider>(/*defer_flush=*/true)));
+
+  media_pipeline.StartPlayingFrom(base::Seconds(0));
+
+  // The audio pipeline's flush completes synchronously while the video
+  // pipeline's flush remains outstanding, so the overall flush has not yet
+  // stopped the backend.
+  bool flush_done = false;
+  media_pipeline.Flush(
+      base::BindOnce([](bool* done) { *done = true; }, &flush_done));
+  EXPECT_FALSE(flush_done);
+
+  // A StartPlayingFrom that arrives in this state must be rejected without
+  // restarting the backend or feeding any new audio buffers.
+  media_pipeline.StartPlayingFrom(base::Seconds(1));
+  task_environment.RunUntilIdle();
+}
+
 }  // namespace
 }  // namespace media
 }  // namespace chromecast
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chromecast/media/cma/pipeline/media_pipeline_impl_unittest.cc b/chromecast/media/cma/pipeline/media_pipeline_impl_unittest.cc
index 88acfe8..bdc5ae0 100644
--- a/chromecast/media/cma/pipeline/media_pipeline_impl_unittest.cc
+++ b/chromecast/media/cma/pipeline/media_pipeline_impl_unittest.cc
@@ -10,10 +10,15 @@
 #include "base/functional/callback_helpers.h"
 #include "base/test/task_environment.h"
 #include "chromecast/media/api/test/mock_cma_backend.h"
+#include "chromecast/media/cma/base/coded_frame_provider.h"
+#include "chromecast/media/cma/pipeline/av_pipeline_client.h"
 #include "chromecast/media/cma/pipeline/load_type.h"
 #include "chromecast/media/cma/pipeline/video_pipeline_client.h"
 #include "chromecast/media/cma/test/mock_frame_provider.h"
+#include "media/base/audio_decoder_config.h"
+#include "media/base/channel_layout.h"
 #include "media/base/encryption_scheme.h"
+#include "media/base/media_util.h"
 #include "media/base/video_codecs.h"
 #include "media/base/video_color_space.h"
 #include "media/base/video_decoder_config.h"
@@ -26,9 +31,40 @@
 namespace media {
 namespace {
 
+using ::testing::_;
 using ::testing::AtLeast;
+using ::testing::NiceMock;
 using ::testing::Return;
 
+// A frame provider that never delivers any frames and whose Flush completion
+// can optionally be deferred indefinitely.
+class StallingFrameProvider : public CodedFrameProvider {
+ public:
+  explicit StallingFrameProvider(bool defer_flush)
+      : defer_flush_(defer_flush) {}
+
+  StallingFrameProvider(const StallingFrameProvider&) = delete;
+  StallingFrameProvider& operator=(const StallingFrameProvider&) = delete;
+
+  ~StallingFrameProvider() override = default;
+
+  void Read(ReadCB read_cb) override { pending_read_cb_ = std::move(read_cb); }
+
+  void Flush(base::OnceClosure flush_cb) override {
+    pending_read_cb_.Reset();
+    if (defer_flush_) {
+      pending_flush_cb_ = std::move(flush_cb);
+    } else {
+      std::move(flush_cb).Run();
+    }
+  }
+
+ private:
+  const bool defer_flush_;
+  ReadCB pending_read_cb_;
+  base::OnceClosure pending_flush_cb_;
+};
+
 TEST(MediaPipelineImplTest, DoesNotCrashOnFlushWhenBufferingIsDisabled) {
   base::test::TaskEnvironment task_environment;
 
@@ -60,6 +96,70 @@
   media_pipeline.Flush(base::DoNothing());
 }
 
+// When both an audio and a video stream are present, the per-stream flushes
+// can complete independently. StartPlayingFrom must not restart the backend
+// or any AV pipeline until both have completed and the backend has been
+// stopped.
+TEST(MediaPipelineImplTest, StartPlayingFromIgnoredWhileFlushPending) {
+  base::test::TaskEnvironment task_environment;
+
+  NiceMock<MockCmaBackend::AudioDecoder> audio_decoder;
+  NiceMock<MockCmaBackend::VideoDecoder> video_decoder;
+  ON_CALL(audio_decoder, SetConfig).WillByDefault(Return(true));
+  ON_CALL(video_decoder, SetConfig).WillByDefault(Return(true));
+
+  auto backend = std::make_unique<NiceMock<MockCmaBackend>>();
+  ON_CALL(*backend, Initialize).WillByDefault(Return(true));
+  ON_CALL(*backend, Start).WillByDefault(Return(true));
+  EXPECT_CALL(*backend, CreateAudioDecoder).WillOnce(Return(&audio_decoder));
+  EXPECT_CALL(*backend, CreateVideoDecoder).WillOnce(Return(&video_decoder));
+  EXPECT_CALL(*backend, Start).Times(1);
+  EXPECT_CALL(*backend, Stop).Times(0);
+
+  EXPECT_CALL(audio_decoder, PushBuffer(_)).Times(0);
+
+  MediaPipelineImpl media_pipeline;
+  media_pipeline.Initialize(LoadType::kLoadTypeMediaStream, std::move(backend),
+                            /*is_buffering_enabled=*/false);
+
+  ::media::AudioDecoderConfig audio_config(
+      ::media::AudioCodec::kMP3, ::media::kSampleFormatS16,
+      ::media::ChannelLayoutConfig::Stereo(), 44100, ::media::EmptyExtraData(),
+      ::media::EncryptionScheme::kUnencrypted);
+  ASSERT_EQ(::media::PIPELINE_OK,
+            media_pipeline.InitializeAudio(
+                audio_config, AvPipelineClient(),
+                std::make_unique<StallingFrameProvider>(
+                    /*defer_flush=*/false)));
+  ASSERT_EQ(
+      ::media::PIPELINE_OK,
+      media_pipeline.InitializeVideo(
+          {::media::VideoDecoderConfig(
+              ::media::VideoCodec::kH264, ::media::H264PROFILE_MAIN,
+              ::media::VideoDecoderConfig::AlphaMode::kIsOpaque,
+              ::media::VideoColorSpace(), ::media::kNoTransformation,
+              gfx::Size(640, 480), gfx::Rect(0, 0, 640, 480),
+              gfx::Size(640, 480), ::media::EmptyExtraData(),
+              ::media::EncryptionScheme::kUnencrypted)},
+          VideoPipelineClient(),
+          std::make_unique<StallingFrameProvider>(/*defer_flush=*/true)));
+
+  media_pipeline.StartPlayingFrom(base::Seconds(0));
+
+  // The audio pipeline's flush completes synchronously while the video
+  // pipeline's flush remains outstanding, so the overall flush has not yet
+  // stopped the backend.
+  bool flush_done = false;
+  media_pipeline.Flush(
+      base::BindOnce([](bool* done) { *done = true; }, &flush_done));
+  EXPECT_FALSE(flush_done);
+
+  // A StartPlayingFrom that arrives in this state must be rejected without
+  // restarting the backend or feeding any new audio buffers.
+  media_pipeline.StartPlayingFrom(base::Seconds(1));
+  task_environment.RunUntilIdle();
+}
+
 }  // namespace
 }  // namespace media
 }  // namespace chromecast
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Cast browser UAF in AvPipelineImpl due to renderer-controlled per-stream Flush asymmetry

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 Use-After-Free (UAF) vulnerability exists in the Chromecast media pipeline due to an incorrect state-machine assumption during stream flushing. By selectively responding to flush requests, a compromised renderer can drive one stream pipeline to a flushed state while the other is still flushing, leaving the backend running. If a play request is subsequently issued, the flushed pipeline will release its references to the active buffers while the running backend continues to access them.

Affected files:

  • chromecast/media/cma/pipeline/media_pipeline_impl.cc
  • chromecast/media/cma/pipeline/av_pipeline_impl.cc
  • chromecast/media/cma/base/demuxer_stream_adapter.cc
  • chromecast/media/common/audio_decoder_wrapper.cc
  • chromecast/media/common/audio_decoder_software_wrapper.cc

Estimated timestamp from git blame: 2026-05-14

Description

An issue has been identified in the Chromecast media pipeline (MediaPipelineImpl and AvPipelineImpl) where a compromised renderer can cause a desynchronization of the stream flushing states, leading to a potential Use-After-Free (UAF) of reference-counted audio/video buffers in the browser process.

To prevent double-start scenarios, a state guard exists in AvPipelineImpl::StartPlayingFrom to ensure the stream is in the kFlushed state:

if (state_ != kFlushed) {
  LOG(ERROR) << __FUNCTION__ << " called in unexpected state " << state_;
  return false;
}

This assumes that if the stream is in the kFlushed state, the backend has been safely stopped and all currently pending buffers have been released. However, in MediaPipelineImpl::OnFlushDone, the media backend is only stopped when both the audio and video streams have finished flushing:

if (pending_flush_task_->audio_flushed &&
    pending_flush_task_->video_flushed) {
  media_pipeline_backend_->Stop();
  backend_state_ = BACKEND_STATE_INITIALIZED;
  ...
}

Because per-stream flush completion is gated on the renderer answering an outstanding mojom::DemuxerStream::Read, a compromised renderer can selectively answer the audio read while stalling the video read. This drives the audio pipeline to the kFlushed state while the video pipeline remains in kFlushing. At this point, the backend is not stopped, and continues to hold a raw CastDecoderBuffer* pointer to the active audio buffer (Buffer A) under the kBufferPending contract.

In a release build, if the renderer subsequently sends a StartPlayingFrom IPC, the safety guard DCHECK(!pending_flush_task_) is stripped. The audio pipeline passes its state_ == kFlushed check, discards its reference to Buffer A (pushed_buffer_ = nullptr), and posts a FetchBuffer task. The video pipeline fails the state check and returns false, triggering OnError, but this does not stop the backend or cancel the posted audio FetchBuffer task.

When the new Buffer B is retrieved and pushed via ActiveAudioDecoderWrapper::PushBuffer (or AudioDecoderSoftwareWrapper::PushBuffer), pushed_buffer_ = std::move(buffer) overwrites the reference to Buffer A. Since this is the last scoped_refptr holding Buffer A, it is deallocated. The media backend, which was never stopped, still holds the raw pointer to Buffer A, resulting in a Use-After-Free when it next dereferences it.

Note: Our tooling currently lacks the capability to compile and execute a functional proof-of-concept exploit, so these steps are suggested based on static analysis of the source code.

Potential Trigger Path / Attack Scenario

  1. A compromised renderer initializes a CastRenderer with both audio and video streams, and starts playing.
  2. The renderer answers the first audio Read with Buffer A, returning kBufferPending and causing the backend to store a raw pointer to Buffer A.
  3. The renderer stalls both the audio and video read-ahead requests.
  4. The renderer triggers a flush via mojom::Renderer::Flush. Both pipelines enter the kFlushing state.
  5. The renderer answers only the audio read request. This transitions the audio pipeline to kFlushed. However, MediaPipelineImpl::OnFlushDone does not stop the backend because the video pipeline is still flushing.
  6. The renderer sends a StartPlayingFrom IPC request. The audio pipeline passes the state guard, drops its reference to Buffer A, and schedules a FetchBuffer task. The video pipeline fails the guard, which triggers OnError but fails to stop the backend or the posted fetch task.
  7. The audio pipeline receives a new Buffer B. When pushing Buffer B, the last reference to Buffer A is overwritten and Buffer A is freed.
  8. The backend eventually dereferences the raw pointer to the freed Buffer A, causing a Use-After-Free.

Impact

This leads to a Use-After-Free of a polymorphic reference-counted object (DecoderBufferBase / CastDecoderBuffer) in the unsandboxed Cast browser process (cast_shell on CastOS). An attacker who has compromised the renderer can potentially leverage this to execute arbitrary code with browser-process privileges, escaping the sandbox. On CastOS, the PartitionAllocBackupRefPtr (MiraclePtr) mitigation is disabled, leaving the vulnerability potentially fully exploitable.

Affected Files

  • chromecast/media/cma/pipeline/media_pipeline_impl.cc
  • chromecast/media/cma/pipeline/av_pipeline_impl.cc
  • chromecast/media/cma/base/demuxer_stream_adapter.cc
  • chromecast/media/common/audio_decoder_wrapper.cc
  • chromecast/media/common/audio_decoder_software_wrapper.cc

Suggested Fix

To prevent this state-machine bypass in production builds, enforce the flush task check in release builds by returning early or throwing an error in MediaPipelineImpl::StartPlayingFrom if a flush task is pending, rather than relying solely on a DCHECK:

if (pending_flush_task_) {
  LOG(ERROR) << "StartPlayingFrom called during pending flush";
  OnError(::media::PIPELINE_ERROR_ABORT);
  return;
}

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.

View on issue tracker