CVE-2026-6317
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fchrome/browser/media/router/providers/cast/cast_activity_manager_unittest.cc |
modified |
Files Changed
chrome/browser/media/router/providers/cast/cast_activity_manager.ccchrome/browser/media/router/providers/cast/cast_activity_manager.hchrome/browser/media/router/providers/cast/cast_activity_manager_unittest.cc
Patch
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
Regression Test / PoC
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
Original Bug Report
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.ccchrome/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.
- 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.
- 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
ReconnectPresentationIPC from both tabs. - Identical Route IDs: Both IPCs use
presentation_id="auto-join"and a Cast URL specifyingautoJoinPolicy=page_scoped. ThekPageScopedpolicy causesFindActivityForAutoJointo returnnullptr, causingJoinSessionto fall back to the active mirroring sinks. - Collision: Both requests generate the exact same
route_id(e.g.,cast:auto-join/<sink_id>/<source_id>) and callLaunchSession, which delegates toAddAppActivity. - The UAF: The first request successfully inserts the activity. The second request hits the
emplacecollision, freeing the secondAppActivitybut storing its dangling pointer intoapp_activities_, overwriting the first request’s pointer. - Exploitation: The attacker sprays the browser process heap to replace the freed
AppActivitychunk with a controlled fake object. - Execution: The attacker sends a final
ReconnectPresentationrequest withautoJoinPolicy=origin_scoped.JoinSessioniterates throughapp_activities_and dereferences the dangling pointer. After passing validation checks against the fake object, the virtual methodactivity->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.