High watch CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Cast
DescriptionUse after free in Cast
ComponentCast
Sub-componentMedia-router
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker500091052
Fix commit1f60ba416fed (chromium/src) +56/-2
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-06 (per vendor advisory)
Fix landed2026-04-10 (public git)
Disclosed2026-04-15
Patch gap5 days — fix public in source before the advisory

Background

`CastActivityManager`
the browser-process object that tracks live Cast sessions, keyed by MediaRoute ID, in two containers: activities_ (owning std::unique_ptr) and app_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/AddMirroringActivity calls arriving with the same media_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.

Key insight
The core mistake is using 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

  1. 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 tests AddAppActivityCollision and AddMirroringActivityCollision model by calling the add method twice with one MediaRoute.
  2. Collision discards the new object The second AddAppActivity/AddMirroringActivity call reaches activities_.emplace(...), which no-ops on the existing key and frees the newly constructed activity held by the moved unique_ptr.
  3. Dangling pointer published app_activities_[route.media_route_id()] = activity_ptr; stores, and the function returns, a pointer to the freed activity.
  4. 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

An attacker who can drive colliding Cast route IDs gets a use-after-free of a 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

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

Audit Directions

  • `emplace`/`insert` with a captured raw pointer
    Flag 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 containers
    Review classes that mirror a unique_ptr map with a raw-pointer map (activities_/app_activities_) to ensure both are updated atomically and consistently on insert, replace, and erase.
  • Racy ID generation
    Audit auto-join / session-matching code paths that can produce duplicate MediaRoute or session IDs under concurrency, since such collisions are the trigger here.
From 1f60ba416fed3360b651837d0b174e0323e7cb39 Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <apaseltiner@chromium.org>
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 <muyaoxu@google.com>
Commit-Queue: Andrew Paseltiner <apaseltiner@chromium.org>
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 vm...@google.com

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