Critical watch CVSS 9.6 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Base
DescriptionUse after free in Base
ComponentBase
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.
Tracker508456788
Fix commit4492ca1c2c11 (chromium/src) +20/-12
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-01 (per vendor advisory)
Fix landed2026-05-05 (public git)
Disclosed2026-05-27
Patch gap22 days — fix public in source before the advisory

Background

Pasteboard observation
macOS has no public notification for NSPasteboard changes, so Chromium swizzles (overrides) an internal AppKit pasteboard method to learn when the general pasteboard changes.
Method swizzling / IMP
replacing an Objective-C method implementation (IMP) with a block so Chromium’s code runs whenever the internal pasteboard-change method is invoked.
Internal CFPasteboard dispatch queue
the swizzled method is invoked by AppKit on arbitrary internal threads/queues, not on Chromium’s main thread.
`GetCallbackList().Notify()`
runs the registered pasteboard-changed callbacks, which execute non-thread-safe Chromium code.

Root Cause Analysis

In base/mac/pasteboard_changed_observation.mm, the swizzled implementation block (installed within the function at the @@ -77,15 hunk, whose enclosing name is not shown — it is the swizzle-installation routine) called GetCallbackList().Notify() directly from inside the override, which AppKit invokes on “random internal AppKit threads” per the commit message. Only the secondary “dirty the cache” step was hopped to the main thread via dispatch_async; the actual callback notification ran on whatever internal CFPasteboard thread fired, even though the notified Chromium code is not thread-safe. Running that non-thread-safe code concurrently with main-thread activity is a data race, and the advisory classifies the resulting defect as a use-after-free in Base.

The fix wraps both the Notify() call and the cache-dirtying read of NSPasteboard.generalPasteboard.changeCount in a single base::BindOnce task posted via base::SingleThreadTaskRunner::GetMainThreadDefault()->PostTask, so all Chromium processing happens on the main thread. It also adds a CHECK that RegisterPasteboardChangedCallback is called on the main thread and drops the previous per-callback base::BindPostTask(SequencedTaskRunner::GetCurrentDefault(), callback) wrapper, since notification is now already marshaled to the main thread.

The restored invariant is that all pasteboard-change Chromium processing — registration and notification — occurs on the main thread.

Key insight
The core mistake was executing non-thread-safe Chromium callback code (GetCallbackList().Notify()) directly on AppKit’s internal pasteboard threads; the fix posts all of that processing to the main thread via GetMainThreadDefault()->PostTask and CHECKs that registration also happens there.

Attack Path

  1. Establish pasteboard observation Chromium swizzles the internal AppKit pasteboard method and registers callbacks via RegisterPasteboardChangedCallback; the swizzled override will fire on internal CFPasteboard threads whenever the general pasteboard changes.
  2. Cause pasteboard changes Any process on the system changing NSPasteboard.generalPasteboard (including this one) causes AppKit to invoke the swizzled method on an arbitrary internal thread.
  3. Run non-thread-safe code off-main-thread Pre-patch, the override called GetCallbackList().Notify() on that internal thread, executing non-thread-safe Chromium callbacks concurrently with main-thread work, producing a data race.
  4. Use-after-free (inferred mechanism) The concurrent access to non-thread-safe state yields the use-after-free the advisory reports; the diff shows the thread-safety violation but not a specific freed object or downstream primitive, which remain (inferred).

Impact Assessment

The bug causes non-thread-safe Chromium code to run on AppKit internal threads in response to pasteboard changes, leading to a data race / use-after-free in the browser process on macOS; the vendor advisory rates this critical and classifies it as a use-after-free in Base. Preconditions are the macOS platform with pasteboard observation active and a pasteboard change (which any process, including Chrome itself, can cause) arriving on an internal thread concurrently with main-thread activity. The diff establishes only the off-thread execution and race; the exact freed object and any exploitation primitive beyond a crash are (inferred), and the advisory does not state in-the-wild exploitation.

