Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Streaming
DescriptionUse after free in Streaming
ComponentStreaming
Sub-componentRemoting
Bug ClassUAF
Tracker499869964
Fix commit02510482e9b1 (chromium/src) +190/-8
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
EXPECT_TRUE
media/remoting/receiver_unittest.cc
modified
TEST_F
media/remoting/receiver_unittest.cc
modified

Files Changed

  • media/remoting/receiver.cc
  • media/remoting/receiver.h
  • media/remoting/receiver_unittest.cc
From 02510482e9b1f256cc7ecc6cb3bb6037dd4bcc7e Mon Sep 17 00:00:00 2001
From: Jordan Bayles <jophba@chromium.org>
Date: Thu, 24 Sep 2026 10:34:14 -0700
Subject: [PATCH] [Remoting] Fix potential UaF in RpcMessenger unregistration

media::remoting::Receiver is constructed on the main thread, where it
registers its RPC callback with openscreen::cast::RpcMessenger, but is
destroyed on the media thread. Its destructor invokes
UnregisterMessageReceiverCallback on the media thread, causing a
cross-thread data race and potential UaF.

This patch resolves this with the following changes:

1. Fix Receiver::~Receiver() to post UnregisterMessageReceiverCallback
   to the main_task_runner_ (matching SendRpcMessageOnMainThread and
   CourierRenderer::DeregisterFromRpcMessaging).

2. Replace sequence checks with
   SEQUENCE_CHECKER(media_sequence_checker_), modernizing this area of
   the code and cleaning it up.

3. Adds some unit tests with a test media thread to ensure the threading
   for this class behaves properly.

Bug: 499869964
Change-Id: Ia22a1a6001a514aefbdc89802531acea0ec5e5aa
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8400832
Reviewed-by: Mark Foltz <mfoltz@chromium.org>
Commit-Queue: Jordan Bayles <jophba@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1704397}
---
diff --git a/media/remoting/receiver.cc b/media/remoting/receiver.cc
index 8e3ed2f..524b0145 100644
--- a/media/remoting/receiver.cc
+++ b/media/remoting/receiver.cc
@@ -56,6 +56,7 @@
   // Note: The constructor is running on the main thread, but will be destroyed
   // on the media thread. Therefore, all weak pointers must be dereferenced on
   // the media thread.
+  DETACH_FROM_SEQUENCE(media_sequence_checker_);
   auto receive_callback = base::BindPostTask(
       media_task_runner_,
       BindRepeating(&Receiver::OnReceivedRpc, weak_factory_.GetWeakPtr()));
@@ -71,7 +72,16 @@
 }
 
 Receiver::~Receiver() {
-  rpc_messenger_->UnregisterMessageReceiverCallback(rpc_handle_);
+  DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_);
+  // |rpc_messenger_| is owned by |receiver_controller_| which is a singleton
+  // per process, so it's safe to use Unretained() here. Unregistering from the
+  // messenger is posted to |main_task_runner_|; since the RPC callback is bound
+  // with a weak pointer to `this` on |media_task_runner_|, messages that arrive
+  // after destruction are also safely dropped.
+  main_task_runner_->PostTask(
+      FROM_HERE,
+      base::BindOnce(&RpcMessenger::UnregisterMessageReceiverCallback,
+                     base::Unretained(rpc_messenger_), rpc_handle_));
 }
 
 // Receiver::Initialize() will be called by the local pipeline, it would only
@@ -80,6 +90,7 @@
 void Receiver::Initialize(MediaResource* media_resource,
                           RendererClient* client,
                           PipelineStatusCallback init_cb) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_);
   demuxer_ = media_resource;
   init_cb_ = std::move(init_cb);
   ShouldInitializeRenderer();
@@ -125,13 +136,14 @@
 
 void Receiver::OnReceivedRpc(
     std::unique_ptr<openscreen::cast::RpcMessage> message) {
-  DCHECK(main_task_runner_->RunsTasksInCurrentSequence());
+  DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_);
   DCHECK(message);
 
   media::cast::DispatchRendererRpcCall(message.get(), this);
 }
 
 void Receiver::SetRemoteHandle(int remote_handle) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_);
   DCHECK_NE(remote_handle, RpcMessenger::kInvalidHandle);
   DCHECK_EQ(remote_handle_, RpcMessenger::kInvalidHandle);
   remote_handle_ = remote_handle;
