CVE-2026-12463
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ScopedSuppressForWindowMoveui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.cc |
modified | |
TEST_Fui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.cc |
modified |
Files Changed
ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.ccui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.hui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.ccui/views/widget/desktop_aura/desktop_window_tree_host_platform.cc
Patch
From 188e278c80a7e2bb03c0f13068163df8de5e858f Mon Sep 17 00:00:00 2001
From: Tom Anderson <thomasanderson@chromium.org>
Date: Tue, 09 Jun 2026 16:33:36 -0700
Subject: [PATCH] [Ozone] Suppress data drag during window move loop
DesktopWindowTreeHostPlatform::RunMoveLoop runs a nested event loop
allowing nestable tasks. While the loop is active, a compromised
renderer could start a data drag, release pointer grab/capture, take
ownership of the global XdndSelection selection atom, and substitute its
own DnD payload.
To protect against this, this CL introduces ScopedSuppressForWindowMove
under DesktopDragDropClientOzone, which sets the existing process-global
flag g_is_dragging. This flag is held for the duration of the window
move loop and used to reject re-entrant data drag starts.
Additionally, this CL adds a DCHECK(!g_is_dragging) in
ScopedSuppressForWindowMove to assert that a drag-and-drop session is
not already active when a window-move loop is entered.
Fixed: 518042749
Change-Id: I1dbe0fe181d7b65be457a32c4400688178bd5136
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7900333
Auto-Submit: Thomas Anderson <thomasanderson@chromium.org>
Reviewed-by: Mike Wasserman <msw@chromium.org>
Commit-Queue: Mike Wasserman <msw@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1644315}
---
diff --git a/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.cc b/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.cc
index d341b848..123561d 100644
--- a/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.cc
+++ b/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.cc
@@ -133,6 +133,13 @@
ui::WmDragHandler* drag_handler)
: root_window_(root_window), drag_handler_(drag_handler) {}
+// static
+base::AutoReset<bool>
+DesktopDragDropClientOzone::ScopedSuppressForWindowMove() {
+ DCHECK(!g_is_dragging);
+ return base::AutoReset<bool>(&g_is_dragging, true);
+}
+
DesktopDragDropClientOzone::~DesktopDragDropClientOzone() {
ResetDragDropTarget();
observers_.Notify(
diff --git a/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.h b/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.h
index 5a14b467..b46de97 100644
--- a/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.h
+++ b/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.h
@@ -7,6 +7,7 @@
#include <memory>
+#include "base/auto_reset.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
@@ -50,6 +51,11 @@
~DesktopDragDropClientOzone() override;
+ // Called by the window-move (tab-drag) entry point so that a
+ // renderer-initiated data drag cannot start inside the window-move's nested
+ // kNestableTasksAllowed RunLoop. See StartDragAndDrop.
+ [[nodiscard]] static base::AutoReset<bool> ScopedSuppressForWindowMove();
+
protected:
friend class DesktopDragDropClientOzoneTest;
diff --git a/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.cc b/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.cc
index 769cd0b..1f6b3d5f7 100644
--- a/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.cc
+++ b/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.cc
@@ -591,4 +591,14 @@
EXPECT_EQ(DragOperation::kCopy, operation);
}
+TEST_F(DesktopDragDropClientOzoneTest, RejectDragDuringWindowMove) {
+ // Simulate that a window-move loop is in progress.
+ auto suppress_drag =
+ DesktopDragDropClientOzone::ScopedSuppressForWindowMove();
+
+ // Attempt to start a drag operation. It should be rejected and return kNone.
+ DragOperation operation = StartDragAndDrop(ui::DragDropTypes::DRAG_COPY);
+ EXPECT_EQ(DragOperation::kNone, operation);
+}
+
} // namespace views
diff --git a/ui/views/widget/desktop_aura/desktop_window_tree_host_platform.cc b/ui/views/widget/desktop_aura/desktop_window_tree_host_platform.cc
index b953949d0..d538135 100644
--- a/ui/views/widget/desktop_aura/desktop_window_tree_host_platform.cc
+++ b/ui/views/widget/desktop_aura/desktop_window_tree_host_platform.cc
@@ -770,6 +770,11 @@
const gfx::Vector2d& drag_offset,
Widget::MoveLoopSource source,
Widget::MoveLoopEscapeBehavior escape_behavior) {
+ // The window-move loop runs a kNestableTasksAllowed RunLoop that pumps Mojo
+ // IPC; suppress renderer-initiated data drags for its duration so a
+ // compromised renderer in another window cannot hijack the user's gesture.
+ auto suppress_data_drag =
+ DesktopDragDropClientOzone::ScopedSuppressForWindowMove();
auto* move_loop_handler = ui::GetWmMoveLoopHandler(*platform_window());
if (move_loop_handler && move_loop_handler->RunMoveLoop(drag_offset)) {
return Widget::MoveLoopResult::kSuccessful;
Regression Test / PoC
diff --git a/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.cc b/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.cc
index 769cd0b..1f6b3d5f7 100644
--- a/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.cc
+++ b/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.cc
@@ -591,4 +591,14 @@
EXPECT_EQ(DragOperation::kCopy, operation);
}
+TEST_F(DesktopDragDropClientOzoneTest, RejectDragDuringWindowMove) {
+ // Simulate that a window-move loop is in progress.
+ auto suppress_drag =
+ DesktopDragDropClientOzone::ScopedSuppressForWindowMove();
+
+ // Attempt to start a drag operation. It should be rejected and return kNone.
+ DragOperation operation = StartDragAndDrop(ui::DragDropTypes::DRAG_COPY);
+ EXPECT_EQ(DragOperation::kNone, operation);
+}
+
} // namespace views
Original Bug Report
Tab-drag nested loop bypass of g_is_dragging guard on Linux/X11
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: A potential logic flaw in Chromium’s Linux/X11 window-move loop allows a compromised renderer to bypass the process-global drag-and-drop re-entrancy guard. When a user performs a tab-drag, a nested event loop is entered without setting the g_is_dragging guard, allowing a compromised renderer to initiate a re-entrant drag-and-drop operation. This can let the attacker hijack the active pointer grab and drop a custom payload, potentially leading to Universal Cross-Site Scripting (UXSS).
Affected files:
ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.ccchrome/browser/ui/views/tabs/dragging/tab_drag_controller.ccui/ozone/platform/x11/x11_window.ccui/base/x/x11_desktop_window_move_client.ccui/base/x/x11_whole_screen_move_loop.cc
Estimated timestamp from git blame: 2026-05-28
Root Cause Analysis
During a tab-detach or window-move operation on Linux/Ozone (X11), the browser process enters a nested event loop via X11WholeScreenMoveLoop::RunMoveLoop (defined in ui/base/x/x11_whole_screen_move_loop.cc line 140) which is run with kNestableTasksAllowed (line 184).
Crucially, the process-global re-entrancy guard g_is_dragging (defined in ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.cc line 39) is only set and checked during system drag-and-drop operations initiated via DesktopDragDropClientOzone::StartDragAndDrop(). Because window-move and tab-drag operations are triggered directly via TabDragController and X11DesktopWindowMoveClient without involving the drag-and-drop client, the g_is_dragging variable remains false during the active window move.
While W1’s nested event loop is pumping tasks, a compromised renderer in a second visible window (W2) can invoke a re-entrant drag operation by sending the LocalFrameHost::StartDragging Mojo IPC. Since the nested loop allows nestable tasks, the IPC is processed re-entrantly. When DesktopDragDropClientOzone::StartDragAndDrop is called on W2’s root window, the guard check:
if (g_is_dragging) {
return DragOperation::kNone;
}
passes because g_is_dragging is still false. W2’s drag client then releases the active mouse capture on W1, grabs the pointer for itself, and hijacks the global XdndSelection selection atom. Subsequent user drag actions are then routed to the attacker’s drag-and-drop handler, substituting the user’s tab drag with the attacker’s payload.
Note that on Windows, this scenario triggers a CHECK(!g_is_dragging) in DesktopDragDropClientWin and terminates the browser process, limiting the impact to Denial of Service. On Linux/Ozone (Wayland), system drag-and-drop is utilized for window moves, which correctly sets g_is_dragging. Consequently, the hijack scenario is specific to the Ozone/X11 platform.
Potential Attack Path
An attacker controlling a compromised renderer process could potentially execute the following steps to trigger this vulnerability (please note that these are suggested/potential steps as our tooling agent does not have the capability to execute code or run a live proof of concept):
- User Action: The user begins dragging a tab in a browser window (W1). This causes
TabDragControllerto initiate a native window move, spawningX11WholeScreenMoveLoop::RunMoveLoopwith a nested event loop allowing nestable tasks. The globalg_is_draggingflag remainsfalse. - IPC Dispatch: The compromised renderer in a second visible window (W2) sends the
LocalFrameHost::StartDraggingMojo IPC with the drag source set to mouse and coordinates targeting W2. - Re-entrant Capture Release: The IPC is processed re-entrantly inside W1’s nested event loop. The guard check in
DesktopDragDropClientOzone::StartDragAndDroppasses. W2’s drag client executesReleaseCapture()to strip capture from W1. - Pointer Grab and Selection Hijack: W2’s drag client starts its drag operation, grabs the pointer (which overrides W1’s grab on X11), and takes ownership of the global
XdndSelectionwith the attacker’s payload (e.g., ajavascript:URL). - Payload Drop: When the user releases the mouse button over a drop target (such as the bookmarks bar or the page content), the attacker’s payload is dropped instead of the tab. If a
javascript:URL is dropped onto a sensitive surface, it can execute arbitrary script, leading to Universal Cross-Site Scripting (UXSS).
Suggested Remediation
To resolve this issue, the process-global re-entrancy guard should be extended to cover both system drag-and-drop operations and platform-level window-move/tab-drag operations.
This can be accomplished by setting a process-global flag or checking if any native move loop is currently active on the platform inside DesktopDragDropClientOzone::StartDragAndDrop(). If a move loop is active, the re-entrant StartDragAndDrop call should be rejected immediately, returning DragOperation::kNone.
Evaluated with Chrome root at commit: fb72408a8493c46bc75fae1c70d03daec96b3040
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.