Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Cast
DescriptionUse after free in Cast
ComponentCast
Bug ClassUAF
Tracker500034684
Fix commit069cd8bd0ee6 (chromium/src) +28/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-04-28

Changed Functions

FunctionChangeNotes
if
media/remoting/stream_provider.cc
modified
TEST_F
media/remoting/stream_provider_unittest.cc
modified

Files Changed

  • media/remoting/stream_provider.cc
  • media/remoting/stream_provider_unittest.cc
From 069cd8bd0ee67af4065e59b5b8cd7dcf6ba5810f Mon Sep 17 00:00:00 2001
From: Jordan Bayles <jophba@chromium.org>
Date: Fri, 10 Apr 2026 14:15:14 -0700
Subject: [PATCH] [remoting] Fix UAF in StreamProvider on duplicate RPC_ACQUIRE_DEMUXER

Receiving multiple RPC_ACQUIRE_DEMUXER messages caused old MediaStream
objects to be freed while the renderer stack retained dangling raw
pointers to them. This led to a Use-After-Free when triggering playback.

This CL adds a check in StreamProvider::OnAcquireDemuxer() to ignore
subsequent acquisition requests if streams have already been acquired.

Added a regression test DuplicateAcquireDemuxer to
stream_provider_unittest.cc.

Bug: 500034684
Change-Id: I300da00ba9045c7de354e39d342a0292ca7d77b9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7748690
Commit-Queue: Jordan Bayles <jophba@chromium.org>
Reviewed-by: Frank Liberato <liberato@chromium.org>
Reviewed-by: Muyao Xu <muyaoxu@google.com>
Cr-Commit-Position: refs/heads/main@{#1613094}
---

diff --git a/media/remoting/stream_provider.cc b/media/remoting/stream_provider.cc
index 6d06c0c8..741f04e3 100644
--- a/media/remoting/stream_provider.cc
+++ b/media/remoting/stream_provider.cc
@@ -546,6 +546,11 @@
   DCHECK(media_task_runner_->RunsTasksInCurrentSequence());
   DCHECK(message->has_acquire_demuxer_rpc());
 
+  if (audio_stream_ || video_stream_) {
+    VLOG(1) << __func__ << " Demuxer streams already acquired, ignoring.";
+    return;
+  }
+
   int32_t audio_demuxer_handle =
       message->acquire_demuxer_rpc().audio_demuxer_handle();
   int32_t video_demuxer_handle =
diff --git a/media/remoting/stream_provider_unittest.cc b/media/remoting/stream_provider_unittest.cc
index 6413abf..ded1ac96 100644
--- a/media/remoting/stream_provider_unittest.cc
+++ b/media/remoting/stream_provider_unittest.cc
@@ -4,6 +4,7 @@
 
 #include "media/remoting/stream_provider.h"
 
+#include "base/functional/callback_helpers.h"
 #include "base/memory/raw_ptr.h"
 #include "base/memory/scoped_refptr.h"
 #include "base/task/single_thread_task_runner.h"
@@ -331,5 +332,27 @@
   EXPECT_EQ(GetVideoCurrentFrameCount(), flush_video_count);
 }
 
+TEST_F(StreamProviderTest, DuplicateAcquireDemuxer) {
+  InitializeDemuxer();
+  SendRpcAcquireDemuxer();
+  task_environment_.RunUntilIdle();
+  EXPECT_TRUE(stream_provider_initialized_);
+
+  // Cache raw pointers.
+  std::vector<DemuxerStream*> streams = stream_provider_->GetAllStreams();
+  ASSERT_EQ(streams.size(), 2u);
+  DemuxerStream* cached_audio =
+      streams[0]->type() == DemuxerStream::AUDIO ? streams[0] : streams[1];
+
+  // Second acquisition.
+  SendRpcAcquireDemuxer();
+  task_environment_.RunUntilIdle();
+
+  // The first streams should still be valid and not destroyed.
+  // If they were destroyed, this call would trigger a UAF.
+  cached_audio->Read(1, base::DoNothing());
+  task_environment_.RunUntilIdle();
+}
+
 }  // namespace remoting
 }  // namespace media
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/media/remoting/stream_provider_unittest.cc b/media/remoting/stream_provider_unittest.cc
index 6413abf..ded1ac96 100644
--- a/media/remoting/stream_provider_unittest.cc
+++ b/media/remoting/stream_provider_unittest.cc
@@ -4,6 +4,7 @@
 
 #include "media/remoting/stream_provider.h"
 
+#include "base/functional/callback_helpers.h"
 #include "base/memory/raw_ptr.h"
 #include "base/memory/scoped_refptr.h"
 #include "base/task/single_thread_task_runner.h"
@@ -331,5 +332,27 @@
   EXPECT_EQ(GetVideoCurrentFrameCount(), flush_video_count);
 }
 
