Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Ozone
DescriptionUse after free in Ozone
ComponentOzone
Bug ClassUAF
Tracker532925350
Fix commitc9c9c645ab28 (chromium/src) +109/-7
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-16

Changed Functions

FunctionChangeNotes
if
ui/ozone/platform/wayland/host/wayland_buffer_manager_host.cc
modified
if
ui/ozone/platform/wayland/host/wayland_output_manager.cc
modified
GetAllWindowsAsWeakPtr
ui/ozone/platform/wayland/host/wayland_window_manager.cc
modified
for
ui/ozone/platform/wayland/host/wayland_window_manager.cc
modified
if
ui/ozone/platform/wayland/host/wayland_window_manager.cc
modified
for
ui/ozone/platform/wayland/wayland_buffer_manager_unittest.cc
modified

Files Changed

  • ui/ozone/platform/wayland/host/wayland_buffer_manager_host.cc
  • ui/ozone/platform/wayland/host/wayland_output_manager.cc
  • ui/ozone/platform/wayland/host/wayland_window_manager.cc
  • ui/ozone/platform/wayland/host/wayland_window_manager.h
  • ui/ozone/platform/wayland/host/wayland_window_manager_unittest.cc
  • ui/ozone/platform/wayland/wayland_buffer_manager_unittest.cc
From c9c9c645ab28e5a002c926a456f10a36ac36029c Mon Sep 17 00:00:00 2001
From: Kramer Ge <[email protected]>
Date: Mon, 13 Jul 2026 13:25:16 -0700
Subject: [PATCH] [Ozone/Wayland]Add window manager GetAllWindowsAsWeakPtr()

A class of uaf are caused by direct/indirect delegate OnStatusUpdate()
calls. Notably iterating on GetAllWindows() while the delegate may
destroy the window and its descendant windows.

Add GetAllWindowsAsWeakPtr() for callers that need to check for
potential window destruction.

Bug: 532925350
Change-Id: I06109f25e6a1d9588fb55e930bc5ba81e82f6267
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8072281
Reviewed-by: Thomas Anderson <[email protected]>
Commit-Queue: Kramer Ge <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1661337}
---

diff --git a/ui/ozone/platform/wayland/host/wayland_buffer_manager_host.cc b/ui/ozone/platform/wayland/host/wayland_buffer_manager_host.cc
index c676771..a7506130 100644
--- a/ui/ozone/platform/wayland/host/wayland_buffer_manager_host.cc
+++ b/ui/ozone/platform/wayland/host/wayland_buffer_manager_host.cc
@@ -81,8 +81,14 @@
 
   buffer_backings_.clear();
   dma_buffers_.clear();
-  for (auto* window : connection_->window_manager()->GetAllWindows())
-    window->OnChannelDestroyed();
+  for (auto window : connection_->window_manager()->GetAllWindowsAsWeakPtr()) {
+    // OnChannelDestroyed() may RequestState() from window delegate and close
+    // its child windows. This can happen if `should_ack_swap_without_commit_`
+    // in the frame manager.
+    if (window) {
+      window->OnChannelDestroyed();
+    }
+  }
 
   buffer_manager_gpu_associated_.reset();
   receiver_.reset();
diff --git a/ui/ozone/platform/wayland/host/wayland_output_manager.cc b/ui/ozone/platform/wayland/host/wayland_output_manager.cc
index 28b7bfab..c3564a0 100644
--- a/ui/ozone/platform/wayland/host/wayland_output_manager.cc
+++ b/ui/ozone/platform/wayland/host/wayland_output_manager.cc
@@ -70,8 +70,13 @@
   // 2. from `WaylandScreen::display_list_`
   // 3. from `WaylandOutputManager::output_list_`
   auto* wayland_window_manager = connection_->window_manager();
-  for (auto* window : wayland_window_manager->GetAllWindows())
-    window->RemoveEnteredOutput(output_id);
+  for (auto window : wayland_window_manager->GetAllWindowsAsWeakPtr()) {
+    // RemoveEnteredOutput() may RequestState() from window delegate and close
+    // its child windows.
+    if (window) {
+      window->RemoveEnteredOutput(output_id);
+    }
+  }
 
   if (wayland_screen_)
     wayland_screen_->OnOutputRemoved(output_id);
@@ -174,7 +179,12 @@
   const bool is_primary =
       wayland_screen_ &&
       metrics.display_id == wayland_screen_->GetPrimaryDisplay().id();
