Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Aura
DescriptionUse after free in Aura
ComponentAura
Bug ClassUAF
Tracker532970574
Fix commitd5f0488a4d6a (chromium/src) +122/-7
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-16

Changed Functions

FunctionChangeNotes
if
ui/aura/window.cc
modified
ReentrantDisplayScreen
ui/aura/window_unittest.cc
modified
TEST_F
ui/aura/window_unittest.cc
modified
BindLambdaForTesting
ui/aura/window_unittest.cc
modified

Files Changed

  • ui/aura/window.cc
  • ui/aura/window_unittest.cc
From d5f0488a4d6a4a7fd8d4941d748b5d6112bb40c9 Mon Sep 17 00:00:00 2001
From: Tom Anderson <[email protected]>
Date: Mon, 13 Jul 2026 15:58:34 -0700
Subject: [PATCH] [aura] Guard against root window destruction in ScopedCursorHider

Avoid Use-After-Free during device scale factor changes on Linux/X11 by
using an aura::WindowTracker inside
ScopedCursorHider::~ScopedCursorHider(). Previously,
display::Screen::Get()->GetDisplayNearestWindow() could synchronously
dispatch events that tear down the root window and the CursorClient. By
tracking window destruction, the window is verified to be alive after
returning from display queries before making calls on the CursorClient.

Fixed: 532970574
Change-Id: I495fa5f12bf0a58fb047009d792366879e75521b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8072900
Commit-Queue: Thomas Anderson <[email protected]>
Reviewed-by: Colin Blundell <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1661444}
---

diff --git a/ui/aura/window.cc b/ui/aura/window.cc
index 9a479a6..3884e23 100644
--- a/ui/aura/window.cc
+++ b/ui/aura/window.cc
@@ -178,18 +178,31 @@
   ScopedCursorHider& operator=(const ScopedCursorHider&) = delete;
 
   ~ScopedCursorHider() {
-    if (!window_->IsRootWindow())
+    // Store the raw window pointer in a local variable and clear the `window_`
+    // raw_ptr to nullptr before carrying out the rest of the destructor. Since
+    // the window can be synchronously destroyed inside display query sink calls
+    // below, clearing the raw_ptr while the window is still alive prevents
+    // Chromium's dangling raw_ptr checks from triggering on destruction.
+    Window* window = window_;
+    window_ = nullptr;
+
+    if (!window->IsRootWindow()) {
       return;
+    }
 
     // Update the device scale factor of the cursor client only when the last
     // mouse location is on this root window.
     if (hid_cursor_) {
-      client::CursorClient* cursor_client = client::GetCursorClient(window_);
-      if (cursor_client) {
-        const display::Display& display =
-            display::Screen::Get()->GetDisplayNearestWindow(window_);
-        cursor_client->SetDisplay(display);
-        cursor_client->ShowCursor();
+      aura::WindowTracker tracker;
+      tracker.Add(window);
+      const display::Display& display =
+          display::Screen::Get()->GetDisplayNearestWindow(window);
+      if (tracker.Contains(window)) {
+        client::CursorClient* cursor_client = client::GetCursorClient(window);
+        if (cursor_client) {
+          cursor_client->SetDisplay(display);
+          cursor_client->ShowCursor();
+        }
       }
     }
   }
diff --git a/ui/aura/window_unittest.cc b/ui/aura/window_unittest.cc
index 5d587a2..98ac6fc0 100644
--- a/ui/aura/window_unittest.cc
+++ b/ui/aura/window_unittest.cc
@@ -22,6 +22,7 @@
 #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"
@@ -30,6 +31,7 @@
 #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"
@@ -4562,6 +4564,106 @@
 }
 #endif
 
