Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Cast
DescriptionUse after free in Cast
ComponentCast
Bug ClassUAF
Tracker500091052
Fix commit1f60ba416fed (chromium/src) +56/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-04-15

Changed Functions

FunctionChangeNotes
TEST_F
chrome/browser/media/router/providers/cast/cast_activity_manager_unittest.cc
modified

Files Changed

  • chrome/browser/media/router/providers/cast/cast_activity_manager.cc
  • chrome/browser/media/router/providers/cast/cast_activity_manager.h
  • chrome/browser/media/router/providers/cast/cast_activity_manager_unittest.cc
From 1f60ba416fed3360b651837d0b174e0323e7cb39 Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <[email protected]>
Date: Fri, 10 Apr 2026 10:55:54 -0700
Subject: [PATCH] [media-router] Prevent UAF on Cast route ID collision

Use insert_or_assign instead of emplace when adding activities to
CastActivityManager. This ensures that if a collision occurs (e.g. due
to a race in auto-join requests), the existing activity is replaced
and the raw pointer in app_activities_ is updated to a live object,
preventing a Use-After-Free.

Fixed: 500091052
Change-Id: Iedffd9d2dc78decf841ffd4f95ca5fb98b1b8f79
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7748265
Reviewed-by: Muyao Xu <[email protected]>
Commit-Queue: Andrew Paseltiner <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1612973}
---

diff --git a/chrome/browser/media/router/providers/cast/cast_activity_manager.cc b/chrome/browser/media/router/providers/cast/cast_activity_manager.cc
index 3cdc97b..2253726 100644
--- a/chrome/browser/media/router/providers/cast/cast_activity_manager.cc
+++ b/chrome/browser/media/router/providers/cast/cast_activity_manager.cc
@@ -527,7 +527,9 @@
                                           session_tracker_, logger_.get(),
                                           debugger_.get()));
   auto* const activity_ptr = activity.get();
-  activities_.emplace(route.media_route_id(), std::move(activity));
+  // Use insert_or_assign to avoid Use-After-Free on route ID collision.
+  // See crbug.com/500091052.
+  activities_.insert_or_assign(route.media_route_id(), std::move(activity));
   app_activities_[route.media_route_id()] = activity_ptr;
   return activity_ptr;
 }
@@ -556,7 +558,9 @@
   activity->BindChannelToServiceReceiver();
   activity->CreateMirroringServiceHost();
   auto* const activity_ptr = activity.get();
-  activities_.emplace(route.media_route_id(), std::move(activity));
+  // Use insert_or_assign to avoid Use-After-Free on route ID collision.
+  // See crbug.com/500091052.
+  activities_.insert_or_assign(route.media_route_id(), std::move(activity));
   return activity_ptr;
 }
 
diff --git a/chrome/browser/media/router/providers/cast/cast_activity_manager.h b/chrome/browser/media/router/providers/cast/cast_activity_manager.h
index 6eb1723..a5c4edae 100644
--- a/chrome/browser/media/router/providers/cast/cast_activity_manager.h
+++ b/chrome/browser/media/router/providers/cast/cast_activity_manager.h
@@ -149,6 +149,9 @@
 
  private:
   friend class CastActivityManagerTest;
+  FRIEND_TEST_ALL_PREFIXES(CastActivityManagerTest, AddAppActivityCollision);
+  FRIEND_TEST_ALL_PREFIXES(CastActivityManagerTest,
+                           AddMirroringActivityCollision);
   FRIEND_TEST_ALL_PREFIXES(CastActivityManagerWithTerminatingTest,
                            LaunchSessionTerminatesExistingSessionOnSink);
   FRIEND_TEST_ALL_PREFIXES(CastActivityManagerTest,
diff --git a/chrome/browser/media/router/providers/cast/cast_activity_manager_unittest.cc b/chrome/browser/media/router/providers/cast/cast_activity_manager_unittest.cc
index f6d23f6..668c134 100644
--- a/chrome/browser/media/router/providers/cast/cast_activity_manager_unittest.cc
+++ b/chrome/browser/media/router/providers/cast/cast_activity_manager_unittest.cc
@@ -985,4 +985,51 @@
   histogram_tester.ExpectTotalCount(histogram, 0);
 }
 
