CVE-2026-17656
Overview
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.Attack Path
- Open a popup A web page (or driven UI) opens a Wayland popup/window that closes itself on click.
- Buffer multi-event frame
The compositor delivers a pointer frame carrying multiple buffered events into
pointer_frames_. - Dispatch closes window
The first event’s dispatch synchronously destroys the focused
WaylandWindow, freeing it. - Reuse dangling pointer Subsequent loop iterations dispatch through the stale cached raw pointer, triggering the UAF.
Impact Assessment
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
| Function | Change | Notes |
|---|---|---|
ifui/ozone/platform/wayland/host/wayland_event_source.cc |
modified | |
TEST_Fui/ozone/platform/wayland/host/wayland_pointer_unittest.cc |
modified | |
ifui/ozone/platform/wayland/host/wayland_pointer_unittest.cc |
modified | |
PostToServerAndWaitui/ozone/platform/wayland/host/wayland_pointer_unittest.cc |
modified |
Files Changed
ui/ozone/platform/wayland/host/wayland_event_source.ccui/ozone/platform/wayland/host/wayland_pointer_unittest.cc
Audit Directions
- Raw pointer cached before a dispatch loopFlag any handler that caches a raw object pointer outside a loop whose body dispatches events or runs callbacks; convert to a per-iteration
WeakPtrre-check. - Reentrant window destruction during inputAudit other
WaylandEventSourcedispatch paths (touch, keyboard, drag) for focused-window pointers assumed live across synchronous dispatch that can close windows. - Missing liveness guard before useSearch for
SetTargetAndDispatchEvent-style helpers that dereference a target without aCHECK/validity guard, ensuring destruction mid-frame is handled.
Patch
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);
Regression Test / PoC
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);
Original Bug Report
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:
- 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). - 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, andwl_pointer.frame). - Because the standard dispatch policy is
wl::EventDispatchPolicy::kOnFrame, the button and motion events are queued inpointer_frames_. - When
wl_pointer.framearrives,OnPointerFrameEvent()begins to drain the queue. - The first event (button release) is dispatched to the
targetwindow. This click causes the popup widget to close synchronously, destroying theWaylandWindow. - The
WaylandWindowmemory is freed by PartitionAlloc. During this free, BackupRefPtr (BRP) clears thekMemoryHeldByAllocatorBitin the slot’s metadata. - The
whileloop continues to the next event (the motion event).SetTargetAndDispatchEventis called with the danglingtargetpointer. - Inside
SetTargetAndDispatchEvent, the dangling pointer is assigned to the event’sraw_ptr<EventTarget> target_field. This triggers BRP’sAcquireInternal(), which increments the reference count on the already-freed slot’s metadata. - When the event is destroyed at the end of the loop iteration, the
raw_ptrdestructor callsReleaseInternal(). 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.PartitionRoot::FreeAfterBRPQuarantineis called, which places the already-freed slot into the PartitionAlloc thread cache again.- 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.