Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Ozone
DescriptionUse after free in Ozone
ComponentOzone
Bug ClassUAF
Tracker517100492
Fix commite9d7be2570c7 (chromium/src) +41/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-14

Changed Functions

FunctionChangeNotes
ScheduleTestTask
ui/ozone/platform/wayland/host/wayland_data_drag_controller_unittest.cc
modified
TEST_P
ui/ozone/platform/wayland/host/wayland_data_drag_controller_unittest.cc
modified

Files Changed

  • ui/ozone/platform/wayland/host/wayland_data_drag_controller_unittest.cc
  • ui/ozone/platform/wayland/host/wayland_window.cc
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) {
Loading diff…

Regression Test / PoC

shipped with the fix
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();
Loading diff…

Original Bug Report

reported by [email protected]

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]
}
  1. At [1], drag_finished_callback_ is executed synchronously. This invokes DesktopDragDropClientOzone::OnDragFinished, notifying drag-and-drop observers (such as TabDragController).
  2. 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 underlying WaylandWindow is deleted immediately. This occurs because DesktopWindowTreeHostPlatform::OnClosed() calls SetPlatformWindow(nullptr), which deletes the std::unique_ptr<ui::PlatformWindow> holding the WaylandWindow instance.
  3. Upon returning from the callback to [2] and [3], the function dereferences this->connection_ and this->drag_loop_quit_closure_ on the already-deleted WaylandWindow instance, leading to a Use-After-Free.

Potential Trigger Path

Based on static analysis, an attacker could potentially trigger this vulnerability through the following steps:

  1. Initiate Drag: The user (or a renderer-initiated script) begins an outgoing drag-and-drop session.
  2. Store Callbacks: WaylandWindow::StartDrag spins a nestable run loop and registers drag_finished_callback_ and drag_loop_quit_closure_.
  3. Close Event: During the active drag session, a close task for the source window is queued or triggered.
  4. Drag Completion: The drag completes, which executes WaylandWindow::OnDragSessionClose.
  5. Synchronous Window Destruction: The synchronous execution of drag_finished_callback_.Run() triggers observer cleanup, which processes the window close synchronously (e.g., via CloseNow()), immediately destroying WaylandWindow.
  6. Use-After-Free: Execution resumes in OnDragSessionClose on the deleted this pointer, resulting in a UAF when dereferencing connection() or drag_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.

View on issue tracker