+class ReentrantDisplayScreen : public display::Screen {
+ public:
+  explicit ReentrantDisplayScreen(display::Screen* original_screen,
+                                  base::RepeatingClosure on_get_display)
+      : original_screen_(original_screen),
+        on_get_display_(std::move(on_get_display)) {
+    set_shutdown(true);
+  }
+
+  ReentrantDisplayScreen(const ReentrantDisplayScreen&) = delete;
+  ReentrantDisplayScreen& operator=(const ReentrantDisplayScreen&) = delete;
+
+  ~ReentrantDisplayScreen() override = default;
+
+  // Overridden from display::Screen:
+  gfx::Point GetCursorScreenPoint() override {
+    return original_screen_->GetCursorScreenPoint();
+  }
+  bool IsWindowUnderCursor(gfx::NativeWindow window) override {
+    return original_screen_->IsWindowUnderCursor(window);
+  }
+  gfx::NativeWindow GetWindowAtScreenPoint(const gfx::Point& point) override {
+    return original_screen_->GetWindowAtScreenPoint(point);
+  }
+  gfx::NativeWindow GetLocalProcessWindowAtPoint(
+      const gfx::Point& point,
+      const std::set<gfx::NativeWindow>& ignore) override {
+    return original_screen_->GetLocalProcessWindowAtPoint(point, ignore);
+  }
+  int GetNumDisplays() const override {
+    return original_screen_->GetNumDisplays();
+  }
+  const std::vector<display::Display>& GetAllDisplays() const override {
+    return original_screen_->GetAllDisplays();
+  }
+  display::Display GetDisplayNearestWindow(
+      gfx::NativeWindow window) const override {
+    on_get_display_.Run();
+    return original_screen_->GetDisplayNearestWindow(window);
+  }
+  display::Display GetDisplayNearestPoint(
+      const gfx::Point& point) const override {
+    return original_screen_->GetDisplayNearestPoint(point);
+  }
+  display::Display GetDisplayMatching(
+      const gfx::Rect& match_rect) const override {
+    return original_screen_->GetDisplayMatching(match_rect);
+  }
+  display::Display GetPrimaryDisplay() const override {
+    return original_screen_->GetPrimaryDisplay();
+  }
+  void AddObserver(display::DisplayObserver* observer) override {
+    original_screen_->AddObserver(observer);
+  }
+  void RemoveObserver(display::DisplayObserver* observer) override {
+    original_screen_->RemoveObserver(observer);
+  }
+
+ private:
+  raw_ptr<display::Screen> original_screen_;
+  base::RepeatingClosure on_get_display_;
+};
+
+TEST_F(WindowTest, ScopedCursorHiderCursorClientFreedDuringGetDisplay) {
+  display::Screen* original_screen = display::Screen::Get();
+
+  std::unique_ptr<Window> test_root = std::make_unique<Window>(nullptr);
+  test_root->Init(ui::LAYER_NOT_DRAWN);
+  test_root->set_host(host());
+  test_root->SetBounds(gfx::Rect(0, 0, 800, 600));
+
+  TestCursorClient cursor_client(root_window());
+  client::SetCursorClient(root_window(), nullptr);
+  client::SetCursorClient(test_root.get(), &cursor_client);
+
+  Env::GetInstance()->SetLastMouseLocation(gfx::Point(10, 10));
+
+  cursor_client.ShowCursor();
+  EXPECT_TRUE(cursor_client.IsCursorVisible());
+
+  ReentrantDisplayScreen reentrant_screen(
+      original_screen,
+      base::BindLambdaForTesting([&]() { test_root.reset(); }));
+
+  display::Screen::SetScreenInstance(&reentrant_screen);
+
+  // This call synchronously destroys `test_root` during the nested display
+  // nearest window query. With the fix, the ScopedCursorHider destructor
+  // detects this destruction via `WindowTracker` and avoids dereferencing the
+  // freed `CursorClient` pointer or `test_root`, so this call should complete
+  // without crashing.
+  test_root->OnDeviceScaleFactorChanged(1.0f, 2.0f);
+
+  display::Screen::SetScreenInstance(nullptr);
+  display::Screen::SetScreenInstance(original_screen);
+
+  // Restore cursor client.
+  client::SetCursorClient(root_window(), &cursor_client);
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace aura
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ui/aura/window_unittest.cc b/ui/aura/window_unittest.cc
index 5d587a2..98ac6fc0 100644
--- a/ui/aura/window_unittest.cc
+++ b/ui/aura/window_unittest.cc
@@ -22,6 +22,7 @@
 #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"
@@ -30,6 +31,7 @@
 #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"
@@ -4562,6 +4564,106 @@
 }
 #endif
 
