CVE-2026-9886
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
imp_implementationWithBlockbase/mac/pasteboard_changed_observation.mm |
modified |
Files Changed
base/mac/pasteboard_changed_observation.hbase/mac/pasteboard_changed_observation.mm
Patch
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
Original Bug Report
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.
- The attacker script rapidly cycles
navigator.clipboard.addEventListenerandremoveEventListener. - Standard processing applied for propagating listener state to the Browser UI thread.
- Standard processing applied for ambient pasteboard invalidations triggering the macOS background queue.
- Concurrent
std::listdestruction and iteration overlap triggers the UAF, leading to execution hijacking viapolymorphic_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.