Overview

Critical
Severity
7.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Ozone
DescriptionUse after free in Ozone
ComponentOzone
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.
Tracker518007484
Fix commit87abcfa5b16b (chromium/src) +149/-2
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-29 (per vendor advisory)
Fix landed2026-07-07 (public git)
Disclosed2026-07-14
Patch gap7 days — fix public in source before the advisory

Background

`WaylandWindow`
Ozone host-side object (ui/ozone/platform/wayland/host/) backing a platform window; subclasses include WaylandBubble, WaylandPopup, and WaylandToplevelWindow.
`PlatformWindowDelegate`
the views-layer delegate a WaylandWindow calls into (OnSequencePoint, OnPaintAsActiveChanged, OnDamageRect, UpdateWindowScale, Show) during configure, scale, and state handling.
Re-entrant destruction
the delegate can synchronously close the owning widget inside such a callout, deleting the WaylandWindow (this) before control returns to the caller.
`AsWeakPtr()` / `weak_this`
a WeakPtr to this captured before a callout so the method can detect its own destruction and bail out.

Root Cause Analysis

Several WaylandWindow subclass methods invoke PlatformWindowDelegate callbacks (directly or indirectly, e.g. via UpdateWindowScale) in the middle of configure, scale-change, and frame-processing logic. These callouts can re-enter the views layer, which may synchronously close the widget and destroy the WaylandWindow, freeing this. The methods then continued executing, dereferencing member state such as subsurface_, root_surface(), pending_frames_, and applied_state() on the freed object. Because the lifetime of this is not owned by these methods and is controllable through window/popup lifecycle events, this yields a use-after-free in the browser process.

The fix captures auto weak_this = AsWeakPtr(); before each dangerous callout and adds if (!weak_this) return; immediately after, aborting any further member access when the window was destroyed.

Key insight
A synchronous delegate callout is an implicit re-entrancy point that can free this, so any member access after it is unsafe. The vulnerability class is not a single bad line but a pattern: every state-changing delegate call followed by a member deref needs a WeakPtr liveness guard.

Attack Path

  1. Trigger lifecycle event An attacker-influenced compositor/window event drives a configure, scale change, or frame sequence point on a WaylandWindow subclass.
  2. Enter delegate callout The method calls into PlatformWindowDelegate (OnSequencePoint, OnPaintAsActiveChanged, Show, or UpdateWindowScale).
  3. Synchronous widget close The views layer closes the widget inside the callout, destroying the WaylandWindow and freeing this.
  4. Post-callout deref Control returns to the method, which accesses freed members (subsurface_, root_surface(), pending_frames_, applied_state()), reading or writing freed memory.

Impact Assessment

Use-after-free in the browser process, the most privileged Chrome process, reachable through window and popup lifecycle events. A freed-object deref can corrupt memory and, combined with heap grooming, may lead to controlled read/write and code execution. Exploitability depends on attacker influence over window lifecycle timing and heap layout on Wayland desktop configurations.

Changed Functions

FunctionChangeNotes
if
ui/ozone/platform/wayland/host/wayland_bubble.cc
modified
if
ui/ozone/platform/wayland/host/wayland_frame_manager.cc
modified
if
ui/ozone/platform/wayland/host/wayland_popup.cc
modified
if
ui/ozone/platform/wayland/host/wayland_toplevel_window.cc
modified
if
ui/ozone/platform/wayland/host/wayland_window.cc
modified
TEST_P
ui/ozone/platform/wayland/host/wayland_window_unittest.cc
modified

Files Changed

  • ui/ozone/platform/wayland/host/wayland_bubble.cc
  • ui/ozone/platform/wayland/host/wayland_frame_manager.cc
  • ui/ozone/platform/wayland/host/wayland_popup.cc
  • ui/ozone/platform/wayland/host/wayland_toplevel_window.cc
  • ui/ozone/platform/wayland/host/wayland_window.cc
  • ui/ozone/platform/wayland/host/wayland_window_unittest.cc

Audit Directions

  • Callout-then-deref
    Audit every WaylandWindow and Ozone platform method that calls a PlatformWindowDelegate method and then reads any member of this; require a WeakPtr guard or a documented no-reentrancy invariant.
  • Indirect delegate calls
    Trace helpers like UpdateWindowScale and configure handlers that call the delegate transitively; the callout may be several frames deep and easy to miss.
  • `this`-owning lifecycles
    Flag methods whose object lifetime is controlled by external window/popup events, where synchronous widget close can free this mid-method.