+class ReentrantDisplayScreen : public display::Screen {
+ public:
+  explicit ReentrantDisplayScreen(display::Screen* original_screen,
+                                  base::RepeatingClosure on_get_display)
+      : original_screen_(original_screen),
+        on_get_display_(std::move(on_get_display)) {
+    set_shutdown(true);
+  }
+
+  ReentrantDisplayScreen(const ReentrantDisplayScreen&) = delete;
+  ReentrantDisplayScreen& operator=(const ReentrantDisplayScreen&) = delete;
+
+  ~ReentrantDisplayScreen() override = default;
+
+  // Overridden from display::Screen:
+  gfx::Point GetCursorScreenPoint() override {
+    return original_screen_->GetCursorScreenPoint();
+  }
+  bool IsWindowUnderCursor(gfx::NativeWindow window) override {
+    return original_screen_->IsWindowUnderCursor(window);
+  }
+  gfx::NativeWindow GetWindowAtScreenPoint(const gfx::Point& point) override {
+    return original_screen_->GetWindowAtScreenPoint(point);
+  }
+  gfx::NativeWindow GetLocalProcessWindowAtPoint(
+      const gfx::Point& point,
+      const std::set<gfx::NativeWindow>& ignore) override {
+    return original_screen_->GetLocalProcessWindowAtPoint(point, ignore);
+  }
+  int GetNumDisplays() const override {
+    return original_screen_->GetNumDisplays();
+  }
+  const std::vector<display::Display>& GetAllDisplays() const override {
+    return original_screen_->GetAllDisplays();
+  }
+  display::Display GetDisplayNearestWindow(
+      gfx::NativeWindow window) const override {
+    on_get_display_.Run();
+    return original_screen_->GetDisplayNearestWindow(window);
+  }
+  display::Display GetDisplayNearestPoint(
+      const gfx::Point& point) const override {
+    return original_screen_->GetDisplayNearestPoint(point);
+  }
+  display::Display GetDisplayMatching(
+      const gfx::Rect& match_rect) const override {
+    return original_screen_->GetDisplayMatching(match_rect);
+  }
+  display::Display GetPrimaryDisplay() const override {
+    return original_screen_->GetPrimaryDisplay();
+  }
+  void AddObserver(display::DisplayObserver* observer) override {
+    original_screen_->AddObserver(observer);
+  }
+  void RemoveObserver(display::DisplayObserver* observer) override {
+    original_screen_->RemoveObserver(observer);
+  }
+
+ private:
+  raw_ptr<display::Screen> original_screen_;
+  base::RepeatingClosure on_get_display_;
+};
+
+TEST_F(WindowTest, ScopedCursorHiderCursorClientFreedDuringGetDisplay) {
+  display::Screen* original_screen = display::Screen::Get();
+
+  std::unique_ptr<Window> test_root = std::make_unique<Window>(nullptr);
+  test_root->Init(ui::LAYER_NOT_DRAWN);
+  test_root->set_host(host());
+  test_root->SetBounds(gfx::Rect(0, 0, 800, 600));
+
+  TestCursorClient cursor_client(root_window());
+  client::SetCursorClient(root_window(), nullptr);
+  client::SetCursorClient(test_root.get(), &cursor_client);
+
+  Env::GetInstance()->SetLastMouseLocation(gfx::Point(10, 10));
+
+  cursor_client.ShowCursor();
+  EXPECT_TRUE(cursor_client.IsCursorVisible());
+
+  ReentrantDisplayScreen reentrant_screen(
+      original_screen,
+      base::BindLambdaForTesting([&]() { test_root.reset(); }));
+
+  display::Screen::SetScreenInstance(&reentrant_screen);
+
+  // This call synchronously destroys `test_root` during the nested display
+  // nearest window query. With the fix, the ScopedCursorHider destructor
+  // detects this destruction via `WindowTracker` and avoids dereferencing the
+  // freed `CursorClient` pointer or `test_root`, so this call should complete
+  // without crashing.
+  test_root->OnDeviceScaleFactorChanged(1.0f, 2.0f);
+
+  display::Screen::SetScreenInstance(nullptr);
+  display::Screen::SetScreenInstance(original_screen);
+
+  // Restore cursor client.
+  client::SetCursorClient(root_window(), &cursor_client);
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace aura
Loading diff…

Original Bug Report

reported by [email protected]

Potential Use-After-Free in ScopedCursorHider during scale factor changes on Linux/X11

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential Use-After-Free (UAF) vulnerability exists in the browser process of Linux/X11 Chrome within the ScopedCursorHider helper destructor. Synchronous event dispatching during display queries can trigger window closure, deleting the shared static CursorManager and the window itself. Subsequent virtual method calls on the freed CursorClient pointer or dereferences of the deleted window lead to memory corruption.

Affected files:

  • ui/aura/window.cc
  • ui/views/widget/desktop_aura/desktop_native_widget_aura.cc
  • ui/ozone/platform/x11/x11_window.cc
  • ui/ozone/platform/x11/x11_screen_ozone.cc
  • ui/aura/screen_ozone.cc
  • ui/gfx/x/geometry_cache.cc

Estimated timestamp from git blame: 2025-03-06

Description

A potential Use-After-Free (UAF) vulnerability has been identified in ScopedCursorHider on Linux/X11 platforms. The vulnerability is located in ui/aura/window.cc inside the destructor of the stack-allocated ScopedCursorHider helper class.

When a device scale factor change occurs, the root window’s OnDeviceScaleFactorChanged creates a ScopedCursorHider on the stack to temporarily hide the cursor. In its destructor, ~ScopedCursorHider() captures a raw pointer to client::CursorClient (which points to the static views::DesktopNativeWidgetAura::cursor_manager_) and then calls display::Screen::Get()->GetDisplayNearestWindow(window_) before making virtual calls on the captured pointer.

// ui/aura/window.cc
~ScopedCursorHider() {
  if (!window_->IsRootWindow())
    return;
  if (hid_cursor_) {
    client::CursorClient* cursor_client = client::GetCursorClient(window_);
    if (cursor_client) {
      const display::Display& display =
          display::Screen::Get()->GetDisplayNearestWindow(window_); // *** Potential Synchronous Deletion Sink ***
      cursor_client->SetDisplay(display);                          // *** Virtual call on potentially freed client ***
      cursor_client->ShowCursor();                           
    }
  }
}

Potential Mechanism of the Vulnerability

  1. On Linux/X11, GetDisplayNearestWindow(window_) propagates to X11ScreenOzone::GetDisplayForAcceleratedWidget(), which queries the window’s pixel bounds via window->GetBoundsInPixels().
  2. If the underlying GeometryCache parent chain is un-Ready (e.g., following a recent ReparentNotify event), GetBoundsPx() synchronously dispatches pending futures using parent_future_.DispatchNow().
  3. The synchronous blocking and response processing can dispatch bounds change callbacks, invoking X11Window::OnBoundsChanged -> NotifyBoundsChanged -> PlatformWindowDelegate::OnBoundsChanged.
  4. If a registered widget or window observer synchronously destroys the window tree host (such as during dynamic tab-snapping or window-snapping closure), this invokes DesktopNativeWidgetAura::OnHostClosed(), deleting the root window and resetting the static cursor_manager_ via RootWindowDestroyed().
  5. When control returns to ~ScopedCursorHider(), both the root Window and the cursor_manager_ (to which cursor_client points) have been freed. The destructor then attempts to perform virtual calls SetDisplay() and ShowCursor() on the freed cursor_client pointer, resulting in a Use-After-Free memory corruption inside the browser process.

Note: Our tooling currently lacks the capability to run code or compile a working proof of concept, so these steps are based on a static code review and are presented as a potential vulnerability scenario.

Suggested Fix

To prevent this vulnerability, the lifetime of the window_ and the validity of the cursor_client must be verified after returning from any potentially synchronous display query. This can be achieved by using aura::WindowTracker to monitor if the window is destroyed during the call:

~ScopedCursorHider() {
  if (!window_->IsRootWindow())
    return;

  if (hid_cursor_) {
    aura::WindowTracker tracker({window_});
    const display::Display& display =
        display::Screen::Get()->GetDisplayNearestWindow(window_);
    
    // Guard against window or cursor client destruction
    if (tracker.Contains(window_)) {
      client::CursorClient* cursor_client = client::GetCursorClient(window_);
      if (cursor_client) {
        cursor_client->SetDisplay(display);
        cursor_client->ShowCursor();
      }
    }
  }
}

Evaluated with Chrome root at commit: 84065d9121f6e48f67755f0ae963cc09617e5c85


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker