High CVSS 9.6 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Ozone
DescriptionUse after free in Ozone
ComponentOzone
Sub-componentOzone/X11
Bug ClassUAF
Tracker523690961
Fix commitb12c924f5828 (chromium/src) +98/-3
Related commitsfc4613ff876d previously mapped (not the fix) · chromium/src
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-06-13 (per vendor advisory)
Fix landed2026-06-15 (public git)
Disclosed2026-06-30
Patch gap15 days — fix public in source before the advisory
Re-mapped on 2026-10-03: our pipeline had picked the earliest commit citing this bug (fc4613ff876d), which an audit found was a preparatory/helper change, not the fix. This page now points to the commit whose diff removes the bug. No root-cause analysis is published for this CVE.

Changed Functions

FunctionChangeNotes
if
ui/ozone/platform/x11/x11_window.cc
modified
TEST_F
ui/ozone/platform/x11/x11_window_ozone_unittest.cc
modified
if
ui/ozone/platform/x11/x11_window_ozone_unittest.cc
modified

Files Changed

  • ui/ozone/platform/x11/x11_window.cc
  • ui/ozone/platform/x11/x11_window.h
  • ui/ozone/platform/x11/x11_window_ozone_unittest.cc
diff --git a/ui/ozone/platform/x11/x11_window.cc b/ui/ozone/platform/x11/x11_window.cc
index 70f25eaa..1366525 100644
--- a/ui/ozone/platform/x11/x11_window.cc
+++ b/ui/ozone/platform/x11/x11_window.cc
@@ -264,6 +264,10 @@
   Close();
 }
 
