CVE-2026-11628
Overview
Background
- Ozone/Wayland
- Chrome’s Linux display abstraction;
WaylandEventSourcetranslates Wayland pointer/keyboard events intoui::Eventobjects dispatched toWaylandWindowinstances in the browser process. - `ReleasePressedPointerButtons`
- Helper that synthesizes a mouse-release
ui::Eventfor each currently-pressed pointer button, e.g. when focus leaves a window or a device disconnects. - Use-after-free (UAF)
- Dereferencing a heap object after it has been freed; here a
WaylandWindow*destroyed by dispatching one synthesized event, then reused for the next. - `base::WeakPtr`
- A smart pointer that invalidates to
nullptrwhen its referent is destroyed, letting a caller safely re-check liveness per use.
Root Cause Analysis
ReleasePressedPointerButtons captured the focused WaylandWindow as a raw pointer once, before iterating over the set of pressed buttons. For each pressed button it synthesized and dispatched a release event through OnPointerButtonEventInternal, which took a raw WaylandWindow*. Event dispatch is reentrant: the first synthesized release can synchronously run application logic that closes and destroys that WaylandWindow. On subsequent loop iterations the now-dangling raw pointer was still dereferenced to build and route the release event, producing a use-after-free in the browser process. Because the synthesized state derives from web-driven pointer input (a drag or a click that closes the window while multiple buttons are held), an attacker can steer the timing of the destroying dispatch to land inside the loop.window_weak.get() provides.Attack Path
- Hold multiple buttons Web content drives pointer state so that two or more pointer buttons are simultaneously pressed over a Wayland-backed window.
- Trigger synthesized release
A focus-loss, device-removal, or close condition causes
ReleasePressedPointerButtonsto iterate the pressed-button set. - Destroy on first dispatch
Dispatching the first synthesized release runs handlers that synchronously close and free the focused
WaylandWindow. - Dereference freed window The loop continues and reuses the dangling raw pointer for the remaining releases, causing a use-after-free.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifui/ozone/platform/wayland/host/wayland_event_source.cc |
modified | |
forui/ozone/platform/wayland/host/wayland_event_source.cc |
modified | |
TEST_Pui/ozone/platform/wayland/host/wayland_event_source_unittest.cc |
modified | |
PostToServerAndWaitui/ozone/platform/wayland/host/wayland_event_source_unittest.cc |
modified |
Files Changed
ui/ozone/platform/wayland/host/wayland_event_source.ccui/ozone/platform/wayland/host/wayland_event_source.hui/ozone/platform/wayland/host/wayland_event_source_unittest.cc
Audit Directions
- Raw pointer captured across dispatch loopsAudit any handler that caches a
WaylandWindow*(or other UI object) before a loop that dispatchesui::Events; reentrant dispatch can free it mid-loop, so requireWeakPtrre-validation per iteration. - Synthesized multi-event emittersReview other synthesis paths (pointer, touch, keyboard release/leave sequences) that emit one event per held input, since each emission is a reentrancy point that may invalidate shared state.
- Reentrant window destructionTrace close/destroy paths reachable synchronously from event dispatch and confirm no caller resumes use of the window afterward.
Patch
From edce928690cb0bbca49e411fc88b8293dcd9dc8a Mon Sep 17 00:00:00 2001
From: Max Ihlenfeldt <max@igalia.com>
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 <thomasanderson@chromium.org>
Commit-Queue: Max Ihlenfeldt <max@igalia.com>
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,
Regression Test / PoC
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,
Original Bug Report
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.
- The user has multiple pointer (mouse) buttons registered as pressed (e.g., Left and Right).
- An event triggers
ReleasePressedPointerButtons, passing the targetWaylandWindowas a raw stack parameter. - The loop processes the first button release, synchronously dispatching the event.
- 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).
- The window map erases the window, and its memory is immediately deallocated and returned to the PartitionAlloc freelist.
- The loop continues to the next iteration to release the second button.
- The dangling window pointer on the stack is passed to
OnPointerButtonEvent, which dereferences it viawindow_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.