CVE-2026-10902
Overview
Background
- `WaylandToplevelWindow`
- the Ozone/Wayland platform window (
ui/ozone/platform/wayland/host/wayland_toplevel_window.cc) that runs in the browser process and handlesxdg_toplevelprotocol events. - `HandleToplevelConfigure`
- the entry point for an
xdg_toplevel.configureevent, which delegates toHandleToplevelConfigureWithOriginand then does follow-up work such asUpdateSessionStateIfNeeded. - `OnActivationChanged` re-entrancy
HandleToplevelConfigureWithOrigincallsUpdateActivationState, which notifies the delegate viaOnActivationChanged; the views/widget layer can synchronously close the widget (e.g.Widget::CloseNow→DesktopWindowTreeHostPlatform::OnClosed→SetPlatformWindow(nullptr)), destroying the platform window.- `base::WeakPtrFactory` / `weak_ptr_factory_`
- supplies a
WeakPtrthat nulls on destruction, used to detect synchronous self-destruction across the delegate call.
Root Cause Analysis
When a Wayland compositor sends an xdg_toplevel.configure, HandleToplevelConfigure calls HandleToplevelConfigureWithOrigin, which in turn calls UpdateActivationState → delegate()->OnActivationChanged(...). As the commit message and regression test document, the views/widget layer can synchronously close the widget inside OnActivationChanged, which frees the WaylandToplevelWindow while the configure handler is still on the stack.
Pre-patch, control then returned to HandleToplevelConfigure, which called UpdateSessionStateIfNeeded() on the freed this, and to the code after UpdateActivationState, which read is_suspended_ and called frame_manager() on freed memory — both use-after-frees in the browser process.
The patch grabs auto alive = weak_ptr_factory_.GetWeakPtr() before each re-entrant call and returns early (if (!alive) return;) if this was destroyed, so no post-callback member access runs on freed memory.
The fix is correct because the weak pointer is invalidated at destruction time, gating all subsequent work.
this after a delegate activation callback that can synchronously destroy the window; the fix inserts WeakPtr liveness checks immediately after HandleToplevelConfigureWithOrigin and after UpdateActivationState, returning early when this is gone.Attack Path
- Send a configure event
A Wayland compositor (or a compromised/malicious one) sends an
xdg_toplevel.configurewith the activation bit cleared, enteringHandleToplevelConfigure. - Propagate to activation change
HandleToplevelConfigureWithOrigin→UpdateActivationState→delegate()->OnActivationChanged(false)is invoked synchronously. - Synchronous widget close
The views layer closes the widget inside
OnActivationChanged(Widget::CloseNow→OnClosed→SetPlatformWindow(nullptr)), freeing theWaylandToplevelWindow. - Use-after-free on return
Pre-patch, control returns and calls
UpdateSessionStateIfNeeded()(and readsis_suspended_/frame_manager()) on the freed object, corrupting browser-process memory (further primitive inferred).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifui/ozone/platform/wayland/host/wayland_toplevel_window.cc |
modified | |
TEST_Pui/ozone/platform/wayland/host/wayland_window_unittest.cc |
modified |
Files Changed
ui/ozone/platform/wayland/host/wayland_toplevel_window.ccui/ozone/platform/wayland/host/wayland_window_unittest.cc
Audit Directions
- Protocol handlers that notify delegatesAudit Ozone/Wayland event handlers (
xdg_*configure/close/activation paths) that calldelegate()methods and then keep usingthis, since the views layer can close the widget synchronously. - Activation/focus callbacksFlag any
OnActivationChanged/focus/visibility notification whose callers continue touching members afterward without aWeakPtrre-check. - Re-entrant destruction across helper callsLook for sequences where one method calls a helper that transitively notifies a delegate, then does cleanup work on return without confirming
thissurvived.
Patch
From dadcd96cf1f88f544995631521471cf64b90a7ff Mon Sep 17 00:00:00 2001 From: Tom Anderson <[email protected]> Date: Wed, 27 May 2026 17:37:03 -0700 Subject: [PATCH] [Ozone/Wayland] Fix UAF in HandleToplevelConfigure via OnActivationChanged During HandleToplevelConfigure, calling HandleToplevelConfigureWithOrigin can trigger OnActivationChanged on the delegate. Since views/widget layer handling can synchronously destroy the widget and thereby the platform window itself, calling subsequent functions on `this` results in a Use-After-Free (UAF). This CL adds WeakPtr guards to both HandleToplevelConfigure and HandleToplevelConfigureWithOrigin to prevent referencing the object after it has been synchronously closed. Fixed: 517046249 Change-Id: Ic7d3ac624033af9fc07d7f73a98e4307e803492d Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7880866 Commit-Queue: Thomas Anderson <[email protected]> Commit-Queue: Thomas Lukaszewicz <[email protected]> Auto-Submit: Thomas Anderson <[email protected]> Reviewed-by: Thomas Lukaszewicz <[email protected]> Cr-Commit-Position: refs/heads/main@{#1637352} --- diff --git a/ui/ozone/platform/wayland/host/wayland_toplevel_window.cc b/ui/ozone/platform/wayland/host/wayland_toplevel_window.cc index 6c3e6a341..fe83d31 100644 --- a/ui/ozone/platform/wayland/host/wayland_toplevel_window.cc +++ b/ui/ozone/platform/wayland/host/wayland_toplevel_window.cc @@ -465,7 +465,15 @@ int32_t width_dip, int32_t height_dip, const WindowStates& window_states) { + // HandleToplevelConfigureWithOrigin() calls into the delegate + // (OnActivationChanged et al.), which the views layer documents may + // synchronously close the widget and destroy this platform window. See + // DesktopWindowTreeHostPlatform::OnActivationChanged(). + auto alive = weak_ptr_factory_.GetWeakPtr(); HandleToplevelConfigureWithOrigin(0, 0, width_dip, height_dip, window_states); + if (!alive) { + return; + } UpdateSessionStateIfNeeded(); } @@ -566,7 +574,11 @@ SetRestoredBoundsInDIP(GetBoundsInDIP()); } + auto alive = weak_ptr_factory_.GetWeakPtr(); UpdateActivationState(); + if (!alive) { + return; + } if (prev_suspended != is_suspended_) { frame_manager()->OnWindowSuspensionChanged(); } diff --git a/ui/ozone/platform/wayland/host/wayland_window_unittest.cc b/ui/ozone/platform/wayland/host/wayland_window_unittest.cc index d0afdc8..7aea341 100644 --- a/ui/ozone/platform/wayland/host/wayland_window_unittest.cc +++ b/ui/ozone/platform/wayland/host/wayland_window_unittest.cc @@ -5428,6 +5428,45 @@ EXPECT_EQ(window_->applied_state(), previous_state); } +// Regression POC: WaylandToplevelWindow::HandleToplevelConfigure() continues to +// use `this` after delegate()->OnActivationChanged() synchronously destroys the +// platform window. This mirrors the production path documented at +// DesktopWindowTreeHostPlatform::OnActivationChanged where +// HandleActivationChanged() can synchronously close the widget, which in turn +// calls SetPlatformWindow(nullptr) and frees the WaylandToplevelWindow while +// the xdg_toplevel.configure handler is still on the stack. +TEST_P(WaylandWindowTest, HandleToplevelConfigureSyncCloseOnDeactivate) { + // After SetUp(), |window_| has already received an activated configure, so + // is_xdg_active_ == is_active_ == true. + ASSERT_TRUE(window_); + WaylandWindow* raw_window = window_.get(); + + // Simulate a delegate that destroys the platform window inside + // OnActivationChanged(false) — exactly what happens in production when a + // WidgetObserver calls Widget::CloseNow() on deactivation, leading to + // DesktopWindowTreeHostPlatform::OnClosed -> SetPlatformWindow(nullptr). + EXPECT_CALL(delegate_, OnActivationChanged(Eq(false))) + .WillOnce(InvokeWithoutArgs([this]() { window_.reset(); })); + + // Don't try to talk to the server after the window has been torn down + // mid-dispatch. + DisableSyncOnTearDown(); + + // Drive the standard xdg_toplevel.configure entry point with the activated + // bit cleared. This calls HandleToplevelConfigureWithOrigin() -> + // UpdateActivationState() -> delegate()->OnActivationChanged(false), which + // (via the mock above) frees `this`. Control then returns to + // HandleToplevelConfigure:469 which calls UpdateSessionStateIfNeeded() on + // the freed object. + WaylandWindow::WindowStates deactivated_states; + deactivated_states.is_activated = false; + raw_window->HandleToplevelConfigure(800, 600, deactivated_states); + + // If we got here without ASAN reporting a heap-use-after-free, the bug is + // fixed. + EXPECT_FALSE(window_); +} + INSTANTIATE_TEST_SUITE_P(XdgVersionStableTest, WaylandWindowTest, Values(wl::ServerConfig{}));
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 d0afdc8..7aea341 100644
--- a/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
+++ b/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
@@ -5428,6 +5428,45 @@
EXPECT_EQ(window_->applied_state(), previous_state);
}
+// Regression POC: WaylandToplevelWindow::HandleToplevelConfigure() continues to
+// use `this` after delegate()->OnActivationChanged() synchronously destroys the
+// platform window. This mirrors the production path documented at
+// DesktopWindowTreeHostPlatform::OnActivationChanged where
+// HandleActivationChanged() can synchronously close the widget, which in turn
+// calls SetPlatformWindow(nullptr) and frees the WaylandToplevelWindow while
+// the xdg_toplevel.configure handler is still on the stack.
+TEST_P(WaylandWindowTest, HandleToplevelConfigureSyncCloseOnDeactivate) {
+ // After SetUp(), |window_| has already received an activated configure, so
+ // is_xdg_active_ == is_active_ == true.
+ ASSERT_TRUE(window_);
+ WaylandWindow* raw_window = window_.get();
+
+ // Simulate a delegate that destroys the platform window inside
+ // OnActivationChanged(false) — exactly what happens in production when a
+ // WidgetObserver calls Widget::CloseNow() on deactivation, leading to
+ // DesktopWindowTreeHostPlatform::OnClosed -> SetPlatformWindow(nullptr).
+ EXPECT_CALL(delegate_, OnActivationChanged(Eq(false)))
+ .WillOnce(InvokeWithoutArgs([this]() { window_.reset(); }));
+
+ // Don't try to talk to the server after the window has been torn down
+ // mid-dispatch.
+ DisableSyncOnTearDown();
+
+ // Drive the standard xdg_toplevel.configure entry point with the activated
+ // bit cleared. This calls HandleToplevelConfigureWithOrigin() ->
+ // UpdateActivationState() -> delegate()->OnActivationChanged(false), which
+ // (via the mock above) frees `this`. Control then returns to
+ // HandleToplevelConfigure:469 which calls UpdateSessionStateIfNeeded() on
+ // the freed object.
+ WaylandWindow::WindowStates deactivated_states;
+ deactivated_states.is_activated = false;
+ raw_window->HandleToplevelConfigure(800, 600, deactivated_states);
+
+ // If we got here without ASAN reporting a heap-use-after-free, the bug is
+ // fixed.
+ EXPECT_FALSE(window_);
+}
+
INSTANTIATE_TEST_SUITE_P(XdgVersionStableTest,
WaylandWindowTest,
Values(wl::ServerConfig{}));
Original Bug Report
Potential UAF in WaylandToplevelWindow::HandleToplevelConfigure via OnActivationChanged
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 WaylandToplevelWindow::HandleToplevelConfigure due to synchronous destruction of the window object during activation state callbacks. When the delegate callback synchronously closes the widget, this is deleted, resulting in a UAF when the function subsequently calls UpdateSessionStateIfNeeded(). This potential issue affects the browser process on Linux/Wayland and is not protected by MiraclePtr.
Affected files:
ui/ozone/platform/wayland/host/wayland_toplevel_window.cc
Estimated timestamp from git blame: 2025-03-27
Summary
There is a potential Use-After-Free (UAF) vulnerability in WaylandToplevelWindow::HandleToplevelConfigure inside the desktop Linux/Wayland browser process. When handling window configuration events, the platform window delegate can synchronously close and destroy the widget and its associated platform window. This deletes the WaylandToplevelWindow object (this) while the execution is still inside one of its member functions, leading to subsequent member variable accesses and method calls on the freed memory.
Root Cause Analysis
In ui/ozone/platform/wayland/host/wayland_toplevel_window.cc, the method WaylandToplevelWindow::HandleToplevelConfigure is implemented as follows:
void WaylandToplevelWindow::HandleToplevelConfigure(
int32_t width_dip, int32_t height_dip,
const WindowStates& window_states) {
HandleToplevelConfigureWithOrigin(0, 0, width_dip, height_dip, window_states); // [1] Can synchronously delete `this`
UpdateSessionStateIfNeeded(); // [2] Use-After-Free
}
-
Synchronous Deletion: Inside
HandleToplevelConfigureWithOrigin[1], the methodUpdateActivationState()is called (line 569). If the activation state changes, it notifies the delegate (line 459):delegate()->OnActivationChanged(is_active_);On Wayland, the platform window delegate is
DesktopWindowTreeHostPlatform. As documented indesktop_window_tree_host_platform.cc, handling activation changes can synchronously close and destroy the widget and thereby its ownedPlatformWindow(theWaylandToplevelWindowobject):// HandleActivationChanged() notifications can cause the widget to be // synchronously closed. auto weak_this = weak_factory_.GetWeakPtr(); desktop_native_widget_aura_->HandleActivationChanged(active);When the widget is synchronously closed,
DesktopWindowTreeHostPlatform::OnClosed()callsSetPlatformWindow(nullptr), which releases and deletes theWaylandToplevelWindowobject. -
Use-After-Free: When execution returns to
HandleToplevelConfigure[2],thishas already been deleted. The method then proceeds to callUpdateSessionStateIfNeeded(), which dereferences members of the deleted object (such asxdg_toplevel_orsession_at line 978), leading to a potential Use-After-Free.
Potential Trigger Steps
Because our tooling lacks the capability to execute code or build functional exploits, these are theoretical steps representing how an attacker might attempt to trigger this potential vulnerability:
- The attacker (or a compromised renderer) causes a state change that makes the Wayland compositor send an
xdg_toplevel.configureevent. - The
xdg_toplevelconfigure event is processed byXdgToplevel::OnToplevelConfigure, which callsHandleToplevelConfigure. - The configure flow notifies the delegate of an activation state change.
- An observer responding to the activation change synchronously calls
Widget::CloseNow(), which immediately deletes theWaylandToplevelWindowobject. - The stack unwinds back to
HandleToplevelConfigure, which callsUpdateSessionStateIfNeeded(), dereferencing the freedthispointer.
Suggested Remediation
Use a WeakPtr guard in HandleToplevelConfigure to verify if the window is still alive before calling UpdateSessionStateIfNeeded(). A similar safe pattern is used elsewhere in the Ozone platform (e.g., in X11):
void WaylandToplevelWindow::HandleToplevelConfigure(
int32_t width_dip,
int32_t height_dip,
const WindowStates& window_states) {
auto weak_this = weak_ptr_factory_.GetWeakPtr();
HandleToplevelConfigureWithOrigin(0, 0, width_dip, height_dip, window_states);
if (!weak_this) {
return;
}
UpdateSessionStateIfNeeded();
}
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.