+base::WeakPtr<X11Window> X11Window::GetWeakPtr() {
+  return weak_ptr_factory_.GetWeakPtr();
+}
+
 void X11Window::Initialize(PlatformWindowInitProperties properties) {
   CreateXWindow(properties);
 
@@ -1462,6 +1466,8 @@
   auto* located_events_grabber = window_manager->located_events_grabber();
   if (event->IsLocatedEvent() && located_events_grabber &&
       located_events_grabber != this) {
+    base::WeakPtr<X11Window> weak_grabber =
+        located_events_grabber->GetWeakPtr();
     if (event->IsMouseEvent() ||
         (event->IsTouchEvent() &&
          event->type() == ui::EventType::kTouchPressed)) {
@@ -1469,17 +1475,23 @@
       // event's location and dispatch to the other.
       const gfx::Point target_origin =
           located_events_grabber->GetBoundsInPixels().origin();
-      if (!weak_this) {
+      if (!weak_this || !weak_grabber ||
+          window_manager->located_events_grabber() != weak_grabber.get()) {
         return;
       }
       const gfx::Point current_origin = GetBoundsInPixels().origin();
-      if (!weak_this) {
+      if (!weak_this || !weak_grabber ||
+          window_manager->located_events_grabber() != weak_grabber.get()) {
         return;
       }
       ConvertEventLocationToTargetWindowLocation(target_origin, current_origin,
                                                  event->AsLocatedEvent());
     }
-    return located_events_grabber->DispatchUiEvent(event, xev);
+    if (weak_grabber &&
+        window_manager->located_events_grabber() == weak_grabber.get()) {
+      weak_grabber->DispatchUiEvent(event, xev);
+    }
+    return;
   }
 
   // If after CoalescePendingMotionEvents the type of xev is resolved to
diff --git a/ui/ozone/platform/x11/x11_window.h b/ui/ozone/platform/x11/x11_window.h
index de5b2fe..9908509d 100644
--- a/ui/ozone/platform/x11/x11_window.h
+++ b/ui/ozone/platform/x11/x11_window.h
@@ -15,6 +15,7 @@
 #include "base/gtest_prod_util.h"
 #include "base/memory/raw_ptr.h"
 #include "base/memory/raw_ref.h"
+#include "base/memory/weak_ptr.h"
 #include "ui/base/dragdrop/mojom/drag_drop_types.mojom-forward.h"
 #include "ui/base/x/x11_desktop_window_move_client.h"
 #include "ui/base/x/x11_drag_drop_client.h"
@@ -70,6 +71,8 @@
 
   virtual void Initialize(PlatformWindowInitProperties properties);
 
+  base::WeakPtr<X11Window> GetWeakPtr();
+
   // X11WindowManager calls this.
   void OnXWindowLostCapture();
 
diff --git a/ui/ozone/platform/x11/x11_window_ozone_unittest.cc b/ui/ozone/platform/x11/x11_window_ozone_unittest.cc
index 9289d8c..42f63fc 100644
--- a/ui/ozone/platform/x11/x11_window_ozone_unittest.cc
+++ b/ui/ozone/platform/x11/x11_window_ozone_unittest.cc
@@ -559,4 +559,84 @@
   connection->DestroyWindow({new_parent});
 }
 
+// Verifies that X11Window::DispatchUiEvent() does not cause a use-after-free
+// if the located_events_grabber is synchronously deleted during geometry fetch
+// (GetBoundsInPixels()).
+TEST_F(X11WindowOzoneTest, DispatchUiEventGrabberFreedDuringGeometryFetch) {
+  // Setup window1 (grabber)
+  testing::NiceMock<MockPlatformWindowDelegate> delegate1;
+  gfx::AcceleratedWidget widget1;
+  constexpr gfx::Rect bounds1(30, 80, 800, 600);
+  std::unique_ptr<PlatformWindow> window1 =
+      CreatePlatformWindow(&delegate1, bounds1, &widget1, nullptr);
+
+  // Setup window2 (target)
+  testing::NiceMock<MockPlatformWindowDelegate> delegate2;
+  gfx::AcceleratedWidget widget2;
+  constexpr gfx::Rect bounds2(900, 80, 800, 600);
+  std::unique_ptr<PlatformWindow> window2 =
+      CreatePlatformWindow(&delegate2, bounds2, &widget2, nullptr);
+
+  auto* connection = x11::Connection::Get();
+  auto xwindow1 = static_cast<x11::Window>(widget1);
+
+  // Set window1 as the grabber.
+  window1->SetCapture();
+  EXPECT_TRUE(window1->HasCapture());
+
+  // Force the grabber window's GeometryCache (and its parent chain) to
+  // become Ready.
+  window1->GetBoundsInPixels();
+
+  // Create a real parent window at a non-zero offset so that after reparenting,
+  // the absolute origin changes. override_redirect avoids any WM interference.
+  x11::Window new_parent = connection->GenerateId<x11::Window>();
+  connection->CreateWindow({
+      .wid = new_parent,
+      .parent = connection->default_root(),
+      .x = 200,
+      .y = 200,
+      .width = 1000,
+      .height = 1000,
+      .c_class = x11::WindowClass::InputOnly,
+      .override_redirect = x11::Bool32(true),
+  });
+
+  // Synthesize the ReparentNotify WM reparent event.
+  x11::ReparentNotifyEvent reparent{};
+  reparent.event = xwindow1;
+  reparent.window = xwindow1;
+  reparent.parent = new_parent;
+  reparent.x = 0;
+  reparent.y = 0;
+  x11::Event reparent_event(/*send_event=*/false, std::move(reparent));
+  connection->DispatchEvent(reparent_event);
+
+  // Arm the grabber window's delegate to free window1 during OnBoundsChanged.
+  bool armed = true;
+  bool freed = false;
+  EXPECT_CALL(delegate1, OnBoundsChanged(_))
+      .WillRepeatedly([&](const PlatformWindowDelegate::BoundsChange&) {
+        if (armed) {
+          armed = false;
+          freed = true;
+          window1.reset();
+        }
+      });
+
+  // Prepare a located event (e.g. mouse press) targeting window2 (widget2).
+  ScopedXI2Event xi_event;
+  xi_event.InitGenericButtonEvent(kPointerDeviceId, EventType::kMousePressed,
+                                  gfx::Point(100, 100), EF_NONE);
+
+  // Dispatch the event to window2.
+  DispatchXEvent(xi_event, widget2);
+
+  // Verify that window1 was indeed synchronously freed during the geometry
+  // fetch.
+  EXPECT_TRUE(freed) << "grabber delegate was never invoked synchronously";
+
+  connection->DestroyWindow({new_parent});
+}
+
 }  // namespace ui
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ui/ozone/platform/x11/x11_window_ozone_unittest.cc b/ui/ozone/platform/x11/x11_window_ozone_unittest.cc
index 9289d8c..42f63fc 100644
--- a/ui/ozone/platform/x11/x11_window_ozone_unittest.cc
+++ b/ui/ozone/platform/x11/x11_window_ozone_unittest.cc
@@ -559,4 +559,84 @@
   connection->DestroyWindow({new_parent});
 }
 
