Overview

Critical
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Aura
DescriptionUse after free in Aura
ComponentAura
Sub-componentOzone/wayland
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker495948109
Fix commit5c7e491162c5 (chromium/src) +32/-10
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-25 (per vendor advisory)
Fix landed2026-05-20 (public git)
Disclosed2026-05-12

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 WaylandWindow method that drains queued configure requests from in_flight_requests_ and pushes the latest state to the window’s delegate().
`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::WeakPtr that 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.

Key insight
The single mistake was assuming the 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

  1. Trigger a configure/state update The attacker drives the compositor or renderer to enqueue a configure request so that MaybeApplyLatestStateRequest proceeds past the empty and in-flight-limit checks to the latest request.
  2. Reach the delegate callback Execution calls delegate()->OnStateUpdate(old, latest.state), handing control to observer code that runs synchronously.
  3. Destroy the window mid-callback During the callback, an observer tears down the WaylandWindow (mirrored by the regression test resetting window_ inside the state-update callback), freeing applying_state_ and in_flight_requests_.
  4. Write through freed memory On return, the code writes latest.viz_seq into the freed queue element and the base::AutoReset destructor writes to the freed applying_state_, yielding a controlled heap-use-after-free write.

Impact Assessment

An attacker who can cause a 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

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

Audit Directions

  • Scoped resets across reentrant callbacks
    Audit any base::AutoReset or RAII helper whose destructor writes to a member across a call that can synchronously destroy the enclosing object.
  • Member access after delegate/observer notifications
    Flag code that reads or writes member variables or held references (like latest) after invoking delegate()/observer callbacks without a WeakPtr liveness check.
  • Queue element references surviving reentry
    Review 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.
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
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 [email protected]

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