Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Ozone
DescriptionUse after free in Ozone
ComponentOzone
Bug ClassUAF
Tracker516501794
Fix commitedce928690cb (chromium/src) +57/-7
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-08

Changed Functions

FunctionChangeNotes
if
ui/ozone/platform/wayland/host/wayland_event_source.cc
modified
for
ui/ozone/platform/wayland/host/wayland_event_source.cc
modified
TEST_P
ui/ozone/platform/wayland/host/wayland_event_source_unittest.cc
modified
PostToServerAndWait
ui/ozone/platform/wayland/host/wayland_event_source_unittest.cc
modified

Files Changed

  • ui/ozone/platform/wayland/host/wayland_event_source.cc
  • ui/ozone/platform/wayland/host/wayland_event_source.h
  • ui/ozone/platform/wayland/host/wayland_event_source_unittest.cc
From edce928690cb0bbca49e411fc88b8293dcd9dc8a Mon Sep 17 00:00:00 2001
From: Max Ihlenfeldt <[email protected]>
Date: Mon, 01 Jun 2026 02:59:56 -0700
Subject: [PATCH] wayland: Fix UAF/dangling ptr in ReleasePressedPointerButtons()

Dispatching the event may destroy the window, and if we release multiple
buttons we use a freed/dangling pointer. Fix this by using a weak
pointer.

Fixed: 516501794
Change-Id: I85b0ab643896cc80d9248f4ae18300125fa7167b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7880226
Reviewed-by: Thomas Anderson <[email protected]>
Commit-Queue: Max Ihlenfeldt <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1639248}
---

diff --git a/ui/ozone/platform/wayland/host/wayland_event_source.cc b/ui/ozone/platform/wayland/host/wayland_event_source.cc
index 0f96ec1..5e70540 100644
--- a/ui/ozone/platform/wayland/host/wayland_event_source.cc
+++ b/ui/ozone/platform/wayland/host/wayland_event_source.cc
@@ -391,8 +391,11 @@
     return;
   }
 
-  WaylandWindow* prev_focused_window =
-      window_manager_->GetCurrentPointerFocusedWindow();
+  // Dispatching the event may delete the previously focused window.
+  base::WeakPtr<WaylandWindow> prev_focused_window =
+      window_manager_->GetCurrentPointerFocusedWindow()
+          ? window_manager_->GetCurrentPointerFocusedWindow()->AsWeakPtr()
+          : nullptr;
   if (window) {
     window_manager_->SetPointerFocusedWindow(window);
   }
@@ -430,10 +433,11 @@
   }
 }
 
-void WaylandEventSource::OnPointerButtonEventInternal(WaylandWindow* window,
-                                                      EventType type) {
+void WaylandEventSource::OnPointerButtonEventInternal(
+    base::WeakPtr<WaylandWindow> window,
+    EventType type) {
   if (window) {
-    window_manager_->SetPointerFocusedWindow(window);
+    window_manager_->SetPointerFocusedWindow(window.get());
   }
 }
 
@@ -957,12 +961,16 @@
     return;
   }
 
