CVE-2026-13854
Overview
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
| Function | Change | Notes |
|---|---|---|
ifui/ozone/platform/x11/x11_window.cc |
modified | |
TEST_Fui/ozone/platform/x11/x11_window_ozone_unittest.cc |
modified | |
ifui/ozone/platform/x11/x11_window_ozone_unittest.cc |
modified |
Files Changed
ui/ozone/platform/x11/x11_window.ccui/ozone/platform/x11/x11_window.hui/ozone/platform/x11/x11_window_ozone_unittest.cc
Patch
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
Regression Test / PoC
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
Original Bug Report
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.
- An attacker compromises a renderer process.
- Using the
content.mojom.WidgetInputHandlerHostIPC, the attacker forces a specific browser window (e.g., a detached tab) to acquire mouse capture, making it thelocated_events_grabber. - The attacker injects synthetic mouse events via the
content.mojom.InputInjectorIPC. - When the event reaches
X11Window::DispatchUiEvent, the code fetches the raw pointer to the grabber window. - The code calls
GetBoundsInPixels(), blocking on the X server. - 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. - The
X11Windowis destroyed and its memory freed. - The attacker sprays the browser process heap via IPC, replacing the freed window object with malicious data.
DispatchUiEventresumes and calls a method on the dangling pointer.- The hijacked object provides a fake
platform_window_delegate(), leading to a virtual function call hijack whenDispatchEventFromNativeUiEventexecutes. - 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.