diff --git a/ui/ozone/platform/x11/test/x11_window_unittest.cc b/ui/ozone/platform/x11/test/x11_window_unittest.cc index 2b51bc079d729..72e94405aee48 100644 --- a/ui/ozone/platform/x11/test/x11_window_unittest.cc +++ b/ui/ozone/platform/x11/test/x11_window_unittest.cc @@ -539,6 +539,123 @@ TEST_F(X11WindowTest, EXPECT_EQ(delegate.state(), PlatformWindowState::kNormal); } +// Delegate that synchronously destroys the owned X11Window from inside +// OnBoundsChanged, simulating the production path where an +// OnHostMovedInPixels observer calls Widget::CloseNow() and the host's +// SetPlatformWindow(nullptr) frees the X11Window while SetOverrideRedirect() +// is still on the stack. +class BoundsChangeDestructionDelegate : public TestPlatformWindowDelegate { + public: + BoundsChangeDestructionDelegate() = default; + ~BoundsChangeDestructionDelegate() override = default; + + void set_window(std::unique_ptr window) { + window_ = std::move(window); + } + void Arm() { armed_ = true; } + bool destroyed() const { return destroyed_; } + + void OnBoundsChanged( + const PlatformWindowDelegate::BoundsChange& change) override { + if (armed_ && change.origin_changed && window_) { + destroyed_ = true; + // This mirrors DesktopWindowTreeHostPlatform::OnClosed() → + // SetPlatformWindow(nullptr), which synchronously runs ~X11Window + // while Map()/SetOverrideRedirect() are still on the call stack. + window_.reset(); + } + } + + SkPath GetWindowMaskForWindowShapeInPixels() override { return SkPath(); } + + private: + std::unique_ptr window_; + bool armed_ = false; + bool destroyed_ = false; +}; + +// Regression POC: X11Window::SetOverrideRedirect() reads/writes |this| after +// Map() → GetBoundsInPixels() → GeometryCache::GetBoundsPx() → DispatchNow() +// re-enters X11Window::OnBoundsChanged() → delegate synchronously frees the +// X11Window. +// +// Map() itself has a weak_this guard around GetBoundsInPixels(), so it returns +// safely. But its caller, SetOverrideRedirect(), then reads +// |has_pointer_grab_| and writes |should_grab_pointer_after_map_| on the +// freed object. +// +// In production this is reached on Linux/X11 under a tiling WM via +// BrowserDesktopWindowTreeHostLinux::TabDraggingKindChanged() → +// SetOverrideRedirect(), and the synchronous free happens when an +// OnHostMovedInPixels observer closes the widget (crbug.com/1068755). +TEST_F(X11WindowTest, SetOverrideRedirectSelfDeleteUaf) { + auto* connection = x11::Connection::Get(); + + auto delegate = std::make_unique(); + constexpr gfx::Rect bounds(100, 100, 200, 200); + auto window = CreateX11Window(delegate.get(), bounds, nullptr); + X11Window* window_ptr = window.get(); + delegate->set_window(std::move(window)); + const auto xwindow = static_cast(delegate->widget()); + ASSERT_NE(xwindow, x11::Window::None); + + // Map the window so SetOverrideRedirect()'s |remap| path is taken. + window_ptr->Show(/*inactive=*/false); + // Drain so the root GeometryCache becomes Ready and last_notified_geometry_ + // is populated (otherwise the later origin-only change would be treated as a + // size change and posted asynchronously). + connection->DispatchAll(); + base::RunLoop().RunUntilIdle(); + // Force the cache to a known Ready state. + (void)window_ptr->GetBoundsInPixels(); + + // Create a real container window to act as the new parent so that the + // GeometryCache for the new parent has valid QueryTree/GetGeometry replies + // when DispatchNow() runs. + x11::Window container = connection->GenerateId(); + connection->CreateWindow(x11::CreateWindowRequest{ + .wid = container, + .parent = connection->default_root(), + .x = 10, + .y = 10, + .width = 300, + .height = 300, + .c_class = x11::WindowClass::InputOutput, + }); + connection->Flush(); + + // Precondition: simulate a WM ReparentNotify processed in a prior tick. + // GeometryCache::OnEvent() creates a fresh un-Ready |parent_| cache whose + // QueryTree/GetGeometry futures are pending; NotifyGeometryChanged() + // early-returns because Ready() is false. This is exactly the precondition + // described for the production bug: a tiling WM reparents the toplevel just + // before the tab-drag event runs SetOverrideRedirect(). + x11::ReparentNotifyEvent reparent; + reparent.event = xwindow; + reparent.window = xwindow; + reparent.parent = container; + reparent.x = 5; + reparent.y = 5; + reparent.override_redirect = 0; + x11::Event xev(false, std::move(reparent)); + connection->DispatchEvent(xev); + + // Arm the synchronous-destroy. The next origin-only OnBoundsChanged + // notification (which will fire from inside Map() → GeometryCache:: + // GetBoundsPx() → DispatchNow()) will free the X11Window. + delegate->Arm(); + + // Under ASAN this reports heap-use-after-free at SetOverrideRedirect()'s + // read of |has_pointer_grab_| after Map() returns. + window_ptr->SetOverrideRedirect(true); + + // The delegate must have been re-entered and the window freed; if not, the + // precondition setup didn't work and the test isn't exercising the bug. + EXPECT_TRUE(delegate->destroyed()); + + connection->DestroyWindow({container}); +} + // Tests that synchronous destruction of the window during event dispatching // does not cause a UAF. TEST_F(X11WindowTest, SynchronousDestructionDuringEventDispatch) {