+  // Dispatching the event may delete the window.
+  base::WeakPtr<WaylandWindow> window_weak =
+      window ? window->AsWeakPtr() : nullptr;
   for (const auto& [button, name] : kMouseButtonToStringMap) {
     if (button & pointer_flags_) {
       VLOG(1) << "Synthesizing pointer release for: " << name;
       TRACE_EVENT_INSTANT("wayland.debug", "SynthesizePointerRelease", "button",
                           name);
-      OnPointerButtonEvent(EventType::kMouseReleased, button, timestamp, window,
+      OnPointerButtonEvent(EventType::kMouseReleased, button, timestamp,
+                           window_weak.get(),
                            wl::EventDispatchPolicy::kImmediate,
                            /*allow_release_of_unpressed_button=*/false,
                            /*is_synthesized=*/true);
diff --git a/ui/ozone/platform/wayland/host/wayland_event_source.h b/ui/ozone/platform/wayland/host/wayland_event_source.h
index de759bb..610771d 100644
--- a/ui/ozone/platform/wayland/host/wayland_event_source.h
+++ b/ui/ozone/platform/wayland/host/wayland_event_source.h
@@ -237,7 +237,8 @@
   gfx::Vector2dF ComputeFlingVelocity();
 
   // Wrap up method to support async pointer down/up event processing.
-  void OnPointerButtonEventInternal(WaylandWindow* window, EventType type);
+  void OnPointerButtonEventInternal(base::WeakPtr<WaylandWindow> window,
+                                    EventType type);
 
   // Wrap up method to support async touch release processing.
   void OnTouchReleaseInternal(PointerId id);
diff --git a/ui/ozone/platform/wayland/host/wayland_event_source_unittest.cc b/ui/ozone/platform/wayland/host/wayland_event_source_unittest.cc
index 7082671..a6c231a 100644
--- a/ui/ozone/platform/wayland/host/wayland_event_source_unittest.cc
+++ b/ui/ozone/platform/wayland/host/wayland_event_source_unittest.cc
@@ -404,6 +404,47 @@
                                         base::TimeTicks::Now());
 }
 
+// Check that if an event dispatched by ReleasePressedPointerButtons causes the
+// target window to be destroyed, we don't cause a UAF or dangling pointer.
+TEST_P(WaylandEventSourceTest, ReleasePressedPointerButtonsUAF) {
+  PostToServerAndWait([](wl::TestWaylandServerThread* server) {
+    wl_seat_send_capabilities(server->seat()->resource(),
+                              WL_SEAT_CAPABILITY_POINTER);
+  });
+  ASSERT_TRUE(connection_->seat()->pointer());
+
+  // Record two pressed buttons so ReleasePressedPointerButtons iterates twice.
+  EXPECT_CALL(delegate_, DispatchEvent(_)).Times(::testing::AnyNumber());
+  PostToServerAndWait([surface_id = window_->root_surface()->get_surface_id()](
+                          wl::TestWaylandServerThread* server) {
+    auto* const surface =
+        server->GetObject<wl::MockSurface>(surface_id)->resource();
+    auto* const pointer = server->seat()->pointer()->resource();
+    wl_pointer_send_enter(pointer, server->GetNextSerial(), surface, 0, 0);
+    wl_pointer_send_button(pointer, server->GetNextSerial(),
+                           server->GetNextTime(), BTN_LEFT,
+                           WL_POINTER_BUTTON_STATE_PRESSED);
+    wl_pointer_send_button(pointer, server->GetNextSerial(),
+                           server->GetNextTime(), BTN_RIGHT,
+                           WL_POINTER_BUTTON_STATE_PRESSED);
+    wl_pointer_send_frame(pointer);
+  });
+  ASSERT_TRUE(pointer_delegate_->IsPointerButtonPressed(EF_LEFT_MOUSE_BUTTON));
+  ASSERT_TRUE(pointer_delegate_->IsPointerButtonPressed(EF_RIGHT_MOUSE_BUTTON));
+
+  // Destroy the window on the first mouse release.
+  EXPECT_CALL(delegate_, DispatchEvent(_))
+      .WillOnce([&](Event* event) {
+        EXPECT_EQ(event->type(), EventType::kMouseReleased);
+        window_.reset();
+      })
+      .WillRepeatedly(::testing::Return());
+
+  // Release both pressed buttons.
+  pointer_delegate_->ReleasePressedPointerButtons(window_.get(),
+                                                  base::TimeTicks::Now());
+}
+
 INSTANTIATE_TEST_SUITE_P(
     EventsDispatchPolicyTest,
     WaylandEventSourceTest,
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ui/ozone/platform/wayland/host/wayland_event_source_unittest.cc b/ui/ozone/platform/wayland/host/wayland_event_source_unittest.cc
index 7082671..a6c231a 100644
--- a/ui/ozone/platform/wayland/host/wayland_event_source_unittest.cc
+++ b/ui/ozone/platform/wayland/host/wayland_event_source_unittest.cc
@@ -404,6 +404,47 @@
                                         base::TimeTicks::Now());
 }
 
