CVE-2026-87588
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchromecast/BUILD.gn |
modified | |
cast_source_setchromecast/renderer/BUILD.gn |
modified | |
testchromecast/renderer/BUILD.gn |
modified | |
UrlFilterReceiverchromecast/renderer/cast_activity_url_filter_manager.h |
modified | |
CastActivityUrlFilterManagerTestchromecast/renderer/cast_activity_url_filter_manager_unittest.cc |
modified | |
LookupRunnerchromecast/renderer/cast_activity_url_filter_manager_unittest.cc |
modified |
Files Changed
chromecast/BUILD.gnchromecast/renderer/BUILD.gnchromecast/renderer/cast_activity_url_filter_manager.ccchromecast/renderer/cast_activity_url_filter_manager.hchromecast/renderer/cast_activity_url_filter_manager_unittest.cc
Patch
From b122b4a0d4587946fd2bffdd5b7c72479d5668e5 Mon Sep 17 00:00:00 2001
From: Richard Nichols <rknichols@google.com>
Date: Tue, 28 Jul 2026 07:23:01 -0700
Subject: [PATCH] [chromecast] Make CastActivityUrlFilterManager thread-safe
CastURLLoaderThrottleProvider and CastWebSocketHandshakeThrottleProvider
are cloned to worker threads where they call
GetActivityUrlFilterForRenderFrameToken() while the main thread adds and
removes entries via OnRenderFrameCreated() and OnRenderFrameRemoved().
Guard the filter map with a lock so lookups never observe a map that is
being mutated, and hold the receivers via raw_ptr.
Adds a cast_renderer_unittests target with coverage for the concurrent
lookup path.
Bug: 495515356
Change-Id: Idedffe49e1849b521c465396611d40e3f1accca3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8108180
Reviewed-by: Shawn Quereshi <shawnq@google.com>
Reviewed-by: Simeon Anfinrud <sanfin@chromium.org>
Commit-Queue: Richard Nichols <rknichols@google.com>
Cr-Commit-Position: refs/heads/main@{#1669459}
---
diff --git a/chromecast/BUILD.gn b/chromecast/BUILD.gn
index 4627748..f3c54fa 100644
--- a/chromecast/BUILD.gn
+++ b/chromecast/BUILD.gn
@@ -101,6 +101,7 @@
if (!is_fuchsia) {
tests += [
+ "//chromecast/renderer:cast_renderer_unittests",
"//chromecast/ui/display_settings:cast_display_settings_unittests",
"//content/test:content_unittests",
]
diff --git a/chromecast/renderer/BUILD.gn b/chromecast/renderer/BUILD.gn
index 6f8da8fd..c98b375 100644
--- a/chromecast/renderer/BUILD.gn
+++ b/chromecast/renderer/BUILD.gn
@@ -3,6 +3,7 @@
# found in the LICENSE file.
import("//chromecast/chromecast.gni")
+import("//testing/test.gni")
import("//tools/grit/grit_rule.gni")
cast_source_set("renderer") {
@@ -81,6 +82,19 @@
}
}
+test("cast_renderer_unittests") {
+ sources = [ "cast_activity_url_filter_manager_unittest.cc" ]
+
+ deps = [
+ ":renderer",
+ "//base",
+ "//base/test:run_all_unittests",
+ "//base/test:test_support",
+ "//testing/gtest",
+ "//third_party/blink/public/common",
+ ]
+}
+
cast_source_set("simple_client") {
sources = [ "cast_content_renderer_client_simple.cc" ]
diff --git a/chromecast/renderer/cast_activity_url_filter_manager.cc b/chromecast/renderer/cast_activity_url_filter_manager.cc
index ef750b8..e484b63 100644
--- a/chromecast/renderer/cast_activity_url_filter_manager.cc
+++ b/chromecast/renderer/cast_activity_url_filter_manager.cc
@@ -74,7 +74,8 @@
ActivityUrlFilter*
CastActivityUrlFilterManager::GetActivityUrlFilterForRenderFrameToken(
const blink::LocalFrameToken& frame_token) {
- const auto& it = activity_url_filters_.find(frame_token);
+ base::AutoLock lock(filters_lock_);
+ auto it = activity_url_filters_.find(frame_token);
if (it == activity_url_filters_.end())
return nullptr;
@@ -91,6 +92,7 @@
base::BindOnce(&CastActivityUrlFilterManager::OnRenderFrameRemoved,
weak_this_, frame_token));
+ base::AutoLock lock(filters_lock_);
auto result = activity_url_filters_.emplace(frame_token, filter_receiver);
if (!result.second)
@@ -101,7 +103,8 @@
void CastActivityUrlFilterManager::OnRenderFrameRemoved(
const blink::LocalFrameToken& frame_token) {
- const auto& it = activity_url_filters_.find(frame_token);
+ base::AutoLock lock(filters_lock_);
+ auto it = activity_url_filters_.find(frame_token);
if (it != activity_url_filters_.end())
activity_url_filters_.erase(it);
diff --git a/chromecast/renderer/cast_activity_url_filter_manager.h b/chromecast/renderer/cast_activity_url_filter_manager.h
index 948bb60..4ed10951 100644
--- a/chromecast/renderer/cast_activity_url_filter_manager.h
+++ b/chromecast/renderer/cast_activity_url_filter_manager.h
@@ -9,6 +9,9 @@
#include <string>
#include "base/containers/flat_map.h"
+#include "base/memory/raw_ptr.h"
+#include "base/synchronization/lock.h"
+#include "base/thread_annotations.h"
#include "chromecast/common/activity_url_filter.h"
#include "chromecast/common/mojom/activity_url_filter.mojom.h"
#include "content/public/renderer/render_frame_observer.h"
@@ -33,6 +36,7 @@
~CastActivityUrlFilterManager();
// Returns nullptr if no Activity URL filter exists for the render frame.
+ // May be called from any thread.
ActivityUrlFilter* GetActivityUrlFilterForRenderFrameToken(
const blink::LocalFrameToken& frame_token);
@@ -40,6 +44,8 @@
void OnRenderFrameRemoved(const blink::LocalFrameToken& frame_token);
private:
+ friend class CastActivityUrlFilterManagerTest;
+
class UrlFilterReceiver
: public content::RenderFrameObserver,
public chromecast::mojom::ActivityUrlFilterConfiguration {
@@ -82,8 +88,9 @@
base::WeakPtrFactory<UrlFilterReceiver> weak_factory_;
};
- base::flat_map<blink::LocalFrameToken, UrlFilterReceiver*>
- activity_url_filters_;
+ base::Lock filters_lock_;
+ base::flat_map<blink::LocalFrameToken, raw_ptr<UrlFilterReceiver>>
+ activity_url_filters_ GUARDED_BY(filters_lock_);
base::WeakPtr<CastActivityUrlFilterManager> weak_this_;
base::WeakPtrFactory<CastActivityUrlFilterManager> weak_factory_;
diff --git a/chromecast/renderer/cast_activity_url_filter_manager_unittest.cc b/chromecast/renderer/cast_activity_url_filter_manager_unittest.cc
new file mode 100644
index 0000000..4dc89b4
--- /dev/null
+++ b/chromecast/renderer/cast_activity_url_filter_manager_unittest.cc
@@ -0,0 +1,95 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chromecast/renderer/cast_activity_url_filter_manager.h"
+
+#include <atomic>
+#include <vector>
+
+#include "base/memory/raw_ptr.h"
+#include "base/synchronization/lock.h"
+#include "base/synchronization/waitable_event.h"
+#include "base/thread_annotations.h"
+#include "base/threading/simple_thread.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/common/tokens/tokens.h"
+
+namespace chromecast {
+
+class CastActivityUrlFilterManagerTest : public testing::Test {
+ protected:
+ void AddFilterForToken(const blink::LocalFrameToken& token)
+ NO_THREAD_SAFETY_ANALYSIS {
+ base::AutoLock lock(manager_.filters_lock_);
+ manager_.activity_url_filters_.emplace(token, nullptr);
+ }
+
+ CastActivityUrlFilterManager manager_;
+};
+
+namespace {
+
+class LookupRunner : public base::DelegateSimpleThread::Delegate {
+ public:
+ LookupRunner(CastActivityUrlFilterManager* manager,
+ const blink::LocalFrameToken& token,
+ base::WaitableEvent* started,
+ std::atomic<bool>* stop)
+ : manager_(manager), token_(token), started_(started), stop_(stop) {}
+
+ void Run() override {
+ started_->Signal();
+ while (!stop_->load(std::memory_order_relaxed)) {
+ EXPECT_EQ(nullptr,
+ manager_->GetActivityUrlFilterForRenderFrameToken(token_));
+ }
+ }
+
+ private:
+ const raw_ptr<CastActivityUrlFilterManager> manager_;
+ const blink::LocalFrameToken token_;
Regression Test / PoC
diff --git a/chromecast/renderer/cast_activity_url_filter_manager_unittest.cc b/chromecast/renderer/cast_activity_url_filter_manager_unittest.cc
new file mode 100644
index 0000000..4dc89b4
--- /dev/null
+++ b/chromecast/renderer/cast_activity_url_filter_manager_unittest.cc
@@ -0,0 +1,95 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chromecast/renderer/cast_activity_url_filter_manager.h"
+
+#include <atomic>
+#include <vector>
+
+#include "base/memory/raw_ptr.h"
+#include "base/synchronization/lock.h"
+#include "base/synchronization/waitable_event.h"
+#include "base/thread_annotations.h"
+#include "base/threading/simple_thread.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/common/tokens/tokens.h"
+
+namespace chromecast {
+
+class CastActivityUrlFilterManagerTest : public testing::Test {
+ protected:
+ void AddFilterForToken(const blink::LocalFrameToken& token)
+ NO_THREAD_SAFETY_ANALYSIS {
+ base::AutoLock lock(manager_.filters_lock_);
+ manager_.activity_url_filters_.emplace(token, nullptr);
+ }
+
+ CastActivityUrlFilterManager manager_;
+};
+
+namespace {
+
+class LookupRunner : public base::DelegateSimpleThread::Delegate {
+ public:
+ LookupRunner(CastActivityUrlFilterManager* manager,
+ const blink::LocalFrameToken& token,
+ base::WaitableEvent* started,
+ std::atomic<bool>* stop)
+ : manager_(manager), token_(token), started_(started), stop_(stop) {}
+
+ void Run() override {
+ started_->Signal();
+ while (!stop_->load(std::memory_order_relaxed)) {
+ EXPECT_EQ(nullptr,
+ manager_->GetActivityUrlFilterForRenderFrameToken(token_));
+ }
+ }
+
+ private:
+ const raw_ptr<CastActivityUrlFilterManager> manager_;
+ const blink::LocalFrameToken token_;
+ const raw_ptr<base::WaitableEvent> started_;
+ const raw_ptr<std::atomic<bool>> stop_;
+};
+
+} // namespace
+
+TEST_F(CastActivityUrlFilterManagerTest, LookupForUnknownTokenReturnsNull) {
+ EXPECT_EQ(nullptr, manager_.GetActivityUrlFilterForRenderFrameToken(
+ blink::LocalFrameToken()));
+}
+
+// CastURLLoaderThrottleProvider clones may look up filters from worker
+// threads while frames are being created and destroyed on the main thread,
+// so the manager must allow concurrent lookups and map mutations.
+TEST_F(CastActivityUrlFilterManagerTest, ConcurrentLookupAndModification) {
+ constexpr size_t kEntryCount = 8;
+ std::vector<blink::LocalFrameToken> tokens(kEntryCount);
+ for (const auto& token : tokens) {
+ AddFilterForToken(token);
+ }
+
+ blink::LocalFrameToken missing_token;
+ base::WaitableEvent started;
+ std::atomic<bool> stop(false);
+ LookupRunner runner(&manager_, missing_token, &started, &stop);
+ base::DelegateSimpleThread thread(&runner, "FilterLookup");
+ thread.Start();
+ started.Wait();
+
+ constexpr size_t kIterations = 4096;
+ for (size_t i = 0; i < kIterations; ++i) {
+ AddFilterForToken(blink::LocalFrameToken());
+ }
+ for (const auto& token : tokens) {
+ manager_.OnRenderFrameRemoved(token);
+ }
+ EXPECT_EQ(nullptr,
+ manager_.GetActivityUrlFilterForRenderFrameToken(tokens[0]));
+
+ stop.store(true, std::memory_order_relaxed);
+ thread.Join();
+}
+
+} // namespace chromecast
Original Bug Report
Potential UAF and RCE in Chromecast renderer via Data Race in CastURLLoaderThrottleProvider
Flapjack (go/flapjack), an LLM-powered static analysis tool, has identified the following potential security issue.
Overview: A critical thread-safety violation exists in CastURLLoaderThrottleProvider where a background worker thread accesses CastActivityUrlFilterManager::activity_url_filters_ (a base::flat_map) without synchronization. This map is concurrently modified on the main thread when RenderFrames are destroyed, causing a Data Race and a Use-After-Free. Because the map stores raw UrlFilterReceiver* pointers instead of raw_ptr, the resulting UAF bypasses MiraclePtr (BRP) and could allow an attacker to achieve Arbitrary Code Execution in the renderer process.
Affected files:
chromecast/renderer/cast_url_loader_throttle_provider.ccchromecast/renderer/cast_activity_url_filter_manager.ccchromecast/common/activity_filtering_url_loader_throttle.cc
Estimated timestamp from git blame: 2023-11-07
Description
A critical thread-safety vulnerability exists in the Chromecast renderer implementation, specifically within CastURLLoaderThrottleProvider and CastActivityUrlFilterManager. This flaw leads to a Data Race and a potential Use-After-Free (UAF) that bypasses MiraclePtr (BRP) protections, potentially allowing an attacker to achieve Arbitrary Code Execution (RCE) in the sandboxed renderer process.
The vulnerability stems from CastURLLoaderThrottleProvider::CreateThrottles being invoked on background worker threads (e.g., when a DedicatedWorker calls fetch()). Inside this method, it accesses cast_activity_url_filter_manager_->GetActivityUrlFilterForRenderFrameToken(). This method subsequently searches activity_url_filters_, which is a base::flat_map<blink::LocalFrameToken, UrlFilterReceiver*>.
However, CastActivityUrlFilterManager is a main-thread object, and its activity_url_filters_ map is modified exclusively on the main thread when RenderFrames are created or destroyed (via OnRenderFrameCreated and OnRenderFrameRemoved). Because there is no synchronization (such as a base::Lock) protecting this flat_map, a worker thread calling find() can race with the main thread executing erase().
Impact and BRP Bypass
The activity_url_filters_ map stores raw UrlFilterReceiver* pointers. When an iframe is removed, the UrlFilterReceiver::OnDestruct() method executes delete this; on the main thread, immediately freeing the memory before the map is erased.
Because the map uses a raw C++ pointer rather than raw_ptr<UrlFilterReceiver>, MiraclePtr (BRP) does not quarantine the allocation. A worker thread racing to call find() can retrieve this dangling, freed pointer. Dereferencing it to call it->second->GetUrlFilter() retrieves an attacker-controlled ActivityUrlFilter*, which is then used by the networking stack. When ActivityFilteringURLLoaderThrottle::FilterURL is called, it executes virtual methods on the attacker-controlled object, leading to RCE.
Potential Attack Scenario
Note: These are suggested steps; a working Proof of Concept has not yet been developed to verify this sequence end-to-end.
- Setup: An attacker hosts a malicious page loaded by the Chromecast browser. The page dynamically creates an
iframeelement. - Worker Creation: The iframe executes JavaScript to spawn a DedicatedWorker (
new Worker('worker.js')). The renderer initializes the worker’sURLLoaderThrottleProvideron the main thread, passing it a raw pointer to theCastActivityUrlFilterManager. - The Race:
- On the worker thread, the JavaScript initiates a network request (
fetch()), which invokesCastURLLoaderThrottleProvider::CreateThrottles. - Concurrently, on the main thread, the attacker removes the iframe from the DOM (
iframe.remove()).
- On the worker thread, the JavaScript initiates a network request (
- UAF Triggered: The iframe removal destroys the
RenderFrame, executingUrlFilterReceiver::OnDestruct(), which callsdelete this;and frees the receiver’s memory. The worker thread’sfind()operation executes precisely at this moment, retrieving the dangling pointer from the unsynchronizedflat_map. - Exploitation: The attacker uses heap spraying on the main thread to reclaim the freed
UrlFilterReceivermemory with a forged object. The worker thread dereferences this fake object, retrieves a fakeActivityUrlFilter, and when the network request proceeds, the throttle executesurl_filter_->UrlMatchesWhitelist(url)on the attacker-controlled memory, hijacking the execution flow.
Suggested Fix
To resolve this vulnerability, the CastActivityUrlFilterManager must be made thread-safe for concurrent reads from worker threads.
- Add Synchronization: Introduce a
base::Lockorbase::ReadWriteLockwithinCastActivityUrlFilterManagerto protect all accesses (reads and writes) to theactivity_url_filters_map. - Use Smart Pointers: Change the map definition to store
std::unique_ptr<UrlFilterReceiver>orscoped_refptr<UrlFilterReceiver>to ensure the object remains alive while it is being accessed, or change it tobase::raw_ptr<UrlFilterReceiver>to enable MiraclePtr protections against the UAF.
Evaluated with Chrome root at commit: 9760e6c70cd33a320713361f17c6dcca85648c0f
Results from Flapjack so far have been promising, but it can be wrong in its deductions. At this time, it does not produce proof of concepts or fuzzer tests. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve Flapjack’s accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.