Overview

Critical
Severity
9.6
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.
Tracker523725277
Fix commit45daaa0cb895 (chromium/src) +44/-3
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-06-14 (per vendor advisory)
Fix landed2026-06-18 (public git)
Disclosed2026-07-29
Patch gap41 days — fix public in source before the advisory

Background

Ozone/Wayland
Chrome’s platform abstraction layer that talks to a Wayland compositor for windowing and input on Linux.
`WaylandEventSource`
Host-side component that receives Wayland input protocol events and dispatches them into Chrome’s event pipeline.
`pointer_frames_`
Queue of buffered pointer events drained and dispatched together as one Wayland pointer “frame.”
UAF (Use-After-Free)
Accessing a heap object through a pointer after the object has been freed, yielding memory corruption.

Root Cause Analysis

WaylandEventSource::OnPointerFrameEvent cached a raw WaylandWindow* for the currently pointer-focused window once, before entering the while-loop that drains pointer_frames_. Each buffered event was dispatched to that cached target via SetTargetAndDispatchEvent. Dispatching a pointer event runs synchronous browser logic that can destroy the focused window — for example, a popup that closes itself when clicked. On the next loop iteration the cached raw pointer referenced freed memory, so SetTargetAndDispatchEvent operated on a dangling WaylandWindow, producing a use-after-free in the browser process. The pointer’s object lifetime was assumed stable across the whole drain, but dispatch itself could invalidate it mid-loop.
Key insight
The window lifetime was not guaranteed for the duration of the drain loop, yet a single cached raw pointer was reused across every iteration. Re-fetching or weakly holding the target per-iteration is required because dispatch can synchronously free the target.

Attack Path

  1. Open a popup A web page (or driven UI) opens a Wayland popup/window that closes itself on click.
  2. Buffer multi-event frame The compositor delivers a pointer frame carrying multiple buffered events into pointer_frames_.
  3. Dispatch closes window The first event’s dispatch synchronously destroys the focused WaylandWindow, freeing it.
  4. Reuse dangling pointer Subsequent loop iterations dispatch through the stale cached raw pointer, triggering the UAF.

Impact Assessment

Use-after-free of a WaylandWindow in the browser process, reachable from web-driven pointer input under a Wayland session. Because it is a controlled-timing UAF on a heap object during event dispatch, it is a candidate for memory corruption and potential exploitation for higher-severity outcomes. Exploitation requires the Linux/Wayland Ozone backend and a synchronously self-closing window.

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

Audit Directions

  • Raw pointer cached before a dispatch loop
    Flag any handler that caches a raw object pointer outside a loop whose body dispatches events or runs callbacks; convert to a per-iteration WeakPtr re-check.
  • Reentrant window destruction during input
    Audit other WaylandEventSource dispatch paths (touch, keyboard, drag) for focused-window pointers assumed live across synchronous dispatch that can close windows.
  • Missing liveness guard before use
    Search for SetTargetAndDispatchEvent-style helpers that dereference a target without a CHECK/validity guard, ensuring destruction mid-frame is handled.
From 45daaa0cb895d8e01f34b3138316bcb83bb7a845 Mon Sep 17 00:00:00 2001
From: Kramer Ge <[email protected]>
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 <[email protected]>
Commit-Queue: Kramer Ge <[email protected]>
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 [email protected]

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