+TEST_F(StreamProviderTest, DuplicateAcquireDemuxer) {
+  InitializeDemuxer();
+  SendRpcAcquireDemuxer();
+  task_environment_.RunUntilIdle();
+  EXPECT_TRUE(stream_provider_initialized_);
+
+  // Cache raw pointers.
+  std::vector<DemuxerStream*> streams = stream_provider_->GetAllStreams();
+  ASSERT_EQ(streams.size(), 2u);
+  DemuxerStream* cached_audio =
+      streams[0]->type() == DemuxerStream::AUDIO ? streams[0] : streams[1];
+
+  // Second acquisition.
+  SendRpcAcquireDemuxer();
+  task_environment_.RunUntilIdle();
+
+  // The first streams should still be valid and not destroyed.
+  // If they were destroyed, this call would trigger a UAF.
+  cached_audio->Read(1, base::DoNothing());
+  task_environment_.RunUntilIdle();
+}
+
 }  // namespace remoting
 }  // namespace media
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Use-After-Free in Cast receiver renderer via duplicate RPC_ACQUIRE_DEMUXER

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 security team.

Overview: A potential Use-After-Free (UAF) vulnerability exists in the Cast receiver’s StreamProvider. Receiving multiple RPC_ACQUIRE_DEMUXER messages causes old MediaStream objects to be freed while the renderer stack retains dangling raw pointers to them. Triggering playback subsequently causes a virtual method call on the freed memory, which could lead to Remote Code Execution.

Affected files:

  • media/remoting/stream_provider.cc
  • media/remoting/stream_provider.h

Estimated timestamp from git blame: 2023-02-14

Description

A Use-After-Free (UAF) vulnerability exists in the StreamProvider class within the Cast receiver renderer. The issue is located in media/remoting/stream_provider.cc, where StreamProvider::OnAcquireDemuxer() handles the RPC_ACQUIRE_DEMUXER Remote Procedure Call (RPC) message. This method lacks a state guard to prevent multiple acquisitions, allowing an attacker to trigger the creation of new MediaStream objects while the renderer stack still holds raw pointers to the previously created ones.

Technical Details

When StreamProvider::OnAcquireDemuxer() is called, it extracts audio and video demuxer handles and posts tasks to the main thread to create MediaStream objects. These objects are returned to the media thread and stored via std::move() into audio_stream_ and video_stream_ (both MediaStream::UniquePtr).

During the renderer initialization phase (triggered via RPC_R_INITIALIZE), the underlying renderer stack (e.g., RendererImpl, AudioRendererImpl, and DecoderStream) caches raw pointers to these demuxer streams. Specifically, it calls demuxer_->GetAllStreams() which returns the raw pointers extracted from the unique_ptrs. Components like DecoderStream cache this in a raw_ptr<DemuxerStream, DanglingUntriaged> stream_ member.

If an attacker sends a second RPC_ACQUIRE_DEMUXER message, StreamProvider::OnAcquireDemuxer() blindly creates new MediaStream objects. When these new streams are assigned to the unique_ptr members, the original MediaStream objects are destroyed and their memory is freed via a DeleteSoon task on the main thread. However, the renderer components are never notified, leaving their cached raw pointers dangling.

An attacker can then send an RPC_R_STARTPLAYINGFROM message, which causes the receiver to transition the renderer state and initiate playback by attempting to read from the demuxer. This traces down to DecoderStream::ReadFromDemuxerStream(), which invokes the virtual function stream_->Read(...) through the dangling pointer. Because MediaStream is a polymorphic object, this virtual call uses the vtable of the freed memory.

On platforms like CastOS and Android, renderer processes generally have MiraclePtr (BackupRefPtr) disabled due to performance overhead, making this a fully exploitable Use-After-Free leading to control-flow hijacking and Remote Code Execution inside the sandboxed renderer.

Suggested Reproduction Steps

(Note: These are potential steps to trigger the bug, as our tooling agent does not have the ability to run or verify PoC code natively).

  1. From a LAN-adjacent host, establish a Cast Streaming Media Remoting session to the Cast receiver.
  2. Send RPC_ACQUIRE_RENDERER to set up the renderer handle.
  3. Send an initial RPC_ACQUIRE_DEMUXER and complete the initialization handshake for the streams.
  4. Send RPC_R_INITIALIZE to cause the renderer to initialize. This forces the renderer stack to cache the raw DemuxerStream* pointers.
  5. Send a second RPC_ACQUIRE_DEMUXER message. This will cause OnAcquireDemuxer to overwrite audio_stream_ and/or video_stream_, silently freeing the original objects on the main thread.
  6. (Optional) Perform heap grooming by allocating controlled data of the same size as the freed MediaStream objects.
  7. Send RPC_R_STARTPLAYINGFROM. This eventually triggers stream_->Read(...) in the renderer via DecoderStream, leading to a virtual call on the freed memory.

Suggested Fix

Add state validation in StreamProvider::OnAcquireDemuxer() to ensure it ignores duplicate initialization requests or safely tears down the renderer stack if a re-acquisition is required. For example:

void StreamProvider::OnAcquireDemuxer(
    std::unique_ptr<openscreen::cast::RpcMessage> message) {
  DCHECK(media_task_runner_->RunsTasksInCurrentSequence());
  DCHECK(message->has_acquire_demuxer_rpc());

  // Prevent UAF by ignoring duplicate RPC_ACQUIRE_DEMUXER calls.
  if (audio_stream_ || video_stream_) {
    VLOG(1) << __func__ << " Demuxer streams already acquired, ignoring.";
    return;
  }
  // ... existing code ...
}

Evaluated with Chrome root at commit: f200f57a19490707ff8bc7aa5de3cbc443a3afad


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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