Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Chromecast
DescriptionUse after free in Chromecast
ComponentChromecast
Sub-componentChromecast
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable — upheld by independent review by AI
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.
Independent review by AIPlausible
Reviewer notes: Map holds raw UrlFilterReceiver* (diff), so erase() does not destroy the receiver. Lock only guards map access; returned pointer is used after unlock, so 'no dangling pointer' overstates the fix. Shown race is unsynchronized find vs emplace/erase.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker495515356
Fix commitb122b4a0d458 (chromium/src) +124/-4
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-03-23 (per vendor advisory)
Fix landed2026-07-28 (public git)
Disclosed2026-09-08
Patch gap42 days — fix public in source before the advisory

Background

`CastActivityUrlFilterManager`
a Chromecast renderer-process object that owns activity_url_filters_, a base::flat_map mapping each blink::LocalFrameToken to a UrlFilterReceiver* that supplies the frame’s ActivityUrlFilter.
`base::flat_map`
an associative container backed by a single sorted contiguous vector, so an emplace() or erase() can reallocate or shift its storage and invalidate any iterator or pointer another thread is dereferencing.
`CastURLLoaderThrottleProvider` / `CastWebSocketHandshakeThrottleProvider`
throttle providers that are cloned onto worker threads and call GetActivityUrlFilterForRenderFrameToken() off the main thread while the main thread creates and destroys frames.
`raw_ptr` / `GUARDED_BY`
a checked smart pointer (MiraclePtr/BackupRefPtr) and a Clang thread-safety annotation, respectively, used to harden the map’s stored pointers and to assert the map is only touched under filters_lock_.

Root Cause Analysis

Verify before building a PoC

This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
What the commit does not establish: Map holds raw UrlFilterReceiver* (diff), so erase() does not destroy the receiver. Lock only guards map access; returned pointer is used after unlock, so 'no dangling pointer' overstates the fix. Shown race is unsynchronized find vs emplace/erase.

GetActivityUrlFilterForRenderFrameToken() performed activity_url_filters_.find(frame_token) with no synchronization, and it is invoked from worker threads by the cloned throttle providers. Concurrently, the main thread mutated the same base::flat_map via the insertion path (emplace) and via OnRenderFrameRemoved() (erase), which reallocates/shifts the backing vector and destroys the removed UrlFilterReceiver. Because no lock serialized these accesses, a worker-thread lookup could observe a partially-mutated map or return a pointer into storage that had been reallocated or into a UrlFilterReceiver/ActivityUrlFilter that was being freed — a data race yielding a use-after-free.

The fix adds filters_lock_ and takes base::AutoLock in the lookup, the insertion path, and OnRenderFrameRemoved(), so every read and write of the map is mutually exclusive, and it stores entries as raw_ptr<UrlFilterReceiver> under a GUARDED_BY(filters_lock_) annotation for defense in depth and static enforcement. This works because the lookup can no longer run while the map’s storage is being resized or an entry destroyed.

Key insight
A container shared across threads was mutated on the main thread while being read lock-free on worker threads; adding a base::Lock guarding all three map accesses removes the race that let a lookup return a dangling pointer.

Attack Path

  1. Frame lifecycle churn on the main thread The renderer creates and destroys frames, invoking the map’s insertion path and OnRenderFrameRemoved(), which emplace/erase on activity_url_filters_ and free the associated UrlFilterReceiver.
  2. Concurrent lookup from a worker thread A cloned CastURLLoaderThrottleProvider or CastWebSocketHandshakeThrottleProvider calls GetActivityUrlFilterForRenderFrameToken() off-thread, doing a lock-free find() on the same map.
  3. Race window hit The lookup runs while the backing vector is being reallocated/shifted or an entry destroyed, so it reads torn state or returns a pointer to freed memory.
  4. Use-after-free The returned ActivityUrlFilter* (or the destroyed UrlFilterReceiver) is dereferenced after free, corrupting renderer-process memory. (inferred: attacker control over the freed-then-reused allocation to turn this into a read/write primitive.)

Impact Assessment

