CVE-2026-11681
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 2459a05..57ffaee 100644
--- a/ui/ozone/platform/x11/x11_window.cc
+++ b/ui/ozone/platform/x11/x11_window.cc
@@ -2441,7 +2441,12 @@
// Some WMs only respect maximize hints after the window has been mapped.
// Check whether we need to re-do a maximization.
if (should_maximize_after_map_) {
+ // Maximize() may synchronously delete `this` via OnBoundsChanged().
+ auto weak_this = weak_ptr_factory_.GetWeakPtr();
Maximize();
+ if (!weak_this) {
+ return;
+ }
should_maximize_after_map_ = false;
}
if (should_grab_pointer_after_map_) {
diff --git a/ui/ozone/platform/x11/x11_window_ozone_unittest.cc b/ui/ozone/platform/x11/x11_window_ozone_unittest.cc
index e759196..e56645c 100644
--- a/ui/ozone/platform/x11/x11_window_ozone_unittest.cc
+++ b/ui/ozone/platform/x11/x11_window_ozone_unittest.cc
@@ -385,4 +385,105 @@
window->SetFullscreen(true, display::kInvalidDisplayId);
}
+// Verifies that X11Window::OnWindowMapped() does not cause a use-after-free
+// if the window is synchronously deleted during Maximize().
+//
+// The MapNotify dispatch path (Connection::DispatchEvent ->
+// EventObserver::OnEvent -> HandleEvent -> OnWindowMapped) is not covered by
+// the weak_this guard in DispatchUiEvent because MapNotify is not a
+// translatable event.
+//
+// This test sets `should_maximize_after_map_` to true, and configures the
+// delegate to synchronously delete the window upon receiving the bounds change
+// from Maximize(). It then dispatches a synthetic MapNotify event and ensures
+// the deletion is handled safely without a use-after-free.
+TEST_F(X11WindowOzoneTest, OnWindowMappedUseAfterFreeAfterMaximize) {
+ 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);
+
+ // Prime the X11Window's `GeometryCache` so the first `Maximize()` below does
+ // not itself fire a synchronous `OnBoundsChanged()`.
+ window->GetBoundsInPixels();
+
+ // Step 1: Maximize() while `!window_mapped_in_client_` -> sets
+ // `should_maximize_after_map_` = true. (The window has never been shown.)
+ window->Maximize();
+
+ // Step 2: Create a real parent window at a non-zero offset so that after
+ // a synthetic reparent, only the absolute origin of `xwindow` 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),
+ });
+
+ // Step 3: Synthesize a `ReparentNotify` so the leaf `GeometryCache` becomes
+ // un-ready. The next `GetBoundsInPixels()` (inside the `Maximize()` called
+ // from `OnWindowMapped()`) will block on the new parent's geometry, observe
+ // an origin change, and fire `OnBoundsChanged()` synchronously.
+ 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: Arm the delegate so the next synchronous `OnBoundsChanged()` frees
+ // the `X11Window` — modelling `views::Widget::CloseNow()` ->
+ // `SetPlatformWindow(nullptr)`, which is the deletion sink explicitly
+ // acknowledged by the `weak_this` guards already present in
+ // `X11Window::Maximize()`, `SetFullscreen()`, and
+ // `WindowTreeHostPlatform::OnBoundsChanged()`.
+ bool armed = true;
+ bool freed = false;
+ EXPECT_CALL(delegate, OnBoundsChanged(_))
+ .WillRepeatedly([&](const PlatformWindowDelegate::BoundsChange&) {
+ if (armed) {
+ armed = false;
+ freed = true;
+ window.reset();
+ }
+ });
+
+ // Step 5: Dispatch a synthetic `MapNotify` for this window. There is no
+ // send_event filtering on the `OnEvent` path; `IsTargetedBy()` only checks
+ // `map->window == xwindow_`.
+ //
+ // `OnEvent` -> `HandleEvent` -> `OnWindowMapped()`:
+ // `should_maximize_after_map_` is true -> `Maximize()`
+ // -> `GetBoundsInPixels()` -> `GeometryCache` resolves new parent
+ // -> origin changed -> `NotifyBoundsChanged()` -> delegate frees
+ // X11Window`
+ // -> `Maximize()` `weak_this` guard returns early.
+ // `OnWindowMapped()` then writes `should_maximize_after_map_ = false` and
+ // reads `should_grab_pointer_after_map_` on the freed `this`.
+ x11::MapNotifyEvent map_notify{};
+ map_notify.event = xwindow;
+ map_notify.window = xwindow;
+ x11::Event map_event(/*send_event=*/false, std::move(map_notify));
+ connection->DispatchEvent(map_event);
+
+ // The synchronous-free path must have been exercised; otherwise the test
+ // didn't reach the vulnerable frame.
+ EXPECT_TRUE(freed) << "delegate->OnBoundsChanged() was not invoked "
+ "synchronously from OnWindowMapped/Maximize";
+
+ 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 e759196..e56645c 100644
--- a/ui/ozone/platform/x11/x11_window_ozone_unittest.cc
+++ b/ui/ozone/platform/x11/x11_window_ozone_unittest.cc
@@ -385,4 +385,105 @@
window->SetFullscreen(true, display::kInvalidDisplayId);
}
+// Verifies that X11Window::OnWindowMapped() does not cause a use-after-free
+// if the window is synchronously deleted during Maximize().
+//
+// The MapNotify dispatch path (Connection::DispatchEvent ->
+// EventObserver::OnEvent -> HandleEvent -> OnWindowMapped) is not covered by
+// the weak_this guard in DispatchUiEvent because MapNotify is not a
+// translatable event.
+//
+// This test sets `should_maximize_after_map_` to true, and configures the
+// delegate to synchronously delete the window upon receiving the bounds change
+// from Maximize(). It then dispatches a synthetic MapNotify event and ensures
+// the deletion is handled safely without a use-after-free.
+TEST_F(X11WindowOzoneTest, OnWindowMappedUseAfterFreeAfterMaximize) {
+ 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);
+
+ // Prime the X11Window's `GeometryCache` so the first `Maximize()` below does
+ // not itself fire a synchronous `OnBoundsChanged()`.
+ window->GetBoundsInPixels();
+
+ // Step 1: Maximize() while `!window_mapped_in_client_` -> sets
+ // `should_maximize_after_map_` = true. (The window has never been shown.)
+ window->Maximize();
+
+ // Step 2: Create a real parent window at a non-zero offset so that after
+ // a synthetic reparent, only the absolute origin of `xwindow` 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),
+ });
+
+ // Step 3: Synthesize a `ReparentNotify` so the leaf `GeometryCache` becomes
+ // un-ready. The next `GetBoundsInPixels()` (inside the `Maximize()` called
+ // from `OnWindowMapped()`) will block on the new parent's geometry, observe
+ // an origin change, and fire `OnBoundsChanged()` synchronously.
+ 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: Arm the delegate so the next synchronous `OnBoundsChanged()` frees
+ // the `X11Window` — modelling `views::Widget::CloseNow()` ->
+ // `SetPlatformWindow(nullptr)`, which is the deletion sink explicitly
+ // acknowledged by the `weak_this` guards already present in
+ // `X11Window::Maximize()`, `SetFullscreen()`, and
+ // `WindowTreeHostPlatform::OnBoundsChanged()`.
+ bool armed = true;
+ bool freed = false;
+ EXPECT_CALL(delegate, OnBoundsChanged(_))
+ .WillRepeatedly([&](const PlatformWindowDelegate::BoundsChange&) {
+ if (armed) {
+ armed = false;
+ freed = true;
+ window.reset();
+ }
+ });
+
+ // Step 5: Dispatch a synthetic `MapNotify` for this window. There is no
+ // send_event filtering on the `OnEvent` path; `IsTargetedBy()` only checks
+ // `map->window == xwindow_`.
+ //
+ // `OnEvent` -> `HandleEvent` -> `OnWindowMapped()`:
+ // `should_maximize_after_map_` is true -> `Maximize()`
+ // -> `GetBoundsInPixels()` -> `GeometryCache` resolves new parent
+ // -> origin changed -> `NotifyBoundsChanged()` -> delegate frees
+ // X11Window`
+ // -> `Maximize()` `weak_this` guard returns early.
+ // `OnWindowMapped()` then writes `should_maximize_after_map_ = false` and
+ // reads `should_grab_pointer_after_map_` on the freed `this`.
+ x11::MapNotifyEvent map_notify{};
+ map_notify.event = xwindow;
+ map_notify.window = xwindow;
+ x11::Event map_event(/*send_event=*/false, std::move(map_notify));
+ connection->DispatchEvent(map_event);
+
+ // The synchronous-free path must have been exercised; otherwise the test
+ // didn't reach the vulnerable frame.
+ EXPECT_TRUE(freed) << "delegate->OnBoundsChanged() was not invoked "
+ "synchronously from OnWindowMapped/Maximize";
+
+ connection->DestroyWindow({new_parent});
+}
+
} // namespace ui
Original Bug Report
Potential UAF in X11Window::OnWindowMapped after synchronous deletion during Maximize
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 Chromium’s Ozone/X11 platform code. Handling a MapNotify event can trigger a synchronous call to OnWindowMapped(), which invokes Maximize() and exits fullscreen mode. This transition notifies observers of a bounds change, which can synchronously delete the X11Window object and lead to a UAF upon stack unwinding.
Affected files:
ui/ozone/platform/x11/x11_window.ccui/ozone/platform/x11/x11_window.h
Estimated timestamp from git blame: 2025-03-06
Root Cause Analysis
In ui/ozone/platform/x11/x11_window.cc, X11Window::OnWindowMapped() is invoked to process MapNotify events:
void X11Window::OnWindowMapped() {
window_mapped_in_server_ = true;
if (should_maximize_after_map_) {
Maximize();
should_maximize_after_map_ = false; // Potential Use-After-Free write
}
if (should_grab_pointer_after_map_) {
has_pointer_grab_ |=
(ui::GrabPointer(xwindow_, true, nullptr) == x11::GrabStatus::Success);
should_grab_pointer_after_map_ = false;
}
}
If the window is fullscreen, Maximize() attempts to exit fullscreen mode by calling SetFullscreen(false, ...) (ui/ozone/platform/x11/x11_window.cc:763). At the end of SetFullscreen(), a delegate callback is fired to notify observers of the bounds change:
platform_window_delegate_->OnBoundsChanged({origin_changed});
This delegate call propagates up the stack. An observer responding to this bounds change can synchronously trigger widget closure by invoking views::Widget::CloseNow(). This destroys the Views widget and its native window, synchronously deleting the X11Window instance via DesktopWindowTreeHostPlatform::OnClosed() -> SetPlatformWindow(nullptr).
When the delegate call returns, the execution flow unwinds through SetFullscreen() back to Maximize(). While Maximize() safely returns early because of its weak_this check (ui/ozone/platform/x11/x11_window.cc:764), control is then returned to OnWindowMapped() where no weak_this guard is present. As a result, the statement should_maximize_after_map_ = false; (and potentially the subsequent pointer-grabbing logic) is executed on a freed this pointer context.
Vulnerability Dispatch Path
Unlike translative UI events that go through PlatformEventDispatcher (which has defensive weak_this guards), MapNotify is treated as a non-translative event (EventType::kUnknown). It is routed through the raw EventObserver path:
Connection::DispatchEvent() -> X11Window::OnEvent() -> HandleEvent() -> OnWindowMapped(). This path is completely missing any weak_ptr verification checks.
Suggested Steps to Trigger the Potential Vulnerability
Note: These are potential steps; our current analysis is based on static code tracing, and we do not yet have a running proof-of-concept.
- Establish a state where
should_maximize_after_map_is set totrue(e.g., callWidget::Maximize()while the window is unmapped) and then set the window state to fullscreen (SetFullscreen(true)). - Register an observer (such as a bounds-changed observer) on the widget that will synchronously call
Widget::CloseNow()upon receiving the next bounds update. - Deliver a
MapNotifyevent to the target window (either naturally or through a synthetic event usingXSendEventif simulating a compromised GPU process context). - Observe the execution of
OnWindowMapped(), where the synchronous destruction occurs during the nestedSetFullscreen()call, and the subsequent crash/UAF upon returning to line 2445.
Suggested Fix
To prevent this vulnerability, a weak_this guard should be introduced in X11Window::OnWindowMapped() immediately after invoking Maximize() to safely exit if the window has been synchronously deleted:
void X11Window::OnWindowMapped() {
window_mapped_in_server_ = true;
if (should_maximize_after_map_) {
auto weak_this = weak_ptr_factory_.GetWeakPtr();
Maximize();
if (!weak_this) {
return;
}
should_maximize_after_map_ = false;
}
...
Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8
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.