+// Verifies that X11Window::DispatchUiEvent() does not cause a use-after-free
+// if the located_events_grabber is synchronously deleted during geometry fetch
+// (GetBoundsInPixels()).
+TEST_F(X11WindowOzoneTest, DispatchUiEventGrabberFreedDuringGeometryFetch) {
+  // Setup window1 (grabber)
+  testing::NiceMock<MockPlatformWindowDelegate> delegate1;
+  gfx::AcceleratedWidget widget1;
+  constexpr gfx::Rect bounds1(30, 80, 800, 600);
+  std::unique_ptr<PlatformWindow> window1 =
+      CreatePlatformWindow(&delegate1, bounds1, &widget1, nullptr);
+
+  // Setup window2 (target)
+  testing::NiceMock<MockPlatformWindowDelegate> delegate2;
+  gfx::AcceleratedWidget widget2;
+  constexpr gfx::Rect bounds2(900, 80, 800, 600);
+  std::unique_ptr<PlatformWindow> window2 =
+      CreatePlatformWindow(&delegate2, bounds2, &widget2, nullptr);
+
+  auto* connection = x11::Connection::Get();
+  auto xwindow1 = static_cast<x11::Window>(widget1);
+
+  // Set window1 as the grabber.
+  window1->SetCapture();
+  EXPECT_TRUE(window1->HasCapture());
+
+  // Force the grabber window's GeometryCache (and its parent chain) to
+  // become Ready.
+  window1->GetBoundsInPixels();
+
+  // Create a real parent window at a non-zero offset so that after reparenting,
+  // the absolute origin changes. override_redirect avoids any WM interference.
+  x11::Window new_parent = connection->GenerateId<x11::Window>();
+  connection->CreateWindow({
+      .wid = new_parent,
+      .parent = connection->default_root(),
+      .x = 200,
+      .y = 200,
+      .width = 1000,
+      .height = 1000,
+      .c_class = x11::WindowClass::InputOnly,
+      .override_redirect = x11::Bool32(true),
+  });
+
+  // Synthesize the ReparentNotify WM reparent event.
+  x11::ReparentNotifyEvent reparent{};
+  reparent.event = xwindow1;
+  reparent.window = xwindow1;
+  reparent.parent = new_parent;
+  reparent.x = 0;
+  reparent.y = 0;
+  x11::Event reparent_event(/*send_event=*/false, std::move(reparent));
+  connection->DispatchEvent(reparent_event);
+
+  // Arm the grabber window's delegate to free window1 during OnBoundsChanged.
+  bool armed = true;
+  bool freed = false;
+  EXPECT_CALL(delegate1, OnBoundsChanged(_))
+      .WillRepeatedly([&](const PlatformWindowDelegate::BoundsChange&) {
+        if (armed) {
+          armed = false;
+          freed = true;
+          window1.reset();
+        }
+      });
+
+  // Prepare a located event (e.g. mouse press) targeting window2 (widget2).
+  ScopedXI2Event xi_event;
+  xi_event.InitGenericButtonEvent(kPointerDeviceId, EventType::kMousePressed,
+                                  gfx::Point(100, 100), EF_NONE);
+
+  // Dispatch the event to window2.
+  DispatchXEvent(xi_event, widget2);
+
+  // Verify that window1 was indeed synchronously freed during the geometry
+  // fetch.
+  EXPECT_TRUE(freed) << "grabber delegate was never invoked synchronously";
+
+  connection->DestroyWindow({new_parent});
+}
+
 }  // namespace ui
Loading diff…

Original Bug Report

reported by [email protected]

