CVE-2026-7349
Overview
Background
- Media Remoting / Cast
- the feature that hands off media playback from the local renderer to a remote Cast receiver, driving the remote demuxer over an RPC channel.
- `StreamProvider`
- the media-remoting component that owns the remote
MediaStream/DemuxerStreamobjects (audio_stream_,video_stream_) the renderer pipeline reads from. - `RPC_ACQUIRE_DEMUXER` / `OnAcquireDemuxer`
- the RPC message (and its handler) that tells
StreamProviderto create the demuxer streams from the handles inacquire_demuxer_rpc(). - `DemuxerStream` raw pointers
- the renderer stack caches raw
DemuxerStream*pointers (e.g. viaGetAllStreams()); these dangle if the underlying stream objects are freed and recreated.
Root Cause Analysis
StreamProvider::OnAcquireDemuxer created new MediaStream/DemuxerStream objects from the RPC’s demuxer handles without checking whether streams had already been acquired. On a second RPC_ACQUIRE_DEMUXER, it rebuilt audio_stream_/video_stream_, freeing the previously created stream objects, while the renderer stack still held cached raw DemuxerStream* pointers to the old objects. Triggering playback (e.g. a Read on a cached stream) then dereferenced those dangling pointers, a use-after-free.
The fix adds an early guard at the top of OnAcquireDemuxer that returns if audio_stream_ || video_stream_ is already set, logging and ignoring the duplicate request so the original streams survive. The guard runs before any handle is read or any stream is reconstructed, preserving the “streams are acquired exactly once” invariant.
if (audio_stream_ || video_stream_) return; guard makes acquisition one-shot so existing pointers stay valid.Attack Path
- Enter media remoting
A Cast session is established and the receiver side drives RPC to the renderer’s
StreamProvider, which acquires audio/video demuxer streams on the firstRPC_ACQUIRE_DEMUXER. - Cache raw stream pointers
The renderer pipeline obtains and retains raw
DemuxerStream*pointers to the acquired streams (as the regression test does viaGetAllStreams()). - Send a duplicate acquire
A second
RPC_ACQUIRE_DEMUXERarrives; the oldOnAcquireDemuxerrebuildsaudio_stream_/video_stream_, freeing the original stream objects while the cached pointers remain. - Trigger playback on a freed stream
Playback performs a
Read(or similar) through a cachedDemuxerStream*, dereferencing freed memory and causing the use-after-free (inferred: downstream exploitation beyond the stale dereference is not shown in the diff).
Impact Assessment
StreamProvider (the renderer-side Cast stack) can free live demuxer streams out from under cached raw pointers and cause a use-after-free during playback; the advisory rates this high severity, “Use after free in Cast.” The precondition is an active remoting session plus the ability to inject a second RPC_ACQUIRE_DEMUXER message. Escalation beyond the dangling-pointer crash is (inferred) and not demonstrated by the patch, which only shows the duplicate-acquisition free and a regression test asserting the old streams stay valid.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmedia/remoting/stream_provider.cc |
modified | |
TEST_Fmedia/remoting/stream_provider_unittest.cc |
modified |
Files Changed
media/remoting/stream_provider.ccmedia/remoting/stream_provider_unittest.cc
Audit Directions
- Non-idempotent RPC/IPC handlersAudit message handlers that (re)create owned objects from message fields without checking whether that one-time initialization already ran, especially where a peer can replay the message.
- Raw pointers across object recreationLook for components that hand out raw
DemuxerStream*/object pointers that are cached by callers while the owner can later replace those objects; prefer weak pointers or acquisition guards. - Media-remoting RPC lifecycleReview other
media/remotinghandlers (demuxer, renderer, flush/initialize) for the same “acquire/initialize only once” assumption not being enforced against duplicate or out-of-order RPCs.
Patch
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
Regression Test / PoC
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
Original Bug Report
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.ccmedia/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).
- From a LAN-adjacent host, establish a Cast Streaming Media Remoting session to the Cast receiver.
- Send
RPC_ACQUIRE_RENDERERto set up the renderer handle. - Send an initial
RPC_ACQUIRE_DEMUXERand complete the initialization handshake for the streams. - Send
RPC_R_INITIALIZEto cause the renderer to initialize. This forces the renderer stack to cache the rawDemuxerStream*pointers. - Send a second
RPC_ACQUIRE_DEMUXERmessage. This will causeOnAcquireDemuxerto overwriteaudio_stream_and/orvideo_stream_, silently freeing the original objects on the main thread. - (Optional) Perform heap grooming by allocating controlled data of the same size as the freed
MediaStreamobjects. - Send
RPC_R_STARTPLAYINGFROM. This eventually triggersstream_->Read(...)in the renderer viaDecoderStream, 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.