Changed Functions

FunctionChangeNotes
imp_implementationWithBlock
base/mac/pasteboard_changed_observation.mm
modified

Files Changed

  • base/mac/pasteboard_changed_observation.h
  • base/mac/pasteboard_changed_observation.mm

Audit Directions

  • Swizzled/overridden system methods
    Audit any Objective-C method swizzling that runs Chromium code inside the override, confirming the override marshals to the correct sequence before touching non-thread-safe state, since OS frameworks may call it on arbitrary threads.
  • Partial thread hops
    Flag code that posts only part of its work to the right thread (here, only the cache-dirty step) while running other callbacks inline; the thread-affinity guarantee must cover every Chromium call in the handler.
  • Callback lists invoked from framework threads
    Review CallbackList/observer notifications triggered by OS events (pasteboard, power, network, display changes) to ensure Notify() runs on the sequence the subscribers expect.
From 4492ca1c2c11ea98174d0433c699adb8d2040b32 Mon Sep 17 00:00:00 2001
From: Avi Drissman <avi@chromium.org>
Date: Tue, 05 May 2026 15:24:05 -0700
Subject: [PATCH] Do pasteboard observation on the main thread

When tracking pasteboard changes, the implementation overrides internal
pasteboard implementation methods that get called on random internal
AppKit threads. Be sure to do all Chromium processing back on the main
thread, as the code called isn't necessarily thread-safe.

Fixed: 508456788
Link: https://chromium-review.googlesource.com/id/I3e494418430189cc821f1c14fd1cfa466a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7806661
Reviewed-by: Mark Mentovai <mark@chromium.org>
Commit-Queue: Avi Drissman <avi@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1625756}
---

diff --git a/base/mac/pasteboard_changed_observation.h b/base/mac/pasteboard_changed_observation.h
index bea89ba..2d80ef5 100644
--- a/base/mac/pasteboard_changed_observation.h
+++ b/base/mac/pasteboard_changed_observation.h
@@ -14,6 +14,9 @@
 // Registers a callback that will be called whenever the
 // NSPasteboard.generalPasteboard has been changed by any process on the system
 // (including this one).
+//
+// This registration call must be made on the main thread, and the callback will
+// be done on the main thread.
 BASE_EXPORT CallbackListSubscription
 RegisterPasteboardChangedCallback(RepeatingClosure callback);
 
diff --git a/base/mac/pasteboard_changed_observation.mm b/base/mac/pasteboard_changed_observation.mm
index 04c0d35..9868a87 100644
--- a/base/mac/pasteboard_changed_observation.mm
+++ b/base/mac/pasteboard_changed_observation.mm
@@ -11,10 +11,11 @@
 #include <string_view>
 
 #include "base/callback_list.h"
+#include "base/functional/bind.h"
 #include "base/functional/callback.h"
+#include "base/location.h"
 #include "base/no_destructor.h"
-#include "base/task/bind_post_task.h"
-#include "base/task/sequenced_task_runner.h"
+#include "base/task/single_thread_task_runner.h"
 
 // There is no notification API on macOS for changes to the pasteboard (unlike
 // on iOS where there is UIPasteboardChangedNotification). However...
@@ -77,15 +78,17 @@
 
   IMP new_imp =
       imp_implementationWithBlock(^(id object_self, int change_count) {
-        GetCallbackList().Notify();
+        // Hop to the main thread, as the cache is processed on an internal
+        // CFPasteboard dispatch queue.
+        base::SingleThreadTaskRunner::GetMainThreadDefault()->PostTask(
+            FROM_HERE, base::BindOnce([] {
+              GetCallbackList().Notify();
 
-        // Dirty the app's pasteboard cache to ensure an invalidation callback
-        // for the next pasteboard change that occurs in other apps. Hop to the
-        // main thread, as the cache is processed on an internal CFPasteboard
-        // dispatch queue.
-        dispatch_async(dispatch_get_main_queue(), ^{
-          std::ignore = NSPasteboard.generalPasteboard.changeCount;
-        });
+              // Dirty the app's pasteboard cache to ensure an invalidation
+              // callback for the next pasteboard change that occurs in other
+              // apps.
+              std::ignore = NSPasteboard.generalPasteboard.changeCount;
+            }));
 
         g_old_imp(object_self, selector, change_count);
       });
