diff --git a/ui/views/BUILD.gn b/ui/views/BUILD.gn index 0000000..0000000 100644 --- a/ui/views/BUILD.gn +++ b/ui/views/BUILD.gn @@ -1450,6 +1450,7 @@ "accessibility/test_list_grid_view.cc", "accessibility/test_list_grid_view.h", "accessibility/view_ax_platform_node_delegate_win_unittest.cc", + "win/hwnd_message_handler_headless_unittest.cc", "win/hwnd_util_unittest.cc", "win/pen_event_processor_unittest.cc", "win/pen_id_handler_unittest.cc", diff --git a/ui/views/win/hwnd_message_handler_headless_unittest.cc b/ui/views/win/hwnd_message_handler_headless_unittest.cc new file mode 100644 index 0000000..0000000 --- /dev/null +++ b/ui/views/win/hwnd_message_handler_headless_unittest.cc @@ -0,0 +1,110 @@ +// Copyright 2025 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Regression / PoC test for missing re-entrancy guards in +// HWNDMessageHandlerHeadless::SetBoundsInternal(). +// +// The non-headless HWNDMessageHandler::SetBoundsInternal() takes a +// msg_handler_weak_factory_ WeakPtr and bails out between delegate callouts +// because those callouts can synchronously delete |this| (see +// crbug.com/1185482, crbug.com/1197436). The headless override issues +// SetHeadlessWindowBounds() / HandleMove() / HandleClientSizeChanged() back to +// back with no liveness check. +// +// This test installs a headless ScreenWin so that HWNDMessageHandler::Create() +// returns an HWNDMessageHandlerHeadless, then registers a +// WindowTreeHostObserver that destroys the Widget (and therefore the +// DesktopWindowTreeHostWin and its owned HWNDMessageHandlerHeadless) inside +// OnHostMovedInPixels(). On an ASan build, the subsequent +// |delegate_->HandleClientSizeChanged(GetClientAreaBounds().size())| at +// hwnd_message_handler_headless.cc:459 reads |this->bounds_| and +// |this->delegate_| from a freed allocation. + +#include +#include + +#include "testing/gtest/include/gtest/gtest.h" +#include "ui/aura/window.h" +#include "ui/aura/window_tree_host.h" +#include "ui/aura/window_tree_host_observer.h" +#include "ui/gfx/geometry/rect.h" +#include "ui/views/test/widget_test.h" +#include "ui/views/widget/desktop_aura/desktop_screen_win_headless.h" +#include "ui/views/widget/widget.h" + +namespace views::test { + +namespace { + +// Destroys the owning Widget from inside OnHostMovedInPixels(), mirroring the +// scenario the base-class guards exist for (see +// WindowTreeHostPlatformTest.DeleteHostFromOnHostMovedInPixels and +// crbug.com/1185482). +class CloseOnMoveObserver : public aura::WindowTreeHostObserver { + public: + explicit CloseOnMoveObserver(std::unique_ptr widget) + : widget_(std::move(widget)) { + widget_->GetNativeWindow()->GetHost()->AddObserver(this); + } + ~CloseOnMoveObserver() override = default; + + Widget* widget() { return widget_.get(); } + + // aura::WindowTreeHostObserver: + void OnHostMovedInPixels(aura::WindowTreeHost* host) override { + host->RemoveObserver(this); + // Synchronously destroys DesktopWindowTreeHostWin and its + // std::unique_ptr message_handler_. Control then + // returns into HWNDMessageHandlerHeadless::SetBoundsInternal() with a + // dangling |this|. + widget_->CloseNow(); + widget_.reset(); + } + + private: + std::unique_ptr widget_; +}; + +} // namespace + +class HWNDMessageHandlerHeadlessTest : public WidgetTest { + public: + HWNDMessageHandlerHeadlessTest() = default; + ~HWNDMessageHandlerHeadlessTest() override = default; + + void SetUp() override { + set_native_widget_type(NativeWidgetType::kDesktop); + // Install a headless ScreenWin *before* WidgetTest::SetUp() so that + // AuraTestHelper does not install its own TestScreen, and + // HWNDMessageHandler::Create() takes the IsHeadless() branch. + screen_ = std::make_unique(); + WidgetTest::SetUp(); + } + + void TearDown() override { + WidgetTest::TearDown(); + screen_.reset(); + } + + private: + std::unique_ptr screen_; +}; + +TEST_F(HWNDMessageHandlerHeadlessTest, DeleteHostFromOnHostMovedInPixels) { + auto widget = std::make_unique(); + Widget::InitParams params = CreateParams( + Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); + params.bounds = gfx::Rect(0, 0, 100, 100); + widget->Init(std::move(params)); + widget->Show(); + + CloseOnMoveObserver observer(std::move(widget)); + + // Change BOTH origin and size so that SetBoundsInternal() executes + // HandleMove() (line 454) and then HandleClientSizeChanged() (line 459). + // The observer frees the message handler during HandleMove(); the second + // delegate_ call is a heap-use-after-free. + observer.widget()->SetBounds(gfx::Rect(50, 50, 400, 400)); +} + +} // namespace views::test