High watch CVSS 7.5 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
7.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Cast
DescriptionUse after free in Cast
ComponentCast
Sub-componentRemoting
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker500034684
Fix commit069cd8bd0ee6 (chromium/src) +28/-0
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:A/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-06 (per vendor advisory)
Fix landed2026-04-10 (public git)
Disclosed2026-04-28
Patch gap18 days — fix public in source before the advisory

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/DemuxerStream objects (audio_stream_, video_stream_) the renderer pipeline reads from.
`RPC_ACQUIRE_DEMUXER` / `OnAcquireDemuxer`
the RPC message (and its handler) that tells StreamProvider to create the demuxer streams from the handles in acquire_demuxer_rpc().
`DemuxerStream` raw pointers
the renderer stack caches raw DemuxerStream* pointers (e.g. via GetAllStreams()); 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.

Key insight
The handler treated an idempotent acquisition as repeatable, destroying and recreating live stream objects on a duplicate message; adding an if (audio_stream_ || video_stream_) return; guard makes acquisition one-shot so existing pointers stay valid.

Attack Path

  1. 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 first RPC_ACQUIRE_DEMUXER.
  2. Cache raw stream pointers The renderer pipeline obtains and retains raw DemuxerStream* pointers to the acquired streams (as the regression test does via GetAllStreams()).
  3. Send a duplicate acquire A second RPC_ACQUIRE_DEMUXER arrives; the old OnAcquireDemuxer rebuilds audio_stream_/video_stream_, freeing the original stream objects while the cached pointers remain.
  4. Trigger playback on a freed stream Playback performs a Read (or similar) through a cached DemuxerStream*, dereferencing freed memory and causing the use-after-free (inferred: downstream exploitation beyond the stale dereference is not shown in the diff).

Impact Assessment

An attacker positioned to deliver RPC messages to the media-remoting 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

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

Audit Directions

  • Non-idempotent RPC/IPC handlers
    Audit 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 recreation
    Look 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 lifecycle
    Review other media/remoting handlers (demuxer, renderer, flush/initialize) for the same “acquire/initialize only once” assumption not being enforced against duplicate or out-of-order RPCs.
From 069cd8bd0ee67af4065e59b5b8cd7dcf6ba5810f Mon Sep 17 00:00:00 2001
From: Jordan Bayles <[email protected]>
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 <[email protected]>
Reviewed-by: Frank Liberato <[email protected]>
Reviewed-by: Muyao Xu <[email protected]>
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 [email protected]

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