CVE-2026-11682
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.cc |
modified | |
ifui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.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_unittest.cc
Patch
From a145ee4668e6ce091552fec304e5cad0f9606322 Mon Sep 17 00:00:00 2001
From: Tom Anderson <thomasanderson@chromium.org>
Date: Thu, 28 May 2026 06:10:08 -0700
Subject: [PATCH] Ozone: Add process-global drag-and-drop state guard
In Linux/Ozone platforms (e.g. Ozone/X11), the drag-and-drop
implementation previously lacked a process-global guard to prevent
concurrent or re-entrant drag-and-drop operations across different
top-level windows. Instead, it relied solely on instance-scoped DCHECKs.
This allowed a compromised renderer to asynchronously initiate a second
drag operation via IPC while a legitimate drag-and-drop was in flight,
stealing ownership of the global XdndSelection atom and hijacking the
event dispatcher.
This CL implements a process-global drag guard `g_is_dragging` and a
`base::AutoReset<bool>` scope in `DesktopDragDropClientOzone::StartDragAndDrop`
to reject concurrent/re-entrant drag attempts with `DragOperation::kNone`.
This aligns the Ozone platform with the existing Windows implementation.
Fixed: 517103584
Change-Id: I9673e57ec2aaae2f36cc20f4223d58240264098c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7879779
Commit-Queue: Allen Bauer <kylixrd@chromium.org>
Auto-Submit: Thomas Anderson <thomasanderson@chromium.org>
Reviewed-by: Allen Bauer <kylixrd@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1637643}
---
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 e5a7bd2f..d341b848 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
@@ -7,6 +7,7 @@
#include <memory>
#include <utility>
+#include "base/auto_reset.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/scoped_observation.h"
@@ -30,6 +31,13 @@
using ::ui::mojom::DragOperation;
+// Process-global re-entrancy guard. DesktopDragDropClientOzone is per
+// top-level window, so the per-instance drag_context_ check below does not
+// catch a second window starting a drag while the first window's nested
+// RunMoveLoop is pumping tasks. On X11 that lets a compromised renderer steal
+// the global XdndSelection mid-drag. Mirrors desktop_drag_drop_client_win.cc.
+bool g_is_dragging = false;
+
// The minimum alpha required so we would treat the pixel as visible.
constexpr uint32_t kMinAlpha = 32;
@@ -142,6 +150,13 @@
return DragOperation::kNone;
}
+ // A renderer can send LocalFrameHost::StartDragging at any time, so reject
+ // (rather than CHECK) re-entrant drags from a second top-level window.
+ if (g_is_dragging) {
+ return DragOperation::kNone;
+ }
+ base::AutoReset<bool> drag_scoper(&g_is_dragging, true);
+
DCHECK(!drag_context_);
drag_context_ = std::make_unique<DragContext>();
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 b7516bb77..769cd0b 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
@@ -166,6 +166,10 @@
drop_handler->OnDragLeave();
}
+ void set_callback_during_drag(base::RepeatingClosure callback) {
+ callback_during_drag_ = std::move(callback);
+ }
+
void CloseDrag(DragOperation operation) {
std::move(drag_finished_callback_).Run(operation);
drag_loop_quit_closure_.Run();
@@ -174,6 +178,9 @@
void ProcessDrag(std::unique_ptr<OSExchangeData> data, int operation) {
std::move(drag_started_callback_).Run();
OnDragEnter(kStartDragLocation, std::move(data), operation);
+ if (callback_during_drag_) {
+ callback_during_drag_.Run();
+ }
int updated_operation = OnDragMotion(kStartDragLocation, operation);
OnDragDrop();
OnDragLeave();
@@ -185,6 +192,7 @@
WmDragHandler::DragFinishedCallback drag_finished_callback_;
std::unique_ptr<ui::OSExchangeData> source_data_;
base::RepeatingClosure drag_loop_quit_closure_;
+ base::RepeatingClosure callback_during_drag_;
int modifiers_ = 0;
};
@@ -560,4 +568,27 @@
EXPECT_EQ(1, dragdrop_delegate_->num_exits());
}
+TEST_F(DesktopDragDropClientOzoneTest, RejectReentrantDrag) {
+ // Set up a callback to be run while the drag is active.
+ platform_window_->set_callback_during_drag(base::BindRepeating(
+ [](DesktopDragDropClientOzoneTest* test) {
+ // Attempt to start a second drag operation while the first is active.
+ DragOperation reentrant_operation =
+ test->StartDragAndDrop(ui::DragDropTypes::DRAG_COPY);
+ // The reentrant drag should be rejected and return kNone.
+ EXPECT_EQ(DragOperation::kNone, reentrant_operation);
+ },
+ base::Unretained(this)));
+
+ // Set the operation which the destination can accept.
+ dragdrop_delegate_->SetOperation(DragOperation::kCopy);
+
+ // Start the first drag and drop.
+ DragOperation operation = StartDragAndDrop(ui::DragDropTypes::DRAG_COPY |
+ ui::DragDropTypes::DRAG_MOVE);
+
+ // The first drag should succeed and complete as expected.
+ EXPECT_EQ(DragOperation::kCopy, operation);
+}
+
} // namespace views
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 b7516bb77..769cd0b 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
@@ -166,6 +166,10 @@
drop_handler->OnDragLeave();
}
+ void set_callback_during_drag(base::RepeatingClosure callback) {
+ callback_during_drag_ = std::move(callback);
+ }
+
void CloseDrag(DragOperation operation) {
std::move(drag_finished_callback_).Run(operation);
drag_loop_quit_closure_.Run();
@@ -174,6 +178,9 @@
void ProcessDrag(std::unique_ptr<OSExchangeData> data, int operation) {
std::move(drag_started_callback_).Run();
OnDragEnter(kStartDragLocation, std::move(data), operation);
+ if (callback_during_drag_) {
+ callback_during_drag_.Run();
+ }
int updated_operation = OnDragMotion(kStartDragLocation, operation);
OnDragDrop();
OnDragLeave();
@@ -185,6 +192,7 @@
WmDragHandler::DragFinishedCallback drag_finished_callback_;
std::unique_ptr<ui::OSExchangeData> source_data_;
base::RepeatingClosure drag_loop_quit_closure_;
+ base::RepeatingClosure callback_during_drag_;
int modifiers_ = 0;
};
@@ -560,4 +568,27 @@
EXPECT_EQ(1, dragdrop_delegate_->num_exits());
}
+TEST_F(DesktopDragDropClientOzoneTest, RejectReentrantDrag) {
+ // Set up a callback to be run while the drag is active.
+ platform_window_->set_callback_during_drag(base::BindRepeating(
+ [](DesktopDragDropClientOzoneTest* test) {
+ // Attempt to start a second drag operation while the first is active.
+ DragOperation reentrant_operation =
+ test->StartDragAndDrop(ui::DragDropTypes::DRAG_COPY);
+ // The reentrant drag should be rejected and return kNone.
+ EXPECT_EQ(DragOperation::kNone, reentrant_operation);
+ },
+ base::Unretained(this)));
+
+ // Set the operation which the destination can accept.
+ dragdrop_delegate_->SetOperation(DragOperation::kCopy);
+
+ // Start the first drag and drop.
+ DragOperation operation = StartDragAndDrop(ui::DragDropTypes::DRAG_COPY |
+ ui::DragDropTypes::DRAG_MOVE);
+
+ // The first drag should succeed and complete as expected.
+ EXPECT_EQ(DragOperation::kCopy, operation);
+}
+
} // namespace views
Original Bug Report
Process-global drag-and-drop guard bypass in Ozone/X11 allows cross-window drag hijacking
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 Linux/X11 drag-and-drop implementation lacks a process-global guard to prevent concurrent or reentrant drag operations across different top-level windows. A compromised renderer could potentially exploit this by asynchronously initiating a drag in a second window while a legitimate user drag is in progress in another window. This could allow a malicious origin to steal ownership of the global XdndSelection atom and inject unauthorized payloads into the active drag-and-drop gesture.
Affected files:
ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.ccui/ozone/platform/x11/x11_window.ccui/base/x/x11_drag_drop_client.cc
Estimated timestamp from git blame: 2020-04-24
Root Cause Analysis
On Windows, DesktopDragDropClientWin::StartDragAndDrop utilizes a process-global boolean flag (g_is_dragging) and asserts via CHECK(!g_is_dragging) to guarantee that no concurrent drag-and-drop operations can be initiated globally across the application’s process boundary.
However, in the Linux/X11 Ozone platform implementation (ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.cc), there is no equivalent process-global guard. Instead, the implementation relies on per-instance pointers and state variables that are only asserted via DCHECK (which are compiled out in production release builds):
DCHECK(!drag_context_)inDesktopDragDropClientOzoneis instance-scoped (one per top-level window).DCHECK(!drag_location_delegate_)inX11Windowis instance-scoped.DCHECK(!in_move_loop_)inX11WholeScreenMoveLoopis loop-instance-scoped.
Since these clients are instantiated once per top-level browser window, a reentrant call coming from a different window/widget can bypass these checks in release builds.
Potential Attack Scenario
- User Action: The user starts a legitimate drag-and-drop operation in Window 1 (W1). W1 establishes selection ownership via the X Server and enters a nested run loop via
X11WholeScreenMoveLoop::RunMoveLoopto process window/mouse events. - Malicious Request: While the user’s drag is active and pumping nested tasks, a compromised renderer hosting an attacker-controlled tab in Window 2 (W2) sends a
LocalFrameHost::StartDraggingMojo IPC with custom payload data (such as ajavascript:URL or shell commands). - Reentrant Bypass: Because W2 is a distinct
WebContentsand native window, its local state guards do not flag the drag as active. Security checks verify that W2’s coordinates are within W2’s visible screen bounds, which succeeds. - Selection Ownership Theft: W2’s drag client starts up and triggers
XDragDropClient::InitDrag, which executesSelectionOwner::TakeOwnershipOfSelection. The X Server transfers theXdndSelectionatom ownership to W2’s window. While W1’s window receivesSelectionClear, it lacks handling to abort its loop, leaving W1 in an inconsistent state. - Pointer Grab Override: W2 calls
GrabPointer, causing the X Server to transfer the pointer grab from W1’s input grab window to W2’s window because they belong to the same X client. W2 overrides the event dispatcher and enters its own nested loop. - Data Delivery: When the user completes the gesture and drops the item, the drop target requests selection data. The X Server routes this request to the current owner (W2’s window), which responds with the attacker’s custom payload rather than the user’s intended selection.
Note: These steps represent a theoretical analysis of the control flow based on static code review; our tooling does not currently run active exploits to verify execution.
Impact
If successfully exploited, a compromised renderer could hijack active drag operations to:
- Trigger UXSS: Injecting
javascript:URLs into high-privilege surfaces such as the browser’s address bar or bookmarks. - Bypass Site Isolation: Forcing data drops across different frames/origins.
- Execute Arbitrary Commands: Dropping crafted terminal commands or file URIs into native external applications if the user drops the payload outside the browser.
Suggested Remediation
To remediate this issue, implement a process-global drag guard for the Ozone platform similar to the Windows implementation.
Specifically, in ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.cc, introduce a static/global boolean flag (e.g., g_is_dragging) to track active drag states across all Ozone drag-drop client instances. Implement a CHECK(!g_is_dragging) at the entry of DesktopDragDropClientOzone::StartDragAndDrop to safely abort the process or reject the request if a drag is already active globally.
Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8
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.