diff --git a/ui/ozone/platform/x11/x11_window_ozone_unittest.cc b/ui/ozone/platform/x11/x11_window_ozone_unittest.cc index a45afbfb87a12..8be3f86ba9abd 100644 --- a/ui/ozone/platform/x11/x11_window_ozone_unittest.cc +++ b/ui/ozone/platform/x11/x11_window_ozone_unittest.cc @@ -283,6 +283,92 @@ class FakeX11ExtensionDelegateForSize : public X11ExtensionDelegate { gfx::Rect guessed_size_px_; }; +// POC: Demonstrates that callers of GetBoundsInPixels() in X11Window do not +// guard against |this| being freed when GeometryCache::GetBoundsPx() runs the +// bounds-changed callback synchronously via DispatchNow(). This models the +// crbug.com/1068755 scenario where an OnHostMovedInPixels observer +// synchronously closes the Widget, freeing the X11Window while one of its +// methods (here, SetBoundsInPixels) is still on the stack. +TEST_F(X11WindowOzoneTest, SetBoundsInPixelsUseAfterFreeViaGeometryCache) { + 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_. This goes through the + // synchronous DispatchNow() path. The resulting X11Window::OnBoundsChanged + // sees a size change and only posts a delayed-resize task, so the delegate + // is not called synchronously here. + 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 leaf cache's chain not Ready. + 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: Arm the delegate so that the *next* synchronous OnBoundsChanged + // (which will fire from inside SetBoundsInPixels → GetBoundsInPixels → + // GetBoundsPx → DispatchNow → OnBoundsChanged → NotifyBoundsChanged) frees + // the X11Window — modelling Widget::CloseNow → SetPlatformWindow(nullptr). + bool armed = true; + bool freed = false; + EXPECT_CALL(delegate, OnBoundsChanged(_)) + .WillRepeatedly([&](const PlatformWindowDelegate::BoundsChange&) { + if (armed) { + armed = false; + freed = true; + // ~X11Window → PrepareForShutdown → Close → CloseXWindow → + // geometry_cache_.reset(). All GeometryCache weak_ptrs are + // invalidated, so every GetBoundsPx() frame on the stack will + // return {}, but SetBoundsInPixels() has no such guard. + window.reset(); + } + }); + + // Step 5: Enter SetBoundsInPixels. Line 544's GetBoundsInPixels() recurses + // into the un-Ready parent cache, DispatchNow()s its replies, the + // NotifyGeometryChanged cascade fires X11Window::OnBoundsChanged with an + // origin-only change (old={30,80,800,600}, new={200,200,800,600}), the + // delegate frees |this|, GetBoundsPx() returns {}, and SetBoundsInPixels + // continues at line 546+ reading and writing freed |this| members and + // finally vcalling through freed platform_window_delegate_ at line 601. + PlatformWindow* raw = window.get(); + raw->SetBoundsInPixels(gfx::Rect(40, 90, 800, 600)); + + // If we got here without ASAN tripping, the synchronous-free path was not + // exercised; surface that as a test failure rather than a silent pass. + EXPECT_TRUE(freed) << "delegate was never invoked synchronously"; + + connection->DestroyWindow({new_parent}); +} + // Verifies X11Window sets fullscreen bounds in pixels when going to fullscreen. TEST_F(X11WindowOzoneTest, SetFullscreen) { constexpr gfx::Rect screen_bounds_in_px(640, 480, 1280, 720);