CVE-2026-8514
Overview
Background
- `WaylandWindow`
- The Ozone/Wayland host-side object representing a platform window, whose lifetime can end synchronously when its owner reacts to a state update.
- `MaybeApplyLatestStateRequest`
- The
WaylandWindowmethod that drains queued configure requests fromin_flight_requests_and pushes the latest state to the window’sdelegate(). - `base::AutoReset<bool>`
- A scoped helper that sets a boolean on construction and restores its prior value in its destructor, which dereferences the boolean’s address when the scope exits.
- `AsWeakPtr`
- A method returning a
base::WeakPtrthat becomes null once the pointed-to object is destroyed, enabling a post-callback liveness check.
Root Cause Analysis
In MaybeApplyLatestStateRequest, the method guarded re-entrancy by placing an applying_state_ flag under a scoped base::AutoReset<bool> named setter, then called delegate()->OnStateUpdate(old, latest.state). That delegate call can synchronously destroy the WaylandWindow (for example when an observer reacts to the state update), which frees the object including its applying_state_ member and its in_flight_requests_ queue. After the call returned, the code still wrote through the freed latest reference (latest.viz_seq = ...) and, on scope exit, the base::AutoReset destructor still wrote to the freed applying_state_ address, both of which are heap-use-after-free writes.
The fix replaces the scoped setter with manual applying_state_ = false assignments on every early-return path and captures AsWeakPtr() before OnStateUpdate, returning immediately if !weak_this so that no freed member is touched. Because the weak pointer check short-circuits before assigning latest.viz_seq and before the final applying_state_ = false, the destroyed window’s memory is never accessed again.
WaylandWindow outlives the delegate()->OnStateUpdate() call, so that both the base::AutoReset<bool> destructor and the latest.viz_seq write remained valid; the fix detects self-destruction via an AsWeakPtr() guard and abandons all further member access when the window is gone.Attack Path
- Trigger a configure/state update
The attacker drives the compositor or renderer to enqueue a configure request so that
MaybeApplyLatestStateRequestproceeds past the empty and in-flight-limit checks to the latest request. - Reach the delegate callback
Execution calls
delegate()->OnStateUpdate(old, latest.state), handing control to observer code that runs synchronously. - Destroy the window mid-callback
During the callback, an observer tears down the
WaylandWindow(mirrored by the regression test resettingwindow_inside the state-update callback), freeingapplying_state_andin_flight_requests_. - Write through freed memory
On return, the code writes
latest.viz_seqinto the freed queue element and thebase::AutoResetdestructor writes to the freedapplying_state_, yielding a controlled heap-use-after-free write.
Impact Assessment
WaylandWindow to be destroyed during OnStateUpdate gains a heap use-after-free write in the browser/GPU-side Ozone Wayland host code, which per the patch author can escalate to remote code execution. The precondition is a Linux/Wayland session where window state updates can be provoked and an observer path exists that synchronously destroys the window during the update.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 | |
BindLambdaForTestingui/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.ccui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.ccui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.h
Audit Directions
- Scoped resets across reentrant callbacksAudit any
base::AutoResetor RAII helper whose destructor writes to a member across a call that can synchronously destroy the enclosing object. - Member access after delegate/observer notificationsFlag code that reads or writes member variables or held references (like
latest) after invokingdelegate()/observer callbacks without aWeakPtrliveness check. - Queue element references surviving reentryReview methods that hold references into containers such as
in_flight_requests_across reentrant or destructive callbacks, since the container may be freed underneath the reference.
Patch
From 5c7e491162c5e94676e95ffe0f9b7f316faec884 Mon Sep 17 00:00:00 2001 From: Mitsuru Oshima <[email protected]> Date: Tue, 19 May 2026 22:44:50 -0700 Subject: [PATCH] [ozone/wayland] Fix Use-After-Free in WaylandWindow::MaybeApplyLatestStateRequest In WaylandWindow::MaybeApplyLatestStateRequest, the call to delegate()->OnStateUpdate() can trigger observer notifications that synchronously destroy the WaylandWindow. This commit avoids accessing member variables like `applying_state_` and the `latest` reference (which points to a destroyed `in_flight_requests_` queue element) after the window is destroyed. This prevents a potential Remote Code Execution vulnerability due to a heap-use-after-free write. Bug: 495948109 Change-Id: I1964a73b644c9e5f132dc913b50b5a5235886a7a Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7858166 Commit-Queue: Mitsuru Oshima <[email protected]> Reviewed-by: Keishi Hattori <[email protected]> Cr-Commit-Position: refs/heads/main@{#1633356} --- diff --git a/ui/ozone/platform/wayland/host/wayland_window.cc b/ui/ozone/platform/wayland/host/wayland_window.cc index 997156c..7701ded 100644 --- a/ui/ozone/platform/wayland/host/wayland_window.cc +++ b/ui/ozone/platform/wayland/host/wayland_window.cc @@ -1559,10 +1559,10 @@ // `in_flight_requests_`. CHECK(!applying_state_) << "MaybeApplyLatestStateRequest called re-entrantly."; - auto setter = - std::make_optional<base::AutoReset<bool>>(&applying_state_, true); + applying_state_ = true; if (in_flight_requests_.empty()) { + applying_state_ = false; return; } @@ -1574,12 +1574,14 @@ // Allow at most 3 configure requests to be waited on at a time. constexpr int MAX_IN_FLIGHT_REQUESTS = 3; if (in_flight_applied >= MAX_IN_FLIGHT_REQUESTS) { + applying_state_ = false; return; } } auto& latest = in_flight_requests_.back(); if (latest.applied) { + applying_state_ = false; return; } latest.applied = true; @@ -1593,7 +1595,12 @@ // frame to be considered synchronized. For example, this can happen if the // old and new states are the same, or it only changes the origin of the // bounds. - latest.viz_seq = delegate()->OnStateUpdate(old, latest.state); + auto weak_this = AsWeakPtr(); + int64_t viz_seq = delegate()->OnStateUpdate(old, latest.state); + if (!weak_this) { + return; + } + latest.viz_seq = viz_seq; if (UseTestConfigForPlatformWindows()) { latest_applied_viz_seq_for_testing_ = std::max( @@ -1606,7 +1613,7 @@ // `ProcessSequencePoint` may re-entrantly call // `MaybeApplyLatestStateRequest`. This is safe as long as we do not hold // references to `in_flight_requests_` after here. - setter.reset(); + applying_state_ = false; // Process any requests added re-entrantly. We need to move the requests out // of `reentrant_requests_` here because each re-entrant request may also add diff --git a/ui/ozone/platform/wayland/host/wayland_window_unittest.cc b/ui/ozone/platform/wayland/host/wayland_window_unittest.cc index 261062d0..d0afdc8 100644 --- a/ui/ozone/platform/wayland/host/wayland_window_unittest.cc +++ b/ui/ozone/platform/wayland/host/wayland_window_unittest.cc @@ -473,6 +473,16 @@ window_->OnDragSessionClose(mojom::DragOperation::kNone); } +// Regression test for https://crbug.com/495948109. +TEST_P(WaylandWindowTest, DeleteWindowFromOnStateUpdate) { + delegate_.set_on_state_update_callback(base::BindLambdaForTesting([&]() { + window_.reset(); + return false; + })); + + window_->SetBoundsInDIP(gfx::Rect(1024, 768)); +} + TEST_P(WaylandWindowTest, SetTitle) { window_->SetTitle(u"hello"); PostToServerAndWait([id = surface_id_](wl::TestWaylandServerThread* server) { @@ -4809,8 +4819,10 @@ EXPECT_CALL(*xdg_surface, AckConfigure(_)).Times(0); }); - delegate_.set_on_state_update_callback( - base::BindLambdaForTesting([&]() { window_->SetBoundsInDIP(kBounds3); })); + delegate_.set_on_state_update_callback(base::BindLambdaForTesting([&]() { + window_->SetBoundsInDIP(kBounds3); + return true; + })); window_->SetBoundsInDIP(kBounds2); AdvanceFrameToCurrent(window_.get(), delegate_); VerifyAndClearExpectations(); diff --git a/ui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.cc b/ui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.cc index 3a025086..9ade411 100644 --- a/ui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.cc +++ b/ui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.cc @@ -63,7 +63,9 @@ } if (!on_state_update_callback_.is_null()) { - on_state_update_callback_.Run(); + if (!on_state_update_callback_.Run()) { + return -1; + } } if (!latest.WillProduceFrameOnUpdateFrom(old)) { diff --git a/ui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.h b/ui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.h index e5ca83e2..d9cbcc6 100644 --- a/ui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.h +++ b/ui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.h @@ -38,8 +38,9 @@ int64_t viz_seq() const { return viz_seq_; } // Callback called during OnStateUpdate. This can be used to simulate - // re-entrant client initiated requests. - void set_on_state_update_callback(base::RepeatingClosure cb) { + // re-entrant client initiated requests. Returning false will cause + // OnStateUpdate to return -1. + void set_on_state_update_callback(base::RepeatingCallback<bool()> cb) { on_state_update_callback_ = cb; } @@ -55,7 +56,7 @@ // what sequence point is required to advance to the latest state. int64_t viz_seq_ = 0; - base::RepeatingClosure on_state_update_callback_; + base::RepeatingCallback<bool()> on_state_update_callback_; }; } // namespace ui
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 261062d0..d0afdc8 100644
--- a/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
+++ b/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
@@ -473,6 +473,16 @@
window_->OnDragSessionClose(mojom::DragOperation::kNone);
}
+// Regression test for https://crbug.com/495948109.
+TEST_P(WaylandWindowTest, DeleteWindowFromOnStateUpdate) {
+ delegate_.set_on_state_update_callback(base::BindLambdaForTesting([&]() {
+ window_.reset();
+ return false;
+ }));
+
+ window_->SetBoundsInDIP(gfx::Rect(1024, 768));
+}
+
TEST_P(WaylandWindowTest, SetTitle) {
window_->SetTitle(u"hello");
PostToServerAndWait([id = surface_id_](wl::TestWaylandServerThread* server) {
@@ -4809,8 +4819,10 @@
EXPECT_CALL(*xdg_surface, AckConfigure(_)).Times(0);
});
- delegate_.set_on_state_update_callback(
- base::BindLambdaForTesting([&]() { window_->SetBoundsInDIP(kBounds3); }));
+ delegate_.set_on_state_update_callback(base::BindLambdaForTesting([&]() {
+ window_->SetBoundsInDIP(kBounds3);
+ return true;
+ }));
window_->SetBoundsInDIP(kBounds2);
AdvanceFrameToCurrent(window_.get(), delegate_);
VerifyAndClearExpectations();
diff --git a/ui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.cc b/ui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.cc
index 3a025086..9ade411 100644
--- a/ui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.cc
+++ b/ui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.cc
@@ -63,7 +63,9 @@
}
if (!on_state_update_callback_.is_null()) {
- on_state_update_callback_.Run();
+ if (!on_state_update_callback_.Run()) {
+ return -1;
+ }
}
if (!latest.WillProduceFrameOnUpdateFrom(old)) {
diff --git a/ui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.h b/ui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.h
index e5ca83e2..d9cbcc6 100644
--- a/ui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.h
+++ b/ui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.h
@@ -38,8 +38,9 @@
int64_t viz_seq() const { return viz_seq_; }
// Callback called during OnStateUpdate. This can be used to simulate
- // re-entrant client initiated requests.
- void set_on_state_update_callback(base::RepeatingClosure cb) {
+ // re-entrant client initiated requests. Returning false will cause
+ // OnStateUpdate to return -1.
+ void set_on_state_update_callback(base::RepeatingCallback<bool()> cb) {
on_state_update_callback_ = cb;
}
@@ -55,7 +56,7 @@
// what sequence point is required to advance to the latest state.
int64_t viz_seq_ = 0;
- base::RepeatingClosure on_state_update_callback_;
+ base::RepeatingCallback<bool()> on_state_update_callback_;
};
} // namespace ui
Original Bug Report
Potential UAF in WindowTreeHostPlatform::OnStateUpdate via nested OnBoundsChanged
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A potential use-after-free vulnerability exists in WindowTreeHostPlatform::OnStateUpdate. The function calls OnBoundsChanged, which may synchronously destroy the host object, but it fails to check if this is still alive before continuing execution, potentially leading to remote code execution in the browser process.
Affected files:
ui/aura/window_tree_host_platform.ccui/ozone/platform/wayland/host/wayland_window.cc
Estimated timestamp from git blame: 2024-04-15
Description
A potential Use-After-Free (UAF) vulnerability has been identified in WindowTreeHostPlatform::OnStateUpdate. The issue occurs because the function calls OnBoundsChanged without any WeakPtr liveness check afterward. If a registered observer synchronously destroys the host during the bounds change, OnStateUpdate will continue to execute using a dangling this pointer and a dangling latest state reference.
Root Cause Analysis
In ui/aura/window_tree_host_platform.cc:
int64_t WindowTreeHostPlatform::OnStateUpdate(
const PlatformWindowDelegate::State& old,
const PlatformWindowDelegate::State& latest) {
// ...
if (old.bounds_dip != latest.bounds_dip || old.size_px != latest.size_px ||
old.window_scale != latest.window_scale) {
bool origin_changed = old.bounds_dip.origin() != latest.bounds_dip.origin();
OnBoundsChanged({origin_changed}); // <--- May synchronously destroy `this`
}
// VULNERABILITY: No WeakPtr check here.
bool needs_frame = latest.WillProduceFrameOnUpdateFrom(old); // UAF read of `latest`
if (old.occlusion_state != latest.occlusion_state &&
NativeWindowOcclusionTracker::
IsNativeWindowOcclusionTrackingAlwaysEnabled(this)) {
const bool visible_before = compositor()->IsVisible(); // UAF read of `this`
OnOcclusionStateChanged(latest.occlusion_state); // UAF virtual call
// ...
}
// ...
window()->AllocateLocalSurfaceId(); // UAF read of `this`
// ...
}
OnBoundsChanged correctly anticipates that it might be destroyed (e.g., via OnHostMovedInPixels notifying observers) and checks GetWeakPtr() internally. However, it returns control to OnStateUpdate, which blithely continues. Because this is an implicit raw pointer and latest is a stack reference to an element in the destroyed WaylandWindow’s internal buffer, MiraclePtr (BRP) does not prevent these UAF reads/writes.
Potential Exploitation Steps
Note: These are suggested potential steps, as our setup does not currently have the ability to run code or build a working Proof of Concept.
- Compromise Renderer: An attacker gains initial code execution in a sandboxed renderer process.
- Trigger State Change: The attacker sends window management IPCs (e.g., bounds changes, fullscreen requests) to the browser process to trigger a Wayland
xdg_surface_configureevent. - Heap Grooming: Concurrently, the attacker sprays the browser process heap using standard primitive techniques (like Blobs) to prepare for memory reclamation.
- Synchronous Destruction: The attacker triggers a specific UI state or timing window where a bounds change notification (like
OnHostMovedInPixels) causes an observer to synchronously destroy theWindowTreeHostPlatform(this pattern is already known and tested inDeleteHostFromOnHostMovedInPixels). - Reclaim and Execute: The
WindowTreeHostPlatformand itsWaylandWindoware freed. The sprayed data reclaims the memory. WhenOnStateUpdateresumes, it uses the attacker-controlled memory to resolvecompositor()and vtable lookups (likeOnOcclusionStateChanged), achieving Remote Code Execution in the browser process.
Impact
This is a potential high-severity Use-After-Free in the highly privileged browser process. It provides a robust primitive for an attacker to achieve Remote Code Execution (RCE) and completely escape the Chrome sandbox.
Suggested Fix
Guard the execution of OnStateUpdate with a WeakPtr check immediately following the call to OnBoundsChanged. If the host is destroyed, the function should safely return early.
bool origin_changed = old.bounds_dip.origin() != latest.bounds_dip.origin();
auto weak_ref = GetWeakPtr();
OnBoundsChanged({origin_changed});
if (!weak_ref)
return -1; // Or appropriate default sequence number indicating no frame needed
Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. Please feel free to reach out to me if you have concerns or feedback.