CVE-2026-95371
Overview
Background
- `CocoaWindowMoveLoop`
- A
remote_cocoahelper that spins a nested run loop on macOS to drive a window/tab move-drag until the held mouse button is released. - Nested run loop
- A
base::RunLooprun inside the browser’s normal message loop, during which the browser process keeps pumping OS events (e.g. while a tab is being dragged) instead of returning to the top-level loop. - Dragging session
- A macOS drag started via
-[NSView beginDraggingSessionWithItems:event:source:], which consumes the currently held mouse button and generates its own event stream that follows the cursor. - Compromised renderer
- A render process controlled by an attacker that can issue Mojo IPCs to the browser process, including requests to begin a drag on behalf of web content.
Root Cause Analysis
While a CocoaWindowMoveLoop is running its nested run loop, the browser process already owns the held mouse button for the in-progress window/tab move. A compromised render process could nonetheless request that the browser begin a web-content dragging session, and the code in content/browser/web_contents/web_contents_view_mac.mm and ui/views/cocoa/drag_drop_client_mac.mm would call beginDraggingSessionWithItems:event:source: unconditionally, injecting a fake mouse event that replaces the one that should be following the cursor during the move. The violated invariant is that only one owner may hold and drive the mouse button at a time; nothing checked whether a move loop was active before starting a new drag.
The fix introduces a process-global CocoaWindowMoveLoop::IsActive() flag (set via base::AutoReset<bool> for the duration of Run()) and makes both drag-start paths consult it, refusing to begin a dragging session while a move loop is active. This closes the window in which a renderer-triggered drag could hijack the move loop’s event stream.
IsActive() guard consulted at every drag-start entry point.Attack Path
- Trigger a move loop
The user (or an attacker-influenced action) starts a window/tab drag, causing the browser to enter
CocoaWindowMoveLoop::Run()and spin a nested run loop while holding the mouse button. - Renderer requests a drag
During that nested loop, a compromised render process issues a drag-start IPC, reaching
WebContentsViewMac::StartDragging/DragDropClientMac::StartDragAndDropin the browser process. - Inject a fake event
The unpatched code calls
beginDraggingSessionWithItems:event:source:, letting the renderer supply an event that replaces the one that should follow the cursor during the active move. (inferred) - Hijack the move loop The injected drag interferes with the move loop’s mouse-button ownership, allowing the cursor-following event stream to be manipulated in a way the user did not intend. (inferred)
Impact Assessment
CocoaWindowMoveLoop is active (e.g. an in-progress tab/window drag) and the renderer is already compromised enough to issue drag IPCs. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 5.4. Any further escalation such as UI spoofing or unintended user action beyond the drag hijack is (inferred), and the advisory states no exploited-in-the-wild or severity beyond the given CVE record.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/web_contents/web_contents_view_mac.mm |
modified | |
TEST_Fui/views/cocoa/drag_drop_client_mac_unittest.mm |
modified | |
EXPECT_TRUEui/views/cocoa/drag_drop_client_mac_unittest.mm |
modified |
Files Changed
content/browser/web_contents/web_contents_view_mac.mmui/views/cocoa/drag_drop_client_mac_unittest.mmui/views/cocoa/drag_drop_client_mac.mmcomponents/remote_cocoa/app_shim/window_move_loop.hcomponents/remote_cocoa/app_shim/window_move_loop.mm
Audit Directions
- Unguarded event-stream ownershipLook for other entry points that call
beginDraggingSessionWithItems:or otherwise assume exclusive control of the held mouse button without checking for an active nested run loop. - Renderer-reachable native operationsAudit browser-process IPC handlers that invoke platform UI/input APIs, verifying they validate the current UI state before acting on renderer requests.
- Nested-run-loop reentrancySearch for nested
base::RunLoopusage (drag, move, modal loops) where concurrently triggered operations could interfere with the loop’s assumed invariants.
Patch
From 4162d19bbe0ccd1f3211e1b4077868b25efae210 Mon Sep 17 00:00:00 2001 From: Keren Zhu <[email protected]> Date: Wed, 26 Aug 2026 13:57:59 -0700 Subject: [PATCH] [mac] Reject drag sessions while a window move loop is active It is currently possible for a compromised render process to start a dragging session while the browser process is in a nested run loop ( e.g. during tab dragging). If this happens, the render process can inject a fake mouse events using -[NSView beginDraggingSessionWithItems:event:source:], replacing the event that should follow the cursor. This CL fixes the loophole by adding a global CocoaWindowMoveLoop::IsActive() that returns true while a nested run loop is active and rejecting the render process's dragging request if so. Fixed: 520504291 Change-Id: I36638d904a651a32a9edb0fb9494e722d57fc798 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8293981 Reviewed-by: Avi Drissman <[email protected]> Commit-Queue: Keren Zhu <[email protected]> Cr-Commit-Position: refs/heads/main@{#1686657} --- diff --git a/components/remote_cocoa/app_shim/window_move_loop.h b/components/remote_cocoa/app_shim/window_move_loop.h index 4b5ab0c..9880cbc4 100644 --- a/components/remote_cocoa/app_shim/window_move_loop.h +++ b/components/remote_cocoa/app_shim/window_move_loop.h @@ -27,6 +27,11 @@ ~CocoaWindowMoveLoop(); + // Returns true if any window in this process is currently inside Run(). The + // move loop tracks the held mouse button, so callers that would otherwise + // start a dragging session should refuse to do so while this returns true. + static bool IsActive(); + // Initiates the drag until a mouse up event is observed, or End() is called. // Returns true if a mouse up event ended the loop. bool Run(); diff --git a/components/remote_cocoa/app_shim/window_move_loop.mm b/components/remote_cocoa/app_shim/window_move_loop.mm index 8398901..61b2c90d 100644 --- a/components/remote_cocoa/app_shim/window_move_loop.mm +++ b/components/remote_cocoa/app_shim/window_move_loop.mm @@ -9,6 +9,7 @@ #include <utility> #include <vector> +#include "base/auto_reset.h" #include "base/run_loop.h" #include "base/strings/stringprintf.h" #import "components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h" @@ -94,6 +95,15 @@ namespace remote_cocoa { +namespace { +bool g_move_loop_active = false; +} // namespace + +// static +bool CocoaWindowMoveLoop::IsActive() { + return g_move_loop_active; +} + CocoaWindowMoveLoop::CocoaWindowMoveLoop(NativeWidgetNSWindowBridge* owner, const NSPoint& initial_mouse_in_screen) : owner_(owner), @@ -111,6 +121,11 @@ } bool CocoaWindowMoveLoop::Run() { + CHECK(!g_move_loop_active); + // |this| may be deleted while the nested run loop below is spinning, so this + // must not refer to any member state on destruction. + base::AutoReset<bool> active_resetter(&g_move_loop_active, true); + LoopExitReason exit_reason = ENDED_EXTERNALLY; exit_reason_ref_ = &exit_reason; NSWindow* window = owner_->ns_window(); diff --git a/content/browser/web_contents/web_contents_view_mac.mm b/content/browser/web_contents/web_contents_view_mac.mm index 6019667f..83db25b5 100644 --- a/content/browser/web_contents/web_contents_view_mac.mm +++ b/content/browser/web_contents/web_contents_view_mac.mm @@ -20,6 +20,7 @@ #include "base/task/current_thread.h" #include "base/task/thread_pool.h" #include "base/threading/thread_restrictions.h" +#include "components/remote_cocoa/app_shim/window_move_loop.h" #include "components/remote_cocoa/browser/ns_view_ids.h" #include "components/remote_cocoa/common/application.mojom.h" #include "content/app_shim_remote_cocoa/web_contents_ns_view_bridge.h" @@ -214,6 +215,12 @@ if (drag_source_start_rwh_) { return; } + // A window move loop already owns the held mouse button; refuse to start a + // dragging session that would interfere with it. + if (remote_cocoa::CocoaWindowMoveLoop::IsActive()) { + web_contents_->SystemDragEnded(source_rwh); + return; + } url::Origin source_origin = source_rfh.GetLastCommittedOrigin(); // By allowing nested tasks, the code below also allows Close(), // which would deallocate |this|. The same problem can occur while diff --git a/ui/views/cocoa/drag_drop_client_mac.mm b/ui/views/cocoa/drag_drop_client_mac.mm index b7aa80c..2a583d1 100644 --- a/ui/views/cocoa/drag_drop_client_mac.mm +++ b/ui/views/cocoa/drag_drop_client_mac.mm @@ -9,6 +9,7 @@ #include "base/strings/sys_string_conversions.h" #import "components/remote_cocoa/app_shim/bridged_content_view.h" #import "components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h" +#include "components/remote_cocoa/app_shim/window_move_loop.h" #include "ui/base/dragdrop/drag_drop_types.h" #include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h" #import "ui/base/dragdrop/os_exchange_data_provider_mac.h" @@ -31,6 +32,12 @@ std::unique_ptr<ui::OSExchangeData> data, int operation, ui::mojom::DragEventSource source) { + // A window move loop already owns the held mouse button; starting a dragging + // session here would interfere with it. + if (remote_cocoa::CocoaWindowMoveLoop::IsActive()) { + return; + } + exchange_data_ = std::move(data); source_operation_ = operation; is_drag_source_ = true; diff --git a/ui/views/cocoa/drag_drop_client_mac_unittest.mm b/ui/views/cocoa/drag_drop_client_mac_unittest.mm index 9c7bf093..15de877 100644 --- a/ui/views/cocoa/drag_drop_client_mac_unittest.mm +++ b/ui/views/cocoa/drag_drop_client_mac_unittest.mm @@ -11,6 +11,7 @@ #include "base/memory/raw_ptr.h" #include "base/strings/utf_string_conversions.h" #include "base/task/single_thread_task_runner.h" +#include "base/test/run_until.h" #import "components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h" #import "ui/base/clipboard/clipboard_util_mac.h" #import "ui/base/dragdrop/drag_drop_types.h" @@ -26,6 +27,10 @@ #include "ui/views/widget/native_widget_mac.h" #include "ui/views/widget/widget.h" +namespace { +int g_begin_dragging_session_call_count = 0; +} // namespace + @interface NSView (DragSessionTestingDonor) @end @@ -34,6 +39,7 @@ event:(NSEvent*)event source:(id<NSDraggingSource>) source { + ++g_begin_dragging_session_call_count; return nil; } @end @@ -292,6 +298,45 @@ EXPECT_FALSE(ns_window_host()->IsMouseCaptureActive()); } +// A drag should not start while a window move loop is running for any window +// in the process. The move loop already owns the held mouse button and a +// concurrent dragging session would interfere with it. +TEST_F(DragDropClientMacTest, DragRejectedDuringWindowMoveLoop) { + std::unique_ptr<OSExchangeData> data = std::make_unique<OSExchangeData>(); + data->SetString(u"text"); + data->provider().SetDragImage(gfx::test::CreateImageSkia(100, 100), + gfx::Vector2d()); + + // There's no way to cleanly stop NSDraggingSession inside unit tests, so just + // don't start it at all. + g_begin_dragging_session_call_count = 0; + base::apple::ScopedObjCClassSwizzler swizzle( + [NSView class], @selector(beginDraggingSessionWithItems:event:source:), + @selector(cr_beginDraggingSessionWithItems:event:source:)); + + // Attempt to start a drag from inside the move loop and then end the move + // loop. + bool drag_attempted = false; + base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( + FROM_HERE, + base::BindOnce( + [](DragDropClientMac* client, + remote_cocoa::NativeWidgetNSWindowBridge* bridge, + std::unique_ptr<OSExchangeData> data, bool* drag_attempted) { + client->StartDragAndDrop(std::move(data), + ui::DragDropTypes::DRAG_COPY, + ui::mojom::DragEventSource::kMouse); + *drag_attempted = true; + bridge->EndMoveLoop(); + }, + drag_drop_client(), bridge(), std::move(data), &drag_attempted)); + + bridge()->RunMoveLoop(gfx::Vector2d()); + + EXPECT_TRUE(base::test::RunUntil([&]() { return drag_attempted; })); + EXPECT_EQ(0, g_begin_dragging_session_call_count); +} + // Tests if the drag and drop target rejects the dropped data with the // incorrect format. TEST_F(DragDropClientMacTest, InvalidFormatDragDrop) {
Regression Test / PoC
diff --git a/ui/views/cocoa/drag_drop_client_mac_unittest.mm b/ui/views/cocoa/drag_drop_client_mac_unittest.mm
index 9c7bf093..15de877 100644
--- a/ui/views/cocoa/drag_drop_client_mac_unittest.mm
+++ b/ui/views/cocoa/drag_drop_client_mac_unittest.mm
@@ -11,6 +11,7 @@
#include "base/memory/raw_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
+#include "base/test/run_until.h"
#import "components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h"
#import "ui/base/clipboard/clipboard_util_mac.h"
#import "ui/base/dragdrop/drag_drop_types.h"
@@ -26,6 +27,10 @@
#include "ui/views/widget/native_widget_mac.h"
#include "ui/views/widget/widget.h"
+namespace {
+int g_begin_dragging_session_call_count = 0;
+} // namespace
+
@interface NSView (DragSessionTestingDonor)
@end
@@ -34,6 +39,7 @@
event:(NSEvent*)event
source:(id<NSDraggingSource>)
source {
+ ++g_begin_dragging_session_call_count;
return nil;
}
@end
@@ -292,6 +298,45 @@
EXPECT_FALSE(ns_window_host()->IsMouseCaptureActive());
}
+// A drag should not start while a window move loop is running for any window
+// in the process. The move loop already owns the held mouse button and a
+// concurrent dragging session would interfere with it.
+TEST_F(DragDropClientMacTest, DragRejectedDuringWindowMoveLoop) {
+ std::unique_ptr<OSExchangeData> data = std::make_unique<OSExchangeData>();
+ data->SetString(u"text");
+ data->provider().SetDragImage(gfx::test::CreateImageSkia(100, 100),
+ gfx::Vector2d());
+
+ // There's no way to cleanly stop NSDraggingSession inside unit tests, so just
+ // don't start it at all.
+ g_begin_dragging_session_call_count = 0;
+ base::apple::ScopedObjCClassSwizzler swizzle(
+ [NSView class], @selector(beginDraggingSessionWithItems:event:source:),
+ @selector(cr_beginDraggingSessionWithItems:event:source:));
+
+ // Attempt to start a drag from inside the move loop and then end the move
+ // loop.
+ bool drag_attempted = false;
+ base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+ FROM_HERE,
+ base::BindOnce(
+ [](DragDropClientMac* client,
+ remote_cocoa::NativeWidgetNSWindowBridge* bridge,
+ std::unique_ptr<OSExchangeData> data, bool* drag_attempted) {
+ client->StartDragAndDrop(std::move(data),
+ ui::DragDropTypes::DRAG_COPY,
+ ui::mojom::DragEventSource::kMouse);
+ *drag_attempted = true;
+ bridge->EndMoveLoop();
+ },
+ drag_drop_client(), bridge(), std::move(data), &drag_attempted));
+
+ bridge()->RunMoveLoop(gfx::Vector2d());
+
+ EXPECT_TRUE(base::test::RunUntil([&]() { return drag_attempted; }));
+ EXPECT_EQ(0, g_begin_dragging_session_call_count);
+}
+
// Tests if the drag and drop target rejects the dropped data with the
// incorrect format.
TEST_F(DragDropClientMacTest, InvalidFormatDragDrop) {
Original Bug Report
Potential gesture hijack during macOS tab dragging due to re-entrant nested RunLoop
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: The macOS tab-detach window-move loop runs a nested C++ RunLoop that can process incoming Mojo IPCs from compromised renderers. An attacker-controlled renderer in any visible WebContents can potentially trigger an OS-level drag session with an arbitrary payload while a user tab drag is active. This allows the attacker to hijack the active mouse grab and force the user to drop a malicious payload onto external drop targets.
Affected files:
components/remote_cocoa/app_shim/window_move_loop.mmcontent/browser/web_contents/web_contents_view_mac.mmcontent/app_shim_remote_cocoa/web_contents_view_cocoa.mmcontent/app_shim_remote_cocoa/web_contents_ns_view_bridge.mm
Estimated timestamp from git blame: 2016-06-17
Detailed Description
There is a potential gesture-hijack vulnerability on macOS during tab-detaching window dragging operations. When a user drags a tab off the tab strip to detach it into a new window, the browser process enters a nested event loop driven by CocoaWindowMoveLoop::Run (components/remote_cocoa/app_shim/window_move_loop.mm).
Because this nested C++ loop is entered directly from native event dispatching ([NSApp sendEvent:]) rather than inside an active DoWork scheduler task, the browser process scheduler’s main_thread_only().task_execution_allowed remains true. As a result, the nested Type::kDefault base::RunLoop does not prevent the dispatching of Mojo IPC messages (which are executed as application tasks).
Consequently, a compromised renderer in any visible WebContents can potentially invoke the LocalFrameHost::StartDragging Mojo IPC. Since the browser-level tab drag does not set the per-WebContents drag_source_start_rwh_ guard for other tabs, the reentrancy check in WebContentsViewMac::StartDragging is successfully bypassed. This allows the attacker-controlled renderer to programmatically initiate a native Cocoa dragging session (beginDraggingSessionWithItems:) using synthesized events. The macOS WindowServer then shifts the active physical mouse drag stream to the new dragging session, freezing the tab drag and forcing the user to inadvertently drop the attacker’s payload when they release the mouse button.
Note: These are potential steps. Our tooling does not yet have the capability to run code or execute a functional proof of concept on macOS.
Potential Trigger Steps
- The user left-clicks a tab and drags it to detach it, causing
TabDragControllerto enter the native move loop viaCocoaWindowMoveLoop::Run(). - Simultaneously, a compromised renderer in a second visible tab sends a
LocalFrameHost::StartDraggingMojo IPC with a custom payload (e.g., text, HTML, or file promises). - The browser process receives and dispatches this IPC within the nested
RunLoop. WebContentsViewMac::StartDraggingreceives the request. Since its localdrag_source_start_rwh_is null, it passes the reentrancy check and routes the drag to AppKit.WebContentsViewCocoasynthesizes a mouse-dragged event and callsbeginDraggingSessionWithItems:event:source:.- The macOS WindowServer transfers the physical drag state to the new session. The user’s physical mouse-up action completes the drop of the attacker’s payload onto whatever OS target is under the cursor.
Suggested Fix
To address this issue, introduce a process-global drag-reentrancy or move-loop guard on macOS, similar to the existing desktop guards on other platforms:
- On Windows,
DesktopDragDropClientWinchecksdesktop_host_->IsInNativeMoveResizeLoop()before starting a drag. - On Ozone, a process-global
g_is_draggingflag is evaluated and set inDesktopDragDropClientOzone.
A similar global tracking mechanism should be implemented in NativeWidgetNSWindowBridge or CocoaWindowMoveLoop to reject incoming StartDragging requests from any WebContents while a window move loop is actively running on macOS.
Evaluated with Chrome root at commit: e9507a33bb4148ee071aaaf8a7e9ad68770359bf
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.