CVE-2026-17656
Overview
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
Patch
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);
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.