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/Wayland
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker532925350
Fix commitc9c9c645ab28 (chromium/src) +109/-7
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-07-09 (per vendor advisory)
Fix landed2026-07-13 (public git)
Disclosed2026-07-16
Patch gap3 days — fix public in source before the advisory

Background

`ui::WaylandWindowManager`
Ozone/Wayland host-side registry that tracks all live WaylandWindow objects and hands them to callers via GetAllWindows().
`GetAllWindows()`
returns a materialized std::vector<WaylandWindow*> of raw pointers, a snapshot that is not updated if windows are destroyed mid-iteration.
Delegate re-entrancy
window methods such as OnChannelDestroyed, RemoveEnteredOutput, and OnFontScaleFactorChanged call into the window’s delegate, which may synchronously RequestState() or close the widget and destroy the window plus its descendants.
UAF (use-after-free)
dereferencing a WaylandWindow* after its backing object has been freed by a re-entrant delegate call, occurring here in the browser process.

Root Cause Analysis

Several Ozone/Wayland call sites iterated the raw WaylandWindow* vector returned by GetAllWindowsAsWeakPtr’s predecessor GetAllWindows() and invoked methods that reach the window’s delegate. Because OnChannelDestroyed, RemoveEnteredOutput, and OnFontScaleFactorChanged can synchronously re-enter the delegate, the delegate could destroy the current window and its descendant windows during the loop body. That destruction frees WaylandWindow objects whose raw pointers still sit in the already-materialized vector, including both the pointer being processed and later entries. Continuing to iterate and dereference those stale pointers is a classic snapshot-invalidation use-after-free. The vector is never revalidated against the manager’s live set, so no liveness check guards the dereference.
Key insight
A container snapshot of raw pointers is unsafe whenever the loop body can synchronously destroy elements of that container. The destruction here is transitive — a single delegate call can free the window and all its descendants, invalidating multiple not-yet-visited entries at once.

Attack Path

  1. Trigger re-entrant event Compositor or channel activity drives a status update (channel teardown, output removal/metrics change, or font-scale change) that begins iterating windows.
  2. Enter vulnerable loop The call site walks the raw GetAllWindows() vector and calls a delegate-reaching method on a window.
  3. Delegate destroys windows The delegate synchronously calls RequestState()/closes the widget, freeing that WaylandWindow and its descendant windows.
  4. Dereference freed memory The loop continues onto the freed pointer or a stale later entry, dereferencing a freed WaylandWindow in the browser process.
  5. Exploit UAF With heap grooming, the freed slot is reclaimed, turning the dangling access into a controlled read/write primitive.

Impact Assessment

Use-after-free of a WaylandWindow in the privileged browser process, reachable through routine compositor/output/channel events on Linux Ozone/Wayland builds. Given browser-process context, a successful exploit can yield memory corruption and potential code execution, escaping renderer-sandbox limits. Reliability depends on winning the reclaim of the freed allocation via heap grooming.

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

Audit Directions

  • Raw-pointer snapshot + re-entrant callback
    Flag any loop over a materialized std::vector<T*> whose body calls a method that can synchronously destroy T or its relatives; migrate such call sites to base::WeakPtr with per-iteration liveness checks.
  • Delegate/observer notification during iteration
    Audit Ozone/Wayland (and other host) code paths where OnStatusUpdate-style delegate calls can trigger RequestState()/widget close, and confirm transitive descendant destruction is tolerated mid-loop.
  • Descendant destruction invariants
    Verify that iteration over window/child collections accounts for a single element’s teardown removing multiple entries, not just the current one.
From c9c9c645ab28e5a002c926a456f10a36ac36029c Mon Sep 17 00:00:00 2001
From: Kramer Ge <fangzhoug@chromium.org>
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 <thomasanderson@chromium.org>
Commit-Queue: Kramer Ge <fangzhoug@chromium.org>
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 vm...@google.com

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