-  for (auto* window : connection_->window_manager()->GetAllWindows()) {
+  for (auto window : connection_->window_manager()->GetAllWindowsAsWeakPtr()) {
+    // RemoveEnteredOutput() may RequestState() from window delegate and close
+    // its child windows.
+    if (!window) {
+      continue;
+    }
     auto entered_output = window->GetPreferredEnteredOutputId();
     if (entered_output == metrics.output_id ||
         (!entered_output && is_primary)) {
diff --git a/ui/ozone/platform/wayland/host/wayland_window_manager.cc b/ui/ozone/platform/wayland/host/wayland_window_manager.cc
index 50db3bd..82bf076 100644
--- a/ui/ozone/platform/wayland/host/wayland_window_manager.cc
+++ b/ui/ozone/platform/wayland/host/wayland_window_manager.cc
@@ -335,6 +335,16 @@
   return result;
 }
 
+std::vector<base::WeakPtr<WaylandWindow>>
+WaylandWindowManager::GetAllWindowsAsWeakPtr() const {
+  std::vector<base::WeakPtr<WaylandWindow>> result;
+  result.reserve(window_map_.size());
+  for (auto& entry : window_map_) {
+    result.push_back(entry.second->AsWeakPtr());
+  }
+  return result;
+}
+
 bool WaylandWindowManager::IsWindowValid(const WaylandWindow* window) const {
   for (auto& pair : window_map_) {
     if (pair.second == window)
@@ -348,8 +358,10 @@
     return;
   }
   font_scale_ = new_font_scale;
-  for (WaylandWindow* window : GetAllWindows()) {
-    window->OnFontScaleFactorChanged();
+  for (auto window : GetAllWindowsAsWeakPtr()) {
+    if (window) {
+      window->OnFontScaleFactorChanged();
+    }
   }
 }
 
diff --git a/ui/ozone/platform/wayland/host/wayland_window_manager.h b/ui/ozone/platform/wayland/host/wayland_window_manager.h
index 77359750..5568396 100644
--- a/ui/ozone/platform/wayland/host/wayland_window_manager.h
+++ b/ui/ozone/platform/wayland/host/wayland_window_manager.h
@@ -10,6 +10,7 @@
 
 #include "base/containers/flat_map.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
 #include "base/observer_list.h"
 #include "ui/gfx/geometry/size_f.h"
 #include "ui/gfx/native_ui_types.h"
@@ -115,6 +116,10 @@
   // Returns all stored windows.
   std::vector<WaylandWindow*> GetAllWindows() const;
 
+  // Same as above, but for callers that may destroy windows while iterating on
+  // them.
+  std::vector<base::WeakPtr<WaylandWindow>> GetAllWindowsAsWeakPtr() const;
+
   // Returns true if the |window| still exists.
   bool IsWindowValid(const WaylandWindow* window) const;
 
diff --git a/ui/ozone/platform/wayland/host/wayland_window_manager_unittest.cc b/ui/ozone/platform/wayland/host/wayland_window_manager_unittest.cc
index 17c2192..6306d513 100644
--- a/ui/ozone/platform/wayland/host/wayland_window_manager_unittest.cc
+++ b/ui/ozone/platform/wayland/host/wayland_window_manager_unittest.cc
@@ -184,6 +184,9 @@
 
   windows = manager_->GetAllWindows();
   EXPECT_EQ(2u, windows.size());
+
+  auto weak_windows = manager_->GetAllWindowsAsWeakPtr();
+  EXPECT_EQ(2u, weak_windows.size());
 }
 
 INSTANTIATE_TEST_SUITE_P(XdgVersionStableTest,
diff --git a/ui/ozone/platform/wayland/wayland_buffer_manager_unittest.cc b/ui/ozone/platform/wayland/wayland_buffer_manager_unittest.cc
index 79f7c96..3ced1005 100644
--- a/ui/ozone/platform/wayland/wayland_buffer_manager_unittest.cc
+++ b/ui/ozone/platform/wayland/wayland_buffer_manager_unittest.cc
@@ -39,6 +39,7 @@
 #include "ui/ozone/platform/wayland/host/wayland_connection.h"
 #include "ui/ozone/platform/wayland/host/wayland_frame_manager.h"
 #include "ui/ozone/platform/wayland/host/wayland_subsurface.h"
+#include "ui/ozone/platform/wayland/host/wayland_window_manager.h"
 #include "ui/ozone/platform/wayland/host/wayland_zwp_linux_dmabuf.h"
 #include "ui/ozone/platform/wayland/test/mock_drm_syncobj_ioctl_wrapper.h"
 #include "ui/ozone/platform/wayland/test/mock_surface.h"
@@ -2970,6 +2971,71 @@
   }
 }
 
+// Regression test: WaylandBufferManagerHost::OnChannelDestroyed iterates a
+// bare-pointer snapshot of all WaylandWindows and calls OnChannelDestroyed()
+// on each. If processing one window synchronously destroys another (via the
+// OnStateUpdate delegate callout, reachable when should_ack_swap_without_commit
+// is set on a suspended window under video capture), the next iteration
+// dereferences a freed WaylandWindow pointer.
+TEST_P(WaylandBufferManagerTest,
+       OnChannelDestroyedSnapshotSurvivesWindowDeletion) {
+  // Create a second toplevel that will be destroyed synchronously from within
+  // the first window's OnChannelDestroyed() -> ... -> delegate()->OnStateUpdate
+  // call chain.
+  testing::NiceMock<MockWaylandPlatformWindowDelegate> victim_delegate(
+      connection_.get());
+  std::unique_ptr<WaylandWindow> victim = CreateWaylandWindowWithParams(
+      PlatformWindowType::kWindow, gfx::Rect(0, 0, 100, 100), &victim_delegate);
+  ASSERT_TRUE(victim);
+  // Both windows must be tracked by the manager.
+  ASSERT_EQ(2u, connection_->window_manager()->GetAllWindows().size());
+  // Iteration order in the flat_map is by widget id; window_ was created first.
+  ASSERT_LT(window_->GetWidget(), victim->GetWidget());
+
+  // Put |window_| into the state where WaylandWindow::OnChannelDestroyed ->
+  // WaylandFrameManager::MaybeProcessPendingFrame takes the
+  // should_ack_swap_without_commit_ branch and reaches delegate->OnStateUpdate.
+  //
+  // 1) Queue enough server-side configures to hit MAX_IN_FLIGHT_REQUESTS so
+  //    the last one remains unapplied for MaybeApplyLatestStateRequest to pick
+  //    up when OnSequencePoint(-1) runs.
+  WaylandWindow::WindowStates states;
+  states.is_activated = true;
+  for (int i = 1; i <= 4; ++i) {
+    window_->HandleToplevelConfigure(200 + i * 10, 200 + i * 10, states);
+    window_->HandleSurfaceConfigure(10 + i);
+  }
+  // 2) Video capture + suspended -> should_ack_swap_without_commit_ = true.
+  window_->SetVideoCapture();
+  states.is_suspended = true;
+  window_->HandleToplevelConfigure(250, 250, states);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ui/ozone/platform/wayland/host/wayland_window_manager_unittest.cc b/ui/ozone/platform/wayland/host/wayland_window_manager_unittest.cc
index 17c2192..6306d513 100644
--- a/ui/ozone/platform/wayland/host/wayland_window_manager_unittest.cc
+++ b/ui/ozone/platform/wayland/host/wayland_window_manager_unittest.cc
@@ -184,6 +184,9 @@
 
   windows = manager_->GetAllWindows();
   EXPECT_EQ(2u, windows.size());
+
+  auto weak_windows = manager_->GetAllWindowsAsWeakPtr();
+  EXPECT_EQ(2u, weak_windows.size());
 }
 
 INSTANTIATE_TEST_SUITE_P(XdgVersionStableTest,
diff --git a/ui/ozone/platform/wayland/wayland_buffer_manager_unittest.cc b/ui/ozone/platform/wayland/wayland_buffer_manager_unittest.cc
index 79f7c96..3ced1005 100644
--- a/ui/ozone/platform/wayland/wayland_buffer_manager_unittest.cc
+++ b/ui/ozone/platform/wayland/wayland_buffer_manager_unittest.cc
@@ -39,6 +39,7 @@
 #include "ui/ozone/platform/wayland/host/wayland_connection.h"
 #include "ui/ozone/platform/wayland/host/wayland_frame_manager.h"
 #include "ui/ozone/platform/wayland/host/wayland_subsurface.h"
+#include "ui/ozone/platform/wayland/host/wayland_window_manager.h"
 #include "ui/ozone/platform/wayland/host/wayland_zwp_linux_dmabuf.h"
 #include "ui/ozone/platform/wayland/test/mock_drm_syncobj_ioctl_wrapper.h"
 #include "ui/ozone/platform/wayland/test/mock_surface.h"
@@ -2970,6 +2971,71 @@
   }
 }
 