+// Check that if an event dispatched by ReleasePressedPointerButtons causes the
+// target window to be destroyed, we don't cause a UAF or dangling pointer.
+TEST_P(WaylandEventSourceTest, ReleasePressedPointerButtonsUAF) {
+  PostToServerAndWait([](wl::TestWaylandServerThread* server) {
+    wl_seat_send_capabilities(server->seat()->resource(),
+                              WL_SEAT_CAPABILITY_POINTER);
+  });
+  ASSERT_TRUE(connection_->seat()->pointer());
+
+  // Record two pressed buttons so ReleasePressedPointerButtons iterates twice.
+  EXPECT_CALL(delegate_, DispatchEvent(_)).Times(::testing::AnyNumber());
+  PostToServerAndWait([surface_id = window_->root_surface()->get_surface_id()](
+                          wl::TestWaylandServerThread* server) {
+    auto* const surface =
+        server->GetObject<wl::MockSurface>(surface_id)->resource();
+    auto* const pointer = server->seat()->pointer()->resource();
+    wl_pointer_send_enter(pointer, server->GetNextSerial(), surface, 0, 0);
+    wl_pointer_send_button(pointer, server->GetNextSerial(),
+                           server->GetNextTime(), BTN_LEFT,
+                           WL_POINTER_BUTTON_STATE_PRESSED);
+    wl_pointer_send_button(pointer, server->GetNextSerial(),
+                           server->GetNextTime(), BTN_RIGHT,
+                           WL_POINTER_BUTTON_STATE_PRESSED);
+    wl_pointer_send_frame(pointer);
+  });
+  ASSERT_TRUE(pointer_delegate_->IsPointerButtonPressed(EF_LEFT_MOUSE_BUTTON));
+  ASSERT_TRUE(pointer_delegate_->IsPointerButtonPressed(EF_RIGHT_MOUSE_BUTTON));
+
+  // Destroy the window on the first mouse release.
+  EXPECT_CALL(delegate_, DispatchEvent(_))
+      .WillOnce([&](Event* event) {
+        EXPECT_EQ(event->type(), EventType::kMouseReleased);
+        window_.reset();
+      })
+      .WillRepeatedly(::testing::Return());
+
+  // Release both pressed buttons.
+  pointer_delegate_->ReleasePressedPointerButtons(window_.get(),
+                                                  base::TimeTicks::Now());
+}
+
 INSTANTIATE_TEST_SUITE_P(
     EventsDispatchPolicyTest,
     WaylandEventSourceTest,
Loading diff…

Original Bug Report

reported by [email protected]

Potential Use-After-Free in WaylandEventSource::ReleasePressedPointerButtons

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 WaylandEventSource::ReleasePressedPointerButtons due to the reuse of a bare WaylandWindow pointer across event dispatch iterations. If a synchronous event dispatch destroys the window, subsequent iterations of the loop access the freed memory. This bypasses MiraclePtr protections because the dangling pointer is held directly on the call stack.

Affected files:

  • ui/ozone/platform/wayland/host/wayland_event_source.cc

Estimated timestamp from git blame: 2024-05-27

Root Cause

In ui/ozone/platform/wayland/host/wayland_event_source.cc, the method ReleasePressedPointerButtons takes a bare WaylandWindow* parameter and loops over pressed pointer buttons, calling OnPointerButtonEvent with the kImmediate dispatch policy:

void WaylandEventSource::ReleasePressedPointerButtons(
    WaylandWindow* window,
    base::TimeTicks timestamp) {
  if (!pointer_flags_) return;
  for (const auto& [button, name] : kMouseButtonToStringMap) {
    if (button & pointer_flags_) {
      OnPointerButtonEvent(EventType::kMouseReleased, button, timestamp, window,
                           wl::EventDispatchPolicy::kImmediate, false, true);
      pointer_flags_ &= ~button;
    }
    if (!pointer_flags_) break;
  }
}

When dispatch_policy is wl::EventDispatchPolicy::kImmediate, OnPointerButtonEvent synchronously dispatches the event via SetTargetAndDispatchEvent. This synchronous dispatch can run UI event handlers or spin a nested message loop (for example, if a modal dialog, menu, or drag controller runs during the event handler).

If a UI task runs during this synchronous execution and destroys the WaylandWindow, the bare window pointer on ReleasePressedPointerButtons’s local stack becomes a dangling pointer. Since it is a local parameter on the stack, it is not tracked or cleared when the window is destroyed. On the next iteration of the loop, this dangling pointer is passed to OnPointerButtonEvent again.

Furthermore, inside OnPointerButtonEvent, a completion closure is created that captures the previous focused window as a raw pointer prev_focused_window. If that previous window is destroyed during the synchronous event pipeline execution, running this closure similarly results in a Use-After-Free.

Why MiraclePtr / BackupRefPtr Does Not Protect This

MiraclePtr (BackupRefPtr) is bypassed because the dangling pointer is a bare pointer on the stack, not a raw_ptr<> member. When the window is destroyed, WaylandWindowManager::RemoveWindow erases the window and nulls out the fields containing raw_ptr<WaylandWindow> (such as pointer_focused_window_). As there are no remaining active raw_ptr references holding the allocation, the refcount drops to 0, and the memory slot is immediately freed and returned to the PartitionAlloc freelist, where it can be reclaimed and overwritten.

Potential Trigger Steps

Note: The following steps are theoretical/potential as our tooling does not currently have the ability to run code or verify via a working proof of concept.

  1. The user has multiple pointer (mouse) buttons registered as pressed (e.g., Left and Right).
  2. An event triggers ReleasePressedPointerButtons, passing the target WaylandWindow as a raw stack parameter.
  3. The loop processes the first button release, synchronously dispatching the event.
  4. During the synchronous dispatch of the first button release, a Views UI event handler or nested message loop is executed, which destroys the target window (and/or the previously focused window).
  5. The window map erases the window, and its memory is immediately deallocated and returned to the PartitionAlloc freelist.
  6. The loop continues to the next iteration to release the second button.
  7. The dangling window pointer on the stack is passed to OnPointerButtonEvent, which dereferences it via window_manager_->SetPointerFocusedWindow(window) -> window->OnPointerFocusChanged(true), leading to a potential Use-After-Free in the unsandboxed browser process.

Suggested Fix

To resolve this issue, capture a base::WeakPtr<WaylandWindow> at the beginning of ReleasePressedPointerButtons and verify its validity before each iteration or event dispatch. Additionally, the bound prev_focused_window in the completion closure inside OnPointerButtonEvent should be tracked using a base::WeakPtr rather than a raw pointer.

void WaylandEventSource::ReleasePressedPointerButtons(
    WaylandWindow* window,
    base::TimeTicks timestamp) {
  if (!pointer_flags_) return;
  base::WeakPtr<WaylandWindow> window_weak = window ? window->AsWeakPtr() : nullptr;
  for (const auto& [button, name] : kMouseButtonToStringMap) {
    if (!window_weak) {
      break;
    }
    if (button & pointer_flags_) {
      OnPointerButtonEvent(EventType::kMouseReleased, button, timestamp, window_weak.get(),
                           wl::EventDispatchPolicy::kImmediate, false, true);
      pointer_flags_ &= ~button;
    }
    if (!pointer_flags_) break;
  }
}

Evaluated with Chrome root at commit: a2bea94528f4bd6cc57739c43fa3bb890b8367d3


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