diff --git a/ui/ozone/platform/x11/x11_window_ozone_unittest.cc b/ui/ozone/platform/x11/x11_window_ozone_unittest.cc index 9289d8cb187fd..18b41aac4693c 100644 --- a/ui/ozone/platform/x11/x11_window_ozone_unittest.cc +++ b/ui/ozone/platform/x11/x11_window_ozone_unittest.cc @@ -192,6 +192,114 @@ TEST_F(X11WindowOzoneTest, SendPlatformEventToCapturedWindow) { EXPECT_EQ(gfx::Point(-277, 215), event->AsLocatedEvent()->location()); } +// Regression test: when the located-events grabber window is synchronously +// destroyed during the GetBoundsInPixels() call inside DispatchUiEvent's +// coordinate-translation path, the stale raw `located_events_grabber` pointer +// must not be dereferenced afterward. +// +// Chain: DispatchUiEvent (on window_1) -> located_events_grabber-> +// GetBoundsInPixels() -> GeometryCache::GetBoundsPx() -> DispatchNow() -> +// X11Window::OnBoundsChanged (on window_2) -> delegate_2->OnBoundsChanged -> +// ~X11Window(window_2). After unwinding, DispatchUiEvent calls +// located_events_grabber->DispatchUiEvent() on the freed window_2 object. +TEST_F(X11WindowOzoneTest, DispatchUiEventGrabberFreedDuringGeometryFetch) { + auto* connection = x11::Connection::Get(); + + // Window 1: the event-target window. Use a NiceMock to ignore uninteresting + // calls (OnCursorUpdate, etc.). + testing::NiceMock delegate_1; + gfx::AcceleratedWidget widget_1; + constexpr gfx::Rect bounds_1(30, 80, 800, 600); + std::unique_ptr window_1 = + CreatePlatformWindow(&delegate_1, bounds_1, &widget_1, nullptr); + // Prime window_1's GeometryCache so the second GetBoundsInPixels() call in + // DispatchUiEvent (on `this`) is a cheap cached read. + window_1->GetBoundsInPixels(); + + // Window 2: the capture / located-events grabber window. + testing::NiceMock delegate_2; + gfx::AcceleratedWidget widget_2; + constexpr gfx::Rect bounds_2(525, 155, 296, 407); + std::unique_ptr window_2 = + CreatePlatformWindow(&delegate_2, bounds_2, &widget_2, nullptr); + auto xwindow_2 = static_cast(widget_2); + + // Prime window_2's GeometryCache so it becomes Ready and records + // last_notified_geometry_. Subsequent reparenting will then produce an + // origin-only change that fires NotifyBoundsChanged synchronously. + window_2->GetBoundsInPixels(); + + // Make window_2 the located_events_grabber. + window_2->SetCapture(); + ASSERT_EQ(window_2.get(), window_manager()->located_events_grabber()); + + // Create a real parent at a non-zero offset so that, after the synthetic + // reparent, only the absolute origin of window_2 changes (size stays + // 296x407). override_redirect avoids any WM interference. + x11::Window new_parent = connection->GenerateId(); + connection->CreateWindow({ + .wid = new_parent, + .parent = connection->default_root(), + .x = 200, + .y = 200, + .width = 1000, + .height = 1000, + .c_class = x11::WindowClass::InputOnly, + .override_redirect = x11::Bool32(true), + }); + + // Synthesize a ReparentNotify so window_2's GeometryCache replaces its + // parent_ with a fresh un-Ready cache. The next GetBoundsInPixels() on + // window_2 will block on the new parent's geometry, observe an origin + // change, and fire OnBoundsChanged synchronously. + x11::ReparentNotifyEvent reparent{}; + reparent.event = xwindow_2; + reparent.window = xwindow_2; + reparent.parent = new_parent; + reparent.x = 0; + reparent.y = 0; + x11::Event reparent_event(/*send_event=*/false, std::move(reparent)); + connection->DispatchEvent(reparent_event); + + // Arm delegate_2 so the next synchronous OnBoundsChanged frees window_2 - + // modelling views::Widget::CloseNow() -> SetPlatformWindow(nullptr) (e.g. a + // TabDragController closing the dragged window in response to bounds + // change). ~X11Window -> PrepareForShutdown clears the window manager's + // raw_ptr<> to the grabber, so MiraclePtr does not quarantine the freed + // memory; the local raw `auto*` in DispatchUiEvent is left dangling. + bool armed = true; + bool freed = false; + EXPECT_CALL(delegate_2, OnBoundsChanged(_)) + .WillRepeatedly([&](const PlatformWindowDelegate::BoundsChange&) { + if (armed) { + armed = false; + freed = true; + window_2.reset(); + } + }); + + // delegate_2 must never receive a DispatchEvent on the freed window. + EXPECT_CALL(delegate_2, DispatchEvent(_)).Times(0); + + // Dispatch a mouse-press targeted at window_1. DispatchUiEvent on window_1 + // sees located_events_grabber == window_2, calls + // window_2->GetBoundsInPixels() (which synchronously frees window_2 via the + // arm above), then later calls located_events_grabber->DispatchUiEvent() on + // the dangling pointer. + ScopedXI2Event xi_event; + xi_event.InitGenericButtonEvent(kPointerDeviceId, EventType::kMousePressed, + gfx::Point(218, 290), EF_NONE); + DispatchXEvent(xi_event, widget_1); + + // The synchronous-free path must have been exercised; otherwise the test + // didn't reach the vulnerable frame. + EXPECT_TRUE(freed) << "delegate_2->OnBoundsChanged() was not invoked " + "synchronously from DispatchUiEvent"; + EXPECT_EQ(nullptr, window_manager()->located_events_grabber()); + + connection->DestroyWindow({new_parent}); +} + // This test case ensures window_manager properly provides X11Window instances // as they are created/destroyed. TEST_F(X11WindowOzoneTest, GetWindowFromAcceleratedWigets) {