@@ -161,7 +173,7 @@
   if (!rpc_initialize_received_ || !init_cb_)
     return;
 
-  DCHECK(main_task_runner_->RunsTasksInCurrentSequence());
+  DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_);
   DCHECK(renderer_);
   DCHECK(demuxer_);
   renderer_->Initialize(demuxer_, this,
@@ -170,7 +182,7 @@
 }
 
 void Receiver::OnRendererInitialized(PipelineStatus status) {
-  DCHECK(main_task_runner_->RunsTasksInCurrentSequence());
+  DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_);
   DCHECK(init_cb_);
   std::move(init_cb_).Run(status);
 
@@ -181,7 +193,7 @@
 }
 
 void Receiver::OnRpcSetPlaybackRate(double playback_rate) {
-  DCHECK(main_task_runner_->RunsTasksInCurrentSequence());
+  DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_);
 
   renderer_->SetPlaybackRate(playback_rate);
 
@@ -198,7 +210,7 @@
 }
 
 void Receiver::OnRpcFlush(uint32_t audio_count, uint32_t video_count) {
-  DCHECK(main_task_runner_->RunsTasksInCurrentSequence());
+  DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_);
 
   receiver_controller_->OnRendererFlush(audio_count, video_count);
 
@@ -214,7 +226,7 @@
 }
 
 void Receiver::OnRpcStartPlayingFrom(base::TimeDelta time) {
-  DCHECK(main_task_runner_->RunsTasksInCurrentSequence());
+  DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_);
 
   renderer_->StartPlayingFrom(time);
   ScheduleMediaTimeUpdates();
@@ -230,11 +242,12 @@
 }
 
 void Receiver::OnRpcSetVolume(double volume) {
-  DCHECK(main_task_runner_->RunsTasksInCurrentSequence());
+  DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_);
   renderer_->SetVolume(volume);
 }
 
 void Receiver::SendMediaTimeUpdate() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_);
   // Issues RPC_RC_ONTIMEUPDATE RPC message.
   auto rpc =
       media::cast::CreateMessageForMediaTimeUpdate(renderer_->GetMediaTime());
diff --git a/media/remoting/receiver.h b/media/remoting/receiver.h
index c42b4e3..d1b21dc6 100644
--- a/media/remoting/receiver.h
+++ b/media/remoting/receiver.h
@@ -11,6 +11,7 @@
 #include "base/memory/raw_ptr.h"
 #include "base/memory/scoped_refptr.h"
 #include "base/memory/weak_ptr.h"
+#include "base/sequence_checker.h"
 #include "base/task/sequenced_task_runner.h"
 #include "base/task/single_thread_task_runner.h"
 #include "base/timer/timer.h"
@@ -152,6 +153,8 @@
   // The timer to periodically update the media time.
   base::RepeatingTimer time_update_timer_;
 
+  SEQUENCE_CHECKER(media_sequence_checker_);
+
   base::WeakPtrFactory<Receiver> weak_factory_{this};
 };
 
diff --git a/media/remoting/receiver_unittest.cc b/media/remoting/receiver_unittest.cc
index 2cc4c6b..9674684f 100644
--- a/media/remoting/receiver_unittest.cc
+++ b/media/remoting/receiver_unittest.cc
@@ -8,10 +8,14 @@
 #include <utility>
 
 #include "base/check.h"
+#include "base/functional/callback_helpers.h"
 #include "base/memory/raw_ptr.h"
+#include "base/run_loop.h"
 #include "base/task/single_thread_task_runner.h"
 #include "base/test/gmock_callback_support.h"
+#include "base/test/run_until.h"
 #include "base/test/task_environment.h"
+#include "base/threading/thread.h"
 #include "media/base/audio_decoder_config.h"
 #include "media/base/media_util.h"
 #include "media/base/mock_filters.h"
@@ -237,6 +241,11 @@
   }
 
   void TearDown() override {
+    mock_renderer_ = nullptr;
+    receiver_.reset();
+    EXPECT_TRUE(base::test::RunUntil([&]() {
+      return !rpc_messenger_->IsRegisteredForTesting(receiver_renderer_handle_);
+    }));
     rpc_messenger_->UnregisterMessageReceiverCallback(
         RpcMessenger::kAcquireRendererHandle);
   }
@@ -325,6 +334,163 @@
   task_environment_.RunUntilIdle();
 }
 
