CVE-2026-15764
Overview
Background
- Ozone/Wayland
- Chrome’s platform abstraction layer where
ui::WaylandWindowrepresents a native Wayland surface in the browser process. - `drag_finished_callback_`
- a
base::OnceCallbackrun at drag-session close that dispatches into arbitrary views/delegate code and a synthetic pointer-button release. - `drag_loop_quit_closure_`
- a member
base::OnceClosurethat quits the nestedbase::RunLoopspun for the duration of a Wayland window/data drag. - Use-after-free (UAF)
- dereferencing
this(or its fields) after the callback has already destroyed theWaylandWindowobject.
Root Cause Analysis
ui::WaylandWindow::OnDragSessionClose invokes std::move(drag_finished_callback_).Run(operation), which re-enters views/delegate code and can spin a nested run loop that destroys the origin WaylandWindow (this). The original code assumed this outlived the callback: after Run(operation) returned, it still dereferenced connection(), event_source(), and the member drag_loop_quit_closure_ to dispatch a synthetic pointer release and quit the run loop. When the callback destroyed the window, every one of those accesses touched freed memory in the browser process. The lifetime of this was implicitly coupled to a callback whose effects (nested loop, delegate teardown) were fully outside the function’s control, so no reentrancy or destruction was accounted for.
The fix captures a base::WeakPtr (alive) before the callback and gates the post-callback this access on it, and copies drag_loop_quit_closure_ to a stack quit_closure so the run loop is quit even after this is freed.
OnceCallback invoked mid-function can synchronously destroy this, so any field or method access after Run() is a UAF unless guarded by a WeakPtr. State that must survive object destruction — here the run-loop quit closure — has to be moved to the stack before the reentrant call.Attack Path
-
A web page initiates a drag-and-drop that maps onto a Wayland window/data drag session originating from a
ui::WaylandWindow. -
The user releases the drag, triggering
OnDragSessionClose, which runsdrag_finished_callback_and dispatches into views/delegate logic. -
That delegate logic (reachable from the web-initiated drag) spins a nested run loop and tears down the origin
WaylandWindow, freeingthis. -
Control returns into
OnDragSessionClose, which dereferences the freedconnection()/event_source()and the memberdrag_loop_quit_closure_. - The dangling accesses read/write freed browser-process memory, yielding a controllable UAF.
Impact Assessment
ui::WaylandWindow and its owned members in the privileged browser process, reachable from a web-initiated drag on Linux/Wayland. Depending on heap grooming of the freed allocation, this can corrupt memory and lead to renderer-to-browser escalation or code execution outside the sandbox. Exploitation requires a drag-and-drop interaction, modestly raising the interaction bar but keeping it web-reachable.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
Audit Directions
- Reentrant callback then field accessaudit any method that runs a
base::OnceCallback/base::RepeatingCallbackinto views/delegate code and then touchesthis, members, orconnection(); require aWeakPtrguard after the call. - Member closures across nested run loopsflag members like
drag_loop_quit_closure_that must be run after a reentrant call; move them to the stack before dispatching so object destruction does not strand the run loop. - Ozone/Wayland drag and dispatch pathsreview
WaylandWindowandWaylandDataDragControllerclose/cancel handlers for other synthetic-event dispatch (e.g.ReleasePressedPointerButtons) that assumesthissurvives delegate reentrancy.
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.