diff --git a/ui/ozone/platform/x11/x11_window_ozone_unittest.cc b/ui/ozone/platform/x11/x11_window_ozone_unittest.cc index 42f63fcdbfe98..0f06d2e38b424 100644 --- a/ui/ozone/platform/x11/x11_window_ozone_unittest.cc +++ b/ui/ozone/platform/x11/x11_window_ozone_unittest.cc @@ -639,4 +639,117 @@ TEST_F(X11WindowOzoneTest, DispatchUiEventGrabberFreedDuringGeometryFetch) { connection->DestroyWindow({new_parent}); } +// Verifies that X11Window::DispatchEvent() does not cause a use-after-free if +// the dispatch-target window is synchronously deleted during the +// MouseOnWindow() -> OnCursorUpdate() callout that precedes HandleAsAtkEvent() +// / DispatchUiEvent(). +// +// In production, MouseOnWindow(this) -> X11Window::OnCursorUpdate() -> +// PlatformWindowDelegate::OnCursorUpdate() reaches +// WindowTreeHostPlatform::OnCursorUpdate(), which calls +// display::Screen::GetDisplayNearestWindow(), which on X11 resolves to +// X11ScreenOzone::GetDisplayForAcceleratedWidget() and synchronously calls +// this->GetBoundsInPixels(). Per the warning at X11Window::GetBoundsInPixels(), +// that call can dispatch pending X server events and destroy `this`. The +// sibling DispatchUiEvent() was hardened with WeakPtr guards, but the caller +// DispatchEvent() was not — it unconditionally proceeds to +// HandleAsAtkEvent()/DispatchUiEvent() on the freed `this`. +// +// This test models the WindowTreeHostPlatform/X11ScreenOzone hops via the mock +// delegate (the same approach used by the sibling tests above). +TEST_F(X11WindowOzoneTest, DispatchEventSelfFreedDuringMouseOnWindow) { + testing::NiceMock delegate; + gfx::AcceleratedWidget widget; + constexpr gfx::Rect bounds(30, 80, 800, 600); + std::unique_ptr window = + CreatePlatformWindow(&delegate, bounds, &widget, nullptr); + + auto* connection = x11::Connection::Get(); + auto xwindow = static_cast(widget); + + // Step 1: Force the X11Window's GeometryCache (and its parent chain) to + // become Ready and record last_notified_geometry_, so the next change can be + // an origin-only change that fires NotifyBoundsChanged() synchronously. + window->GetBoundsInPixels(); + + // Step 2: Create a real parent window at a non-zero offset so that after + // reparenting, only the absolute origin of |xwindow| changes (size stays + // 800x600). 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), + }); + + // Step 3: Synthesize the ReparentNotify the X server would send for a WM + // reparent. GeometryCache::OnEvent replaces parent_ with a fresh un-Ready + // GeometryCache for |new_parent|, leaving the chain not Ready until the next + // GetBoundsInPixels() call dispatches the pending X replies synchronously. + x11::ReparentNotifyEvent reparent{}; + reparent.event = xwindow; + reparent.window = xwindow; + reparent.parent = new_parent; + reparent.x = 0; + reparent.y = 0; + x11::Event reparent_event(/*send_event=*/false, std::move(reparent)); + connection->DispatchEvent(reparent_event); + + // Step 4: Model WindowTreeHostPlatform::OnCursorUpdate() -> + // Screen::GetDisplayNearestWindow() -> + // X11ScreenOzone::GetDisplayForAcceleratedWidget() -> + // window->GetBoundsInPixels(). The mock fixture cannot use the real + // WindowTreeHostPlatform/X11ScreenOzone (depending on //ui/aura here is a + // layering violation), so the delegate performs the equivalent re-entrant + // call directly. + EXPECT_CALL(delegate, OnCursorUpdate()).WillRepeatedly([&]() { + // This is the un-guarded re-entrant edge: it reaches + // GeometryCache::GetBoundsPx(), which DispatchNow()s the pending parent + // replies and synchronously fires OnBoundsChanged below. + PlatformWindow* w = window.get(); + if (w) { + w->GetBoundsInPixels(); + } + }); + + // Step 5: Arm the delegate so that the synchronous OnBoundsChanged fired from + // inside GetBoundsInPixels() frees the X11Window — modelling + // WindowTreeHostPlatform::OnBoundsChanged -> OnHostMovedInPixels -> + // observer -> Widget::CloseNow -> SetPlatformWindow(nullptr) -> ~X11Window, + // the deletion sink explicitly acknowledged at + // window_tree_host_platform.cc:277-285 and x11_window.cc:2579-2581. + bool armed = true; + bool freed = false; + EXPECT_CALL(delegate, OnBoundsChanged(_)) + .WillRepeatedly([&](const PlatformWindowDelegate::BoundsChange&) { + if (armed) { + armed = false; + freed = true; + window.reset(); + } + }); + + // Step 6: Dispatch a mouse event to |window|. window_mouse_currently_on_ is + // not yet |window|, so X11Window::DispatchEvent() calls MouseOnWindow(this), + // which synchronously runs steps 4+5 and destroys `this`. DispatchEvent then + // continues to HandleAsAtkEvent()/DispatchUiEvent() on the freed `this`, + // reading freed members (x11_extension_delegate_, weak_ptr_factory_, ...). + ScopedXI2Event xi_event; + xi_event.InitGenericButtonEvent(kPointerDeviceId, EventType::kMousePressed, + gfx::Point(50, 50), EF_NONE); + DispatchXEvent(xi_event, widget); + + // The synchronous-free path must have been exercised; otherwise the test + // didn't reach the vulnerable frame. + EXPECT_TRUE(freed) << "delegate->OnBoundsChanged() was not invoked " + "synchronously from MouseOnWindow/OnCursorUpdate"; + + connection->DestroyWindow({new_parent}); +} + } // namespace ui