CVE-2026-15904
Overview
Background
- `ui::WaylandWindowManager`
- Ozone/Wayland host-side registry that tracks all live
WaylandWindowobjects and hands them to callers viaGetAllWindows(). - `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, andOnFontScaleFactorChangedcall into the window’s delegate, which may synchronouslyRequestState()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
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.Attack Path
- 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.
- Enter vulnerable loop
The call site walks the raw
GetAllWindows()vector and calls a delegate-reaching method on a window. - Delegate destroys windows
The delegate synchronously calls
RequestState()/closes the widget, freeing thatWaylandWindowand its descendant windows. - Dereference freed memory
The loop continues onto the freed pointer or a stale later entry, dereferencing a freed
WaylandWindowin the browser process. - Exploit UAF With heap grooming, the freed slot is reclaimed, turning the dangling access into a controlled read/write primitive.
Impact Assessment
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
| Function | Change | Notes |
|---|---|---|
ifui/ozone/platform/wayland/host/wayland_buffer_manager_host.cc |
modified | |
ifui/ozone/platform/wayland/host/wayland_output_manager.cc |
modified | |
GetAllWindowsAsWeakPtrui/ozone/platform/wayland/host/wayland_window_manager.cc |
modified | |
forui/ozone/platform/wayland/host/wayland_window_manager.cc |
modified | |
ifui/ozone/platform/wayland/host/wayland_window_manager.cc |
modified | |
forui/ozone/platform/wayland/wayland_buffer_manager_unittest.cc |
modified |
Files Changed
ui/ozone/platform/wayland/host/wayland_buffer_manager_host.ccui/ozone/platform/wayland/host/wayland_output_manager.ccui/ozone/platform/wayland/host/wayland_window_manager.ccui/ozone/platform/wayland/host/wayland_window_manager.hui/ozone/platform/wayland/host/wayland_window_manager_unittest.ccui/ozone/platform/wayland/wayland_buffer_manager_unittest.cc
Audit Directions
- Raw-pointer snapshot + re-entrant callbackFlag any loop over a materialized
std::vector<T*>whose body calls a method that can synchronously destroyTor its relatives; migrate such call sites tobase::WeakPtrwith per-iteration liveness checks. - Delegate/observer notification during iterationAudit Ozone/Wayland (and other host) code paths where
OnStatusUpdate-style delegate calls can triggerRequestState()/widget close, and confirm transitive descendant destruction is tolerated mid-loop. - Descendant destruction invariantsVerify that iteration over window/child collections accounts for a single element’s teardown removing multiple entries, not just the current one.
Patch
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);
Regression Test / PoC
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{}));
Original Bug Report
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.ccui/ozone/platform/wayland/host/wayland_window.ccui/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:
Window_N->OnChannelDestroyed()callsframe_manager_->RecordFrame(...)with a placeholder frame.- This triggers
MaybeProcessPendingFrame(), entering the immediate swap-ack path. - The window calls
OnSequencePoint()->MaybeApplyLatestStateRequest()->delegate()->OnStateUpdate(). WindowTreeHostPlatform::OnStateUpdate()observes a bounds/scale change and firesOnBoundsChanged().- 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(). - This calls
~WaylandWindow()on Window N+1, erasing it fromWaylandWindowManager::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.
- Launch Chromium on Linux/Wayland.
- Open a main application window (Window N) and have a webpage initiate a tab capture (e.g., via
getDisplayMedia()). - 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.
- Minimize or occlude Window N so it transitions to a suspended state (setting
is_suspended_ = true). - Force the GPU process to disconnect (e.g., by terminating the GPU process or simulating a GPU crash/pipe closure).
- The browser process executes
WaylandBufferManagerHost::OnChannelDestroyed(). - Iteration on Window N causes a synchronous state and bounds update callout, triggering observers that synchronously close and destroy Window N+1.
- The loop advances to the next element, attempting to dereference the now-freed
Window_N+1pointer, 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.