+// Regression test: WaylandBufferManagerHost::OnChannelDestroyed iterates a
+// bare-pointer snapshot of all WaylandWindows and calls OnChannelDestroyed()
+// on each. If processing one window synchronously destroys another (via the
+// OnStateUpdate delegate callout, reachable when should_ack_swap_without_commit
+// is set on a suspended window under video capture), the next iteration
+// dereferences a freed WaylandWindow pointer.
+TEST_P(WaylandBufferManagerTest,
+       OnChannelDestroyedSnapshotSurvivesWindowDeletion) {
+  // Create a second toplevel that will be destroyed synchronously from within
+  // the first window's OnChannelDestroyed() -> ... -> delegate()->OnStateUpdate
+  // call chain.
+  testing::NiceMock<MockWaylandPlatformWindowDelegate> victim_delegate(
+      connection_.get());
+  std::unique_ptr<WaylandWindow> victim = CreateWaylandWindowWithParams(
+      PlatformWindowType::kWindow, gfx::Rect(0, 0, 100, 100), &victim_delegate);
+  ASSERT_TRUE(victim);
+  // Both windows must be tracked by the manager.
+  ASSERT_EQ(2u, connection_->window_manager()->GetAllWindows().size());
+  // Iteration order in the flat_map is by widget id; window_ was created first.
+  ASSERT_LT(window_->GetWidget(), victim->GetWidget());
+
+  // Put |window_| into the state where WaylandWindow::OnChannelDestroyed ->
+  // WaylandFrameManager::MaybeProcessPendingFrame takes the
+  // should_ack_swap_without_commit_ branch and reaches delegate->OnStateUpdate.
+  //
+  // 1) Queue enough server-side configures to hit MAX_IN_FLIGHT_REQUESTS so
+  //    the last one remains unapplied for MaybeApplyLatestStateRequest to pick
+  //    up when OnSequencePoint(-1) runs.
+  WaylandWindow::WindowStates states;
+  states.is_activated = true;
+  for (int i = 1; i <= 4; ++i) {
+    window_->HandleToplevelConfigure(200 + i * 10, 200 + i * 10, states);
+    window_->HandleSurfaceConfigure(10 + i);
+  }
+  // 2) Video capture + suspended -> should_ack_swap_without_commit_ = true.
+  window_->SetVideoCapture();
+  states.is_suspended = true;
+  window_->HandleToplevelConfigure(250, 250, states);
+
+  // 3) Hook OnStateUpdate on |window_|'s delegate so that when it fires from
+  //    inside the OnChannelDestroyed loop, it synchronously destroys the other
+  //    window that is still referenced by the snapshot vector.
+  bool destroyed_from_callback = false;
+  delegate_.set_on_state_update_callback(
+      base::BindLambdaForTesting([&]() -> bool {
+        if (victim) {
+          victim.reset();
+          destroyed_from_callback = true;
+        }
+        return false;
+      }));
+
+  // Simulate GPU-process channel loss. The dangling snapshot loop will call
+  // OnChannelDestroyed() on the freed |victim| pointer without a per-iteration
+  // liveness check.
+  manager_host_->OnChannelDestroyed();
+
+  EXPECT_TRUE(destroyed_from_callback);
+  EXPECT_EQ(1u, connection_->window_manager()->GetAllWindows().size());
+
+  delegate_.set_on_state_update_callback({});
+  manager_host_ = connection_->buffer_manager_host();
+  DisableSyncOnTearDown();
+}
+
 INSTANTIATE_TEST_SUITE_P(XdgVersionStableTest,
                          WaylandBufferManagerTest,
                          Values(wl::ServerConfig{}));