From 87abcfa5b16b689587f94f40e6e7de88db11313a Mon Sep 17 00:00:00 2001
From: Kramer Ge <[email protected]>
Date: Mon, 06 Jul 2026 17:03:17 -0700
Subject: [PATCH] [Ozone/Wayland]Guard all delegate() calls with weak_this

delegate() calls may implicitly destroy WaylandWindow. Guard direct and
indirect calls to delegate() state changes with weak_this if such calls
are followed by potential deref of WaylandWindow members.

Fixed: 518007484
Change-Id: I9fe00496ee807f3416c9a9a2dedeef352e4a63e6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8043107
Reviewed-by: Thomas Anderson <[email protected]>
Commit-Queue: Kramer Ge <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1657588}
---

diff --git a/ui/ozone/platform/wayland/host/wayland_bubble.cc b/ui/ozone/platform/wayland/host/wayland_bubble.cc
index ef876a7..99379bf1 100644
--- a/ui/ozone/platform/wayland/host/wayland_bubble.cc
+++ b/ui/ozone/platform/wayland/host/wayland_bubble.cc
@@ -34,8 +34,15 @@
     return;
   }
 
+  auto weak_this = AsWeakPtr();
   UpdateWindowScale(false);
+  if (!weak_this) {
+    return;
+  }
   AddToParentAsSubsurface();
+  if (!weak_this) {
+    return;
+  }
   WaylandWindow::Show(inactive);
 }
 