+// Regression test for crbug.com/500091052.
+TEST_F(CastActivityManagerTest, AddAppActivityCollision) {
+  MediaSource source(MakeSourceId(kAppId1));
+  MediaRoute route(kPresentationId, source, sink_.sink().id(), "description",
+                   true);
+
+  // First call to AddAppActivity should succeed.
+  AppActivity* activity1 = manager_->AddAppActivity(route, kAppId1);
+  ASSERT_TRUE(activity1);
+  EXPECT_EQ(1u, manager_->GetRoutes().size());
+
+  // Second call with same route_id should overwrite the existing activity
+  // instead of creating a dangling pointer.
+  AppActivity* activity2 = manager_->AddAppActivity(route, kAppId1);
+  ASSERT_TRUE(activity2);
+  EXPECT_NE(activity1, activity2);
+  EXPECT_EQ(1u, manager_->GetRoutes().size());
+
+  // Verify that the pointer in app_activities_ has been updated to the new
+  // activity.
+  EXPECT_EQ(activity2, manager_->app_activities_[route.media_route_id()]);
+}
+
+// Regression test for crbug.com/500091052.
+TEST_F(CastActivityManagerTest, AddMirroringActivityCollision) {
+  MediaSource source = MediaSource::ForTab(123);
+  MediaRoute route(kPresentationId, source, sink_.sink().id(), "description",
+                   true);
+  route.set_controller_type(RouteControllerType::kMirroring);
+
+  // First call to AddMirroringActivity should succeed.
+  CastActivity* activity1 = manager_->AddMirroringActivity(
+      route, cast_streaming_app_id_, kFrameTreeNodeId, sink_.cast_data());
+  ASSERT_TRUE(activity1);
+  EXPECT_EQ(1u, manager_->activities_.size());
+
+  // Second call with same route_id should overwrite the existing activity.
+  CastActivity* activity2 = manager_->AddMirroringActivity(
+      route, cast_streaming_app_id_, kFrameTreeNodeId, sink_.cast_data());
+  ASSERT_TRUE(activity2);
+  EXPECT_NE(activity1, activity2);
+  EXPECT_EQ(1u, manager_->activities_.size());
+
+  // Verify that activities_ map has been updated to the new activity.
+  EXPECT_EQ(activity2, manager_->activities_[route.media_route_id()].get());
+}
+
 }  // namespace media_router
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/media/router/providers/cast/cast_activity_manager_unittest.cc b/chrome/browser/media/router/providers/cast/cast_activity_manager_unittest.cc
index f6d23f6..668c134 100644
--- a/chrome/browser/media/router/providers/cast/cast_activity_manager_unittest.cc
+++ b/chrome/browser/media/router/providers/cast/cast_activity_manager_unittest.cc
@@ -985,4 +985,51 @@
   histogram_tester.ExpectTotalCount(histogram, 0);
 }
 
+// Regression test for crbug.com/500091052.
+TEST_F(CastActivityManagerTest, AddAppActivityCollision) {
+  MediaSource source(MakeSourceId(kAppId1));
+  MediaRoute route(kPresentationId, source, sink_.sink().id(), "description",
+                   true);
+
+  // First call to AddAppActivity should succeed.
+  AppActivity* activity1 = manager_->AddAppActivity(route, kAppId1);
+  ASSERT_TRUE(activity1);
+  EXPECT_EQ(1u, manager_->GetRoutes().size());
+
+  // Second call with same route_id should overwrite the existing activity
+  // instead of creating a dangling pointer.
+  AppActivity* activity2 = manager_->AddAppActivity(route, kAppId1);
+  ASSERT_TRUE(activity2);
+  EXPECT_NE(activity1, activity2);
+  EXPECT_EQ(1u, manager_->GetRoutes().size());
+
+  // Verify that the pointer in app_activities_ has been updated to the new
+  // activity.
+  EXPECT_EQ(activity2, manager_->app_activities_[route.media_route_id()]);
+}
+
+// Regression test for crbug.com/500091052.
+TEST_F(CastActivityManagerTest, AddMirroringActivityCollision) {
+  MediaSource source = MediaSource::ForTab(123);
+  MediaRoute route(kPresentationId, source, sink_.sink().id(), "description",
+                   true);
+  route.set_controller_type(RouteControllerType::kMirroring);
+
+  // First call to AddMirroringActivity should succeed.
+  CastActivity* activity1 = manager_->AddMirroringActivity(
+      route, cast_streaming_app_id_, kFrameTreeNodeId, sink_.cast_data());
+  ASSERT_TRUE(activity1);
+  EXPECT_EQ(1u, manager_->activities_.size());
+
+  // Second call with same route_id should overwrite the existing activity.
+  CastActivity* activity2 = manager_->AddMirroringActivity(
+      route, cast_streaming_app_id_, kFrameTreeNodeId, sink_.cast_data());
+  ASSERT_TRUE(activity2);
+  EXPECT_NE(activity1, activity2);
+  EXPECT_EQ(1u, manager_->activities_.size());
+
+  // Verify that activities_ map has been updated to the new activity.
+  EXPECT_EQ(activity2, manager_->activities_[route.media_route_id()].get());
+}
+
 }  // namespace media_router
Loading diff…

Original Bug Report

