diff --git a/ui/ozone/platform/wayland/host/wayland_window_unittest.cc b/ui/ozone/platform/wayland/host/wayland_window_unittest.cc index 7aea341d9dd14..8e1b5afad6d52 100644 --- a/ui/ozone/platform/wayland/host/wayland_window_unittest.cc +++ b/ui/ozone/platform/wayland/host/wayland_window_unittest.cc @@ -5467,6 +5467,59 @@ TEST_P(WaylandWindowTest, HandleToplevelConfigureSyncCloseOnDeactivate) { EXPECT_FALSE(window_); } +// Regression POC: WaylandBubble::SetBoundsInDIP() continues to dereference +// `this` after the base WaylandWindow::SetBoundsInDIP() -> +// MaybeApplyLatestStateRequest() -> delegate()->OnStateUpdate() chain +// synchronously destroys the platform window. This mirrors the production path +// where a WindowTreeHostObserver / WidgetObserver registered on the bubble's +// host calls Widget::CloseNow() inside OnHostMovedInPixels / +// OnWidgetBoundsChanged, which leads to +// DesktopWindowTreeHostPlatform::OnClosed -> SetPlatformWindow(nullptr) and +// frees the WaylandBubble while its SetBoundsInDIP() override frame is still +// on the stack. The base class already weak-guards the leaf +// (wayland_window.cc MaybeApplyLatestStateRequest), but WaylandBubble's +// override does not. +TEST_P(WaylandWindowTest, BubbleSetBoundsSyncCloseOnStateUpdate) { + // Create a bubble parented to the toplevel window. CreateWaylandWindowWith- + // Params() also calls Show(), which creates the wl_subsurface so that the + // post-SetBoundsInDIP `if (subsurface_ && ...)` branch is taken. + testing::NiceMock bubble_delegate( + connection_.get()); + std::unique_ptr bubble = CreateWaylandWindowWithParams( + PlatformWindowType::kBubble, gfx::Rect(10, 10, 100, 100), + &bubble_delegate, widget_); + ASSERT_TRUE(bubble); + ASSERT_TRUE(bubble->IsVisible()); // subsurface_ is set. + WaylandWindow* raw_bubble = bubble.get(); + + // Don't try to talk to the server after the bubble has been torn down + // mid-dispatch (its wl_subsurface is destroyed without a roundtrip). + DisableSyncOnTearDown(); + + // Simulate a delegate that destroys the platform window inside + // OnStateUpdate() — exactly what happens in production when a + // WindowTreeHostObserver deletes the host during OnHostMovedInPixels (see + // window_tree_host_platform_unittest.cc DeleteHostFromOnStateUpdate). + bubble_delegate.set_on_state_update_callback( + base::BindLambdaForTesting([&]() { + bubble.reset(); + return false; + })); + + // Drive WaylandBubble::SetBoundsInDIP with changed bounds. Inside the call: + // WaylandWindow::SetBoundsInDIP -> RequestStateFromClient -> + // MaybeApplyLatestStateRequest -> delegate()->OnStateUpdate -> + // (callback above) bubble.reset() -> ~WaylandBubble(). + // The leaf weak-guard returns; the stack unwinds to + // WaylandBubble::SetBoundsInDIP which then reads `this->subsurface_` and + // calls SetSubsurfacePosition() on the freed object. + raw_bubble->SetBoundsInDIP(gfx::Rect(20, 20, 200, 200)); + + // If we got here without ASAN reporting a heap-use-after-free, the bug is + // fixed. + EXPECT_FALSE(bubble); +} + INSTANTIATE_TEST_SUITE_P(XdgVersionStableTest, WaylandWindowTest, Values(wl::ServerConfig{}));