Overview

Critical
Severity
7.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Ozone
DescriptionUse after free in Ozone
ComponentOzone
Sub-componentOzone/wayland
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker517100492
Fix commite9d7be2570c7 (chromium/src) +41/-2
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-27 (per vendor advisory)
Fix landed2026-07-07 (public git)
Disclosed2026-07-14
Patch gap7 days — fix public in source before the advisory

Background

Ozone/Wayland
Chrome’s platform abstraction layer where ui::WaylandWindow represents a native Wayland surface in the browser process.
`drag_finished_callback_`
a base::OnceCallback run at drag-session close that dispatches into arbitrary views/delegate code and a synthetic pointer-button release.
`drag_loop_quit_closure_`
a member base::OnceClosure that quits the nested base::RunLoop spun for the duration of a Wayland window/data drag.
Use-after-free (UAF)
dereferencing this (or its fields) after the callback has already destroyed the WaylandWindow object.

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.

Key insight
A 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

  1. A web page initiates a drag-and-drop that maps onto a Wayland window/data drag session originating from a ui::WaylandWindow.
  2. The user releases the drag, triggering OnDragSessionClose, which runs drag_finished_callback_ and dispatches into views/delegate logic.
  3. That delegate logic (reachable from the web-initiated drag) spins a nested run loop and tears down the origin WaylandWindow, freeing this.
  4. Control returns into OnDragSessionClose, which dereferences the freed connection()/event_source() and the member drag_loop_quit_closure_.
  5. The dangling accesses read/write freed browser-process memory, yielding a controllable UAF.

Impact Assessment

Use-after-free of a 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

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

Audit Directions

  • Reentrant callback then field access
    audit any method that runs a base::OnceCallback/base::RepeatingCallback into views/delegate code and then touches this, members, or connection(); require a WeakPtr guard after the call.
  • Member closures across nested run loops
    flag 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 paths
    review WaylandWindow and WaylandDataDragController close/cancel handlers for other synthetic-event dispatch (e.g. ReleasePressedPointerButtons) that assumes this survives delegate reentrancy.
From e9d7be2570c7e86a960b21554b9563b19ad38f91 Mon Sep 17 00:00:00 2001
From: Kramer Ge <fangzhoug@chromium.org>
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 <fangzhoug@chromium.org>
Reviewed-by: Thomas Anderson <thomasanderson@chromium.org>
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 vm...@google.com

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