CVE-2026-13855
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_ozone_unittest.cc
Patch
diff --git a/ui/ozone/platform/x11/x11_window.cc b/ui/ozone/platform/x11/x11_window.cc
index 1366525..27b995c5 100644
--- a/ui/ozone/platform/x11/x11_window.cc
+++ b/ui/ozone/platform/x11/x11_window.cc
@@ -1424,7 +1424,11 @@
auto& current_xevent = *connection_->dispatching_event();
if (event->IsMouseEvent()) {
+ auto weak_this = weak_ptr_factory_.GetWeakPtr();
X11WindowManager::GetInstance()->MouseOnWindow(this);
+ if (!weak_this) {
+ return POST_DISPATCH_STOP_PROPAGATION;
+ }
}
#if BUILDFLAG(USE_ATK)
if (HandleAsAtkEvent(current_xevent)) {
diff --git a/ui/ozone/platform/x11/x11_window_ozone_unittest.cc b/ui/ozone/platform/x11/x11_window_ozone_unittest.cc
index 42f63fc..c3ddf03 100644
--- a/ui/ozone/platform/x11/x11_window_ozone_unittest.cc
+++ b/ui/ozone/platform/x11/x11_window_ozone_unittest.cc
@@ -639,4 +639,81 @@
connection->DestroyWindow({new_parent});
}
+// Verifies that X11Window::DispatchEvent() does not cause a use-after-free
+// if the window is synchronously deleted during MouseOnWindow().
+TEST_F(X11WindowOzoneTest, DispatchEventSelfFreedDuringMouseOnWindow) {
+ testing::NiceMock<MockPlatformWindowDelegate> delegate;
+ gfx::AcceleratedWidget widget;
+ constexpr gfx::Rect bounds(30, 80, 800, 600);
+ std::unique_ptr<PlatformWindow> window =
+ CreatePlatformWindow(&delegate, bounds, &widget, nullptr);
+
+ auto* connection = x11::Connection::Get();
+ auto xwindow = static_cast<x11::Window>(widget);
+
+ // Step 1: Force the X11Window's GeometryCache (and its parent chain) to
+ // become Ready and record last_notified_geometry_.
+ window->GetBoundsInPixels();
+
+ // Step 2: Create a real parent window at a non-zero offset so that after
+ // reparenting, only the absolute origin changes.
+ 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),
+ });
+
+ // Step 3: Synthesize the ReparentNotify WM reparent event.
+ x11::ReparentNotifyEvent reparent{};
+ reparent.event = xwindow;
+ reparent.window = xwindow;
+ reparent.parent = new_parent;
+ reparent.x = 0;
+ reparent.y = 0;
+ x11::Event reparent_event(/*send_event=*/false, std::move(reparent));
+ connection->DispatchEvent(reparent_event);
+
+ // Step 4: Mock delegate->OnCursorUpdate() to call
+ // window->GetBoundsInPixels(), modelling production
+ // WindowTreeHostPlatform::OnCursorUpdate() -> etc.
+ ON_CALL(delegate, OnCursorUpdate()).WillByDefault([&]() {
+ if (window) {
+ window->GetBoundsInPixels();
+ }
+ });
+
+ // Step 5: Arm the delegate so that the *next* synchronous OnBoundsChanged
+ // frees the X11Window.
+ bool armed = true;
+ bool freed = false;
+ EXPECT_CALL(delegate, OnBoundsChanged(_))
+ .WillRepeatedly([&](const PlatformWindowDelegate::BoundsChange&) {
+ if (armed) {
+ armed = false;
+ freed = true;
+ window.reset();
+ }
+ });
+
+ // Step 6: Prepare a located event (e.g. mouse press) targeting widget.
+ ScopedXI2Event xi_event;
+ xi_event.InitGenericButtonEvent(kPointerDeviceId, EventType::kMousePressed,
+ gfx::Point(100, 100), EF_NONE);
+
+ // Step 7: Dispatch the event to widget. This will trigger DispatchEvent.
+ DispatchXEvent(xi_event, widget);
+
+ // If we got here without ASAN/crash tripping, verify the window was indeed
+ // freed.
+ EXPECT_TRUE(freed) << "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 42f63fc..c3ddf03 100644
--- a/ui/ozone/platform/x11/x11_window_ozone_unittest.cc
+++ b/ui/ozone/platform/x11/x11_window_ozone_unittest.cc
@@ -639,4 +639,81 @@
connection->DestroyWindow({new_parent});
}
+// Verifies that X11Window::DispatchEvent() does not cause a use-after-free
+// if the window is synchronously deleted during MouseOnWindow().
+TEST_F(X11WindowOzoneTest, DispatchEventSelfFreedDuringMouseOnWindow) {
+ testing::NiceMock<MockPlatformWindowDelegate> delegate;
+ gfx::AcceleratedWidget widget;
+ constexpr gfx::Rect bounds(30, 80, 800, 600);
+ std::unique_ptr<PlatformWindow> window =
+ CreatePlatformWindow(&delegate, bounds, &widget, nullptr);
+
+ auto* connection = x11::Connection::Get();
+ auto xwindow = static_cast<x11::Window>(widget);
+
+ // Step 1: Force the X11Window's GeometryCache (and its parent chain) to
+ // become Ready and record last_notified_geometry_.
+ window->GetBoundsInPixels();
+
+ // Step 2: Create a real parent window at a non-zero offset so that after
+ // reparenting, only the absolute origin changes.
+ 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),
+ });
+
+ // Step 3: Synthesize the ReparentNotify WM reparent event.
+ x11::ReparentNotifyEvent reparent{};
+ reparent.event = xwindow;
+ reparent.window = xwindow;
+ reparent.parent = new_parent;
+ reparent.x = 0;
+ reparent.y = 0;
+ x11::Event reparent_event(/*send_event=*/false, std::move(reparent));
+ connection->DispatchEvent(reparent_event);
+
+ // Step 4: Mock delegate->OnCursorUpdate() to call
+ // window->GetBoundsInPixels(), modelling production
+ // WindowTreeHostPlatform::OnCursorUpdate() -> etc.
+ ON_CALL(delegate, OnCursorUpdate()).WillByDefault([&]() {
+ if (window) {
+ window->GetBoundsInPixels();
+ }
+ });
+
+ // Step 5: Arm the delegate so that the *next* synchronous OnBoundsChanged
+ // frees the X11Window.
+ bool armed = true;
+ bool freed = false;
+ EXPECT_CALL(delegate, OnBoundsChanged(_))
+ .WillRepeatedly([&](const PlatformWindowDelegate::BoundsChange&) {
+ if (armed) {
+ armed = false;
+ freed = true;
+ window.reset();
+ }
+ });
+
+ // Step 6: Prepare a located event (e.g. mouse press) targeting widget.
+ ScopedXI2Event xi_event;
+ xi_event.InitGenericButtonEvent(kPointerDeviceId, EventType::kMousePressed,
+ gfx::Point(100, 100), EF_NONE);
+
+ // Step 7: Dispatch the event to widget. This will trigger DispatchEvent.
+ DispatchXEvent(xi_event, widget);
+
+ // If we got here without ASAN/crash tripping, verify the window was indeed
+ // freed.
+ EXPECT_TRUE(freed) << "delegate was never invoked synchronously";
+
+ connection->DestroyWindow({new_parent});
+}
+
} // namespace ui
Original Bug Report
Potential browser-process UAF in X11Window::DispatchEvent due to re-entrant window destruction
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 browser-process Use-After-Free (UAF) vulnerability exists in X11Window::DispatchEvent on Linux/X11. Synchronous dispatch of pending X11 events during mouse dispatch can lead to re-entrant window destruction. Since there is no WeakPtr guard on ’this’ inside DispatchEvent, execution proceeds with a freed window pointer, resulting in a UAF.
Affected files:
ui/ozone/platform/x11/x11_window.ccui/ozone/platform/x11/x11_window_manager.ccui/ozone/platform/x11/x11_screen_ozone.cc
Estimated timestamp from git blame: 2025-03-06
Root Cause
In ui/ozone/platform/x11/x11_window.cc, X11Window::DispatchEvent invokes X11WindowManager::GetInstance()->MouseOnWindow(this) for incoming mouse events, but does not guard itself against potential re-entrant destruction of this:
uint32_t X11Window::DispatchEvent(const PlatformEvent& event) {
...
if (event->IsMouseEvent()) {
X11WindowManager::GetInstance()->MouseOnWindow(this); // Synchronously dispatches events/replies
}
#if BUILDFLAG(USE_ATK)
if (HandleAsAtkEvent(current_xevent)) { // Dereferences freed 'this'
return POST_DISPATCH_STOP_PROPAGATION;
}
#endif
DispatchUiEvent(event, current_xevent); // Dereferences freed 'this'
return POST_DISPATCH_STOP_PROPAGATION;
}
MouseOnWindow can synchronously reach window->GetBoundsInPixels(), which in turn queries geometry_cache_->GetBoundsPx(). If the geometry cache is not yet ready, it can synchronously run pending X server events via parent_future_.DispatchNow() and geometry_future_.DispatchNow(). Synchronous event processing can trigger re-entrant window destruction via observers (e.g. Widget::CloseNow()), rendering the current this pointer dangling prior to the remaining member accesses in DispatchEvent.
Potential Trigger Path
- A mouse event is delivered to a top-level
X11WindowW (where the mouse has newly entered). This triggersX11Window::DispatchEvent->MouseOnWindow(W)->W->OnCursorUpdate(). OnCursorUpdatetriggers screen display lookups viaScreenOzone::GetDisplayNearestWindowandX11ScreenOzone::GetDisplayForAcceleratedWidget.GetDisplayForAcceleratedWidgetcallswindow->GetBoundsInPixels()to determine screen coordinates, which delegates togeometry_cache_->GetBoundsPx().GetBoundsPxprocesses pending responses synchronously viaDispatchNow(). Re-entrant callbacks trigger window observers on bounds change, which may synchronously invokeWidget::CloseNow()and destroy W.- Upon unwinding to
X11Window::DispatchEvent, thethispointer is dangling and subsequent member function calls (HandleAsAtkEventorDispatchUiEvent) trigger a browser-process Use-After-Free (UAF).
Note: These are potential steps and our tooling agent does not currently have the capability to run code to confirm this with a fully working PoC.
Suggested Fix
Add a WeakPtr<X11Window> guard in X11Window::DispatchEvent right after MouseOnWindow(this) is called:
uint32_t X11Window::DispatchEvent(const PlatformEvent& event) {
TRACE_EVENT1("views", "X11PlatformWindow::Dispatch", "event->type()",
event->type());
DCHECK_NE(window(), x11::Window::None);
DCHECK(event);
auto& current_xevent = *connection_->dispatching_event();
auto weak_this = weak_ptr_factory_.GetWeakPtr();
if (event->IsMouseEvent()) {
X11WindowManager::GetInstance()->MouseOnWindow(this);
if (!weak_this) {
return POST_DISPATCH_STOP_PROPAGATION;
}
}
#if BUILDFLAG(USE_ATK)
if (HandleAsAtkEvent(current_xevent)) {
return POST_DISPATCH_STOP_PROPAGATION;
}
#endif
DispatchUiEvent(event, current_xevent);
return POST_DISPATCH_STOP_PROPAGATION;
}
Evaluated with Chrome root at commit: 70c6813870b6701fa16670076bf633ee6c3a439f
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.