Potential UAF in X11Window::DispatchUiEvent via synchronous geometry fetch

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 Use-After-Free (UAF) vulnerability exists in X11Window::DispatchUiEvent when translating coordinates for a located event grabber. A synchronous XCB call to fetch window bounds can trigger callbacks that destroy the grabber window, leaving a raw stack pointer dangling. MiraclePtr does not quarantine the memory because all raw_ptr references are cleared during destruction, potentially allowing a sandbox escape.

Affected files:

  • ui/ozone/platform/x11/x11_window.cc

Estimated timestamp from git blame: 2025-03-06

Description

A Use-After-Free (UAF) vulnerability has been identified in ui/ozone/platform/x11/x11_window.cc within X11Window::DispatchUiEvent. The issue occurs when an event is routed to a window that currently holds the event grab (the located_events_grabber).

The code fetches the grabber window as a raw pointer and stores it in a local stack variable. It then attempts to translate the event coordinates by calling GetBoundsInPixels() on the grabber window. If the window’s geometry is not cached, this call will block and dispatch a synchronous request to the X server (geometry_future_.DispatchNow()).

During this synchronous wait, XCB replies are processed out-of-order. Processing the geometry response triggers an OnBoundsChanged callback cascade that reaches the UI layer (e.g., TabDragController). Depending on the state, the UI layer can synchronously close and destroy the widget and its underlying X11Window.

MiraclePtr Bypass

When the X11Window is destroyed, the X11WindowManager clears its raw_ptr<X11Window> reference to the grabber. Because all raw_ptr fields pointing to the object are cleared, PartitionAlloc’s BackupRefPtr reference count drops to zero. Consequently, the memory is not quarantined by MiraclePtr and is immediately freed.

Execution then returns to DispatchUiEvent. The local located_events_grabber variable is a raw C++ pointer and is not updated. It now points to freed memory. The code subsequently dereferences this pointer (located_events_grabber->DispatchUiEvent(...)), resulting in a UAF.

Potential Exploit Scenario (Suggested Steps)

Note: These steps trace a theoretical path through the codebase; a working Proof of Concept has not yet been executed.

  1. An attacker compromises a renderer process.
  2. Using the content.mojom.WidgetInputHandlerHost IPC, the attacker forces a specific browser window (e.g., a detached tab) to acquire mouse capture, making it the located_events_grabber.
  3. The attacker injects synthetic mouse events via the content.mojom.InputInjector IPC.
  4. When the event reaches X11Window::DispatchUiEvent, the code fetches the raw pointer to the grabber window.
  5. The code calls GetBoundsInPixels(), blocking on the X server.
  6. The out-of-order processing of the X11 reply triggers a bounds change notification to TabDragController, which (based on carefully timed attacker input) synchronously closes the widget.
  7. The X11Window is destroyed and its memory freed.
  8. The attacker sprays the browser process heap via IPC, replacing the freed window object with malicious data.
  9. DispatchUiEvent resumes and calls a method on the dangling pointer.
  10. The hijacked object provides a fake platform_window_delegate(), leading to a virtual function call hijack when DispatchEventFromNativeUiEvent executes.
  11. This results in arbitrary Remote Code Execution (RCE) in the browser process, escaping the sandbox.

Suggested Fix

Use a base::WeakPtr<X11Window> to track the grabber window across the potentially blocking GetBoundsInPixels() call.

auto* located_events_grabber = window_manager->located_events_grabber();
if (event->IsLocatedEvent() && located_events_grabber &&
    located_events_grabber != this) {
  
  base::WeakPtr<X11Window> weak_grabber = located_events_grabber->GetWeakPtr();

  if (event->IsMouseEvent() || ...) {
    const gfx::Point target_origin = located_events_grabber->GetBoundsInPixels().origin();
    
    // Check both the current window AND the grabber window
    if (!weak_this || !weak_grabber) {
      return;
    }
    // ...
  }
  return weak_grabber->DispatchUiEvent(event, xev);
}

Alternatively, retrieve window_manager->located_events_grabber() again after coordinate translation to verify it hasn’t changed or been destroyed.

Evaluated with Chrome root at commit: 65b3256311f3ab6fb9870eaa522de7e6dd2663bb


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