An attacker who can drive frame creation/destruction and network requests within a Chromecast renderer can win the race to trigger a use-after-free in the renderer process, corrupting memory. Exploitation requires the timing race between worker-thread lookups and main-thread map mutation, which occurs during normal frame churn under load. Beyond the memory-corruption primitive the patch demonstrates, escalation to code execution is (inferred); the advisory reports this only as a use-after-free in Chromecast with no in-the-wild status stated here.

Changed Functions

FunctionChangeNotes
UrlFilterReceiver
chromecast/renderer/cast_activity_url_filter_manager.h
modified
CastActivityUrlFilterManagerTest
chromecast/renderer/cast_activity_url_filter_manager_unittest.cc
modified
LookupRunner
chromecast/renderer/cast_activity_url_filter_manager_unittest.cc
modified
TEST_F
chromecast/renderer/cast_activity_url_filter_manager_unittest.cc
modified
for
chromecast/renderer/cast_activity_url_filter_manager_unittest.cc
modified
if
chromecast/BUILD.gn
modified
cast_source_set
chromecast/renderer/BUILD.gn
modified
test
chromecast/renderer/BUILD.gn
modified

Files Changed

  • chromecast/renderer/BUILD.gn
  • chromecast/renderer/cast_activity_url_filter_manager.cc
  • chromecast/BUILD.gn
  • chromecast/renderer/cast_activity_url_filter_manager_unittest.cc
  • chromecast/renderer/cast_activity_url_filter_manager.h

Audit Directions

  • Lock-free reads of main-thread-owned containers from cloned throttle providers
    audit other *ThrottleProvider clones and worker-thread callbacks that read renderer-side maps/lists (e.g. token-keyed lookups) without holding a lock.
  • `base::flat_map`/vector-backed containers shared across threads
    flag any such container mutated on one thread and read on another, since realloc/shift invalidates pointers and iterators; require a base::Lock with GUARDED_BY or thread-affinity confinement.
  • Raw pointers stored in cross-thread containers
    look for maps holding raw Foo* whose pointees are destroyed on removal; prefer raw_ptr/WeakPtr and verify the destruction is serialized against every reader.
From b122b4a0d4587946fd2bffdd5b7c72479d5668e5 Mon Sep 17 00:00:00 2001
From: Richard Nichols <[email protected]>
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 <[email protected]>
Reviewed-by: Simeon Anfinrud <[email protected]>
Commit-Queue: Richard Nichols <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1669459}
---
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_;
+  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
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" ]
 
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • chromecast/renderer/cast_activity_url_filter_manager.cc
  • chromecast/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.

  1. Setup: An attacker hosts a malicious page loaded by the Chromecast browser. The page dynamically creates an iframe element.
  2. Worker Creation: The iframe executes JavaScript to spawn a DedicatedWorker (new Worker('worker.js')). The renderer initializes the worker’s URLLoaderThrottleProvider on the main thread, passing it a raw pointer to the CastActivityUrlFilterManager.
  3. The Race:
    • On the worker thread, the JavaScript initiates a network request (fetch()), which invokes CastURLLoaderThrottleProvider::CreateThrottles.
    • Concurrently, on the main thread, the attacker removes the iframe from the DOM (iframe.remove()).
  4. UAF Triggered: The iframe removal destroys the RenderFrame, executing UrlFilterReceiver::OnDestruct(), which calls delete this; and frees the receiver’s memory. The worker thread’s find() operation executes precisely at this moment, retrieving the dangling pointer from the unsynchronized flat_map.
  5. Exploitation: The attacker uses heap spraying on the main thread to reclaim the freed UrlFilterReceiver memory with a forged object. The worker thread dereferences this fake object, retrieves a fake ActivityUrlFilter, and when the network request proceeds, the throttle executes url_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.

  1. Add Synchronization: Introduce a base::Lock or base::ReadWriteLock within CastActivityUrlFilterManager to protect all accesses (reads and writes) to the activity_url_filters_ map.
  2. Use Smart Pointers: Change the map definition to store std::unique_ptr<UrlFilterReceiver> or scoped_refptr<UrlFilterReceiver> to ensure the object remains alive while it is being accessed, or change it to base::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.

View on issue tracker