+// Receiver is constructed on the main sequence but, as a media::Renderer,
+// processes RPC messages and is destroyed on the media sequence. The
+// RpcMessenger it registers with is only safe to use from the main sequence,
+// so unregistration must hop back there.
+TEST_F(ReceiverTest, UnregistersOnMainSequenceWhenDestroyedOnMediaSequence) {
+  base::Thread media_thread("TestMediaThread");
+  ASSERT_TRUE(media_thread.Start());
+
+  auto renderer = std::make_unique<NiceMock<MockRenderer>>();
+  MockRenderer* mock_renderer = renderer.get();
+  EXPECT_CALL(*mock_renderer, SetVolume(0.5f))
+      .WillOnce([&media_thread](float volume) {
+        EXPECT_TRUE(media_thread.task_runner()->RunsTasksInCurrentSequence());
+      });
+
+  auto receiver = std::make_unique<Receiver>(
+      receiver_renderer_handle_, sender_renderer_handle_, mock_controller_,
+      media_thread.task_runner(), std::move(renderer),
+      base::BindOnce(&ReceiverTest::OnAcquireRendererDone,
+                     weak_factory_.GetWeakPtr()));
+  EXPECT_TRUE(
+      rpc_messenger_->IsRegisteredForTesting(receiver_renderer_handle_));
+
+  // Verify that RPC messages received on the main thread are dispatched and
+  // handled on the media thread without sequence DCHECK failures.
+  openscreen::cast::RpcMessage rpc;
+  rpc.set_handle(receiver_renderer_handle_);
+  rpc.set_proc(openscreen::cast::RpcMessage::RPC_R_SETVOLUME);
+  rpc.set_double_value(0.5);
+  rpc_messenger_->ProcessMessageFromRemote(
+      std::make_unique<openscreen::cast::RpcMessage>(rpc));
+  media_thread.FlushForTesting();
+
+  // Destroy the Receiver on the media sequence, as the production pipeline
+  // does, and wait for destruction to complete.
+  media_thread.task_runner()->DeleteSoon(FROM_HERE, std::move(receiver));
+  media_thread.FlushForTesting();
+
+  // The handle must still be registered: only the main sequence may mutate the
+  // messenger, and no main-sequence tasks have run yet.
+  EXPECT_TRUE(
+      rpc_messenger_->IsRegisteredForTesting(receiver_renderer_handle_));
+
+  // Send an RPC message on the main sequence post-destruction. The message will
+  // be forwarded to the media sequence after Receiver is destroyed, where
+  // WeakPtr drops the invocation safely without crashing or UAF.
+  openscreen::cast::RpcMessage rpc_post_destruction;
+  rpc_post_destruction.set_handle(receiver_renderer_handle_);
+  rpc_post_destruction.set_proc(openscreen::cast::RpcMessage::RPC_R_SETVOLUME);
+  rpc_post_destruction.set_double_value(0.8);
+  rpc_messenger_->ProcessMessageFromRemote(
+      std::make_unique<openscreen::cast::RpcMessage>(rpc_post_destruction));
+  media_thread.FlushForTesting();
+
+  EXPECT_TRUE(base::test::RunUntil([&]() {
+    return !rpc_messenger_->IsRegisteredForTesting(receiver_renderer_handle_);
+  }));
+}
+
+// Verifies end-to-end multi-threaded RPC message dispatch from the main
+// sequence to the media sequence, renderer method execution on the media
+// sequence, and response RPC messages sent back on the main sequence.
+TEST_F(ReceiverTest, MultiThreadedRpcDispatchAndRendererCalls) {
+  base::Thread media_thread("TestMediaThread");
+  ASSERT_TRUE(media_thread.Start());
+
+  mock_sender_->SendRpcAcquireRenderer();
+  EXPECT_TRUE(base::test::RunUntil([&]() {
+    return sender_renderer_handle_ != RpcMessenger::kInvalidHandle;
+  }));
+
+  auto renderer = std::make_unique<NiceMock<MockRenderer>>();
+  MockRenderer* mock_renderer = renderer.get();
+
+  EXPECT_CALL(*mock_sender_, AcquireRendererDone()).Times(1);
+  auto receiver = std::make_unique<Receiver>(
+      receiver_renderer_handle_, sender_renderer_handle_, mock_controller_,
+      media_thread.task_runner(), std::move(renderer),
+      base::BindOnce(&ReceiverTest::OnAcquireRendererDone,
+                     weak_factory_.GetWeakPtr()));
+
+  // Call Initialize on the media sequence (as PipelineImpl does in production).
+  EXPECT_CALL(*mock_renderer,
+              OnInitialize(&mock_media_resource_, testing::NotNull(), _))
+      .WillOnce([&media_thread](MediaResource* media_resource,
+                                RendererClient* client,
+                                PipelineStatusCallback& init_cb) {
+        EXPECT_TRUE(media_thread.task_runner()->RunsTasksInCurrentSequence());
+        std::move(init_cb).Run(PIPELINE_OK);
+      });
+  media_thread.task_runner()->PostTask(
+      FROM_HERE,
+      base::BindOnce(&Receiver::Initialize, base::Unretained(receiver.get()),
+                     &mock_media_resource_, receiver.get(), base::DoNothing()));
+
+  // Send RPC_R_INITIALIZE on main sequence and wait for response on main
+  // sequence.
+  {
+    base::RunLoop run_loop;
+    EXPECT_CALL(*mock_sender_, InitializeCallback(true))
+        .WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
+    mock_sender_->SendRpcInitialize();
+    run_loop.Run();
+  }
+
+  // Send RPC_R_SETPLAYBACKRATE on main sequence and flush media thread.
+  EXPECT_CALL(*mock_renderer, SetPlaybackRate(1.0))
+      .WillOnce([&media_thread](double playback_rate) {
+        EXPECT_TRUE(media_thread.task_runner()->RunsTasksInCurrentSequence());
+      });
+  mock_sender_->SendRpcSetPlaybackRate(1.0);
+  media_thread.FlushForTesting();
+
+  // Send RPC_R_STARTPLAYINGFROM on main sequence and flush media thread.
+  EXPECT_CALL(*mock_renderer, StartPlayingFrom(base::Seconds(5)))
+      .WillOnce([&media_thread](base::TimeDelta time) {
+        EXPECT_TRUE(media_thread.task_runner()->RunsTasksInCurrentSequence());
+      });
+  EXPECT_CALL(*mock_sender_, OnTimeUpdate(_, _)).Times(AtLeast(1));
+  mock_sender_->SendRpcStartPlayingFrom(base::Seconds(5));
+  media_thread.FlushForTesting();
+
+  // Send RPC_R_FLUSHUNTIL on main sequence and wait for response on main
+  // sequence.
+  EXPECT_CALL(*mock_renderer, OnFlush(_))
+      .WillOnce([&media_thread](base::OnceClosure& flush_cb) {
+        EXPECT_TRUE(media_thread.task_runner()->RunsTasksInCurrentSequence());
+        std::move(flush_cb).Run();
+      });
+  {
+    base::RunLoop run_loop;
+    EXPECT_CALL(*mock_sender_, FlushUntilCallback())
+        .WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
+    mock_sender_->SendRpcFlushUntil(0, 0);
+    run_loop.Run();
+  }
+
+  // Trigger OnEnded on media sequence (as the underlying renderer does) and
+  // wait for response on main sequence.
+  {
+    base::RunLoop run_loop;
+    EXPECT_CALL(*mock_sender_, OnEnded())
+        .WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
+    media_thread.task_runner()->PostTask(
+        FROM_HERE, base::BindOnce(&RendererClient::OnEnded,
+                                  base::Unretained(receiver.get())));
+    run_loop.Run();
+  }
+
+  // Destroy Receiver on media sequence.
+  media_thread.task_runner()->DeleteSoon(FROM_HERE, std::move(receiver));
+
+  EXPECT_TRUE(base::test::RunUntil([&]() {
+    return !rpc_messenger_->IsRegisteredForTesting(receiver_renderer_handle_);
+  }));
+}
+
 // |Receiver::Initialize| will be called by the local pipeline, and the
 // |Receiver::RpcInitialize| will be called once it received the
 // RPC_R_INITIALIZE messages, so these two initialization functions are possible
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/media/remoting/receiver_unittest.cc b/media/remoting/receiver_unittest.cc
index 2cc4c6b..9674684f 100644
--- a/media/remoting/receiver_unittest.cc
+++ b/media/remoting/receiver_unittest.cc
@@ -8,10 +8,14 @@
 #include <utility>
 
 #include "base/check.h"