@@ -96,7 +103,11 @@
 }
 
 void WaylandBubble::UpdateWindowScale(bool update_bounds) {
+  auto weak_this = AsWeakPtr();
   WaylandWindow::UpdateWindowScale(update_bounds);
+  if (!weak_this) {
+    return;
+  }
   if (subsurface_) {
     SetSubsurfacePosition();
   }
@@ -131,7 +142,11 @@
   CHECK(parent_window());
 
   // We need to make sure that window scale matches the parent window.
+  auto weak_this = AsWeakPtr();
   UpdateWindowScale(true);
+  if (!weak_this) {
+    return;
+  }
 
   subsurface_ =
       root_surface()->CreateSubsurface(parent_window()->root_surface());
diff --git a/ui/ozone/platform/wayland/host/wayland_frame_manager.cc b/ui/ozone/platform/wayland/host/wayland_frame_manager.cc
index dd3fb2e5c94..e84a0c0 100644
--- a/ui/ozone/platform/wayland/host/wayland_frame_manager.cc
+++ b/ui/ozone/platform/wayland/host/wayland_frame_manager.cc
@@ -199,7 +199,11 @@
     if (!ValidateRect(config.bounds_rect)) {
       fatal_error_message_ = kBoundsRectNanOrInf;
     } else {
+      auto weak_this = weak_factory_.GetWeakPtr();
       window_->OnSequencePoint(frame->seq);
+      if (!weak_this) {
+        return;
+      }
       // During a tab dragging session, OnSequencePoint() can implicitly invoke
       // Hide(). |pending_frames_| will be cleared and we should return
       // directly.
diff --git a/ui/ozone/platform/wayland/host/wayland_popup.cc b/ui/ozone/platform/wayland/host/wayland_popup.cc
index 8b5b03e..75c74757 100644
--- a/ui/ozone/platform/wayland/host/wayland_popup.cc
+++ b/ui/ozone/platform/wayland/host/wayland_popup.cc
@@ -57,7 +57,11 @@
       parent_window()->applied_state().window_scale) {
     // If scale changed while this was hidden (when WaylandPopup hides, parent
     // window's child is reset), update buffer scale accordingly.
+    auto weak_this = AsWeakPtr();
     UpdateWindowScale(true);
+    if (!weak_this) {
+      return false;
+    }
   }
 
   auto bounds_dip =
@@ -123,13 +127,21 @@
   // Map parent window as WaylandPopup cannot become a visible child of a
   // window that is not mapped.
   DCHECK(parent_window());
-  if (!parent_window()->IsVisible())
+  auto weak_this = AsWeakPtr();
+  if (!parent_window()->IsVisible()) {
     parent_window()->Show(false);
+  }
+  if (!weak_this) {
+    return;
+  }
 
-  if (!CreateShellPopup()) {
+  if (!CreateShellPopup() && weak_this) {
     Close();
     return;
   }
+  if (!weak_this) {
+    return;
+  }
 
   connection()->Flush();
   WaylandWindow::Show(inactive);
@@ -250,7 +262,11 @@
 
 void WaylandPopup::HandleSurfaceConfigure(uint32_t serial) {
   if (schedule_redraw_) {
+    auto weak_this = AsWeakPtr();
     delegate()->OnDamageRect(gfx::Rect{applied_state().size_px});
+    if (!weak_this) {
+      return;
+    }
     schedule_redraw_ = false;
   }
   ProcessPendingConfigureState(serial);
diff --git a/ui/ozone/platform/wayland/host/wayland_toplevel_window.cc b/ui/ozone/platform/wayland/host/wayland_toplevel_window.cc
index 4a5b2255..46afd1b 100644
--- a/ui/ozone/platform/wayland/host/wayland_toplevel_window.cc
+++ b/ui/ozone/platform/wayland/host/wayland_toplevel_window.cc
@@ -547,7 +547,11 @@
   // xdg_toplevel::activated is a paint-only hint, separate from input
   // activation which is driven by keyboard focus in UpdateActivationState.
   if (prev_xdg_active != is_xdg_active_) {
+    auto weak_this = AsWeakPtr();
     delegate()->OnPaintAsActiveChanged(is_xdg_active_);
+    if (!weak_this) {
+      return;
+    }
   }
   bool prev_suspended = is_suspended_;
   is_suspended_ = window_states.is_suspended;
@@ -562,7 +566,11 @@
   if (window_states.tiled_edges != applied_state().tiled_edges) {
     // This configure changes the decoration insets.  We should adjust the
     // bounds appropriately.
+    auto weak_this = AsWeakPtr();
     delegate()->OnWindowTiledStateChanged(window_states.tiled_edges);
+    if (!weak_this) {
+      return;
+    }
   }
 
   pending_configure_state_.tiled_edges = window_states.tiled_edges;
@@ -905,7 +913,11 @@
 
   auto previous_state = applied_state().window_state;
   ForceApplyWindowStateDoNotUse(window_state);
+  auto weak_this = AsWeakPtr();
   delegate()->OnWindowStateChanged(previous_state, window_state);
+  if (!weak_this) {
+    return;
+  }
   connection()->Flush();
 }
 
diff --git a/ui/ozone/platform/wayland/host/wayland_window.cc b/ui/ozone/platform/wayland/host/wayland_window.cc
index dcd3ff3..6ac7b05 100644
--- a/ui/ozone/platform/wayland/host/wayland_window.cc
+++ b/ui/ozone/platform/wayland/host/wayland_window.cc
@@ -170,7 +170,11 @@
   const auto window_scale = connection_->UsePerSurfaceScaling()
                                 ? GetPreferredScaleFactor()
                                 : GetScaleFactorFromEnteredOutputs();
+  auto weak_this = AsWeakPtr();
   SetWindowScale(window_scale.value_or(1.0f));
+  if (!weak_this) {
+    return;
+  }
 
   // Propagate update to the popups.
   if (child_popup_) {
diff --git a/ui/ozone/platform/wayland/host/wayland_window_unittest.cc b/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
index ecebab0..389846e 100644
--- a/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
+++ b/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
@@ -5841,6 +5841,102 @@
   window_->RemoveBubble(wayland_bubble->AsWaylandBubble());
 }
 
+TEST_P(WaylandWindowTest, WaylandBubbleUpdateWindowScaleUaf) {
+  MockWaylandPlatformWindowDelegate bubble_delegate(connection_.get());
+  gfx::Rect bubble_bounds(10, 10, 50, 50);
+  auto wayland_bubble =
+      CreateWaylandWindowWithParams(PlatformWindowType::kBubble, bubble_bounds,
+                                    &bubble_delegate, window_->GetWidget());
+  ASSERT_TRUE(wayland_bubble);
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 ecebab0..389846e 100644
--- a/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
+++ b/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
@@ -5841,6 +5841,102 @@
   window_->RemoveBubble(wayland_bubble->AsWaylandBubble());
 }
 
+TEST_P(WaylandWindowTest, WaylandBubbleUpdateWindowScaleUaf) {
+  MockWaylandPlatformWindowDelegate bubble_delegate(connection_.get());
+  gfx::Rect bubble_bounds(10, 10, 50, 50);
+  auto wayland_bubble =
+      CreateWaylandWindowWithParams(PlatformWindowType::kBubble, bubble_bounds,
+                                    &bubble_delegate, window_->GetWidget());
+  ASSERT_TRUE(wayland_bubble);
+
+  bubble_delegate.set_on_state_update_callback(
+      base::BindLambdaForTesting([&]() {
+        wayland_bubble.reset();
+        return true;
+      }));
+
+  // This should not crash.
+  wayland_bubble->UpdateWindowScale(true);
+}
+
+TEST_P(WaylandWindowTest, WaylandBubbleShowUaf) {
+  MockWaylandPlatformWindowDelegate bubble_delegate(connection_.get());
+  PlatformWindowInitProperties properties;
+  properties.bounds = gfx::Rect(10, 10, 50, 50);
+  properties.type = PlatformWindowType::kBubble;
+  properties.parent_widget = window_->GetWidget();
+  auto wayland_bubble = bubble_delegate.CreateWaylandWindow(
+      connection_.get(), std::move(properties));
+  ASSERT_TRUE(wayland_bubble);
+
+  bubble_delegate.set_on_state_update_callback(
+      base::BindLambdaForTesting([&]() {
+        wayland_bubble.reset();
+        return true;
+      }));
+
+  // This should not crash.
+  wayland_bubble->Show(false);
+}
+
+TEST_P(WaylandWindowTest, WaylandPopupShowUaf) {
+  MockWaylandPlatformWindowDelegate popup_delegate(connection_.get());
+  PlatformWindowInitProperties properties;
+  properties.bounds = gfx::Rect(10, 10, 50, 50);
+  properties.type = PlatformWindowType::kPopup;
+  properties.parent_widget = window_->GetWidget();
+  auto wayland_popup = popup_delegate.CreateWaylandWindow(
+      connection_.get(), std::move(properties));
+  ASSERT_TRUE(wayland_popup);
+
+  popup_delegate.set_on_state_update_callback(base::BindLambdaForTesting([&]() {
+    wayland_popup.reset();
+    return true;
+  }));
+
+  // This should not crash.
+  wayland_popup->Show(false);
+}
+
+TEST_P(WaylandWindowTest, WaylandToplevelWindowOnPaintAsActiveChangedUaf) {
+  testing::NiceMock<MockWaylandPlatformWindowDelegate> toplevel_delegate(
+      connection_.get());
+  PlatformWindowInitProperties properties;
+  properties.bounds = gfx::Rect(10, 10, 100, 100);
+  properties.type = PlatformWindowType::kWindow;
+  auto toplevel_window = toplevel_delegate.CreateWaylandWindow(
+      connection_.get(), std::move(properties));
+  ASSERT_TRUE(toplevel_window);
+
+  EXPECT_CALL(toplevel_delegate, OnPaintAsActiveChanged(::testing::_))
+      .WillOnce(
+          ::testing::InvokeWithoutArgs([&]() { toplevel_window.reset(); }));
+
+  WaylandWindow::WindowStates window_states;
+  window_states.is_activated = true;
+  // This should not crash.
+  toplevel_window->HandleToplevelConfigure(100, 100, window_states);
+}
+
+TEST_P(WaylandWindowTest, WaylandToplevelWindowTriggerStateChangesUaf) {
+  testing::NiceMock<MockWaylandPlatformWindowDelegate> toplevel_delegate(
+      connection_.get());
+  PlatformWindowInitProperties properties;
+  properties.bounds = gfx::Rect(10, 10, 100, 100);
+  properties.type = PlatformWindowType::kWindow;
+  auto toplevel_window = toplevel_delegate.CreateWaylandWindow(
+      connection_.get(), std::move(properties));
+  ASSERT_TRUE(toplevel_window);
+
+  EXPECT_CALL(toplevel_delegate,
+              OnWindowStateChanged(::testing::_, ::testing::_))
+      .WillOnce(
+          ::testing::InvokeWithoutArgs([&]() { toplevel_window.reset(); }));
+
+  // This should not crash.
+  toplevel_window->Maximize();
+}
+
 INSTANTIATE_TEST_SUITE_P(XdgVersionStableTest,
                          WaylandWindowTest,
                          Values(wl::ServerConfig{}));
Loading diff…

Original Bug Report

reported by [email protected]

Potential Use-After-Free in WaylandBubble due to synchronous widget destruction in OnStateUpdate

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 WaylandBubble on Linux Wayland due to synchronous widget destruction during bounds or scale updates. When the calling window state-request pipeline synchronously destroys the widget, the call stack unwinds back to WaylandBubble override methods, which continue to access members of the freed object. This issue resides in the browser process, affecting systems running Chrome on Linux under Wayland.

Affected files:

  • ui/ozone/platform/wayland/host/wayland_bubble.cc
  • ui/ozone/platform/wayland/host/wayland_bubble.h

Estimated timestamp from git blame: 2024-03-30

Root Cause Analysis

Inside ui/ozone/platform/wayland/host/wayland_bubble.cc, multiple overrides of WaylandBubble call into the base WaylandWindow state-request machinery. These base class methods can synchronously apply the requested state and notify delegates, which can lead to the synchronous destruction of the widget (and the calling WaylandBubble instance itself). When control returns from the base class to the subclass overrides, execution continues on the freed this pointer with no liveness check, despite the class implementing a WeakPtrFactory.

For example, in WaylandBubble::SetBoundsInDIP:

void WaylandBubble::SetBoundsInDIP(const gfx::Rect& bounds_dip) {
  auto old_bounds_dip = GetBoundsInDIP();
  WaylandWindow::SetBoundsInDIP(bounds_dip);          // May synchronously free |this|
  if (subsurface_ && old_bounds_dip != bounds_dip) {  // Potential UAF read of this->subsurface_
    SetSubsurfacePosition();                          // Potential UAF dereference
  }
}

Similarly, WaylandBubble::UpdateWindowScale is vulnerable:

void WaylandBubble::UpdateWindowScale(bool update_bounds) {
  WaylandWindow::UpdateWindowScale(update_bounds);    // May synchronously free |this|
  if (subsurface_) {                                  // Potential UAF read of this->subsurface_
    SetSubsurfacePosition();                          // Potential UAF dereference
  }
}

Potential Trigger Path

An attacker controlling web content could potentially trigger this by inducing a state update that leads to synchronous window destruction through the following sequence:

  1. Web content interacts with a UI component that creates a bubble window (such as an Autofill popup or custom menu widget) implemented as a WaylandBubble.
  2. The UI layout engine modifies the bounds or preferred scale of the widget, invoking WaylandBubble::SetBoundsInDIP or WaylandBubble::UpdateWindowScale.
  3. The override delegates to the base class implementation, e.g., WaylandWindow::SetBoundsInDIP -> RequestStateFromClient -> MaybeApplyLatestStateRequest.
  4. Inside MaybeApplyLatestStateRequest, the code synchronously calls delegate()->OnStateUpdate().
  5. The delegate notifies window tree and widget observers of the bounds or scale change. If an observer synchronously tears down or closes the widget (for instance, if the layout change places the bubble out-of-bounds or invalidates its parent window), the WaylandBubble object is synchronously deleted.
  6. While the base class WaylandWindow::MaybeApplyLatestStateRequest correctly guards against this destruction via a WeakPtr check (AsWeakPtr()), the WaylandBubble overrides do not perform any check when the stack unwinds back to them, resulting in a Use-After-Free when accessing subsurface_ or calling SetSubsurfacePosition().

Note: Our tooling does not currently have the capability to run a live proof-of-concept exploit, so these steps represent a potential execution flow based on static analysis of the source code.

Suggested Fix

To prevent this vulnerability, introduce weak pointer checks immediately after calls to base class methods that handle state changes, ensuring that execution returns early if the window has been destroyed:

void WaylandBubble::SetBoundsInDIP(const gfx::Rect& bounds_dip) {
  auto old_bounds_dip = GetBoundsInDIP();
  auto weak_this = AsWeakPtr();
  WaylandWindow::SetBoundsInDIP(bounds_dip);
  if (!weak_this) {
    return;
  }

  if (subsurface_ && old_bounds_dip != bounds_dip) {
    SetSubsurfacePosition();
  }
}

void WaylandBubble::UpdateWindowScale(bool update_bounds) {
  auto weak_this = AsWeakPtr();
  WaylandWindow::UpdateWindowScale(update_bounds);
  if (!weak_this) {
    return;
  }

  if (subsurface_) {
    SetSubsurfacePosition();
  }
}

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.

View on issue tracker