Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in GFX
DescriptionUse after free in GFX
ComponentGFX
Bug ClassUAF
Tracker513160681
Fix commite18e2f6c1117 (chromium/src) +30/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
ui/gfx/x/geometry_cache.cc
modified
TEST
ui/gfx/x/geometry_cache_unittest.cc
modified

Files Changed

  • ui/gfx/x/geometry_cache.cc
  • ui/gfx/x/geometry_cache_unittest.cc
From e18e2f6c1117ff73c7b23ec1ac6c961ef13bf004 Mon Sep 17 00:00:00 2001
From: Tom Anderson <[email protected]>
Date: Thu, 14 May 2026 10:33:44 -0700
Subject: [PATCH] [X11] Fix Use-After-Free in GeometryCache::GetBoundsPx()

Synchronous X11 dispatching in GetBoundsPx() can trigger a callback
chain that destroys the GeometryCache instance. This CL adds WeakPtr
checks after each synchronous call to protect against this.

Fixed: 513160681
Test: GeometryCacheTest.DestroyInCallback
Change-Id: I6fbce5def4a521eee2f4e04a8a28311c15471b44
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7849103
Auto-Submit: Thomas Anderson <[email protected]>
Commit-Queue: Lei Zhang <[email protected]>
Commit-Queue: Thomas Anderson <[email protected]>
Reviewed-by: Lei Zhang <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1630691}
---

diff --git a/ui/gfx/x/geometry_cache.cc b/ui/gfx/x/geometry_cache.cc
index 2549838..60c40b3d 100644
--- a/ui/gfx/x/geometry_cache.cc
+++ b/ui/gfx/x/geometry_cache.cc
@@ -34,12 +34,19 @@
 GeometryCache::~GeometryCache() = default;
 
 gfx::Rect GeometryCache::GetBoundsPx() {
+  auto weak_this = weak_ptr_factory_.GetWeakPtr();
   if (!have_parent_) {
     parent_future_.DispatchNow();
+    if (!weak_this) {
+      return {};
+    }
   }
   CHECK(have_parent_);
   if (!have_geometry_) {
     geometry_future_.DispatchNow();
+    if (!weak_this) {
+      return {};
+    }
   }
   CHECK(have_geometry_);
 
@@ -47,6 +54,9 @@
     return geometry_;
   }
   auto parent_bounds = parent_->GetBoundsPx();
+  if (!weak_this) {
+    return {};
+  }
   gfx::Vector2d offset(parent_bounds.x(), parent_bounds.y());
   return geometry_ + offset;
 }
diff --git a/ui/gfx/x/geometry_cache_unittest.cc b/ui/gfx/x/geometry_cache_unittest.cc
index b37a9fe..01e55747 100644
--- a/ui/gfx/x/geometry_cache_unittest.cc
+++ b/ui/gfx/x/geometry_cache_unittest.cc
@@ -161,4 +161,24 @@
       new_child_bounds + gfx::Vector2d(parent_bounds.x(), parent_bounds.y()));
 }
 
+TEST(GeometryCacheTest, DestroyInCallback) {
+  Connection* connection = Connection::Get();
+  ScopedWindow window(connection, connection->default_root(),
+                      gfx::Rect(12, 34, 56, 78));
+
+  std::unique_ptr<GeometryCache> geometry_cache;
+  auto bounds_changed_callback =
+      [](std::unique_ptr<GeometryCache>* geometry_cache_ptr,
+         const std::optional<gfx::Rect>& old_bounds,
+         const gfx::Rect& new_bounds) { geometry_cache_ptr->reset(); };
+
+  geometry_cache = std::make_unique<GeometryCache>(
+      connection, window.id(),
+      base::BindRepeating(bounds_changed_callback, &geometry_cache));
+
+  // This will trigger the callback via DispatchNow() and then return.
+  // If there's a UAF, this should crash or trigger ASAN.
+  geometry_cache->GetBoundsPx();
+}
+
 }  // namespace x11
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ui/gfx/x/geometry_cache_unittest.cc b/ui/gfx/x/geometry_cache_unittest.cc
index b37a9fe..01e55747 100644
--- a/ui/gfx/x/geometry_cache_unittest.cc
+++ b/ui/gfx/x/geometry_cache_unittest.cc
@@ -161,4 +161,24 @@
       new_child_bounds + gfx::Vector2d(parent_bounds.x(), parent_bounds.y()));
 }
 