+#include "base/functional/callback_helpers.h"
 #include "base/memory/raw_ptr.h"
+#include "base/run_loop.h"
 #include "base/task/single_thread_task_runner.h"
 #include "base/test/gmock_callback_support.h"
+#include "base/test/run_until.h"
 #include "base/test/task_environment.h"
+#include "base/threading/thread.h"
 #include "media/base/audio_decoder_config.h"
 #include "media/base/media_util.h"
 #include "media/base/mock_filters.h"
@@ -237,6 +241,11 @@
   }
 
   void TearDown() override {
+    mock_renderer_ = nullptr;
+    receiver_.reset();
+    EXPECT_TRUE(base::test::RunUntil([&]() {
+      return !rpc_messenger_->IsRegisteredForTesting(receiver_renderer_handle_);
+    }));
     rpc_messenger_->UnregisterMessageReceiverCallback(
         RpcMessenger::kAcquireRendererHandle);
   }
@@ -325,6 +334,163 @@
   task_environment_.RunUntilIdle();
 }
 
+// Receiver is constructed on the main sequence but, as a media::Renderer,
+// processes RPC messages and is destroyed on the media sequence. The
+// RpcMessenger it registers with is only safe to use from the main sequence,
+// so unregistration must hop back there.
+TEST_F(ReceiverTest, UnregistersOnMainSequenceWhenDestroyedOnMediaSequence) {
+  base::Thread media_thread("TestMediaThread");
+  ASSERT_TRUE(media_thread.Start());
+
+  auto renderer = std::make_unique<NiceMock<MockRenderer>>();
+  MockRenderer* mock_renderer = renderer.get();
+  EXPECT_CALL(*mock_renderer, SetVolume(0.5f))
+      .WillOnce([&media_thread](float volume) {
+        EXPECT_TRUE(media_thread.task_runner()->RunsTasksInCurrentSequence());
+      });
+
+  auto receiver = std::make_unique<Receiver>(
+      receiver_renderer_handle_, sender_renderer_handle_, mock_controller_,
+      media_thread.task_runner(), std::move(renderer),
+      base::BindOnce(&ReceiverTest::OnAcquireRendererDone,
+                     weak_factory_.GetWeakPtr()));
+  EXPECT_TRUE(
+      rpc_messenger_->IsRegisteredForTesting(receiver_renderer_handle_));
+
+  // Verify that RPC messages received on the main thread are dispatched and
+  // handled on the media thread without sequence DCHECK failures.
+  openscreen::cast::RpcMessage rpc;
+  rpc.set_handle(receiver_renderer_handle_);
+  rpc.set_proc(openscreen::cast::RpcMessage::RPC_R_SETVOLUME);
+  rpc.set_double_value(0.5);
+  rpc_messenger_->ProcessMessageFromRemote(
+      std::make_unique<openscreen::cast::RpcMessage>(rpc));
+  media_thread.FlushForTesting();
+
+  // Destroy the Receiver on the media sequence, as the production pipeline
+  // does, and wait for destruction to complete.
+  media_thread.task_runner()->DeleteSoon(FROM_HERE, std::move(receiver));
+  media_thread.FlushForTesting();
+
+  // The handle must still be registered: only the main sequence may mutate the
+  // messenger, and no main-sequence tasks have run yet.
+  EXPECT_TRUE(
+      rpc_messenger_->IsRegisteredForTesting(receiver_renderer_handle_));
+
+  // Send an RPC message on the main sequence post-destruction. The message will
+  // be forwarded to the media sequence after Receiver is destroyed, where
+  // WeakPtr drops the invocation safely without crashing or UAF.
+  openscreen::cast::RpcMessage rpc_post_destruction;
+  rpc_post_destruction.set_handle(receiver_renderer_handle_);
+  rpc_post_destruction.set_proc(openscreen::cast::RpcMessage::RPC_R_SETVOLUME);
+  rpc_post_destruction.set_double_value(0.8);
+  rpc_messenger_->ProcessMessageFromRemote(
+      std::make_unique<openscreen::cast::RpcMessage>(rpc_post_destruction));
+  media_thread.FlushForTesting();
+
+  EXPECT_TRUE(base::test::RunUntil([&]() {
+    return !rpc_messenger_->IsRegisteredForTesting(receiver_renderer_handle_);
+  }));
+}
+
+// Verifies end-to-end multi-threaded RPC message dispatch from the main
+// sequence to the media sequence, renderer method execution on the media
+// sequence, and response RPC messages sent back on the main sequence.
+TEST_F(ReceiverTest, MultiThreadedRpcDispatchAndRendererCalls) {
+  base::Thread media_thread("TestMediaThread");
+  ASSERT_TRUE(media_thread.Start());
+
+  mock_sender_->SendRpcAcquireRenderer();
+  EXPECT_TRUE(base::test::RunUntil([&]() {
+    return sender_renderer_handle_ != RpcMessenger::kInvalidHandle;
+  }));
+
+  auto renderer = std::make_unique<NiceMock<MockRenderer>>();
+  MockRenderer* mock_renderer = renderer.get();
+
+  EXPECT_CALL(*mock_sender_, AcquireRendererDone()).Times(1);
+  auto receiver = std::make_unique<Receiver>(
+      receiver_renderer_handle_, sender_renderer_handle_, mock_controller_,
+      media_thread.task_runner(), std::move(renderer),
+      base::BindOnce(&ReceiverTest::OnAcquireRendererDone,
+                     weak_factory_.GetWeakPtr()));
+
+  // Call Initialize on the media sequence (as PipelineImpl does in production).
+  EXPECT_CALL(*mock_renderer,
+              OnInitialize(&mock_media_resource_, testing::NotNull(), _))
+      .WillOnce([&media_thread](MediaResource* media_resource,
+                                RendererClient* client,
+                                PipelineStatusCallback& init_cb) {
+        EXPECT_TRUE(media_thread.task_runner()->RunsTasksInCurrentSequence());
+        std::move(init_cb).Run(PIPELINE_OK);
+      });
+  media_thread.task_runner()->PostTask(
+      FROM_HERE,
+      base::BindOnce(&Receiver::Initialize, base::Unretained(receiver.get()),
+                     &mock_media_resource_, receiver.get(), base::DoNothing()));
+
+  // Send RPC_R_INITIALIZE on main sequence and wait for response on main
+  // sequence.
+  {
+    base::RunLoop run_loop;
+    EXPECT_CALL(*mock_sender_, InitializeCallback(true))
+        .WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
+    mock_sender_->SendRpcInitialize();
+    run_loop.Run();
+  }
+
+  // Send RPC_R_SETPLAYBACKRATE on main sequence and flush media thread.
+  EXPECT_CALL(*mock_renderer, SetPlaybackRate(1.0))
+      .WillOnce([&media_thread](double playback_rate) {
+        EXPECT_TRUE(media_thread.task_runner()->RunsTasksInCurrentSequence());
+      });
+  mock_sender_->SendRpcSetPlaybackRate(1.0);
+  media_thread.FlushForTesting();
+
+  // Send RPC_R_STARTPLAYINGFROM on main sequence and flush media thread.
+  EXPECT_CALL(*mock_renderer, StartPlayingFrom(base::Seconds(5)))
+      .WillOnce([&media_thread](base::TimeDelta time) {
+        EXPECT_TRUE(media_thread.task_runner()->RunsTasksInCurrentSequence());
+      });
+  EXPECT_CALL(*mock_sender_, OnTimeUpdate(_, _)).Times(AtLeast(1));
+  mock_sender_->SendRpcStartPlayingFrom(base::Seconds(5));
+  media_thread.FlushForTesting();
+
+  // Send RPC_R_FLUSHUNTIL on main sequence and wait for response on main
+  // sequence.
+  EXPECT_CALL(*mock_renderer, OnFlush(_))
+      .WillOnce([&media_thread](base::OnceClosure& flush_cb) {
+        EXPECT_TRUE(media_thread.task_runner()->RunsTasksInCurrentSequence());
+        std::move(flush_cb).Run();
+      });
+  {
+    base::RunLoop run_loop;
+    EXPECT_CALL(*mock_sender_, FlushUntilCallback())
+        .WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
+    mock_sender_->SendRpcFlushUntil(0, 0);
+    run_loop.Run();
+  }
+
+  // Trigger OnEnded on media sequence (as the underlying renderer does) and
+  // wait for response on main sequence.
+  {
+    base::RunLoop run_loop;
+    EXPECT_CALL(*mock_sender_, OnEnded())
+        .WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
+    media_thread.task_runner()->PostTask(
+        FROM_HERE, base::BindOnce(&RendererClient::OnEnded,
+                                  base::Unretained(receiver.get())));
+    run_loop.Run();
+  }
+
+  // Destroy Receiver on media sequence.
+  media_thread.task_runner()->DeleteSoon(FROM_HERE, std::move(receiver));
+
+  EXPECT_TRUE(base::test::RunUntil([&]() {
+    return !rpc_messenger_->IsRegisteredForTesting(receiver_renderer_handle_);
+  }));
+}
+
 // |Receiver::Initialize| will be called by the local pipeline, and the
 // |Receiver::RpcInitialize| will be called once it received the
 // RPC_R_INITIALIZE messages, so these two initialization functions are possible
