CVE-2026-6317
Overview
Background
- `CastActivityManager`
- the browser-process object that tracks live Cast sessions, keyed by
MediaRouteID, in two containers:activities_(owningstd::unique_ptr) andapp_activities_(non-owning raw pointers). - `std::map::emplace`
- inserts a key/value pair only if the key is absent; on a key collision it leaves the existing element untouched and discards the argument that was moved into it.
- `insert_or_assign`
- replaces the mapped value when the key already exists (and inserts otherwise), so the container always ends up owning the newly supplied object.
- Route ID collision
- two
AddAppActivity/AddMirroringActivitycalls arriving with the samemedia_route_id(), which the advisory attributes to a race in auto-join requests.
Root Cause Analysis
In AddAppActivity and AddMirroringActivity a new activity object is created, its raw address captured in activity_ptr, and the owning std::unique_ptr handed to activities_.emplace(route.media_route_id(), std::move(activity)). When the route ID already exists, emplace does not insert: the moved-in unique_ptr is destroyed, freeing the just-created activity object, while the pre-existing activity stays in activities_. The code then unconditionally executes app_activities_[route.media_route_id()] = activity_ptr; and return activity_ptr;, publishing and returning a pointer to the object that emplace just freed. The invariant that every pointer in app_activities_/activities_ references a live, owned object is violated, leaving a dangling raw pointer.
The fix swaps emplace for insert_or_assign, which always stores the new unique_ptr (destroying the old activity instead), so activity_ptr and the app_activities_ entry point to the live, owned object.
emplace, which silently keeps the old value on a key collision, while the surrounding code assumed the new object was always inserted and kept publishing/returning its pointer; insert_or_assign restores that assumption by guaranteeing the new object becomes the owned one.Attack Path
- Induce a duplicate route ID
Trigger two Cast activity-creation requests (e.g. racing auto-join requests) that resolve to the same
media_route_id(), as the regression testsAddAppActivityCollisionandAddMirroringActivityCollisionmodel by calling the add method twice with oneMediaRoute. - Collision discards the new object
The second
AddAppActivity/AddMirroringActivitycall reachesactivities_.emplace(...), which no-ops on the existing key and frees the newly constructed activity held by the movedunique_ptr. - Dangling pointer published
app_activities_[route.media_route_id()] = activity_ptr;stores, and the function returns, a pointer to the freed activity. - Use-after-free on subsequent use
Any later dereference of that
app_activities_entry or the returned pointer operates on freed memory (inferred downstream; the diff shows the dangling pointer but not a specific sink).
Impact Assessment
CastActivity object in the browser process, the Media Router / Cast component. Preconditions are the ability to initiate Cast sessions that race into the same route ID; the advisory rates this high severity and does not report exploitation in the wild. Escalation from the dangling pointer to control-flow hijack or code execution is (inferred) and not demonstrated by the diff.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
Audit Directions
- `emplace`/`insert` with a captured raw pointerFlag any map insertion whose return value is ignored while a separately captured
.get()pointer is later stored or returned; a collision frees the discarded object and dangles that pointer. - Owning + non-owning parallel containersReview classes that mirror a
unique_ptrmap with a raw-pointer map (activities_/app_activities_) to ensure both are updated atomically and consistently on insert, replace, and erase. - Racy ID generationAudit auto-join / session-matching code paths that can produce duplicate
MediaRouteor session IDs under concurrency, since such collisions are the trigger here.
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.