Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Aura
DescriptionUse after free in Aura
ComponentAura
Bug ClassUAF
Tracker495948109
Fix commit5c7e491162c5 (chromium/src) +32/-10
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-12

Changed Functions

FunctionChangeNotes
if
ui/ozone/platform/wayland/host/wayland_window.cc
modified
TEST_P
ui/ozone/platform/wayland/host/wayland_window_unittest.cc
modified
PostToServerAndWait
ui/ozone/platform/wayland/host/wayland_window_unittest.cc
modified
BindLambdaForTesting
ui/ozone/platform/wayland/host/wayland_window_unittest.cc
modified

Files Changed

  • ui/ozone/platform/wayland/host/wayland_window.cc
  • ui/ozone/platform/wayland/host/wayland_window_unittest.cc
  • ui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.cc
  • ui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.h
From 5c7e491162c5e94676e95ffe0f9b7f316faec884 Mon Sep 17 00:00:00 2001
From: Mitsuru Oshima <oshima@chromium.org>
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 <oshima@chromium.org>
Reviewed-by: Keishi Hattori <keishi@chromium.org>
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
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • ui/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.

  1. Compromise Renderer: An attacker gains initial code execution in a sandboxed renderer process.
  2. 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_configure event.
  3. Heap Grooming: Concurrently, the attacker sprays the browser process heap using standard primitive techniques (like Blobs) to prepare for memory reclamation.
  4. 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 the WindowTreeHostPlatform (this pattern is already known and tested in DeleteHostFromOnHostMovedInPixels).
  5. Reclaim and Execute: The WindowTreeHostPlatform and its WaylandWindow are freed. The sprayed data reclaims the memory. When OnStateUpdate resumes, it uses the attacker-controlled memory to resolve compositor() and vtable lookups (like OnOcclusionStateChanged), 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.

View on issue tracker