CVE-2026-15765
Overview
Background
- `WaylandWindow`
- Ozone host-side object (
ui/ozone/platform/wayland/host/) backing a platform window; subclasses includeWaylandBubble,WaylandPopup, andWaylandToplevelWindow. - `PlatformWindowDelegate`
- the views-layer delegate a
WaylandWindowcalls into (OnSequencePoint,OnPaintAsActiveChanged,OnDamageRect,UpdateWindowScale,Show) during configure, scale, and state handling. - Re-entrant destruction
- the delegate can synchronously close the owning widget inside such a callout, deleting the
WaylandWindow(this) before control returns to the caller. - `AsWeakPtr()` / `weak_this`
- a
WeakPtrtothiscaptured before a callout so the method can detect its own destruction and bail out.
Root Cause Analysis
Several WaylandWindow subclass methods invoke PlatformWindowDelegate callbacks (directly or indirectly, e.g. via UpdateWindowScale) in the middle of configure, scale-change, and frame-processing logic. These callouts can re-enter the views layer, which may synchronously close the widget and destroy the WaylandWindow, freeing this. The methods then continued executing, dereferencing member state such as subsurface_, root_surface(), pending_frames_, and applied_state() on the freed object. Because the lifetime of this is not owned by these methods and is controllable through window/popup lifecycle events, this yields a use-after-free in the browser process.
The fix captures auto weak_this = AsWeakPtr(); before each dangerous callout and adds if (!weak_this) return; immediately after, aborting any further member access when the window was destroyed.
this, so any member access after it is unsafe. The vulnerability class is not a single bad line but a pattern: every state-changing delegate call followed by a member deref needs a WeakPtr liveness guard.Attack Path
- Trigger lifecycle event
An attacker-influenced compositor/window event drives a configure, scale change, or frame sequence point on a
WaylandWindowsubclass. - Enter delegate callout
The method calls into
PlatformWindowDelegate(OnSequencePoint,OnPaintAsActiveChanged,Show, orUpdateWindowScale). - Synchronous widget close
The views layer closes the widget inside the callout, destroying the
WaylandWindowand freeingthis. - Post-callout deref
Control returns to the method, which accesses freed members (
subsurface_,root_surface(),pending_frames_,applied_state()), reading or writing freed memory.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifui/ozone/platform/wayland/host/wayland_bubble.cc |
modified | |
ifui/ozone/platform/wayland/host/wayland_frame_manager.cc |
modified | |
ifui/ozone/platform/wayland/host/wayland_popup.cc |
modified | |
ifui/ozone/platform/wayland/host/wayland_toplevel_window.cc |
modified | |
ifui/ozone/platform/wayland/host/wayland_window.cc |
modified | |
TEST_Pui/ozone/platform/wayland/host/wayland_window_unittest.cc |
modified |
Files Changed
ui/ozone/platform/wayland/host/wayland_bubble.ccui/ozone/platform/wayland/host/wayland_frame_manager.ccui/ozone/platform/wayland/host/wayland_popup.ccui/ozone/platform/wayland/host/wayland_toplevel_window.ccui/ozone/platform/wayland/host/wayland_window.ccui/ozone/platform/wayland/host/wayland_window_unittest.cc
Audit Directions
- Callout-then-derefAudit every
WaylandWindowand Ozone platform method that calls aPlatformWindowDelegatemethod and then reads any member ofthis; require aWeakPtrguard or a documented no-reentrancy invariant. - Indirect delegate callsTrace helpers like
UpdateWindowScaleand configure handlers that call the delegate transitively; the callout may be several frames deep and easy to miss. - `this`-owning lifecyclesFlag methods whose object lifetime is controlled by external window/popup events, where synchronous widget close can free
thismid-method.
Patch
From 87abcfa5b16b689587f94f40e6e7de88db11313a Mon Sep 17 00:00:00 2001
From: Kramer Ge <fangzhoug@chromium.org>
Date: Mon, 06 Jul 2026 17:03:17 -0700
Subject: [PATCH] [Ozone/Wayland]Guard all delegate() calls with weak_this
delegate() calls may implicitly destroy WaylandWindow. Guard direct and
indirect calls to delegate() state changes with weak_this if such calls
are followed by potential deref of WaylandWindow members.
Fixed: 518007484
Change-Id: I9fe00496ee807f3416c9a9a2dedeef352e4a63e6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8043107
Reviewed-by: Thomas Anderson <thomasanderson@chromium.org>
Commit-Queue: Kramer Ge <fangzhoug@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1657588}
---
diff --git a/ui/ozone/platform/wayland/host/wayland_bubble.cc b/ui/ozone/platform/wayland/host/wayland_bubble.cc
index ef876a7..99379bf1 100644
--- a/ui/ozone/platform/wayland/host/wayland_bubble.cc
+++ b/ui/ozone/platform/wayland/host/wayland_bubble.cc
@@ -34,8 +34,15 @@
return;
}
+ auto weak_this = AsWeakPtr();
UpdateWindowScale(false);
+ if (!weak_this) {
+ return;
+ }
AddToParentAsSubsurface();
+ if (!weak_this) {
+ return;
+ }
WaylandWindow::Show(inactive);
}
@@ -96,7 +103,11 @@
}
void WaylandBubble::UpdateWindowScale(bool update_bounds) {
+ auto weak_this = AsWeakPtr();
WaylandWindow::UpdateWindowScale(update_bounds);
+ if (!weak_this) {
+ return;
+ }
if (subsurface_) {
SetSubsurfacePosition();
}
@@ -131,7 +142,11 @@
CHECK(parent_window());
// We need to make sure that window scale matches the parent window.
+ auto weak_this = AsWeakPtr();
UpdateWindowScale(true);
+ if (!weak_this) {
+ return;
+ }
subsurface_ =
root_surface()->CreateSubsurface(parent_window()->root_surface());
diff --git a/ui/ozone/platform/wayland/host/wayland_frame_manager.cc b/ui/ozone/platform/wayland/host/wayland_frame_manager.cc
index dd3fb2e5c94..e84a0c0 100644
--- a/ui/ozone/platform/wayland/host/wayland_frame_manager.cc
+++ b/ui/ozone/platform/wayland/host/wayland_frame_manager.cc
@@ -199,7 +199,11 @@
if (!ValidateRect(config.bounds_rect)) {
fatal_error_message_ = kBoundsRectNanOrInf;
} else {
+ auto weak_this = weak_factory_.GetWeakPtr();
window_->OnSequencePoint(frame->seq);
+ if (!weak_this) {
+ return;
+ }
// During a tab dragging session, OnSequencePoint() can implicitly invoke
// Hide(). |pending_frames_| will be cleared and we should return
// directly.
diff --git a/ui/ozone/platform/wayland/host/wayland_popup.cc b/ui/ozone/platform/wayland/host/wayland_popup.cc
index 8b5b03e..75c74757 100644
--- a/ui/ozone/platform/wayland/host/wayland_popup.cc
+++ b/ui/ozone/platform/wayland/host/wayland_popup.cc
@@ -57,7 +57,11 @@
parent_window()->applied_state().window_scale) {
// If scale changed while this was hidden (when WaylandPopup hides, parent
// window's child is reset), update buffer scale accordingly.
+ auto weak_this = AsWeakPtr();
UpdateWindowScale(true);
+ if (!weak_this) {
+ return false;
+ }
}
auto bounds_dip =
@@ -123,13 +127,21 @@
// Map parent window as WaylandPopup cannot become a visible child of a
// window that is not mapped.
DCHECK(parent_window());
- if (!parent_window()->IsVisible())
+ auto weak_this = AsWeakPtr();
+ if (!parent_window()->IsVisible()) {
parent_window()->Show(false);
+ }
+ if (!weak_this) {
+ return;
+ }
- if (!CreateShellPopup()) {
+ if (!CreateShellPopup() && weak_this) {
Close();
return;
}
+ if (!weak_this) {
+ return;
+ }
connection()->Flush();
WaylandWindow::Show(inactive);
@@ -250,7 +262,11 @@
void WaylandPopup::HandleSurfaceConfigure(uint32_t serial) {
if (schedule_redraw_) {
+ auto weak_this = AsWeakPtr();
delegate()->OnDamageRect(gfx::Rect{applied_state().size_px});
+ if (!weak_this) {
+ return;
+ }
schedule_redraw_ = false;
}
ProcessPendingConfigureState(serial);
diff --git a/ui/ozone/platform/wayland/host/wayland_toplevel_window.cc b/ui/ozone/platform/wayland/host/wayland_toplevel_window.cc
index 4a5b2255..46afd1b 100644
--- a/ui/ozone/platform/wayland/host/wayland_toplevel_window.cc
+++ b/ui/ozone/platform/wayland/host/wayland_toplevel_window.cc
@@ -547,7 +547,11 @@
// xdg_toplevel::activated is a paint-only hint, separate from input
// activation which is driven by keyboard focus in UpdateActivationState.
if (prev_xdg_active != is_xdg_active_) {
+ auto weak_this = AsWeakPtr();
delegate()->OnPaintAsActiveChanged(is_xdg_active_);
+ if (!weak_this) {
+ return;
+ }
}
bool prev_suspended = is_suspended_;
is_suspended_ = window_states.is_suspended;
@@ -562,7 +566,11 @@
if (window_states.tiled_edges != applied_state().tiled_edges) {
// This configure changes the decoration insets. We should adjust the
// bounds appropriately.
+ auto weak_this = AsWeakPtr();
delegate()->OnWindowTiledStateChanged(window_states.tiled_edges);
+ if (!weak_this) {
+ return;
+ }
}
pending_configure_state_.tiled_edges = window_states.tiled_edges;
@@ -905,7 +913,11 @@
auto previous_state = applied_state().window_state;
ForceApplyWindowStateDoNotUse(window_state);
+ auto weak_this = AsWeakPtr();
delegate()->OnWindowStateChanged(previous_state, window_state);
+ if (!weak_this) {
+ return;
+ }
connection()->Flush();
}
diff --git a/ui/ozone/platform/wayland/host/wayland_window.cc b/ui/ozone/platform/wayland/host/wayland_window.cc
index dcd3ff3..6ac7b05 100644
--- a/ui/ozone/platform/wayland/host/wayland_window.cc
+++ b/ui/ozone/platform/wayland/host/wayland_window.cc
@@ -170,7 +170,11 @@
const auto window_scale = connection_->UsePerSurfaceScaling()
? GetPreferredScaleFactor()
: GetScaleFactorFromEnteredOutputs();
+ auto weak_this = AsWeakPtr();
SetWindowScale(window_scale.value_or(1.0f));
+ if (!weak_this) {
+ return;
+ }
// Propagate update to the popups.
if (child_popup_) {
diff --git a/ui/ozone/platform/wayland/host/wayland_window_unittest.cc b/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
index ecebab0..389846e 100644
--- a/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
+++ b/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
@@ -5841,6 +5841,102 @@
window_->RemoveBubble(wayland_bubble->AsWaylandBubble());
}
+TEST_P(WaylandWindowTest, WaylandBubbleUpdateWindowScaleUaf) {
+ MockWaylandPlatformWindowDelegate bubble_delegate(connection_.get());
+ gfx::Rect bubble_bounds(10, 10, 50, 50);
+ auto wayland_bubble =
+ CreateWaylandWindowWithParams(PlatformWindowType::kBubble, bubble_bounds,
+ &bubble_delegate, window_->GetWidget());
+ ASSERT_TRUE(wayland_bubble);
Regression Test / PoC
diff --git a/ui/ozone/platform/wayland/host/wayland_window_unittest.cc b/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
index ecebab0..389846e 100644
--- a/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
+++ b/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
@@ -5841,6 +5841,102 @@
window_->RemoveBubble(wayland_bubble->AsWaylandBubble());
}
+TEST_P(WaylandWindowTest, WaylandBubbleUpdateWindowScaleUaf) {
+ MockWaylandPlatformWindowDelegate bubble_delegate(connection_.get());
+ gfx::Rect bubble_bounds(10, 10, 50, 50);
+ auto wayland_bubble =
+ CreateWaylandWindowWithParams(PlatformWindowType::kBubble, bubble_bounds,
+ &bubble_delegate, window_->GetWidget());
+ ASSERT_TRUE(wayland_bubble);
+
+ bubble_delegate.set_on_state_update_callback(
+ base::BindLambdaForTesting([&]() {
+ wayland_bubble.reset();
+ return true;
+ }));
+
+ // This should not crash.
+ wayland_bubble->UpdateWindowScale(true);
+}
+
+TEST_P(WaylandWindowTest, WaylandBubbleShowUaf) {
+ MockWaylandPlatformWindowDelegate bubble_delegate(connection_.get());
+ PlatformWindowInitProperties properties;
+ properties.bounds = gfx::Rect(10, 10, 50, 50);
+ properties.type = PlatformWindowType::kBubble;
+ properties.parent_widget = window_->GetWidget();
+ auto wayland_bubble = bubble_delegate.CreateWaylandWindow(
+ connection_.get(), std::move(properties));
+ ASSERT_TRUE(wayland_bubble);
+
+ bubble_delegate.set_on_state_update_callback(
+ base::BindLambdaForTesting([&]() {
+ wayland_bubble.reset();
+ return true;
+ }));
+
+ // This should not crash.
+ wayland_bubble->Show(false);
+}
+
+TEST_P(WaylandWindowTest, WaylandPopupShowUaf) {
+ MockWaylandPlatformWindowDelegate popup_delegate(connection_.get());
+ PlatformWindowInitProperties properties;
+ properties.bounds = gfx::Rect(10, 10, 50, 50);
+ properties.type = PlatformWindowType::kPopup;
+ properties.parent_widget = window_->GetWidget();
+ auto wayland_popup = popup_delegate.CreateWaylandWindow(
+ connection_.get(), std::move(properties));
+ ASSERT_TRUE(wayland_popup);
+
+ popup_delegate.set_on_state_update_callback(base::BindLambdaForTesting([&]() {
+ wayland_popup.reset();
+ return true;
+ }));
+
+ // This should not crash.
+ wayland_popup->Show(false);
+}
+
+TEST_P(WaylandWindowTest, WaylandToplevelWindowOnPaintAsActiveChangedUaf) {
+ testing::NiceMock<MockWaylandPlatformWindowDelegate> toplevel_delegate(
+ connection_.get());
+ PlatformWindowInitProperties properties;
+ properties.bounds = gfx::Rect(10, 10, 100, 100);
+ properties.type = PlatformWindowType::kWindow;
+ auto toplevel_window = toplevel_delegate.CreateWaylandWindow(
+ connection_.get(), std::move(properties));
+ ASSERT_TRUE(toplevel_window);
+
+ EXPECT_CALL(toplevel_delegate, OnPaintAsActiveChanged(::testing::_))
+ .WillOnce(
+ ::testing::InvokeWithoutArgs([&]() { toplevel_window.reset(); }));
+
+ WaylandWindow::WindowStates window_states;
+ window_states.is_activated = true;
+ // This should not crash.
+ toplevel_window->HandleToplevelConfigure(100, 100, window_states);
+}
+
+TEST_P(WaylandWindowTest, WaylandToplevelWindowTriggerStateChangesUaf) {
+ testing::NiceMock<MockWaylandPlatformWindowDelegate> toplevel_delegate(
+ connection_.get());
+ PlatformWindowInitProperties properties;
+ properties.bounds = gfx::Rect(10, 10, 100, 100);
+ properties.type = PlatformWindowType::kWindow;
+ auto toplevel_window = toplevel_delegate.CreateWaylandWindow(
+ connection_.get(), std::move(properties));
+ ASSERT_TRUE(toplevel_window);
+
+ EXPECT_CALL(toplevel_delegate,
+ OnWindowStateChanged(::testing::_, ::testing::_))
+ .WillOnce(
+ ::testing::InvokeWithoutArgs([&]() { toplevel_window.reset(); }));
+
+ // This should not crash.
+ toplevel_window->Maximize();
+}
+
INSTANTIATE_TEST_SUITE_P(XdgVersionStableTest,
WaylandWindowTest,
Values(wl::ServerConfig{}));
Original Bug Report
Potential Use-After-Free in WaylandBubble due to synchronous widget destruction in OnStateUpdate
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 WaylandBubble on Linux Wayland due to synchronous widget destruction during bounds or scale updates. When the calling window state-request pipeline synchronously destroys the widget, the call stack unwinds back to WaylandBubble override methods, which continue to access members of the freed object. This issue resides in the browser process, affecting systems running Chrome on Linux under Wayland.
Affected files:
ui/ozone/platform/wayland/host/wayland_bubble.ccui/ozone/platform/wayland/host/wayland_bubble.h
Estimated timestamp from git blame: 2024-03-30
Root Cause Analysis
Inside ui/ozone/platform/wayland/host/wayland_bubble.cc, multiple overrides of WaylandBubble call into the base WaylandWindow state-request machinery. These base class methods can synchronously apply the requested state and notify delegates, which can lead to the synchronous destruction of the widget (and the calling WaylandBubble instance itself). When control returns from the base class to the subclass overrides, execution continues on the freed this pointer with no liveness check, despite the class implementing a WeakPtrFactory.
For example, in WaylandBubble::SetBoundsInDIP:
void WaylandBubble::SetBoundsInDIP(const gfx::Rect& bounds_dip) {
auto old_bounds_dip = GetBoundsInDIP();
WaylandWindow::SetBoundsInDIP(bounds_dip); // May synchronously free |this|
if (subsurface_ && old_bounds_dip != bounds_dip) { // Potential UAF read of this->subsurface_
SetSubsurfacePosition(); // Potential UAF dereference
}
}
Similarly, WaylandBubble::UpdateWindowScale is vulnerable:
void WaylandBubble::UpdateWindowScale(bool update_bounds) {
WaylandWindow::UpdateWindowScale(update_bounds); // May synchronously free |this|
if (subsurface_) { // Potential UAF read of this->subsurface_
SetSubsurfacePosition(); // Potential UAF dereference
}
}
Potential Trigger Path
An attacker controlling web content could potentially trigger this by inducing a state update that leads to synchronous window destruction through the following sequence:
- Web content interacts with a UI component that creates a bubble window (such as an Autofill popup or custom menu widget) implemented as a
WaylandBubble. - The UI layout engine modifies the bounds or preferred scale of the widget, invoking
WaylandBubble::SetBoundsInDIPorWaylandBubble::UpdateWindowScale. - The override delegates to the base class implementation, e.g.,
WaylandWindow::SetBoundsInDIP->RequestStateFromClient->MaybeApplyLatestStateRequest. - Inside
MaybeApplyLatestStateRequest, the code synchronously callsdelegate()->OnStateUpdate(). - The delegate notifies window tree and widget observers of the bounds or scale change. If an observer synchronously tears down or closes the widget (for instance, if the layout change places the bubble out-of-bounds or invalidates its parent window), the
WaylandBubbleobject is synchronously deleted. - While the base class
WaylandWindow::MaybeApplyLatestStateRequestcorrectly guards against this destruction via aWeakPtrcheck (AsWeakPtr()), theWaylandBubbleoverrides do not perform any check when the stack unwinds back to them, resulting in a Use-After-Free when accessingsubsurface_or callingSetSubsurfacePosition().
Note: Our tooling does not currently have the capability to run a live proof-of-concept exploit, so these steps represent a potential execution flow based on static analysis of the source code.
Suggested Fix
To prevent this vulnerability, introduce weak pointer checks immediately after calls to base class methods that handle state changes, ensuring that execution returns early if the window has been destroyed:
void WaylandBubble::SetBoundsInDIP(const gfx::Rect& bounds_dip) {
auto old_bounds_dip = GetBoundsInDIP();
auto weak_this = AsWeakPtr();
WaylandWindow::SetBoundsInDIP(bounds_dip);
if (!weak_this) {
return;
}
if (subsurface_ && old_bounds_dip != bounds_dip) {
SetSubsurfacePosition();
}
}
void WaylandBubble::UpdateWindowScale(bool update_bounds) {
auto weak_this = AsWeakPtr();
WaylandWindow::UpdateWindowScale(update_bounds);
if (!weak_this) {
return;
}
if (subsurface_) {
SetSubsurfacePosition();
}
}
Evaluated with Chrome root at commit: fb72408a8493c46bc75fae1c70d03daec96b3040
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.