diff --git a/ui/ozone/platform/x11/x11_window_ozone_unittest.cc b/ui/ozone/platform/x11/x11_window_ozone_unittest.cc index e759196685a80..e52c652b00afb 100644 --- a/ui/ozone/platform/x11/x11_window_ozone_unittest.cc +++ b/ui/ozone/platform/x11/x11_window_ozone_unittest.cc @@ -367,6 +367,111 @@ TEST_F(X11WindowOzoneTest, SetBoundsInPixelsUseAfterFreeViaGeometryCache) { connection->DestroyWindow({new_parent}); } +// Regression test: X11Window::OnWindowMapped() writes to |this| after calling +// Maximize(), which can synchronously delete |this| via OnBoundsChanged. The +// MapNotify dispatch path (Connection::DispatchEvent -> EventObserver::OnEvent +// -> HandleEvent -> OnWindowMapped) is NOT covered by the weak_this guard that +// exists in DispatchUiEvent, because MapNotify is not a translatable event. +// +// This test arranges: +// 1. should_maximize_after_map_ == true (Maximize() called while unmapped) +// 2. The next GetBoundsInPixels() inside Maximize() synchronously frees the +// X11Window (modelling the documented "OnBoundsChanged may delete this" +// sink that motivated the weak_this guards already present in Maximize() +// and SetFullscreen()). +// Then it dispatches a synthetic MapNotify exactly as the X server (or a +// compromised process holding an X connection) would, and expects no UAF. +// +// Under ASAN this fails with heap-use-after-free at OnWindowMapped() on the +// `should_maximize_after_map_ = false` write that follows Maximize(), because +// OnWindowMapped() has no weak_this liveness check after the call. +TEST_F(X11WindowOzoneTest, OnWindowMappedUseAfterFreeAfterMaximize) { + 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); + + // Prime the X11Window's GeometryCache so the first Maximize() below does + // not itself fire a synchronous OnBoundsChanged. + window->GetBoundsInPixels(); + + // Step 1: Maximize() while !window_mapped_in_client_ -> sets + // should_maximize_after_map_ = true. (The window has never been Show()n.) + window->Maximize(); + + // Step 2: Create a real parent window at a non-zero offset so that after + // a synthetic reparent, only the absolute origin of |xwindow| changes. + // 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 a ReparentNotify so the leaf GeometryCache becomes + // un-Ready. The next GetBoundsInPixels() (inside the Maximize() called from + // OnWindowMapped) will block on the new parent's geometry, observe an + // origin change, and fire OnBoundsChanged 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: Arm the delegate so the *next* synchronous OnBoundsChanged frees + // the X11Window — modelling Widget::CloseNow -> SetPlatformWindow(nullptr), + // which is the deletion sink explicitly acknowledged by the weak_this guards + // already present in X11Window::Maximize() / SetFullscreen() / + // WindowTreeHostPlatform::OnBoundsChanged(). + bool armed = true; + bool freed = false; + EXPECT_CALL(delegate, OnBoundsChanged(_)) + .WillRepeatedly([&](const PlatformWindowDelegate::BoundsChange&) { + if (armed) { + armed = false; + freed = true; + window.reset(); + } + }); + + // Step 5: Dispatch a synthetic MapNotify for this window. There is no + // send_event filtering on the OnEvent path; IsTargetedBy() only checks + // map->window == xwindow_. + // + // OnEvent -> HandleEvent -> OnWindowMapped: + // should_maximize_after_map_ is true -> Maximize() + // -> GetBoundsInPixels() -> GeometryCache resolves new parent + // -> origin changed -> NotifyBoundsChanged -> delegate frees X11Window + // -> Maximize() weak_this guard returns early. + // OnWindowMapped then writes `should_maximize_after_map_ = false` and + // reads `should_grab_pointer_after_map_` on the freed |this|. + x11::MapNotifyEvent map_notify{}; + map_notify.event = xwindow; + map_notify.window = xwindow; + x11::Event map_event(/*send_event=*/false, std::move(map_notify)); + connection->DispatchEvent(map_event); + + // 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 OnWindowMapped/Maximize"; + + 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);