Medium chrome Memory Corruption 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in Core
DescriptionMemory corruption in Core
ComponentCore
Sub-componentMac
Bug ClassMemory Corruption
Tracker553159183
Fix commit1f563afaa47f (chromium/src) +72/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
TestMouseCaptureDelegate
content/browser/web_contents/web_contents_view_mac_unittest.mm
modified
WebContentsViewMacTest
content/browser/web_contents/web_contents_view_mac_unittest.mm
modified
TEST_F
content/browser/web_contents/web_contents_view_mac_unittest.mm
modified
if
content/test/BUILD.gn
modified

Files Changed

  • content/browser/web_contents/web_contents_view_mac.mm
  • content/browser/web_contents/web_contents_view_mac_unittest.mm
  • content/test/BUILD.gn
From 1f563afaa47fb4cbd0b3e7a56d8001c13ac5fc12 Mon Sep 17 00:00:00 2001
From: Victor Miura <[email protected]>
Date: Mon, 21 Sep 2026 16:14:35 -0700
Subject: [PATCH] [mac] Don't start web content drags during mouse capture

WebContentsViewMac::StartDragging refuses to start a dragging
session while a window move loop owns the held mouse button, but
not while a views widget owns it via mouse capture. A tab drag
only runs the move loop for its detached-window phase: dragging
tabs within the strip, and continuing the drag after the dragged
window attaches to another tab strip, hold mouse capture instead
(so do menus). A dragging session started mid-gesture would take
over the held mouse button and interfere with the gesture.

Refuse to start a dragging session while
CocoaMouseCapture::GetGlobalCaptureWindow() is non-nil, next to
the existing move loop check. Capture is released before
RunMoveLoop and re-acquired synchronously when the dragged
window re-attaches, so the two checks together cover the whole
gesture without overlapping. Web content drags are unaffected:
their mouse events are delivered to the RenderWidgetHostView's
NSView directly and never take views mouse capture.

TAG=agy
CONV=a8d563ec-9e59-4af7-87ea-349ca7292bd9

Bug: 553159183
Change-Id: Ia6b5560a7d50f84dd4c62b0b1976972e0dc88eef
Fixed: 553159183
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8433701
Auto-Submit: Victor Miura <[email protected]>
Reviewed-by: Avi Drissman <[email protected]>
Commit-Queue: Avi Drissman <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1702168}
---
diff --git a/content/browser/web_contents/web_contents_view_mac.mm b/content/browser/web_contents/web_contents_view_mac.mm
index 0950326..f6a08d0 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/mouse_capture.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"
@@ -234,6 +235,13 @@
     web_contents_->SystemDragEnded(source_rwh);
     return;
   }
+  // Likewise, a widget holding mouse capture (e.g. a tab strip during a tab
+  // drag, or an open menu) owns the held mouse button; refuse to start a
+  // dragging session that would interfere with its gesture.
+  if (remote_cocoa::CocoaMouseCapture::GetGlobalCaptureWindow()) {
+    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/content/browser/web_contents/web_contents_view_mac_unittest.mm b/content/browser/web_contents/web_contents_view_mac_unittest.mm
index c348ed8..f5a3746 100644
--- a/content/browser/web_contents/web_contents_view_mac_unittest.mm
+++ b/content/browser/web_contents/web_contents_view_mac_unittest.mm
@@ -15,6 +15,8 @@
 #include "base/functional/callback_helpers.h"
 #include "base/memory/weak_ptr.h"
 #include "base/test/run_until.h"
+#include "components/remote_cocoa/app_shim/mouse_capture.h"
+#include "components/remote_cocoa/app_shim/mouse_capture_delegate.h"
 #include "content/browser/renderer_host/render_widget_host_impl.h"
 #include "content/browser/web_contents/web_contents_impl.h"
 #import "content/browser/web_contents/web_drag_dest_mac.h"
@@ -49,6 +51,25 @@
   bool was_called_ = false;
 };
 
