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 708267103ca10..631bf81ebe49d 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,76 @@ TEST_P(WaylandEventSourceTest, TabletToolProximityInUAF) { base::TimeTicks::Now()); } +// ReleasePressedPointerButtons holds a bare WaylandWindow* across multiple +// kImmediate dispatch iterations. If the first synthetic kMouseReleased causes +// the target window to be destroyed (in production: a handler spins a nested +// RunLoop during which the window is closed), the second iteration calls +// SetPointerFocusedWindow on freed memory. This is the unpatched sibling of +// the OnTabletToolProximityIn WeakPtr fix above. +TEST_P(WaylandEventSourceTest, ReleasePressedPointerButtonsUAF) { + PostToServerAndWait([](wl::TestWaylandServerThread* server) { + wl_seat_send_capabilities(server->seat()->resource(), + WL_SEAT_CAPABILITY_POINTER); + }); + ASSERT_TRUE(connection_->seat()->pointer()); + + // Create a victim window with its own delegate (separate from |window_| so + // that no live raw_ptr<> to it survives destruction). + MockWaylandPlatformWindowDelegate victim_delegate(connection_.get()); + auto victim = CreateWaylandWindowWithParams(PlatformWindowType::kWindow, + kDefaultBounds, &victim_delegate); + ASSERT_TRUE(victim); + WaylandWindow* victim_raw = victim.get(); + + // Record TWO pressed buttons so ReleasePressedPointerButtons must iterate at + // least twice. Enter on |window_| (not |victim|) so the iteration-1 closure + // captures prev_focused_window != victim and thus provides no incidental + // BRP cover for |victim|. + EXPECT_CALL(delegate_, DispatchEvent(_)).Times(::testing::AnyNumber()); + PostToServerAndWait([surface_id = window_->root_surface()->get_surface_id()]( + wl::TestWaylandServerThread* server) { + auto* const surface = + server->GetObject(surface_id)->resource(); + auto* const pointer = server->seat()->pointer()->resource(); + wl_pointer_send_enter(pointer, server->GetNextSerial(), surface, 0, 0); + wl_pointer_send_frame(pointer); + wl_pointer_send_button(pointer, server->GetNextSerial(), + server->GetNextTime(), BTN_LEFT, + WL_POINTER_BUTTON_STATE_PRESSED); + wl_pointer_send_frame(pointer); + 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)); + + // Mirror the OnDragSessionClose precondition: the compositor has sent + // wl_pointer.leave when the drag began, so pointer focus is cleared. + SetPointerFocusedWindow(nullptr); + + // The first synthetic kMouseReleased dispatched to |victim| simulates a + // handler that spins a nested RunLoop during which |victim| is destroyed + // (e.g. renderer-driven window.close()). ~WaylandWindow → RemoveWindow nulls + // window_map_ / pointer_focused_window_, and the OnWindowRemoved observer + // chain nulls every raw_ptr<> reference, so the slot is genuinely freed. + EXPECT_CALL(victim_delegate, DispatchEvent(_)) + .WillOnce([&](Event* event) { + EXPECT_EQ(event->type(), EventType::kMouseReleased); + victim.reset(); + }) + .WillRepeatedly(::testing::Return()); + + // Iteration 2 then calls OnPointerButtonEvent(..., victim_freed, kImmediate) + // → SetPointerFocusedWindow(victim_freed) + // → victim_freed->OnPointerFocusChanged(true) [member read + virtual call] + // → SetTargetAndDispatchEvent → GetRootTarget → victim_freed->GetParentTarget + // ASAN reports heap-use-after-free here. + pointer_delegate_->ReleasePressedPointerButtons(victim_raw, + base::TimeTicks::Now()); +} + INSTANTIATE_TEST_SUITE_P( EventsDispatchPolicyTest, WaylandEventSourceTest,