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

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Ozone
DescriptionUse after free in Ozone
ComponentOzone
Sub-componentOzone/X11
Bug ClassUAF
Tracker517050585
Fix commit6674e8db6701 (chromium/src) +106/-0
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:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-27 (per vendor advisory)
Fix landed2026-05-28 (public git)
Disclosed2026-06-08
Patch gap11 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_ozone_unittest.cc
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
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 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
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • ui/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.

  1. Establish a state where should_maximize_after_map_ is set to true (e.g., call Widget::Maximize() while the window is unmapped) and then set the window state to fullscreen (SetFullscreen(true)).
  2. Register an observer (such as a bounds-changed observer) on the widget that will synchronously call Widget::CloseNow() upon receiving the next bounds update.
  3. Deliver a MapNotify event to the target window (either naturally or through a synthetic event using XSendEvent if simulating a compromised GPU process context).
  4. Observe the execution of OnWindowMapped(), where the synchronous destruction occurs during the nested SetFullscreen() 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.

View on issue tracker