@@ -100,6 +103,9 @@
 
 CallbackListSubscription RegisterPasteboardChangedCallback(
     RepeatingClosure callback) {
+  CHECK(base::SingleThreadTaskRunner::GetMainThreadDefault()
+            ->BelongsToCurrentThread());
+
   static bool swizzle_internal_class [[maybe_unused]] = SwizzleInternalClass();
   // Intentionally DCHECK so that in the field it doesn't rely on that specific
   // internal class (as listening for pasteboard changes isn't critical), but
@@ -107,8 +113,7 @@
   // ever change it will be noticed.
   DCHECK(swizzle_internal_class);
 
-  return GetCallbackList().Add(
-      base::BindPostTask(SequencedTaskRunner::GetCurrentDefault(), callback));
+  return GetCallbackList().Add(callback);
 }
 
 }  // namespace base
Loading diff…

Original Bug Report

reported by li...@chromium.org

Potential Use-After-Free in Browser via base::mac::PasteboardChangedObserver

Flapjack, 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 Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential Use-After-Free (UAF) exists in the Browser process due to a thread-safety violation in base::mac::PasteboardChangedObserver. Concurrent access to an unsynchronized CallbackList from the UI thread and a macOS background dispatch queue corrupts underlying memory. This could allow a full sandbox escape and arbitrary Remote Code Execution (RCE).

Affected files:

  • base/mac/pasteboard_changed_observation.mm

Estimated timestamp from git blame: 2025-06-23

Conclusion: This report details a potential Use-After-Free (UAF) vulnerability in the Browser process, yielding a highly probable sandbox escape and Remote Code Execution (RCE).

Analysis: The initial logic and parameters regarding DOM-based clipboard event handling and Mojo IPC propagation to the Browser process are validated. ClipboardHostImpl correctly manages observer registration via base::RegisterPasteboardChangedCallback on the UI thread. Concurrently, macOS system pasteboard invalidations correctly trigger the swizzled _CFPasteboardCache setChangeCount: method on an internal background CFPasteboard dispatch queue.

Leaping directly to the final memory transformation: the concurrent execution of UI thread CancelCallback mutations and background thread Notify() iterations fundamentally corrupts the unsynchronized std::list within CallbackListBase. Because the std::list nodes and scoped_refptr<BindStateBase> utilize RAW_PTR_EXCLUSION, MiraclePtr (BackupRefPtr) is completely bypassed. Dereferencing the dangling iterator grants the attacker direct control over the hijacked polymorphic_invoke_ function pointer upon Run(), enabling instantaneous RCE in the Browser process.

Suggested Steps to Trigger (Theoretical): Note: These are potential steps as our tooling agent does not yet have the ability to run code.

  1. The attacker script rapidly cycles navigator.clipboard.addEventListener and removeEventListener.
  2. Standard processing applied for propagating listener state to the Browser UI thread.
  3. Standard processing applied for ambient pasteboard invalidations triggering the macOS background queue.
  4. Concurrent std::list destruction and iteration overlap triggers the UAF, leading to execution hijacking via polymorphic_invoke_.

Suggested Fix: Move the GetCallbackList().Notify() call inside the existing dispatch_async(dispatch_get_main_queue(), ...) block in base/mac/pasteboard_changed_observation.mm to ensure all list accesses remain synchronized on the main thread.

Evaluated with Chrome root at commit: cc901875d53bf4e4fe0e01f02843871da4106e70


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