diff --git a/ui/aura/window_unittest.cc b/ui/aura/window_unittest.cc index a46bb519c5d04..f599c4ebf400a 100644 --- a/ui/aura/window_unittest.cc +++ b/ui/aura/window_unittest.cc @@ -22,14 +22,17 @@ #include "cc/trees/layer_tree_frame_sink.h" #include "testing/gtest/include/gtest/gtest.h" #include "ui/aura/client/capture_client.h" +#include "ui/aura/client/cursor_client.h" #include "ui/aura/client/focus_change_observer.h" #include "ui/aura/client/visibility_client.h" #include "ui/aura/client/window_parenting_client.h" +#include "ui/aura/env.h" #include "ui/aura/layout_manager.h" #include "ui/aura/scoped_window_capture_request.h" #include "ui/aura/scoped_window_event_targeting_blocker.h" #include "ui/aura/test/aura_test_base.h" #include "ui/aura/test/aura_test_utils.h" +#include "ui/aura/test/test_cursor_client.h" #include "ui/aura/test/test_window_delegate.h" #include "ui/aura/test/test_windows.h" #include "ui/aura/test/window_test_api.h" @@ -4505,6 +4508,117 @@ TEST_P(WindowActualScreenBoundsTest, VerifyWindowActualBoundsDuringAnimation) { bounds_checker.WaitForAnimationCompletion(); } +// A display::Screen wrapper that runs a closure inside GetDisplayNearestWindow +// and otherwise delegates to the previously-installed Screen. This models the +// documented behavior of X11ScreenOzone::GetDisplayForAcceleratedWidget -> +// X11Window::GetBoundsInPixels(), which can synchronously dispatch pending X +// server events and destroy objects (see the warning comment above +// X11Window::GetBoundsInPixels()). +class ReentrantDisplayScreen : public display::Screen { + public: + explicit ReentrantDisplayScreen(base::RepeatingClosure on_get_display) + : on_get_display_(std::move(on_get_display)) { + prev_ = display::Screen::SetScreenInstance(this); + } + ~ReentrantDisplayScreen() override { + display::Screen::SetScreenInstance(prev_); + } + + // display::Screen: + gfx::Point GetCursorScreenPoint() override { + return prev_->GetCursorScreenPoint(); + } + bool IsWindowUnderCursor(gfx::NativeWindow window) override { + return prev_->IsWindowUnderCursor(window); + } + gfx::NativeWindow GetWindowAtScreenPoint(const gfx::Point& point) override { + return prev_->GetWindowAtScreenPoint(point); + } + gfx::NativeWindow GetLocalProcessWindowAtPoint( + const gfx::Point& point, + const std::set& ignore) override { + return prev_->GetLocalProcessWindowAtPoint(point, ignore); + } + int GetNumDisplays() const override { return prev_->GetNumDisplays(); } + const std::vector& GetAllDisplays() const override { + return prev_->GetAllDisplays(); + } + display::Display GetDisplayNearestWindow( + gfx::NativeWindow window) const override { + on_get_display_.Run(); + return prev_->GetDisplayNearestWindow(window); + } + display::Display GetDisplayNearestPoint( + const gfx::Point& point) const override { + return prev_->GetDisplayNearestPoint(point); + } + display::Display GetDisplayMatching( + const gfx::Rect& match_rect) const override { + return prev_->GetDisplayMatching(match_rect); + } + display::Display GetPrimaryDisplay() const override { + return prev_->GetPrimaryDisplay(); + } + void AddObserver(display::DisplayObserver* observer) override { + prev_->AddObserver(observer); + } + void RemoveObserver(display::DisplayObserver* observer) override { + prev_->RemoveObserver(observer); + } + + private: + raw_ptr prev_; + base::RepeatingClosure on_get_display_; +}; + +// Regression test: ~ScopedCursorHider() captures a bare CursorClient* and then +// calls display::Screen::GetDisplayNearestWindow(), which on Linux/X11 reaches +// X11Window::GetBoundsInPixels() -> GeometryCache::GetBoundsPx(). That path is +// documented to synchronously dispatch pending X events, which can tear down +// the last DesktopNativeWidgetAura and delete the static wm::CursorManager that +// the captured pointer refers to. The subsequent SetDisplay()/ShowCursor() +// virtual calls at window.cc:~191 then dispatch through a freed vptr. +// +// This test simulates that sink by installing a Screen whose +// GetDisplayNearestWindow() synchronously deletes the root window's +// CursorClient. Under ASAN it produces a heap-use-after-free at +// aura::(anonymous namespace)::ScopedCursorHider::~ScopedCursorHider(). +TEST_F(WindowTest, ScopedCursorHiderCursorClientFreedDuringGetDisplay) { + // Heap-allocate so ASAN sees a real free. TestCursorClient's ctor installs + // itself as the root window's CursorClient property. + auto* cursor_client = new TestCursorClient(root_window()); + ASSERT_EQ(cursor_client, aura::client::GetCursorClient(root_window())); + ASSERT_TRUE(cursor_client->IsCursorVisible()); + + // Ensure the mouse is inside the root window so ScopedCursorHider's ctor + // sets hid_cursor_ = true and the dtor takes the vulnerable branch. + aura::Env::GetInstance()->SetLastMouseLocation(gfx::Point(10, 10)); + ASSERT_TRUE(root_window()->GetBoundsInScreen().Contains(gfx::Point(10, 10))); + + // Install a Screen that frees the CursorClient during + // GetDisplayNearestWindow() -- modeling the X11 self-delete sink chain that + // ends in DesktopNativeWidgetAura::RootWindowDestroyed() -> + // `delete cursor_manager_`. + // + // GetDisplayNearestWindow() is also reached from the ScopedCursorHider + // *ctor* (via GetBoundsInScreen -> WindowTreeHost::GetBoundsInDIP -> + // ScreenToDIPRectInWindow), so only fire the delete once HideCursor() has + // already run -- i.e., we are now in the dtor at window.cc:~190. + ReentrantDisplayScreen screen(base::BindLambdaForTesting([&] { + if (cursor_client && !cursor_client->IsCursorVisible()) { + delete cursor_client; + cursor_client = nullptr; + } + })); + + // Entry point: Window::OnDeviceScaleFactorChanged() constructs a + // ScopedCursorHider on the stack; its dtor performs the UAF vcalls. + root_window()->OnDeviceScaleFactorChanged(1.0f, 2.0f); + + // If we survive (non-ASAN build / bug fixed), don't leak. + delete cursor_client; +} + } // namespace } // namespace test } // namespace aura