Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Ozone
DescriptionUse after free in Ozone
ComponentOzone
Bug ClassUAF
Tracker518007484
Fix commit87abcfa5b16b (chromium/src) +149/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-14

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
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