+TEST(GeometryCacheTest, DestroyInCallback) {
+  Connection* connection = Connection::Get();
+  ScopedWindow window(connection, connection->default_root(),
+                      gfx::Rect(12, 34, 56, 78));
+
+  std::unique_ptr<GeometryCache> geometry_cache;
+  auto bounds_changed_callback =
+      [](std::unique_ptr<GeometryCache>* geometry_cache_ptr,
+         const std::optional<gfx::Rect>& old_bounds,
+         const gfx::Rect& new_bounds) { geometry_cache_ptr->reset(); };
+
+  geometry_cache = std::make_unique<GeometryCache>(
+      connection, window.id(),
+      base::BindRepeating(bounds_changed_callback, &geometry_cache));
+
+  // This will trigger the callback via DispatchNow() and then return.
+  // If there's a UAF, this should crash or trigger ASAN.
+  geometry_cache->GetBoundsPx();
+}
+
 }  // namespace x11
Loading diff…

Original Bug Report

reported by [email protected]

Potential Heap Use-After-Free in x11::GeometryCache::GetBoundsPx

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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential heap Use-After-Free (UAF) vulnerability exists in the Linux/X11 browser process within x11::GeometryCache::GetBoundsPx(). The issue is caused by synchronous dispatching of X11 replies which can trigger a callback chain that destroys the GeometryCache object while its methods are still executing on the stack.

Affected files:

  • ui/gfx/x/geometry_cache.cc
  • ui/ozone/platform/x11/x11_window.cc
  • ui/aura/window_tree_host_platform.cc

Estimated timestamp from git blame: 2023-12-01

Summary

A potential heap Use-After-Free vulnerability has been identified in the browser process on Linux/X11. The vulnerability occurs in x11::GeometryCache::GetBoundsPx() because it performs synchronous X11 reply dispatching that can trigger a recursive callback chain resulting in the destruction of the GeometryCache instance while its code is still executing.

Root Cause Analysis

In ui/gfx/x/geometry_cache.cc, the GetBoundsPx() function ensures that parent and geometry information is retrieved by calling DispatchNow() on pending X11 futures:

gfx::Rect GeometryCache::GetBoundsPx() {
  if (!have_parent_) {
    parent_future_.DispatchNow();      // [1] Synchronous dispatch
  }
  CHECK(have_parent_);
  if (!have_geometry_) {
    geometry_future_.DispatchNow();    // [2] Synchronous dispatch
  }
  CHECK(have_geometry_);               // [3] Potential UAF read
  if (!parent_) {
    return geometry_;                  // [4] Potential UAF read
  }
  // ...
}

DispatchNow() (in ui/gfx/x/future.cc) is synchronous; it waits for the X11 response and immediately executes the associated callback. For the GeometryCache instance owned by an X11Window, the response callback eventually triggers X11Window::OnBoundsChanged via NotifyGeometryChanged().

X11Window::OnBoundsChanged propagates the bounds update to its delegate, typically WindowTreeHostPlatform. In ui/aura/window_tree_host_platform.cc, the OnBoundsChanged implementation explicitly warns that propagating these changes can synchronously destroy the host:

void WindowTreeHostPlatform::OnBoundsChanged(const BoundsChange& change) {
  // ...
  auto weak_ref = GetWeakPtr();
  OnHostMovedInPixels();
  // Changing the bounds may destroy this.
  if (!weak_ref) return;
  // ...
}

If an observer of these bounds changes (e.g., during window snapping or closure) causes the Widget to close, the X11Window is destroyed. The X11Window destructor resets its geometry_cache_ (a std::unique_ptr), which deletes the GeometryCache object. When the stack unwinds back to GetBoundsPx() from the synchronous DispatchNow() call, the function continues to access members of the now-freed this object at locations [3], [4], and during subsequent logic.

Potential Attack Vector

An attacker could potentially trigger this by influencing window destruction timing during bounds updates. For example, by carefully timed window manipulation (like snapping a window into a tab strip or closing a popup window) that triggers a synchronous bounds check while X11 replies are pending. Since this occurs in the browser process, successful exploitation could lead to arbitrary code execution with the user’s privileges.

Impact

This is a browser-process heap UAF. The affected members in GeometryCache (have_geometry_, parent_, and geometry_) are not protected by MiraclePtr/BackupRefPtr. The browser process is unsandboxed, making this a critical security concern.

Suggested Fix

The GetBoundsPx() method should protect itself against object destruction during DispatchNow() calls. This can be achieved by using a base::WeakPtr to check for object validity after each synchronous dispatch:

gfx::Rect GeometryCache::GetBoundsPx() {
  auto weak_this = weak_ptr_factory_.GetWeakPtr();
  if (!have_parent_) {
    parent_future_.DispatchNow();
    if (!weak_this) return {};
  }
  CHECK(have_parent_);
  if (!have_geometry_) {
    geometry_future_.DispatchNow();
    if (!weak_this) return {};
  }
  CHECK(have_geometry_);
  // ...
}

Alternatively, avoiding synchronous dispatch in this context or ensuring that bounds change notifications are always debounced/asynchronous would mitigate the risk.

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


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