CVE-2026-8514
Overview
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
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.