Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Ozone
DescriptionUse after free in Ozone
ComponentOzone
Bug ClassUAF
Tracker523725277
Fix commit45daaa0cb895 (chromium/src) +44/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
if
ui/ozone/platform/wayland/host/wayland_event_source.cc
modified
TEST_F
ui/ozone/platform/wayland/host/wayland_pointer_unittest.cc
modified
if
ui/ozone/platform/wayland/host/wayland_pointer_unittest.cc
modified
PostToServerAndWait
ui/ozone/platform/wayland/host/wayland_pointer_unittest.cc
modified

Files Changed

  • ui/ozone/platform/wayland/host/wayland_event_source.cc
  • ui/ozone/platform/wayland/host/wayland_pointer_unittest.cc
From 45daaa0cb895d8e01f34b3138316bcb83bb7a845 Mon Sep 17 00:00:00 2001
From: Kramer Ge <fangzhoug@chromium.org>
Date: Thu, 18 Jun 2026 08:11:54 -0700
Subject: [PATCH] [Ozone/Wayland]Fix Use-After-Free in OnPointerFrameEvent

In WaylandEventSource::OnPointerFrameEvent, a raw pointer to the
focused WaylandWindow was cached outside the loop that drains
pointer_frames_. If the window is synchronously closed during event
dispatch, subsequent events in the same frame would use the dangling
pointer, leading to a Use-After-Free.

This CL fixes the issue by caching the target window as a WeakPtr
and verifying its validity before each event dispatch.

BUG=523725277
TAG=agy
CONV=600e2394-2a5e-4d13-93b9-91470f489eeb

Change-Id: I8cd33570342fc1592ef5f78c5a2ec72064af1ea7
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7958261
Reviewed-by: Jonathan Ross <jonross@chromium.org>
Commit-Queue: Kramer Ge <fangzhoug@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1649050}
---

diff --git a/ui/ozone/platform/wayland/host/wayland_event_source.cc b/ui/ozone/platform/wayland/host/wayland_event_source.cc
index 5e70540..6f4238a 100644
--- a/ui/ozone/platform/wayland/host/wayland_event_source.cc
+++ b/ui/ozone/platform/wayland/host/wayland_event_source.cc
@@ -550,17 +550,22 @@
 
   last_pointer_frame_time_ = now;
 
