diff --git a/ui/ozone/BUILD.gn b/ui/ozone/BUILD.gn index 7bf64c40e9f14..16aee61514436 100644 --- a/ui/ozone/BUILD.gn +++ b/ui/ozone/BUILD.gn @@ -37,6 +37,7 @@ ozone_platform_interactive_ui_tests_sources = if (ozone_platform_headless) { ozone_platforms += [ "headless" ] ozone_platform_deps += [ "platform/headless" ] + ozone_platform_test_deps += [ "platform/headless:headless_unittests" ] } if (ozone_platform_drm) { diff --git a/ui/ozone/platform/headless/BUILD.gn b/ui/ozone/platform/headless/BUILD.gn index 650058400a245..cead343a04fd3 100644 --- a/ui/ozone/platform/headless/BUILD.gn +++ b/ui/ozone/platform/headless/BUILD.gn @@ -59,3 +59,16 @@ source_set("headless") { deps += [ "//gpu/vulkan" ] } } + +source_set("headless_unittests") { + testonly = true + sources = [ "headless_window_unittest.cc" ] + deps = [ + ":headless", + "//base", + "//testing/gtest", + "//ui/display:test_support", + "//ui/gfx/geometry", + "//ui/platform_window", + ] +} diff --git a/ui/ozone/platform/headless/headless_window_unittest.cc b/ui/ozone/platform/headless/headless_window_unittest.cc new file mode 100644 index 0000000000000..655c51cbf9c25 --- /dev/null +++ b/ui/ozone/platform/headless/headless_window_unittest.cc @@ -0,0 +1,138 @@ +// 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. + +#include "ui/ozone/platform/headless/headless_window.h" + +#include + +#include "testing/gtest/include/gtest/gtest.h" +#include "ui/display/test/test_screen.h" +#include "ui/gfx/geometry/rect.h" +#include "ui/ozone/platform/headless/headless_window_manager.h" +#include "ui/platform_window/platform_window_delegate.h" + +namespace ui { +namespace { + +// A PlatformWindowDelegate that simulates the production behaviour of +// DesktopWindowTreeHostPlatform: when the platform window calls back into the +// delegate (OnBoundsChanged / OnWindowStateChanged), an observer may +// synchronously call Widget::CloseNow(), which ends up calling +// HeadlessWindow::Close() -> delegate_->OnClosed() -> +// DWTHP::OnClosed() -> SetPlatformWindow(nullptr) -> ~HeadlessWindow(). +// +// This delegate models that by resetting its owned unique_ptr +// from inside the chosen callback. +class DestroyingDelegate : public PlatformWindowDelegate { + public: + enum class DestroyOn { + kBoundsChanged, + kWindowStateChanged, + }; + + explicit DestroyingDelegate(DestroyOn destroy_on) + : destroy_on_(destroy_on) {} + ~DestroyingDelegate() override = default; + + void SetWindow(std::unique_ptr window) { + window_ = std::move(window); + } + + // PlatformWindowDelegate: + void OnBoundsChanged(const BoundsChange& change) override { + if (destroy_on_ == DestroyOn::kBoundsChanged) { + // Simulates: WindowTreeHostPlatform::OnBoundsChanged -> + // OnHostResizedInPixels -> WidgetObserver::OnWidgetBoundsChanged -> + // Widget::CloseNow() -> DWTHP::CloseNow() -> platform_window()->Close() + // -> delegate_->OnClosed() -> DWTHP::OnClosed() -> + // SetPlatformWindow(nullptr) -> ~HeadlessWindow(). + window_.reset(); + } + } + void OnDamageRect(const gfx::Rect& damaged_region) override {} + void DispatchEvent(Event* event) override {} + void OnCloseRequest() override {} + void OnClosed() override { window_.reset(); } + void OnWindowStateChanged(PlatformWindowState old_state, + PlatformWindowState new_state) override { + if (destroy_on_ == DestroyOn::kWindowStateChanged) { + // Simulates: DWTHP::OnWindowStateChanged -> + // Widget::OnNativeWidgetWindowShowStateChanged -> + // WidgetObserver::OnWidgetShowStateChanged -> Widget::CloseNow() -> + // ... -> SetPlatformWindow(nullptr) -> ~HeadlessWindow(). + window_.reset(); + } + } + void OnLostCapture() override {} + void OnAcceleratedWidgetAvailable(gfx::AcceleratedWidget widget) override {} + void OnWillDestroyAcceleratedWidget() override {} + void OnAcceleratedWidgetDestroyed() override {} + void OnActivationChanged(bool active) override {} + void OnCursorUpdate() override {} + bool CanMaximize() const override { return true; } + bool CanFullscreen() const override { return true; } + gfx::Rect ConvertRectToPixels(const gfx::Rect& rect_in_dip) const override { + return rect_in_dip; + } + gfx::Rect ConvertRectToDIP(const gfx::Rect& rect_in_pixels) const override { + return rect_in_pixels; + } + + private: + const DestroyOn destroy_on_; + std::unique_ptr window_; +}; + +// HeadlessWindow::SetFullscreen() calls ZoomWindowBounds() (which dispatches +// delegate_->OnBoundsChanged()) followed by UpdateWindowState() with no +// liveness check between them. If the OnBoundsChanged() dispatch synchronously +// destroys |this| (as DesktopWindowTreeHostPlatform::OnClosed() does via +// SetPlatformWindow(nullptr)), UpdateWindowState() reads and writes +// |window_state_| on the freed object and dispatches a virtual call through the +// freed |delegate_| slot. +// +// Under ASAN this test produces a heap-use-after-free at +// HeadlessWindow::UpdateWindowState(). +TEST(HeadlessWindowUAFTest, SetFullscreenAfterDestroyInOnBoundsChanged) { + display::test::TestScreen test_screen(/*create_display=*/true, + /*register_screen=*/true); + HeadlessWindowManager manager; + DestroyingDelegate delegate(DestroyingDelegate::DestroyOn::kBoundsChanged); + + auto window = std::make_unique(&delegate, &manager, + gfx::Rect(0, 0, 100, 100)); + HeadlessWindow* raw_window = window.get(); + delegate.SetWindow(std::move(window)); + + // Web-reachable entry point: element.requestFullscreen() -> + // Widget::SetFullscreen() -> DWTHP::SetFullscreen() -> + // HeadlessWindow::SetFullscreen(). + raw_window->SetFullscreen(/*fullscreen=*/true, + /*target_display_id=*/-1); +} + +// HeadlessWindow::Minimize() calls UpdateWindowState() (which dispatches +// delegate_->OnWindowStateChanged()) followed by Deactivate() with no liveness +// check between them. If the OnWindowStateChanged() dispatch synchronously +// destroys |this|, Deactivate() reads and writes |activation_state_| on the +// freed object and dispatches delegate_->OnActivationChanged() through the +// freed |delegate_| slot. +// +// Under ASAN this test produces a heap-use-after-free at +// HeadlessWindow::Deactivate(). +TEST(HeadlessWindowUAFTest, MinimizeAfterDestroyInOnWindowStateChanged) { + HeadlessWindowManager manager; + DestroyingDelegate delegate( + DestroyingDelegate::DestroyOn::kWindowStateChanged); + + auto window = std::make_unique(&delegate, &manager, + gfx::Rect(0, 0, 100, 100)); + HeadlessWindow* raw_window = window.get(); + delegate.SetWindow(std::move(window)); + + raw_window->Minimize(); +} + +} // namespace +} // namespace ui