+// Minimal capture delegate standing in for a widget that has acquired mouse
+// capture (as the bridge of a views::Widget does on SetCapture()).
+class TestMouseCaptureDelegate
+    : public remote_cocoa::CocoaMouseCaptureDelegate {
+ public:
+  explicit TestMouseCaptureDelegate(NSWindow* window) : window_(window) {}
+
+  TestMouseCaptureDelegate(const TestMouseCaptureDelegate&) = delete;
+  TestMouseCaptureDelegate& operator=(const TestMouseCaptureDelegate&) = delete;
+
+  // remote_cocoa::CocoaMouseCaptureDelegate:
+  bool PostCapturedEvent(NSEvent* event) override { return false; }
+  void OnMouseCaptureLost() override {}
+  NSWindow* GetWindow() const override { return window_; }
+
+ private:
+  NSWindow* __strong window_;
+};
+
 class WebContentsViewMacTest : public RenderViewHostImplTestHarness {
  public:
   void SetUp() override {
@@ -126,6 +147,48 @@
   EXPECT_TRUE(fake_client().was_called());
 }
 
+// A drag must not start while a widget holds mouse capture: the held mouse
+// button is driving that widget's gesture (e.g. a tab drag or an open menu).
+TEST_F(WebContentsViewMacTest, StartDragging_RefusedWhileMouseCaptureHeld) {
+  CocoaTestHelperWindow* window = [[CocoaTestHelperWindow alloc] init];
+  TestMouseCaptureDelegate capture_delegate(window);
+  remote_cocoa::CocoaMouseCapture capture(&capture_delegate);
+  ASSERT_TRUE(remote_cocoa::CocoaMouseCapture::GetGlobalCaptureWindow() != nil);
+
+  DropData drop_data;
+  drop_data.text = u"test data";
+
+  view()->StartDragging(*main_rfh(), drop_data, blink::kDragOperationCopy,
+                        CreateValidDragImage(), gfx::Vector2d(), gfx::Rect(),
+                        blink::mojom::DragEventSourceInfo());
+
+  // The drag request must be rejected before it reaches the policy check.
+  EXPECT_FALSE(fake_client().was_called());
+  [window close];
+}
+
+// Once mouse capture is released, drag requests must go through again.
+TEST_F(WebContentsViewMacTest, StartDragging_AllowedAfterMouseCaptureReleased) {
+  CocoaTestHelperWindow* window = [[CocoaTestHelperWindow alloc] init];
+  {
+    TestMouseCaptureDelegate capture_delegate(window);
+    remote_cocoa::CocoaMouseCapture capture(&capture_delegate);
+    ASSERT_TRUE(remote_cocoa::CocoaMouseCapture::GetGlobalCaptureWindow() !=
+                nil);
+  }
+  ASSERT_TRUE(remote_cocoa::CocoaMouseCapture::GetGlobalCaptureWindow() == nil);
+
+  DropData drop_data;
+  drop_data.text = u"test data";
+
+  view()->StartDragging(*main_rfh(), drop_data, blink::kDragOperationCopy,
+                        CreateValidDragImage(), gfx::Vector2d(), gfx::Rect(),
+                        blink::mojom::DragEventSourceInfo());
+
+  EXPECT_TRUE(fake_client().was_called());
+  [window close];
+}
+
 TEST_F(WebContentsViewMacTest, InitiallyHiddenButPaintingNativeView) {
   WebContents::CreateParams params(browser_context());
   params.initially_hidden_but_painting = true;
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 8df08ed6..b25c211 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -3576,6 +3576,7 @@
   }
   if (is_mac) {
     deps += [
+      "//components/remote_cocoa/app_shim",
       "//crypto:test_support",
       "//device/fido:icloud_keychain_test_support",
       "//sandbox/mac:seatbelt",
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/web_contents/web_contents_view_mac_unittest.mm b/content/browser/web_contents/web_contents_view_mac_unittest.mm
index c348ed8..f5a3746 100644
--- a/content/browser/web_contents/web_contents_view_mac_unittest.mm
+++ b/content/browser/web_contents/web_contents_view_mac_unittest.mm
@@ -15,6 +15,8 @@
 #include "base/functional/callback_helpers.h"
 #include "base/memory/weak_ptr.h"
 #include "base/test/run_until.h"
+#include "components/remote_cocoa/app_shim/mouse_capture.h"
+#include "components/remote_cocoa/app_shim/mouse_capture_delegate.h"
 #include "content/browser/renderer_host/render_widget_host_impl.h"
 #include "content/browser/web_contents/web_contents_impl.h"
 #import "content/browser/web_contents/web_drag_dest_mac.h"
@@ -49,6 +51,25 @@
   bool was_called_ = false;
 };
 
+// Minimal capture delegate standing in for a widget that has acquired mouse
+// capture (as the bridge of a views::Widget does on SetCapture()).
+class TestMouseCaptureDelegate
+    : public remote_cocoa::CocoaMouseCaptureDelegate {
+ public:
+  explicit TestMouseCaptureDelegate(NSWindow* window) : window_(window) {}
+
+  TestMouseCaptureDelegate(const TestMouseCaptureDelegate&) = delete;
+  TestMouseCaptureDelegate& operator=(const TestMouseCaptureDelegate&) = delete;
+
+  // remote_cocoa::CocoaMouseCaptureDelegate:
+  bool PostCapturedEvent(NSEvent* event) override { return false; }
+  void OnMouseCaptureLost() override {}
+  NSWindow* GetWindow() const override { return window_; }
+
+ private:
+  NSWindow* __strong window_;
+};
+
 class WebContentsViewMacTest : public RenderViewHostImplTestHarness {
  public:
   void SetUp() override {
@@ -126,6 +147,48 @@
   EXPECT_TRUE(fake_client().was_called());
 }
 
+// A drag must not start while a widget holds mouse capture: the held mouse
+// button is driving that widget's gesture (e.g. a tab drag or an open menu).
+TEST_F(WebContentsViewMacTest, StartDragging_RefusedWhileMouseCaptureHeld) {
+  CocoaTestHelperWindow* window = [[CocoaTestHelperWindow alloc] init];
+  TestMouseCaptureDelegate capture_delegate(window);
+  remote_cocoa::CocoaMouseCapture capture(&capture_delegate);
+  ASSERT_TRUE(remote_cocoa::CocoaMouseCapture::GetGlobalCaptureWindow() != nil);
+
+  DropData drop_data;
+  drop_data.text = u"test data";
+
+  view()->StartDragging(*main_rfh(), drop_data, blink::kDragOperationCopy,
+                        CreateValidDragImage(), gfx::Vector2d(), gfx::Rect(),
+                        blink::mojom::DragEventSourceInfo());
+
+  // The drag request must be rejected before it reaches the policy check.
+  EXPECT_FALSE(fake_client().was_called());
+  [window close];
+}
+
+// Once mouse capture is released, drag requests must go through again.
+TEST_F(WebContentsViewMacTest, StartDragging_AllowedAfterMouseCaptureReleased) {
+  CocoaTestHelperWindow* window = [[CocoaTestHelperWindow alloc] init];
+  {
+    TestMouseCaptureDelegate capture_delegate(window);
+    remote_cocoa::CocoaMouseCapture capture(&capture_delegate);
+    ASSERT_TRUE(remote_cocoa::CocoaMouseCapture::GetGlobalCaptureWindow() !=
+                nil);
+  }
+  ASSERT_TRUE(remote_cocoa::CocoaMouseCapture::GetGlobalCaptureWindow() == nil);
+
+  DropData drop_data;
+  drop_data.text = u"test data";
+
+  view()->StartDragging(*main_rfh(), drop_data, blink::kDragOperationCopy,
+                        CreateValidDragImage(), gfx::Vector2d(), gfx::Rect(),
+                        blink::mojom::DragEventSourceInfo());
+
+  EXPECT_TRUE(fake_client().was_called());
+  [window close];
+}
+
 TEST_F(WebContentsViewMacTest, InitiallyHiddenButPaintingNativeView) {
   WebContents::CreateParams params(browser_context());
   params.initially_hidden_but_painting = true;
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 8df08ed6..b25c211 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -3576,6 +3576,7 @@
   }
   if (is_mac) {
     deps += [
+      "//components/remote_cocoa/app_shim",
       "//crypto:test_support",
       "//device/fido:icloud_keychain_test_support",
       "//sandbox/mac:seatbelt",
Loading diff…

Original Bug Report

reported by [email protected]

Tab drag hijacking on macOS via WebContentsViewMac move loop guard bypass

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: On macOS, WebContentsViewMac::StartDragging relies on CocoaWindowMoveLoop::IsActive() to prevent renderer-initiated drag sessions while a user is dragging a window. However, this guard is only active during the detached window move loop (kDraggingWindow), leaving within-strip tab reordering (kDraggingTabs) and post-reattach continuation phases completely unprotected. A compromised renderer could potentially invoke LocalFrameHost::StartDragging during these phases to hijack the held mouse button into an attacker-controlled NSDraggingSession.

Affected files:

  • content/browser/web_contents/web_contents_view_mac.mm
  • components/remote_cocoa/app_shim/window_move_loop.mm
  • chrome/browser/ui/views/tabs/dragging/tab_drag_controller.cc
  • ui/views/cocoa/drag_drop_client_mac.mm

Estimated timestamp from git blame: 2026-08-26

Summary

On macOS, WebContentsViewMac::StartDragging uses remote_cocoa::CocoaWindowMoveLoop::IsActive() to prevent renderer-initiated drag sessions from interfering with active user drag gestures. However, CocoaWindowMoveLoop::IsActive() is scoped strictly to the lifetime of CocoaWindowMoveLoop::Run(), which only executes during the detached window move phase (DragState::kDraggingWindow) of a tab drag. Other phases where the left mouse button is held down—namely within-strip tab reordering (DragState::kDraggingTabs) and post-reattach drag continuation (DragState::kWaitingToDragTabs -> DragState::kDraggingTabs)—run without a window move loop. Consequently, a compromised renderer can trigger LocalFrameHost::StartDragging to start a native NSDraggingSession that latches onto the physically held mouse button.

Root Cause Analysis

  1. Guard Scope (components/remote_cocoa/app_shim/window_move_loop.mm:98-127): CocoaWindowMoveLoop::IsActive() checks g_move_loop_active, which is set to true exclusively via base::AutoReset inside CocoaWindowMoveLoop::Run():

    bool CocoaWindowMoveLoop::Run() {
      CHECK(!g_move_loop_active);
      base::AutoReset<bool> active_resetter(&g_move_loop_active, true);
    
  2. Consumer Check (content/browser/web_contents/web_contents_view_mac.mm:214-223): WebContentsViewMac::StartDragging checks IsActive():

    if (remote_cocoa::CocoaWindowMoveLoop::IsActive()) {
      web_contents_->SystemDragEnded(source_rwh);
      return;
    }
    

    If IsActive() returns false and no prior web drag is active on this WebContents (drag_source_start_rwh_ is null), the call proceeds directly to in_process_ns_view_bridge_->StartDrag(...).

  3. Unprotected Tab Drag Phases (chrome/browser/ui/views/tabs/dragging/tab_drag_controller.cc):

    • Within-strip dragging (DragState::kDraggingTabs): Initiated at line 768 (StartDraggingTabsSession(true, point_in_screen)). No nested run loop is entered. Mouse capture on macOS (AttachImpl, line 1537) uses CocoaMouseCapture (components/remote_cocoa/app_shim/mouse_capture.mm:114-118), which registers passive local and global NSEvent monitors.
    • Post-reattach continuation: When a detached window is dragged back over a tab strip, DragBrowserToNewTabStrip (lines 1100-1132) calls browser_widget->EndMoveLoop(). When RunMoveLoop() returns (lines 1904-1917), g_move_loop_active resets to false, but the tab drag transitions back to kDraggingTabs and continues with the physical button still held.
  4. Renderer Initiation and Lack of Validation:

    • RenderFrameHostImpl::StartDragging (content/browser/renderer_host/render_frame_host_impl.cc:11328-11355) only verifies that the document is active (IsInactiveAndDisallowActivation), omitting visibility or gesture token checks.
    • RenderWidgetHostImpl::StartDragging (content/browser/renderer_host/render_widget_host_impl.cc:3037-3043) contains an explicit carve-out permitting javascript: URLs.
    • WebContentsViewCocoa (content/app_shim_remote_cocoa/web_contents_view_cocoa.mm:247-318) synthesizes an NSEventTypeLeftMouseDragged event from mouseLocationOutsideOfEventStream and calls [self beginDraggingSessionWithItems:draggingItems event:dragEvent source:self]. AppKit starts an OS-level dragging session that hooks the physically held button and diverts mouse events away from the tab strip’s event monitors.

Potential Impact

A compromised renderer process can hijack the user’s ongoing tab drag gesture. When the user releases the mouse button:

  • Bookmarklet Planting: If dropped over the bookmarks bar (directly adjacent to the tab strip), the un-filtered javascript: URL creates a bookmarklet that can execute attacker code in any origin when later clicked (UXSS).
  • Navigation / Cross-Origin Injection: If dropped on the tab strip or another WebContents, it can trigger arbitrary URL navigation or inject attacker DataTransfer payloads.

Suggested Reproduction Steps (Potential)

(Note: These are suggested/potential steps based on source code analysis; our tooling agent does not currently have the capability to execute code in a live macOS environment.)

  1. From a compromised renderer (or test harness calling LocalFrameHost::StartDragging), prepare a payload containing a javascript: URL or target URL.
  2. In the browser UI, initiate an ordinary tab reordering drag within the tab strip and hold the left mouse button.
  3. While the button is physically held down in DragState::kDraggingTabs, dispatch the LocalFrameHost::StartDragging IPC from the renderer.
  4. Observe whether WebContentsViewMac::StartDragging allows the request (since CocoaWindowMoveLoop::IsActive() is false) and starts an NSDraggingSession via beginDraggingSessionWithItems:event:source:.
  5. Release the mouse button over the bookmarks bar or tab strip to observe whether the dropped attacker payload is processed.

Suggested Fix

Rather than relying on CocoaWindowMoveLoop::IsActive(), guard drag initiation against any active tab dragging session across all drag states:

  1. Check a gesture-scoped flag (e.g., query TabDragController::IsActive() or a delegate interface on the browser window) in WebContentsViewMac::StartDragging and DragDropClientMac::StartDragAndDrop.
  2. Alternatively, require valid user-activation tokens or verify mouse button ownership before initiating an OS drag session via LocalFrameHost::StartDragging in the browser process.

Evaluated with Chrome root at commit: c6718ebbf2c677c6f63691e28c7801c563f6b19c


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.

View on issue tracker