CVE-2026-113158
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmedia/filters/demuxer_manager.cc |
modified | |
ifmedia/remoting/stream_provider.cc |
modified | |
switchmedia/remoting/stream_provider.cc |
modified | |
GetContainerForMetricsmedia/remoting/stream_provider.cc |
modified |
Files Changed
media/remoting/stream_provider_unittest.ccmedia/remoting/end2end_test_renderer.hmedia/filters/demuxer_manager.ccmedia/remoting/stream_provider.ccmedia/remoting/stream_provider.h
Patch
From b481ca9cbae92b05fafba2de34265df301edd4b2 Mon Sep 17 00:00:00 2001 From: mark a. foltz <[email protected]> Date: Tue, 15 Sep 2026 18:35:15 -0700 Subject: [PATCH] [remoting] Improve threading semantics for StreamProvider StreamProvider owns MediaStream instances which bind Mojo endpoints and WeakPtrs to the media task runner. When DemuxerManager was destroyed on the main thread (e.g. during player teardown), it destroyed StreamProvider inline on the main thread instead of dispatching to media_task_runner_. This led to Mojo sequence check failures and UAFs. This CL: - Updates DemuxerManager to delete StreamProvider (kStreamProviderDemuxer) on the media task runner via DeleteSoon(). - Enforces via CHECK that StreamProvider is deleted on the media task runner (or main thread in single-threaded test environments). - Ensures StreamProvider::Stop() invalidates weak pointers and releases media resources (audio/video streams) immediately, and is called from the destructor if not already stopped. - Uses base::SequenceChecker in StreamProvider and MediaStream to verify calls on the main and media sequences. - Documents the threading model in StreamProvider. - Adds unit tests verifying Stop() resource cleanup and media-sequence deletion. - Uses a base::SequenceChecker throughout to validate threading assumptions. - Improves naming of weak pointer factories. Fixed: 553157579 Change-Id: I70d851199116d69a4400082946b0e1644a18ad4e Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8394446 Reviewed-by: Jordan Bayles <[email protected]> Commit-Queue: Jordan Bayles <[email protected]> Cr-Commit-Position: refs/heads/main@{#1698580} --- diff --git a/media/filters/demuxer_manager.cc b/media/filters/demuxer_manager.cc index 7401dbf..bf56240 100644 --- a/media/filters/demuxer_manager.cc +++ b/media/filters/demuxer_manager.cc @@ -80,10 +80,19 @@ } DemuxerManager::~DemuxerManager() { - // ManifestDemuxer and FrameInjectingDemuxer have multiple outstanding weak - // pointers bound to the media thread, and need to be deleted there. + if (demuxer_override_) { + DemuxerType type = demuxer_override_->GetDemuxerType(); + if (type == DemuxerType::kStreamProviderDemuxer || + type == DemuxerType::kFrameInjectingDemuxer) { + media_task_runner_->DeleteSoon(FROM_HERE, std::move(demuxer_override_)); + } + } + // ManifestDemuxer, FrameInjectingDemuxer and StreamProvider have multiple + // outstanding weak pointers bound to the media thread, and need to be deleted + // there. if (GetDemuxerType() == DemuxerType::kManifestDemuxer || - GetDemuxerType() == DemuxerType::kFrameInjectingDemuxer) { + GetDemuxerType() == DemuxerType::kFrameInjectingDemuxer || + GetDemuxerType() == DemuxerType::kStreamProviderDemuxer) { media_task_runner_->DeleteSoon(FROM_HERE, std::move(demuxer_)); } } diff --git a/media/remoting/end2end_test_renderer.h b/media/remoting/end2end_test_renderer.h index f70c300..690369a 100644 --- a/media/remoting/end2end_test_renderer.h +++ b/media/remoting/end2end_test_renderer.h @@ -88,8 +88,10 @@ // Receiver components. std::unique_ptr<TestRemotee> media_remotee_; raw_ptr<ReceiverController> receiver_controller_; - std::unique_ptr<Receiver> receiver_; + // `stream_provider_` must be declared before `receiver_` so that `receiver_` + // (which holds a raw_ptr to `stream_provider_`) is destroyed first. std::unique_ptr<StreamProvider> stream_provider_; + std::unique_ptr<Receiver> receiver_; raw_ptr<openscreen::cast::RpcMessenger> receiver_rpc_messenger_; // Handle of |receiver_| diff --git a/media/remoting/stream_provider.cc b/media/remoting/stream_provider.cc index 49ea1d76..14d4e52b 100644 --- a/media/remoting/stream_provider.cc +++ b/media/remoting/stream_provider.cc @@ -64,8 +64,10 @@ remote_handle_(remote_handle), rpc_handle_(rpc_messenger_->GetUniqueHandle()) { DCHECK(remote_handle_ != RpcMessenger::kInvalidHandle); + DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_checker_); + DETACH_FROM_SEQUENCE(media_sequence_checker_); - media_weak_this_ = media_weak_factory_.GetWeakPtr(); + media_weak_this_ = media_stream_weak_factory_.GetWeakPtr(); auto receive_callback = base::BindPostTask( media_task_runner_, @@ -78,23 +80,23 @@ } StreamProvider::MediaStream::~MediaStream() { - DCHECK(main_task_runner_->BelongsToCurrentThread()); + DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_checker_); rpc_messenger_->UnregisterMessageReceiverCallback(rpc_handle_); } void StreamProvider::MediaStream::Destroy() { - DCHECK(media_task_runner_->RunsTasksInCurrentSequence()); + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); - // Invalid weak pointers to prevent |this| from receiving RPC calls on the + // Invalid weak pointers to prevent `this` from receiving RPC calls on the // media thread. - media_weak_factory_.InvalidateWeakPtrs(); + media_stream_weak_factory_.InvalidateWeakPtrs(); // Unbind all mojo pipes and bindings. receiver_.reset(); decoder_buffer_reader_.reset(); - // After invalidating all weak ptrs of |media_weak_factory_|, MediaStream - // won't be access anymore, so using |this| here is safe. + // After invalidating all weak ptrs of `media_stream_weak_factory_`, + // MediaStream won't be access anymore, so using `this` here is safe. main_task_runner_->DeleteSoon(FROM_HERE, this); } @@ -109,7 +111,7 @@ void StreamProvider::MediaStream::Initialize( base::OnceClosure init_done_callback) { - DCHECK(media_task_runner_->RunsTasksInCurrentSequence()); + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); DCHECK(init_done_callback); if (init_done_callback_) { @@ -128,7 +130,7 @@ void StreamProvider::MediaStream::InitializeDataPipe( mojo::ScopedDataPipeConsumerHandle data_pipe) { - DCHECK(media_task_runner_->RunsTasksInCurrentSequence()); + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); decoder_buffer_reader_ = std::make_unique<MojoDecoderBufferReader>(std::move(data_pipe)); @@ -137,7 +139,7 @@ void StreamProvider::MediaStream::ReceiveFrame(uint32_t count, mojom::DecoderBufferPtr buffer) { - DCHECK(media_task_runner_->RunsTasksInCurrentSequence()); + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); DCHECK(decoder_buffer_reader_); auto callback = base::BindPostTaskToCurrentDefault( @@ -147,7 +149,7 @@ } void StreamProvider::MediaStream::FlushUntil(uint32_t count) { - DCHECK(media_task_runner_->RunsTasksInCurrentSequence()); + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); if (count < current_frame_count_) return; @@ -170,7 +172,7 @@ void StreamProvider::MediaStream::OnReceivedRpc( std::unique_ptr<openscreen::cast::RpcMessage> message) { - DCHECK(media_task_runner_->RunsTasksInCurrentSequence()); + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); DCHECK(message->handle() == rpc_handle_); switch (message->proc()) { @@ -187,7 +189,7 @@ void StreamProvider::MediaStream::OnInitializeCallback( std::unique_ptr<openscreen::cast::RpcMessage> message) { - DCHECK(media_task_runner_->RunsTasksInCurrentSequence()); + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); const openscreen::cast::DemuxerStreamInitializeCallback callback_message = message->demuxerstream_initializecb_rpc(); if (callback_message.type() != type_) { @@ -233,7 +235,7 @@ } void StreamProvider::MediaStream::CompleteInitialize() { - DCHECK(media_task_runner_->RunsTasksInCurrentSequence()); + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); // Initialization finished when received RPC_DS_INITIALIZE_CALLBACK and // |decoder_buffer_reader_| is created. @@ -250,7 +252,7 @@ void StreamProvider::MediaStream::OnReadUntilCallback( std::unique_ptr<openscreen::cast::RpcMessage> message) { - DCHECK(media_task_runner_->RunsTasksInCurrentSequence()); + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); if (!read_until_sent_) { OnError("Unexpected ReadUntilCallback"); @@ -326,7 +328,7 @@ // Only return one buffer at a time so we ignore the count. void StreamProvider::MediaStream::Read(uint32_t /*count*/, ReadCB read_cb) { - DCHECK(media_task_runner_->RunsTasksInCurrentSequence()); + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); DCHECK(read_complete_callback_.is_null()); DCHECK(read_cb); @@ -347,7 +349,7 @@ } void StreamProvider::MediaStream::CompleteRead(DemuxerStream::Status status) { - DCHECK(media_task_runner_->RunsTasksInCurrentSequence()); + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); switch (status) { case DemuxerStream::kConfigChanged: @@ -378,11 +380,13 @@ } AudioDecoderConfig StreamProvider::MediaStream::audio_decoder_config() { + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); DCHECK(type_ == DemuxerStream::AUDIO); return audio_decoder_config_; } VideoDecoderConfig StreamProvider::MediaStream::video_decoder_config() { + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); DCHECK(type_ == DemuxerStream::VIDEO); return video_decoder_config_; } @@ -402,7 +406,7 @@ void StreamProvider::MediaStream::AppendBuffer( uint32_t count, scoped_refptr<DecoderBuffer> buffer) { - DCHECK(media_task_runner_->RunsTasksInCurrentSequence()); + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); // Drop flushed frame. if (count < current_frame_count_) @@ -434,8 +438,10 @@ rpc_messenger_(receiver_controller_->rpc_messenger()) { DCHECK(receiver_controller_); DCHECK(rpc_messenger_); + DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_checker_); + DETACH_FROM_SEQUENCE(media_sequence_checker_); - media_weak_this_ = media_weak_factory_.GetWeakPtr(); + media_weak_this_ = stream_provider_weak_factory_.GetWeakPtr(); auto callback = base::BindPostTask( media_task_runner_, @@ -449,9 +455,30 @@ } StreamProvider::~StreamProvider() { - DCHECK(main_task_runner_->BelongsToCurrentThread()); - rpc_messenger_->UnregisterMessageReceiverCallback( - RpcMessenger::kAcquireDemuxerHandle); + // Must be destroyed on `media_task_runner_` (or `main_task_runner_` in + // single-threaded tests). + CHECK(media_task_runner_->RunsTasksInCurrentSequence() && + (!main_task_runner_->BelongsToCurrentThread() || + main_task_runner_ == media_task_runner_)); + + if (!stopped_) { + Stop(); + } + + if (rpc_messenger_) { + if (main_task_runner_->BelongsToCurrentThread()) { + rpc_messenger_->UnregisterMessageReceiverCallback( + RpcMessenger::kAcquireDemuxerHandle); + } else { + // |rpc_messenger_| is owned by |receiver_controller_| which is a + // singleton per process, so it's safe to use Unretained() here. + main_task_runner_->PostTask( + FROM_HERE, base::BindOnce(&openscreen::cast::RpcMessenger:: + UnregisterMessageReceiverCallback, + base::Unretained(rpc_messenger_), + RpcMessenger::kAcquireDemuxerHandle)); + } + } } std::string StreamProvider::GetDisplayName() const { @@ -464,43 +491,70 @@ void StreamProvider::Initialize(DemuxerHost* host, PipelineStatusCallback status_cb) { - DCHECK(media_task_runner_->RunsTasksInCurrentSequence()); + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); init_done_callback_ = std::move(status_cb); CompleteInitialize(); } -void StreamProvider::AbortPendingReads() {} +void StreamProvider::AbortPendingReads() { + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); +} -void StreamProvider::StartWaitingForSeek(base::TimeDelta seek_time) {} +void StreamProvider::StartWaitingForSeek(base::TimeDelta seek_time) { + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); +} -void StreamProvider::CancelPendingSeek(base::TimeDelta seek_time) {} +void StreamProvider::CancelPendingSeek(base::TimeDelta seek_time) { + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); +} void StreamProvider::Seek(base::TimeDelta time, PipelineStatusCallback seek_cb) { + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); media_task_runner_->PostTask(FROM_HERE, base::BindOnce(std::move(seek_cb), PIPELINE_OK)); } bool StreamProvider::IsSeekable() const { + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); return false; } -void StreamProvider::Stop() {} +void StreamProvider::Stop() { + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); + if (stopped_) { + return; + } + stopped_ = true; + + if (init_done_callback_) { + std::move(init_done_callback_).Run(PIPELINE_ERROR_ABORT); + } + + stream_provider_weak_factory_.InvalidateWeakPtrs(); + + audio_stream_.reset(); + video_stream_.reset(); +} base::TimeDelta StreamProvider::GetStartTime() const { + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); return base::TimeDelta(); } base::Time StreamProvider::GetTimelineOffset() const { + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); return base::Time(); } int64_t StreamProvider::GetMemoryUsage() const { + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); return 0; } std::optional<container_names::MediaContainerName> StreamProvider::GetContainerForMetrics() const { + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); return std::optional<container_names::MediaContainerName>(); } @@ -508,30 +562,14 @@ std::optional<MediaTrack::Id> track_id, base::TimeDelta curr_time, TrackChangeCB change_completed_cb) { + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); std::move(change_completed_cb).Run(nullptr); DVLOG(1) << "Track changes are not supported."; } -void StreamProvider::Destroy() { - DCHECK(media_task_runner_->RunsTasksInCurrentSequence()); - - if (init_done_callback_) - std::move(init_done_callback_).Run(PIPELINE_ERROR_ABORT); - - // Invalid weak pointers to prevent |this| from receiving RPC calls on the - // media thread. - media_weak_factory_.InvalidateWeakPtrs(); - - audio_stream_.reset(); - video_stream_.reset(); - - // After invalidating all weak ptrs of |media_weak_factory_|, StreamProvider - // won't be access anymore, so using |this| here is safe. - main_task_runner_->DeleteSoon(FROM_HERE, this); -} - void StreamProvider::OnReceivedRpc( std::unique_ptr<openscreen::cast::RpcMessage> message) { + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); switch (message->proc()) { case openscreen::cast::RpcMessage::RPC_ACQUIRE_DEMUXER: OnAcquireDemuxer(std::move(message)); @@ -543,7 +581,10 @@ void StreamProvider::OnAcquireDemuxer( std::unique_ptr<openscreen::cast::RpcMessage> message) { - DCHECK(media_task_runner_->RunsTasksInCurrentSequence()); + DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_); + if (stopped_) { + return; ... (file diff truncated)
Regression Test / PoC
diff --git a/media/remoting/stream_provider_unittest.cc b/media/remoting/stream_provider_unittest.cc
index 84d0cff..9d125289 100644
--- a/media/remoting/stream_provider_unittest.cc
+++ b/media/remoting/stream_provider_unittest.cc
@@ -7,7 +7,9 @@
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
+#include "base/run_loop.h"
#include "base/task/single_thread_task_runner.h"
+#include "base/test/run_until.h"
#include "base/test/task_environment.h"
#include "media/base/audio_decoder_config.h"
#include "media/base/demuxer_stream.h"
@@ -355,5 +357,51 @@
task_environment_.RunUntilIdle();
}
+TEST_F(StreamProviderTest, StopReleasesResourcesAndInvalidatesCallbacks) {
+ InitializeDemuxer();
+ SendRpcAcquireDemuxer();
+ EXPECT_TRUE(
+ base::test::RunUntil([&]() { return stream_provider_initialized_; }));
+ EXPECT_EQ(2u, stream_provider_->GetAllStreams().size());
+
+ stream_provider_->Stop();
+ // Clear unowned pointers in the test fixture since the streams were released.
+ audio_stream_ = nullptr;
+ video_stream_ = nullptr;
+ // Streams should be released immediately.
+ EXPECT_TRUE(stream_provider_->GetAllStreams().empty());
+
+ // Further RPC messages should be ignored.
+ SendRpcAcquireDemuxer();
+ base::RunLoop run_loop;
+ base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+ FROM_HERE, run_loop.QuitClosure());
+ run_loop.Run();
+ EXPECT_TRUE(stream_provider_->GetAllStreams().empty());
+
+ // Calling Stop() again should be a safe no-op.
+ stream_provider_->Stop();
+}
+
+TEST_F(StreamProviderTest, StopAbortsPendingInitialize) {
+ PipelineStatus status = PIPELINE_OK;
+ stream_provider_->Initialize(
+ nullptr, base::BindOnce([](PipelineStatus* out_status,
+ PipelineStatus s) { *out_status = s; },
+ &status));
+ stream_provider_->Stop();
+ EXPECT_EQ(PIPELINE_ERROR_ABORT, status);
+}
+
+TEST_F(StreamProviderTest, DestructorCallsStopIfNotStopped) {
+ PipelineStatus status = PIPELINE_OK;
+ stream_provider_->Initialize(
+ nullptr, base::BindOnce([](PipelineStatus* out_status,
+ PipelineStatus s) { *out_status = s; },
+ &status));
+ stream_provider_.reset();
+ EXPECT_EQ(PIPELINE_ERROR_ABORT, status);
+}
+
} // namespace remoting
} // namespace media
Original Bug Report
UAF in Chromecast renderer due to StreamProvider custom deleter lost on Demuxer upcast
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: In the Chromecast media-remoting receiver, converting StreamProvider to std::unique_ptr<Demuxer> discards its specialized deleter that routes destruction through Destroy() on the media thread. Because DemuxerManager deletes the demuxer on the main thread and StreamProvider::Stop() is a no-op, StreamProvider and its MediaStream instances are destructed on the main thread during playback teardown. This causes cross-sequence WeakPtr race conditions and improper-sequence Mojo receiver resets, potentially causing use-after-free reads and writes on the media thread.
Affected files:
media/filters/demuxer_manager.ccchromecast/renderer/cast_content_renderer_client.ccmedia/remoting/stream_provider.ccmedia/remoting/stream_provider.h
Estimated timestamp from git blame: 2020-06-10
Summary
There is a potential Use-After-Free (UAF) read and write vulnerability in the Chromecast (cast_shell) renderer process during media-remoting teardown. The demuxer used for media-remoting receivers (media::remoting::StreamProvider) relies on a specialized std::default_delete to route destruction to Destroy() on the media thread. When StreamProvider is created and returned as std::unique_ptr<Demuxer>, this specialized deleter is lost, leading to main-thread inline destruction and cross-thread race conditions.
Root Cause Analysis
-
Loss of Custom Deleter on Pointer Upcast:
media::remoting::StreamProviderdefines a specializedstd::default_delete(media/remoting/stream_provider.h:274-284,media/remoting/stream_provider.cc:654-657) that routes destruction throughStreamProvider::Destroy()onmedia_task_runner_(media/remoting/stream_provider.cc:515-531). However,CastContentRendererClient::OverrideDemuxerForUrlcreates and returns the demuxer upcast tostd::unique_ptr<::media::Demuxer>:// chromecast/renderer/cast_content_renderer_client.cc:292-304 std::unique_ptr<::media::Demuxer> CastContentRendererClient::OverrideDemuxerForUrl(...) { if (render_frame->GetRenderFrameMediaPlaybackOptions() .is_remoting_renderer_enabled() && url.SchemeIs(::media::remoting::kRemotingScheme)) { return std::make_unique<::media::remoting::StreamProvider>( ::media::remoting::ReceiverController::GetInstance(), task_runner); } return nullptr; }The upcast to
std::unique_ptr<Demuxer>replacesdefault_delete<StreamProvider>with standarddefault_delete<Demuxer>(third_party/libc++/src/include/__memory/unique_ptr.h:68-69). BecauseDemuxer::~Demuxer()is public virtual (media/base/demuxer.h:90), standarddeleteis used, andStreamProvider::Destroy()is never invoked. -
Missing Media-Thread Dispatch in DemuxerManager: During
WebMediaPlayerImplteardown,DestructionHelper(third_party/blink/renderer/platform/media/web_media_player_impl.cc:262-266) postsDemuxerManagerto the main thread viaDeleteSoon. InDemuxerManager::~DemuxerManager()(media/filters/demuxer_manager.cc:82-89), onlykManifestDemuxerandkFrameInjectingDemuxerare dispatched tomedia_task_runner_->DeleteSoon:kStreamProviderDemuxeris omitted. Consequently,demuxer_is destructed directly on the main thread. -
No-Op Pipeline Stop Quiescence: Unlike
FrameInjectingDemuxer::Stop()(components/cast_streaming/renderer/frame/frame_injecting_demuxer.cc:450-461),StreamProvider::Stop()is an empty stub (media/remoting/stream_provider.cc:488). CallingStop()during teardown does not invalidate weak pointers or unbind streams. -
Cross-Sequence Invalidation and Mojo Reset Races: When
~StreamProvider()runs on the main thread:media_weak_factory_destructor invalidates weak pointers cross-sequence (base/memory/weak_ptr.cc:22-34). In release builds,Flag::Invalidate()is a bare atomic set without thread checks.audio_stream_andvideo_stream_runMediaStream::Destroy()on the main thread (media/remoting/stream_provider.cc:85-99), resettingreceiver_(mojo::Receiver<RemotingDataStreamReceiver>, which was bound to the media sequence) and posting aDeleteSoonto freeMediaStreamon the main thread.- Concurrently, the media thread receives in-flight RPCs or frame data (
ReceiveFrame->AppendBufferinstream_provider.cc:402-419/FlushUntilin:149-169). Tasks that pass the weak-pointer gate before invalidation or Mojo dispatches using rawthisexecute on the deallocatedMediaStreamobject, resulting in UAF reads and writes (such asbuffers_.push_back(std::move(buffer))into freed memory).
Mitigations Checked
- MiraclePtr:
base::WeakPtr::ptr_is explicitly markedRAW_PTR_EXCLUSION(base/memory/weak_ptr.h:311), and Mojo receiver dispatch uses raw object pointers. MiraclePtr does not mitigate this issue.
Potential Trigger Steps
(Note: These are suggested/potential steps derived from source code analysis; our automated tooling does not currently have the capability to execute live code against hardware).
- Establish a Cast media-remoting session to a Chromecast device from a LAN Cast-sender peer.
- Have the receiver load a media-remoting URL (
media-remoting:), causingCastContentRendererClient::OverrideDemuxerForUrlto instantiate and upcastStreamProvider. - Send
RPC_ACQUIRE_DEMUXERand completeRPC_DS_INITIALIZEto bind audio/videoMediaStreamMojo endpoints on the renderer media thread. - Stream media frames continuously so
ReceiveFrameand data-pipe read tasks are regularly dispatched on the media thread. - While sending a high-frequency stream of RPC messages and media frames, trigger playback teardown (e.g., terminate the session or remove the
<video>element). - When
DemuxerManagerdestructs on the main thread, the race window opens: in-flightReceiveFrame/AppendBuffer/ RPC tasks on the media thread can execute on the freedMediaStream/StreamProviderinstance.
Suggested Fix
- Implement
StreamProvider::Stop()on the media thread to invalidatemedia_weak_factory_and resetaudio_stream_/video_stream_before teardown, mirroringFrameInjectingDemuxer::Stop(). - Ensure
StreamProviderdestruction is properly routed to the media thread (or wrap the demuxer inOverrideDemuxerForUrlwith an appropriate deleter forwarding mechanism), and addDemuxerType::kStreamProviderDemuxertoDemuxerManager::~DemuxerManager()’sDeleteSoondispatch if applicable.
Evaluated with Chrome root at commit: c6718ebbf2c677c6f63691e28c7801c563f6b19c
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.