-  auto* target = window_manager_->GetCurrentPointerFocusedWindow();
-  if (!target) {
+  auto* target_window = window_manager_->GetCurrentPointerFocusedWindow();
+  if (!target_window) {
     return;
   }
+  // Dispatching an event may synchronously destroy the focused window (e.g. a
+  // popup closing on click), so hold a WeakPtr and re-check on each iteration.
+  base::WeakPtr<WaylandWindow> target = target_window->AsWeakPtr();
 
   while (!pointer_frames_.empty()) {
     // It is safe to pop the first queued event for processing.
     auto pointer_frame = std::move(pointer_frames_.front());
     pointer_frames_.pop_front();
 
-    SetTargetAndDispatchEvent(pointer_frame->event.get(), target);
+    if (target) {
+      SetTargetAndDispatchEvent(pointer_frame->event.get(), target.get());
+    }
     if (!pointer_frame->completion_cb.is_null()) {
       std::move(pointer_frame->completion_cb).Run();
     }
@@ -739,6 +744,7 @@
 
 void WaylandEventSource::SetTargetAndDispatchEvent(Event* event,
                                                    EventTarget* target) {
+  CHECK(target);
   Event::DispatcherApi(event).set_target(target);
   if (event->IsLocatedEvent()) {
     auto* located_event = event->AsLocatedEvent();
diff --git a/ui/ozone/platform/wayland/host/wayland_pointer_unittest.cc b/ui/ozone/platform/wayland/host/wayland_pointer_unittest.cc
index 7e2cd4a7..26d87aa5 100644
--- a/ui/ozone/platform/wayland/host/wayland_pointer_unittest.cc
+++ b/ui/ozone/platform/wayland/host/wayland_pointer_unittest.cc
@@ -721,6 +721,41 @@
   EXPECT_EQ(0.0f, scroll_event->y_offset_ordinal());
 }
 
+TEST_F(WaylandPointerTest, FrameTargetUseAfterFreeOnSyncWindowClose) {
+  SendEnter(50, 50);
+
+  // Frame 1: axis_stop -> FlingStart is queued and drained (1 event). This
+  // sets is_fling_active_=true so the next finger-scroll frame will queue 2
+  // events (FlingCancel + Scroll).
+  SendAxisStopEvents();
+
+  // Simulate a popup widget closing from inside its own event handler.
+  bool destroyed = false;
+  EXPECT_CALL(delegate_, DispatchEvent(_))
+      .WillRepeatedly([this, &destroyed](Event* event) {
+        if (!destroyed) {
+          destroyed = true;
+          window_.reset();
+        }
+      });
+
+  // Frame 2: finger axis -> ProcessPointerScrollData() pushes 2 events
+  // [FlingCancel, Scroll] into pointer_frames_, then OnPointerFrameEvent()
+  // drains both against the cached `target`. The first dispatch frees
+  // `target`; the second dispatch should be dismissed.
+  PostToServerAndWait([](wl::TestWaylandServerThread* server) {
+    auto* const pointer = server->seat()->pointer()->resource();
+    SendAxisEvents(pointer, server->GetNextTime(),
+                   WL_POINTER_AXIS_SOURCE_FINGER,
+                   WL_POINTER_AXIS_VERTICAL_SCROLL, 10);
+  });
+
+  EXPECT_TRUE(destroyed);
+
+  // The test fixture's window has been torn down mid-test.
+  DisableSyncOnTearDown();
+}
+
 TEST_F(WaylandPointerTest, FlingVelocityWithSingleLeadingAxis) {
   SendEnter(50, 75);
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ui/ozone/platform/wayland/host/wayland_pointer_unittest.cc b/ui/ozone/platform/wayland/host/wayland_pointer_unittest.cc
index 7e2cd4a7..26d87aa5 100644
--- a/ui/ozone/platform/wayland/host/wayland_pointer_unittest.cc
+++ b/ui/ozone/platform/wayland/host/wayland_pointer_unittest.cc
@@ -721,6 +721,41 @@
   EXPECT_EQ(0.0f, scroll_event->y_offset_ordinal());
 }
 
+TEST_F(WaylandPointerTest, FrameTargetUseAfterFreeOnSyncWindowClose) {
+  SendEnter(50, 50);
+
+  // Frame 1: axis_stop -> FlingStart is queued and drained (1 event). This
+  // sets is_fling_active_=true so the next finger-scroll frame will queue 2
+  // events (FlingCancel + Scroll).
+  SendAxisStopEvents();
+
+  // Simulate a popup widget closing from inside its own event handler.
+  bool destroyed = false;
+  EXPECT_CALL(delegate_, DispatchEvent(_))
+      .WillRepeatedly([this, &destroyed](Event* event) {
+        if (!destroyed) {
+          destroyed = true;
+          window_.reset();
+        }
+      });
+
+  // Frame 2: finger axis -> ProcessPointerScrollData() pushes 2 events
+  // [FlingCancel, Scroll] into pointer_frames_, then OnPointerFrameEvent()
+  // drains both against the cached `target`. The first dispatch frees
+  // `target`; the second dispatch should be dismissed.
+  PostToServerAndWait([](wl::TestWaylandServerThread* server) {
+    auto* const pointer = server->seat()->pointer()->resource();
+    SendAxisEvents(pointer, server->GetNextTime(),
+                   WL_POINTER_AXIS_SOURCE_FINGER,
+                   WL_POINTER_AXIS_VERTICAL_SCROLL, 10);
+  });
+
+  EXPECT_TRUE(destroyed);
+
+  // The test fixture's window has been torn down mid-test.
+  DisableSyncOnTearDown();
+}
+
 TEST_F(WaylandPointerTest, FlingVelocityWithSingleLeadingAxis) {
   SendEnter(50, 75);
Loading diff…

Original Bug Report

reported by rj...@google.com

Potential UAF / Double-Free in WaylandEventSource::OnPointerFrameEvent

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 raw pointer to a WaylandWindow is cached across multiple event dispatches in WaylandEventSource::OnPointerFrameEvent. If a window is synchronously closed during dispatch, subsequent events in the same frame will use the dangling pointer, leading to a highly reliable double-free via BackupRefPtr in the browser process.

Affected files:

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

Estimated timestamp from git blame: Unknown (Google3 checkout)

Description

A Use-After-Free (UAF) vulnerability exists in WaylandEventSource::OnPointerFrameEvent within Chromium’s Wayland implementation (ui/ozone/platform/wayland/host/wayland_event_source.cc).

The function is responsible for draining the pointer_frames_ queue, which can contain multiple events (e.g., mouse movement and button release) received in a single Wayland protocol frame. It incorrectly caches a raw pointer to the currently focused window (target) outside of the dispatch loop and reuses it without verifying the window’s liveness.

void WaylandEventSource::OnPointerFrameEvent() {
  // ...
  auto* target = window_manager_->GetCurrentPointerFocusedWindow();
  if (!target) {
    return;
  }

  while (!pointer_frames_.empty()) {
    auto pointer_frame = std::move(pointer_frames_.front());
    pointer_frames_.pop_front();

    SetTargetAndDispatchEvent(pointer_frame->event.get(), target);
    if (!pointer_frame->completion_cb.is_null()) {
      std::move(pointer_frame->completion_cb).Run();
    }
  }
}

During the execution of SetTargetAndDispatchEvent, an event is dispatched to the UI system. This dispatch can trigger synchronous window destruction (e.g., closing a dropdown popup or menu widget that uses WIDGET_OWNS_NATIVE_WIDGET). If the window is destroyed during the first iteration of the loop, the target pointer becomes dangling. In subsequent iterations, SetTargetAndDispatchEvent is called with this dangling pointer.

Exploitation and Impact

This UAF occurs in the Browser process and can be leveraged into a highly reliable double-free primitive, leading to a potential Sandbox Escape.

Suggested potential steps for an attacker to trigger the vulnerability:

  1. The attacker controls a malicious web page that convinces a user to interact with a native UI element backed by a WaylandWindow (e.g., a native <select> dropdown menu or context menu).
  2. The user interaction triggers multiple Wayland pointer events within a single display frame (e.g., moving the mouse while releasing a click generates wl_pointer.button, wl_pointer.motion, and wl_pointer.frame).
  3. Because the standard dispatch policy is wl::EventDispatchPolicy::kOnFrame, the button and motion events are queued in pointer_frames_.
  4. When wl_pointer.frame arrives, OnPointerFrameEvent() begins to drain the queue.
  5. The first event (button release) is dispatched to the target window. This click causes the popup widget to close synchronously, destroying the WaylandWindow.
  6. The WaylandWindow memory is freed by PartitionAlloc. During this free, BackupRefPtr (BRP) clears the kMemoryHeldByAllocatorBit in the slot’s metadata.
  7. The while loop continues to the next event (the motion event). SetTargetAndDispatchEvent is called with the dangling target pointer.
  8. Inside SetTargetAndDispatchEvent, the dangling pointer is assigned to the event’s raw_ptr<EventTarget> target_ field. This triggers BRP’s AcquireInternal(), which increments the reference count on the already-freed slot’s metadata.
  9. When the event is destroyed at the end of the loop iteration, the raw_ptr destructor calls ReleaseInternal().
  10. ReleaseInternal() decrements the reference count back to 0. It then checks if the memory is held by the allocator (count & kMemoryHeldByAllocatorBit). Because this bit was cleared during the initial free (Step 6), BRP mistakenly believes it should free the object from quarantine.
  11. PartitionRoot::FreeAfterBRPQuarantine is called, which places the already-freed slot into the PartitionAlloc thread cache again.
  12. This creates a cyclic/corrupted freelist (Double-Free), allowing the attacker to spray the heap, overlap objects, and hijack execution flow in the browser process.

(Note: These are suggested steps based on static analysis; our tooling agent does not yet execute code to provide a working PoC).

Suggested Fix

Similar functions in the same file correctly handle this scenario by using base::WeakPtr (e.g., OnPointerButtonEvent and ReleasePressedPointerButtons). OnPointerFrameEvent should be updated to use a WeakPtr for the target window and check for its validity before each dispatch:

  auto* target_window = window_manager_->GetCurrentPointerFocusedWindow();
  if (!target_window) {
    return;
  }
  base::WeakPtr<WaylandWindow> target = target_window->AsWeakPtr();

  while (!pointer_frames_.empty()) {
    auto pointer_frame = std::move(pointer_frames_.front());
    pointer_frames_.pop_front();

    if (target) {
      SetTargetAndDispatchEvent(pointer_frame->event.get(), target.get());
    }
    // ...

Evaluated with Chrome root at commit: 65b3256311f3ab6fb9870eaa522de7e6dd2663bb


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