CVE-2026-15764
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ScheduleTestTaskui/ozone/platform/wayland/host/wayland_data_drag_controller_unittest.cc |
modified | |
TEST_Pui/ozone/platform/wayland/host/wayland_data_drag_controller_unittest.cc |
modified |
Files Changed
ui/ozone/platform/wayland/host/wayland_data_drag_controller_unittest.ccui/ozone/platform/wayland/host/wayland_window.cc
Patch
From e9d7be2570c7e86a960b21554b9563b19ad38f91 Mon Sep 17 00:00:00 2001 From: Kramer Ge <[email protected]> Date: Tue, 07 Jul 2026 09:29:47 -0700 Subject: [PATCH] [ozone/wayland]Safely handle destruction during drag_finished_callback drag_finished_callback_.Run(operation) can invoke arbitrary views/delegate logic which may destroy this WaylandWindow. To avoid UAF of fields, add a weakptr check. To ensure base::RunLoop is quit, move the member quit_closure to stack. Fixed: 517100492 Change-Id: Ib3fa884e86a1088de9cb356f721ca490736d3acf Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8041831 Commit-Queue: Kramer Ge <[email protected]> Reviewed-by: Thomas Anderson <[email protected]> Cr-Commit-Position: refs/heads/main@{#1658008} --- diff --git a/ui/ozone/platform/wayland/host/wayland_data_drag_controller_unittest.cc b/ui/ozone/platform/wayland/host/wayland_data_drag_controller_unittest.cc index fe28777..d9a89a6 100644 --- a/ui/ozone/platform/wayland/host/wayland_data_drag_controller_unittest.cc +++ b/ui/ozone/platform/wayland/host/wayland_data_drag_controller_unittest.cc @@ -31,6 +31,8 @@ #include "ui/base/dragdrop/os_exchange_data.h" #include "ui/base/dragdrop/os_exchange_data_provider_factory.h" #include "ui/events/base_event_utils.h" +#include "ui/events/event.h" +#include "ui/events/types/event_type.h" #include "ui/gfx/geometry/point.h" #include "ui/gfx/geometry/vector2d.h" #include "ui/gfx/native_ui_types.h" @@ -912,6 +914,36 @@ SendDndCancelled(); } +// Verifies the drag loop exits gracefully when the origin window is destroyed +// while dispatching the synthetic pointer release at drag-session close. +TEST_P(WaylandDataDragControllerTest, + DestroyOriginWindowDuringDragSessionClose) { + FocusAndPressLeftPointerButton(window_.get(), &delegate_); + + // Once the drag session has started, emulate a successful drop. The + // controller will synthesize a release for the still-pressed pointer button; + // have the delegate destroy the origin window from within that event's + // dispatch, mimicking a queued close task running in a nested loop. + ScheduleTestTask(base::BindLambdaForTesting([&]() { + EXPECT_CALL(delegate_, DispatchEvent(_)).WillRepeatedly([&](Event* event) { + if (event->type() == EventType::kMouseReleased) { + window_.reset(); + } + }); + SendDndFinished(); + })); + + OSExchangeData os_exchange_data; + os_exchange_data.SetString(sample_text_for_dnd()); + EXPECT_CALL(drag_started_callback_, Run()).Times(1); + EXPECT_FALSE(window_->StartDrag( + os_exchange_data, DragDropTypes::DRAG_COPY, DragEventSource::kMouse, + /*cursor=*/{}, /*can_grab_pointer=*/true, drag_started_callback_.Get(), + drag_finished_callback_.Get(), /*location_delegate=*/nullptr)); + + EXPECT_FALSE(window_); +} + // Ensures drag/drop events are properly propagated to non-toplevel windows. TEST_P(WaylandDataDragControllerTest, DragToNonToplevelWindows) { auto* origin_window = window_.get(); diff --git a/ui/ozone/platform/wayland/host/wayland_window.cc b/ui/ozone/platform/wayland/host/wayland_window.cc index 6ac7b05..81a046be4 100644 --- a/ui/ozone/platform/wayland/host/wayland_window.cc +++ b/ui/ozone/platform/wayland/host/wayland_window.cc @@ -948,16 +948,23 @@ // is about to shut down. Do nothing and return. return; } + // Running `drag_finished_callback_` and dispatching the synthetic pointer + // release below may spin a nested run loop in which `this` gets destroyed, + // so move the quit closure onto the stack to ensure the drag loop is still + // quit in that case. + base::OnceClosure quit_closure = std::move(drag_loop_quit_closure_); + auto alive = AsWeakPtr(); std::move(drag_finished_callback_).Run(operation); // Skip releasing any pointer buttons for the case of a window drag driven by // the data drag controller. // TODO: crbug.com/40238145 - Refactor this per discussion at // crrev.com/c/5570335/comment/0b8811fc_818028c9/. - if (!connection()->data_drag_controller()->IsWindowDragSessionRunning()) { + if (alive && + !connection()->data_drag_controller()->IsWindowDragSessionRunning()) { connection()->event_source()->ReleasePressedPointerButtons( this, EventTimeForNow()); } - std::move(drag_loop_quit_closure_).Run(); + std::move(quit_closure).Run(); } bool WaylandWindow::Initialize(PlatformWindowInitProperties properties) {
Regression Test / PoC
diff --git a/ui/ozone/platform/wayland/host/wayland_data_drag_controller_unittest.cc b/ui/ozone/platform/wayland/host/wayland_data_drag_controller_unittest.cc
index fe28777..d9a89a6 100644
--- a/ui/ozone/platform/wayland/host/wayland_data_drag_controller_unittest.cc
+++ b/ui/ozone/platform/wayland/host/wayland_data_drag_controller_unittest.cc
@@ -31,6 +31,8 @@
#include "ui/base/dragdrop/os_exchange_data.h"
#include "ui/base/dragdrop/os_exchange_data_provider_factory.h"
#include "ui/events/base_event_utils.h"
+#include "ui/events/event.h"
+#include "ui/events/types/event_type.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/gfx/native_ui_types.h"
@@ -912,6 +914,36 @@
SendDndCancelled();
}
+// Verifies the drag loop exits gracefully when the origin window is destroyed
+// while dispatching the synthetic pointer release at drag-session close.
+TEST_P(WaylandDataDragControllerTest,
+ DestroyOriginWindowDuringDragSessionClose) {
+ FocusAndPressLeftPointerButton(window_.get(), &delegate_);
+
+ // Once the drag session has started, emulate a successful drop. The
+ // controller will synthesize a release for the still-pressed pointer button;
+ // have the delegate destroy the origin window from within that event's
+ // dispatch, mimicking a queued close task running in a nested loop.
+ ScheduleTestTask(base::BindLambdaForTesting([&]() {
+ EXPECT_CALL(delegate_, DispatchEvent(_)).WillRepeatedly([&](Event* event) {
+ if (event->type() == EventType::kMouseReleased) {
+ window_.reset();
+ }
+ });
+ SendDndFinished();
+ }));
+
+ OSExchangeData os_exchange_data;
+ os_exchange_data.SetString(sample_text_for_dnd());
+ EXPECT_CALL(drag_started_callback_, Run()).Times(1);
+ EXPECT_FALSE(window_->StartDrag(
+ os_exchange_data, DragDropTypes::DRAG_COPY, DragEventSource::kMouse,
+ /*cursor=*/{}, /*can_grab_pointer=*/true, drag_started_callback_.Get(),
+ drag_finished_callback_.Get(), /*location_delegate=*/nullptr));
+
+ EXPECT_FALSE(window_);
+}
+
// Ensures drag/drop events are properly propagated to non-toplevel windows.
TEST_P(WaylandDataDragControllerTest, DragToNonToplevelWindows) {
auto* origin_window = window_.get();
Original Bug Report
Potential Use-After-Free in WaylandWindow::OnDragSessionClose
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 Use-After-Free (UAF) vulnerability exists in WaylandWindow::OnDragSessionClose on Linux Wayland. The function invokes a synchronous callback that can result in the immediate destruction of the WaylandWindow. Upon returning from the callback, the function dereferences the already deallocated window’s member variables, resulting in a use-after-free.
Affected files:
ui/ozone/platform/wayland/host/wayland_window.cc
Estimated timestamp from git blame: 2024-06-05
Root Cause
In WaylandWindow::OnDragSessionClose (located in ui/ozone/platform/wayland/host/wayland_window.cc), the implementation invokes a synchronous callback and subsequently accesses member variables via the this pointer without verifying the window’s liveness:
void WaylandWindow::OnDragSessionClose(DragOperation operation) {
if (!drag_finished_callback_) {
return;
}
std::move(drag_finished_callback_).Run(operation); // [1]
if (!connection()->data_drag_controller()->IsWindowDragSessionRunning()) { // [2]
connection()->event_source()->ReleasePressedPointerButtons(
this, EventTimeForNow());
}
std::move(drag_loop_quit_closure_).Run(); // [3]
}
- At
[1],drag_finished_callback_is executed synchronously. This invokesDesktopDragDropClientOzone::OnDragFinished, notifying drag-and-drop observers (such asTabDragController). - If an observer synchronously destroys the parent widget (for example, if the last tab is dragged out and the window closes via a synchronous
CloseNow()), the underlyingWaylandWindowis deleted immediately. This occurs becauseDesktopWindowTreeHostPlatform::OnClosed()callsSetPlatformWindow(nullptr), which deletes thestd::unique_ptr<ui::PlatformWindow>holding theWaylandWindowinstance. - Upon returning from the callback to
[2]and[3], the function dereferencesthis->connection_andthis->drag_loop_quit_closure_on the already-deletedWaylandWindowinstance, leading to a Use-After-Free.
Potential Trigger Path
Based on static analysis, an attacker could potentially trigger this vulnerability through the following steps:
- Initiate Drag: The user (or a renderer-initiated script) begins an outgoing drag-and-drop session.
- Store Callbacks:
WaylandWindow::StartDragspins a nestable run loop and registersdrag_finished_callback_anddrag_loop_quit_closure_. - Close Event: During the active drag session, a close task for the source window is queued or triggered.
- Drag Completion: The drag completes, which executes
WaylandWindow::OnDragSessionClose. - Synchronous Window Destruction: The synchronous execution of
drag_finished_callback_.Run()triggers observer cleanup, which processes the window close synchronously (e.g., viaCloseNow()), immediately destroyingWaylandWindow. - Use-After-Free: Execution resumes in
OnDragSessionCloseon the deletedthispointer, resulting in a UAF when dereferencingconnection()ordrag_loop_quit_closure_.
Note: Our analysis is currently based on static code review, as our tooling does not yet have the ability to run code or compile a functional proof-of-concept.
Proposed Fix
To remediate this issue, the lifetime of the WaylandWindow must be monitored using a WeakPtr. Additionally, the loop quit closure should be moved to a local variable prior to executing any client callbacks to ensure the nested run loop can be cleanly terminated even if the window is destroyed.
void WaylandWindow::OnDragSessionClose(DragOperation operation) {
if (!drag_finished_callback_) {
return;
}
// Move the callback and loop closure to local variables.
auto drag_finished_callback = std::move(drag_finished_callback_);
auto drag_loop_quit_closure = std::move(drag_loop_quit_closure_);
// Track the liveness of 'this' using a WeakPtr.
base::WeakPtr<WaylandWindow> weak_this = AsWeakPtr();
std::move(drag_finished_callback).Run(operation);
// If the window was destroyed during the callback, quit the nested loop safely and exit.
if (!weak_this) {
if (drag_loop_quit_closure) {
std::move(drag_loop_quit_closure).Run();
}
return;
}
if (!connection()->data_drag_controller()->IsWindowDragSessionRunning()) {
connection()->event_source()->ReleasePressedPointerButtons(
this, EventTimeForNow());
}
if (drag_loop_quit_closure) {
std::move(drag_loop_quit_closure).Run();
}
}
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.