reported by [email protected]

Browser Process UAF in CastActivityManager due to map emplace failure

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) exists in the browser process’s Cast Activity Manager due to unsafe handling of base::flat_map::emplace. If a route ID collision occurs when adding a new Cast activity, the activity object is freed, but its raw pointer is incorrectly stored in the app_activities_ map. An attacker can reliably trigger this collision via concurrent “auto-join” requests and subsequently exploit the dangling pointer to achieve Remote Code Execution.

Affected files:

  • chrome/browser/media/router/providers/cast/cast_activity_manager.cc
  • chrome/browser/media/router/providers/cast/cast_activity_manager.h

Estimated timestamp from git blame: 2020-07-15

Description

A potential Use-After-Free (UAF) vulnerability exists in CastActivityManager::AddAppActivity, which executes in the highly privileged browser process. The issue stems from failing to check the return value of a base::flat_map::emplace operation, leading to the storage of a dangling pointer when a key collision occurs.

In chrome/browser/media/router/providers/cast/cast_activity_manager.cc, the AddAppActivity method creates a new AppActivity and attempts to insert it into the activities_ map:

AppActivity* CastActivityManager::AddAppActivity(const MediaRoute& route,
                                                 const std::string& app_id) {
  std::unique_ptr<AppActivity> activity = ...;
  auto* const activity_ptr = activity.get();
  activities_.emplace(route.media_route_id(), std::move(activity));
  app_activities_[route.media_route_id()] = activity_ptr;
  return activity_ptr;
}

When base::flat_map::emplace is called, it constructs a temporary std::pair containing the moved unique_ptr. If the key (route.media_route_id()) already exists in the map, emplace returns early without inserting the temporary pair. At the end of the expression, this temporary pair is destroyed, immediately freeing the AppActivity. However, AddAppActivity proceeds unconditionally to store the previously captured activity_ptr into the app_activities_ map.

Because app_activities_ is a standard map storing bare AppActivity* pointers, MiraclePtr (BackupRefPtr) does not protect these entries. When this dangling pointer is later accessed, it results in an exploitable UAF.

Potential Trigger and Exploitation Steps

Note: Our automated tooling agent cannot run code, so these are theoretical steps to trigger the vulnerability based on static code analysis.

An attacker can reliably trigger a route ID collision by exploiting how predictable route IDs are generated for “auto-join” presentation IDs.

  1. Setup: The victim visits an attacker-controlled site which opens two separate tabs (Tab A and Tab B) in the same origin. The victim initiates Cast Tab Mirroring from both tabs to the same local Cast sink.
  2. Race Window: Tab B’s connection will prompt the receiver to terminate Tab A’s session. Before the browser asynchronously cleans up Tab A’s internal state, the attacker executes a ReconnectPresentation IPC from both tabs.
  3. Identical Route IDs: Both IPCs use presentation_id="auto-join" and a Cast URL specifying autoJoinPolicy=page_scoped. The kPageScoped policy causes FindActivityForAutoJoin to return nullptr, causing JoinSession to fall back to the active mirroring sinks.
  4. Collision: Both requests generate the exact same route_id (e.g., cast:auto-join/<sink_id>/<source_id>) and call LaunchSession, which delegates to AddAppActivity.
  5. The UAF: The first request successfully inserts the activity. The second request hits the emplace collision, freeing the second AppActivity but storing its dangling pointer into app_activities_, overwriting the first request’s pointer.
  6. Exploitation: The attacker sprays the browser process heap to replace the freed AppActivity chunk with a controlled fake object.
  7. Execution: The attacker sends a final ReconnectPresentation request with autoJoinPolicy=origin_scoped. JoinSession iterates through app_activities_ and dereferences the dangling pointer. After passing validation checks against the fake object, the virtual method activity->AddClient(...) is invoked, allowing the attacker to hijack the control flow and achieve arbitrary Remote Code Execution (RCE) / Sandbox Escape.

Suggested Fix

Check the return value of emplace or insert to determine if the insertion was successful. Only insert the pointer into app_activities_ if the initial insertion into activities_ succeeded.

AppActivity* CastActivityManager::AddAppActivity(const MediaRoute& route,
                                                 const std::string& app_id) {
  std::unique_ptr<AppActivity> activity = ...;
  auto* const activity_ptr = activity.get();
  auto result = activities_.emplace(route.media_route_id(), std::move(activity));
  if (result.second) {
    app_activities_[route.media_route_id()] = activity_ptr;
    return activity_ptr;
  }
  // Handle insertion failure (e.g., log error, return existing activity, or return nullptr)
  return nullptr;
}

Alternatively, consider changing app_activities_ to store base::WeakPtr<AppActivity> or using a safe reference holding mechanism instead of raw pointers.

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