Loading diff…

Original Bug Report

reported by [email protected]

Potential Use-After-Free in WaylandBufferManagerHost::OnChannelDestroyed during GPU disconnect

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 the Wayland Ozone platform host inside the browser process. During a GPU process disconnect, WaylandBufferManagerHost iterates over an on-stack snapshot of bare WaylandWindow pointers. Synchronous observer-driven destruction of a window can occur during the iteration, leaving subsequent elements in the snapshot pointing to freed memory.

Affected files:

  • ui/ozone/platform/wayland/host/wayland_buffer_manager_host.cc
  • ui/ozone/platform/wayland/host/wayland_window.cc
  • ui/ozone/platform/wayland/host/wayland_frame_manager.cc

Estimated timestamp from git blame: 2021-12-03

Description

A potential Use-After-Free (UAF) memory corruption vulnerability exists in the Wayland Ozone platform host within the Chromium Browser process.

When a GPU process disconnects or terminates, WaylandBufferManagerHost::OnChannelDestroyed() is invoked. This method snapshots all active platform windows into an on-stack std::vector<WaylandWindow*> of raw pointers and iterates over them to clear their states:

// ui/ozone/platform/wayland/host/wayland_buffer_manager_host.cc
void WaylandBufferManagerHost::OnChannelDestroyed() {
  DCHECK(base::CurrentUIThread::IsSet());
  buffer_backings_.clear();
  dma_buffers_.clear();
  for (auto* window : connection_->window_manager()->GetAllWindows())   // Raw pointer snapshot
    window->OnChannelDestroyed();                                       // Potential UAF dereference
  buffer_manager_gpu_associated_.reset();
  receiver_.reset();
}