Loading diff…

Original Bug Report

reported by vm...@google.com

UAF Data Race in Cast RpcMessenger via media::remoting::Receiver

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 data race exists in the Chrome Cast Remoting implementation because media::remoting::Receiver mutates the RpcMessenger callback registry on the media thread while it is concurrently accessed on the main thread. This lack of synchronization during session teardown can lead to a Use-After-Free (UAF) of std::function state. An attacker could potentially exploit this race to achieve remote code execution in the sandboxed renderer process.

Affected files:

  • media/remoting/receiver.cc
  • third_party/openscreen/src/cast/streaming/public/rpc_messenger.cc
  • media/remoting/receiver_controller.cc
  • third_party/openscreen/src/util/flat_map.h

Estimated timestamp from git blame: 2021-08-19

Vulnerability Detail

A critical concurrency vulnerability exists in the Cast Remoting implementation, specifically in how the media::remoting::Receiver interacts with the process-global openscreen::cast::RpcMessenger registry.

The RpcMessenger class maintains a registry of callbacks in a FlatMap<Handle, ReceiveMessageCallback> called receive_callbacks_. FlatMap is a wrapper around std::vector<std::pair<K, V>> and provides no internal thread synchronization.

In the renderer process, this vector is accessed concurrently from two different threads without locking:

  1. Main Thread (Read/Execute): The ReceiverController singleton receives RPC messages via Mojo. ReceiverController::OnMessageFromSource executes on the main thread and calls RpcMessenger::ProcessMessageFromRemote. This function performs a std::find_if on receive_callbacks_ and invokes the resulting std::function callback (entry->second(std::move(message))).
  2. Media Thread (Write/Erase): The media::remoting::Receiver object is owned by the media pipeline (PipelineImpl::RendererWrapper) and is destroyed on the media thread when a streaming session terminates. Its destructor, ~Receiver(), calls rpc_messenger_->UnregisterMessageReceiverCallback(rpc_handle_). This immediately calls std::vector::erase on the media thread.

