CVE-2026-15112
Overview
Background
- Ozone/Wayland
- Chromium’s platform abstraction where
ui::WaylandWindowbridges Wayland surface state and the browser’sviewslayer. - Surface configure
- a Wayland compositor event carrying window state (size, activation) that the client must acknowledge via
RequestStateFromServer(). - `OnStateUpdate()`
- the delegate callout reached transitively from
RequestStateFromServer()that lets theviewslayer react to state, including synchronously closing the widget. - UAF
- use-after-free; here the browser-process
WaylandWindowobject is freed mid-call yet the caller keeps writing its members.
Root Cause Analysis
ui::WaylandWindow::ProcessPendingConfigureState applies a pending configure by calling RequestStateFromServer(), which transitively invokes delegate()->OnStateUpdate(). That delegate callout is a reentrancy hazard: the views layer may synchronously close the widget and destroy the WaylandWindow, so control returns to a method whose this no longer exists. An inner guard in MaybeApplyLatestStateRequest() already detected destruction and returned early, but that early return only unwound the inner frame; the outer ProcessPendingConfigureState() resumed and reset pending_configure_state_ on the freed object. Writing to a member of a deleted WaylandWindow is a use-after-free in the browser process.
The fix captures auto weak_this = AsWeakPtr() before the call and returns immediately when !weak_this, before touching any members.
Attack Path
- Trigger configure
a page or compositor interaction drives a Wayland configure that queues
pending_configure_state_. - Reentrant callout
ProcessPendingConfigureState()callsRequestStateFromServer(), reachingdelegate()->OnStateUpdate(). - Synchronous destruction
the
viewslayer closes the widget inside the callout, freeing theWaylandWindow. - Stale write
the unwound outer frame resets
pending_configure_state_on the freed object, corrupting reclaimed browser-process heap.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifui/ozone/platform/wayland/host/wayland_window.cc |
modified | |
TEST_Pui/ozone/platform/wayland/host/wayland_window_unittest.cc |
modified | |
PostToServerAndWaitui/ozone/platform/wayland/host/wayland_window_unittest.cc |
modified |
Files Changed
ui/ozone/platform/wayland/host/wayland_window.ccui/ozone/platform/wayland/host/wayland_window_unittest.cc
Audit Directions
- Post-callout member accessaudit every method that touches instance members after invoking a delegate/observer/client callback; an inner guard does not protect outer frames on the stack.
- Reentrant destruction pathstrace all
RequestStateFromServer()/OnStateUpdate()style callouts in Ozone for widget-close reentrancy and confirm each caller re-checksWeakPtrliveness before continuing.
Patch
From 06d1bbdddd1f176c3443ca2ddb1f1d21dfe9a094 Mon Sep 17 00:00:00 2001
From: Kramer Ge <fangzhoug@chromium.org>
Date: Wed, 24 Jun 2026 08:10:04 -0700
Subject: [PATCH] [ozone/wayland]Handle window destruction during surface configure
ProcessPendingConfigureState() calls RequestStateFromServer(), which
reaches the delegate's OnStateUpdate(). The views layer may
synchronously close the widget and destroy the platform window inside
that callout. The existing weak-ptr guard in
MaybeApplyLatestStateRequest() returns early in that case, but
ProcessPendingConfigureState() then continued to access members after
the call returned.
Add a weak-ptr liveness check around RequestStateFromServer(), mirroring
the existing pattern used elsewhere in this file and in
WaylandToplevelWindow::HandleToplevelConfigure().
Fixed: 518006275
Change-Id: I8812d780c4e46b42bc1a8302c69e4f2e8256b660
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7981871
Commit-Queue: Kramer Ge <fangzhoug@chromium.org>
Reviewed-by: Thomas Anderson <thomasanderson@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1651724}
---
diff --git a/ui/ozone/platform/wayland/host/wayland_window.cc b/ui/ozone/platform/wayland/host/wayland_window.cc
index 81603cc..dcd3ff3 100644
--- a/ui/ozone/platform/wayland/host/wayland_window.cc
+++ b/ui/ozone/platform/wayland/host/wayland_window.cc
@@ -1349,7 +1349,13 @@
}
}
+ // RequestStateFromServer transitively calls delegate()->OnStateUpdate(),
+ // which may synchronously delete |this|.
+ auto weak_this = AsWeakPtr();
RequestStateFromServer(state, serial);
+ if (!weak_this) {
+ return;
+ }
// Reset values.
pending_configure_state_ = PendingConfigureState();
diff --git a/ui/ozone/platform/wayland/host/wayland_window_unittest.cc b/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
index 410f828d..dd00658 100644
--- a/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
+++ b/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
@@ -483,6 +483,22 @@
window_->SetBoundsInDIP(gfx::Rect(1024, 768));
}
+// Regression test for https://crbug.com/495948109.
+TEST_P(WaylandWindowTest, DeleteWindowFromOnStateUpdateDuringSurfaceConfigure) {
+ delegate_.set_on_state_update_callback(base::BindLambdaForTesting([&]() {
+ window_.reset();
+ return false;
+ }));
+
+ WaylandWindow* window = window_.get();
+ WaylandWindow::WindowStates window_states;
+ window_states.is_activated = true;
+ window->HandleToplevelConfigure(1024, 768, window_states);
+ window->HandleSurfaceConfigure(2);
+
+ EXPECT_FALSE(window_);
+}
+
TEST_P(WaylandWindowTest, SetTitle) {
window_->SetTitle(u"hello");
PostToServerAndWait([id = surface_id_](wl::TestWaylandServerThread* server) {
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 410f828d..dd00658 100644
--- a/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
+++ b/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
@@ -483,6 +483,22 @@
window_->SetBoundsInDIP(gfx::Rect(1024, 768));
}
+// Regression test for https://crbug.com/495948109.
+TEST_P(WaylandWindowTest, DeleteWindowFromOnStateUpdateDuringSurfaceConfigure) {
+ delegate_.set_on_state_update_callback(base::BindLambdaForTesting([&]() {
+ window_.reset();
+ return false;
+ }));
+
+ WaylandWindow* window = window_.get();
+ WaylandWindow::WindowStates window_states;
+ window_states.is_activated = true;
+ window->HandleToplevelConfigure(1024, 768, window_states);
+ window->HandleSurfaceConfigure(2);
+
+ EXPECT_FALSE(window_);
+}
+
TEST_P(WaylandWindowTest, SetTitle) {
window_->SetTitle(u"hello");
PostToServerAndWait([id = surface_id_](wl::TestWaylandServerThread* server) {
Original Bug Report
Potential UAF write and virtual call in WaylandWindow::ProcessPendingConfigureState
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 WaylandWindow::ProcessPendingConfigureState due to the absence of a liveness check after a synchronous, deletion-capable callout. The state application chain can synchronously delete the WaylandWindow instance, leading to subsequent writes to and reads from the freed ’this’ object when execution returns to the caller function.
Affected files:
ui/ozone/platform/wayland/host/wayland_window.ccui/ozone/platform/wayland/host/wayland_window.h
Estimated timestamp from git blame: Unknown (Google3 checkout)
Root Cause Analysis
In ui/ozone/platform/wayland/host/wayland_window.cc, WaylandWindow::ProcessPendingConfigureState invokes RequestStateFromServer() which subsequently delegates state updates to the window delegate:
void WaylandWindow::ProcessPendingConfigureState(uint32_t serial) {
...
RequestStateFromServer(state, serial);
// Reset values.
pending_configure_state_ = PendingConfigureState(); // Potential UAF WRITE
if (state == applied_state_ && state == latched_state_ &&
in_flight_requests_.empty() && root_surface()->has_buffer()) {
root_surface_->Commit(/*flush=*/true); // Potential wild pointer dereference and virtual call
}
}
When RequestStateFromServer runs, the call path is as follows:
RequestStateFromServercallsRequestState(Line 1343).RequestStatecallsMaybeApplyLatestStateRequest(Line 1427).MaybeApplyLatestStateRequesttriggersdelegate()->OnStateUpdate(old, latest.state)(Line 1599).
The delegate callback triggers synchronous bounds and visibility observer notifications. In certain scenarios, these observers can synchronously call Widget::CloseNow(), leading to the synchronous deletion of the WaylandWindow instance.
While MaybeApplyLatestStateRequest contains a weak pointer guard (weak_this) to detect self-deletion and abort cleanly, the outer callers (RequestState and RequestStateFromServer) do not check liveness before returning. Consequently, execution returns back to ProcessPendingConfigureState where the freed this pointer is unconditionally accessed, causing a use-after-free write on pending_configure_state_ and a potential use-after-free read and virtual call on root_surface_.
Potential Attack Path / Trigger Scenario
Because our automated analysis tools do not have the capability to execute code dynamically, these steps represent a potential exploitation sequence:
- An attacker-controlled web page in a sandboxed renderer process opens a popup window (
window.open). - The page forces frequent configure and window state updates (e.g. through Fullscreen API toggles or
resizeTo). - The attacker engineers or triggers a synchronous widget closure (e.g.
Widget::CloseNow()) within the synchronous observer callback path duringOnStateUpdate. - When the
WaylandWindowis deleted synchronously, execution returns back toProcessPendingConfigureState, causing memory corruption in the browser process.
Suggested Fix
To prevent this issue, introduce a weak-pointer liveness check immediately after calling RequestStateFromServer inside WaylandWindow::ProcessPendingConfigureState:
auto weak_this = AsWeakPtr();
RequestStateFromServer(state, serial);
if (!weak_this) {
return;
}
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.