During the first iteration (e.g., on Window N), if the window has tab-capture active and is suspended, its should_ack_swap_without_commit_ flag will be true. This flag forces OnChannelDestroyed to execute a sequence of synchronous state-update actions that propagates out to platform-level window/widget observers via WindowTreeHostPlatform::OnBoundsChanged:

  1. Window_N->OnChannelDestroyed() calls frame_manager_->RecordFrame(...) with a placeholder frame.
  2. This triggers MaybeProcessPendingFrame(), entering the immediate swap-ack path.
  3. The window calls OnSequencePoint() -> MaybeApplyLatestStateRequest() -> delegate()->OnStateUpdate().
  4. WindowTreeHostPlatform::OnStateUpdate() observes a bounds/scale change and fires OnBoundsChanged().
  5. Observers responding to the bounds change (such as an anchored bubble/popup tied to Window N’s bounds) may synchronously destroy Window N+1 via Widget::CloseNow().
  6. This calls ~WaylandWindow() on Window N+1, erasing it from WaylandWindowManager::window_map_ and freeing the memory.

However, because the on-stack snapshot in WaylandBufferManagerHost::OnChannelDestroyed() still holds the raw pointer to Window N+1, the subsequent loop iteration will dereference this freed pointer when calling window->OnChannelDestroyed(). Inside OnChannelDestroyed(), the code attempts to dereference frame_manager_ (a std::unique_ptr), resulting in a wild read and write (such as clearing internal deques) within deallocated memory inside the browser process.

Note: This vulnerability is not protected by MiraclePtr because the raw pointers are on-stack local variables (std::vector<WaylandWindow*>), and the dereferenced member frame_manager_ is a std::unique_ptr rather than a raw_ptr member variable.


Suggested Potential Steps to Reproduce

Note: Our analysis is based on static code tracing; we do not currently have a fully running proof-of-concept.

  1. Launch Chromium on Linux/Wayland.
  2. Open a main application window (Window N) and have a webpage initiate a tab capture (e.g., via getDisplayMedia()).
  3. Open a secondary platform-backed widget/bubble (Window N+1) that is anchored to Window N and designed to close when Window N changes bounds.
  4. Minimize or occlude Window N so it transitions to a suspended state (setting is_suspended_ = true).
  5. Force the GPU process to disconnect (e.g., by terminating the GPU process or simulating a GPU crash/pipe closure).
  6. The browser process executes WaylandBufferManagerHost::OnChannelDestroyed().
  7. Iteration on Window N causes a synchronous state and bounds update callout, triggering observers that synchronously close and destroy Window N+1.
  8. The loop advances to the next element, attempting to dereference the now-freed Window_N+1 pointer, resulting in a browser process crash or memory corruption.

Suggested Fix

To prevent the UAF, ensure that windows are verified to be alive before they are dereferenced during the loop. Since WaylandWindow inherits from base::SupportsWeakPtr<WaylandWindow>, we can snapshot the windows using weak pointers instead of raw pointers:

void WaylandBufferManagerHost::OnChannelDestroyed() {
  DCHECK(base::CurrentUIThread::IsSet());

  buffer_backings_.clear();
  dma_buffers_.clear();

  std::vector<base::WeakPtr<WaylandWindow>> windows;
  for (auto* window : connection_->window_manager()->GetAllWindows()) {
    windows.push_back(window->AsWeakPtr());
  }

  for (auto& weak_window : windows) {
    if (weak_window) {
      weak_window->OnChannelDestroyed();
    }
  }

  buffer_manager_gpu_associated_.reset();
  receiver_.reset();
}

Evaluated with Chrome root at commit: 84065d9121f6e48f67755f0ae963cc09617e5c85


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