Impact

When std::vector::erase is called on the media thread concurrently with the main thread iterating over or invoking a callback from the same vector, a severe data race occurs. The erase operation shifts elements and destroys std::function objects. If the main thread attempts to call through a std::function that is actively being moved or destroyed, it accesses corrupted heap state (specifically, the internal polymorphic state of the std::function or the base::BindState it holds).

Because the corruption involves the execution flow through an implicit this pointer on the stack or STL-internal control blocks, MiraclePtr (BRP) does not provide protection.

Potential Attacker Steps

Please note: These are theoretical steps as our tooling cannot currently execute a Proof of Concept.

  1. An attacker connects to a vulnerable Chrome Cast Receiver (e.g., CastOS or Cast Android) and initiates a remoting session to act as the Cast Sender.
  2. The attacker floods the receiver with a high volume of valid RPC messages (e.g., RPC_R_SET_VOLUME), forcing the main thread to constantly iterate and execute callbacks inside RpcMessenger::OnMessage.
  3. Simultaneously, the attacker abruptly forces a session teardown (e.g., by dropping the connection or sending a fatal error). This triggers the teardown of PipelineImpl on the media thread.
  4. The ~Receiver destructor runs on the media thread, erasing the callback from the vector while the main thread is simultaneously invoking it.
  5. The ensuing Use-After-Free allows the attacker to hijack the instruction pointer when the main thread calls into the freed std::function state, potentially achieving arbitrary code execution within the sandboxed renderer.

Suggested Fix

The root cause is that Receiver modifies a main-thread data structure from the media thread. The fix is to ensure that unregistration happens on the main thread.

In ~Receiver(), instead of calling unregister directly, post a task to the main_task_runner_:

Receiver::~Receiver() {
  main_task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&RpcMessenger::UnregisterMessageReceiverCallback,
                     base::Unretained(rpc_messenger_), rpc_handle_));
}

(Note: This is the exact pattern already safely used by the sender-side CourierRenderer in media/remoting/courier_renderer.cc:846).

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