Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Ozone
DescriptionUse after free in Ozone
ComponentOzone
Bug ClassUAF
Tracker518007821
Fix commit273ca3dff1ee (chromium/src) +55/-19
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
ui/ozone/platform/wayland/host/wayland_bubble.cc
modified
if
ui/ozone/platform/wayland/host/wayland_popup.cc
modified
TEST_P
ui/ozone/platform/wayland/host/wayland_window_unittest.cc
modified
BindLambdaForTesting
ui/ozone/platform/wayland/host/wayland_window_unittest.cc
modified
if
ui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.cc
modified
WaylandWindow
ui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.h
modified
MockWaylandPlatformWindowDelegate
ui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.h
modified

Files Changed

  • ui/ozone/platform/wayland/host/wayland_bubble.cc
  • ui/ozone/platform/wayland/host/wayland_popup.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 273ca3dff1ee9ce9b3e27800b11cdf5de9572928 Mon Sep 17 00:00:00 2001
From: Tom Anderson <thomasanderson@chromium.org>
Date: Mon, 01 Jun 2026 15:18:29 -0700
Subject: [PATCH] [Ozone/Wayland] Fix Use-After-Free in SetBoundsInDIP

When window bounds are modified via SetBoundsInDIP(), the base-class
call WaylandWindow::SetBoundsInDIP can synchronously notify observers of
state updates. If an observer triggers window destruction, the
underlying platform window is synchronously deleted.

This CL adds weak pointer liveness checks in WaylandPopup and
WaylandBubble immediately after the base class call to prevent
subsequent member access on a freed instance.

Bug: 518007821
Test: WaylandWindowTest.WaylandPopupSetBoundsUaf, WaylandWindowTest.WaylandBubbleSetBoundsUaf
Change-Id: Iaea6741e49644c4fa06fe72185db4684365ed767
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7886516
Reviewed-by: Thomas Lukaszewicz <tluk@chromium.org>
Auto-Submit: Thomas Anderson <thomasanderson@chromium.org>
Commit-Queue: Thomas Lukaszewicz <tluk@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1639722}
---

diff --git a/ui/ozone/platform/wayland/host/wayland_bubble.cc b/ui/ozone/platform/wayland/host/wayland_bubble.cc
index 5b1a967..ef876a7 100644
--- a/ui/ozone/platform/wayland/host/wayland_bubble.cc
+++ b/ui/ozone/platform/wayland/host/wayland_bubble.cc
@@ -61,7 +61,11 @@
   // There is currently no guarantee that the 2 compositor frames arrive
   // together atomically.
   auto old_bounds_dip = GetBoundsInDIP();
+  auto weak_this = AsWeakPtr();
   WaylandWindow::SetBoundsInDIP(bounds_dip);
+  if (!weak_this) {
+    return;
+  }
 
   // TODO(crbug.com/329145822): Don't apply position immediately here, wait for
   // ackconfigure, otherwise it might jitter if offset changes.
diff --git a/ui/ozone/platform/wayland/host/wayland_popup.cc b/ui/ozone/platform/wayland/host/wayland_popup.cc
index c19d680..8b5b03e 100644
--- a/ui/ozone/platform/wayland/host/wayland_popup.cc
+++ b/ui/ozone/platform/wayland/host/wayland_popup.cc
@@ -180,7 +180,11 @@
   }
 
   auto old_bounds_dip = GetBoundsInDIP();
+  auto weak_this = AsWeakPtr();
   WaylandWindow::SetBoundsInDIP(bounds_dip);
+  if (!weak_this) {
+    return;
+  }
 
   // The shell popup can be null if bounds are being fixed during
   // the initialization. See WaylandPopup::CreateShellPopup.
diff --git a/ui/ozone/platform/wayland/host/wayland_window_unittest.cc b/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
index 7aea341..cca1682 100644
--- a/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
+++ b/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
@@ -5467,6 +5467,41 @@
   EXPECT_FALSE(window_);
 }
 
+TEST_P(WaylandWindowTest, WaylandPopupSetBoundsUaf) {
+  MockWaylandPlatformWindowDelegate popup_delegate(connection_.get());
+  gfx::Rect popup_bounds(10, 10, 50, 50);
+  auto wayland_popup =
+      CreateWaylandWindowWithParams(PlatformWindowType::kPopup, popup_bounds,
+                                    &popup_delegate, window_->GetWidget());
+  ASSERT_TRUE(wayland_popup);
+
+  popup_delegate.set_on_state_update_callback(base::BindLambdaForTesting([&]() {
+    wayland_popup.reset();
+    return true;
+  }));
+
+  // This should not crash if the fix is applied.
+  wayland_popup->SetBoundsInDIP(gfx::Rect(15, 15, 60, 60));
+}
+
+TEST_P(WaylandWindowTest, WaylandBubbleSetBoundsUaf) {
+  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 if the fix is applied.
+  wayland_bubble->SetBoundsInDIP(gfx::Rect(15, 15, 60, 60));
+}
+
 INSTANTIATE_TEST_SUITE_P(XdgVersionStableTest,
                          WaylandWindowTest,
                          Values(wl::ServerConfig{}));
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 9ade411..f8c06cea 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
@@ -10,21 +10,12 @@
 
 namespace ui {
 
-void MockWaylandPlatformWindowDelegate::OnWindowRemoved(WaylandWindow* window) {
-  if (wayland_window_ == window) {
-    wayland_window_ = nullptr;
-  }
-}
-
 MockWaylandPlatformWindowDelegate::MockWaylandPlatformWindowDelegate(
     raw_ptr<WaylandConnection> connection)
-    : connection_(connection) {
-  connection_->window_manager()->AddObserver(this);
-}
+    : connection_(connection) {}
 
-MockWaylandPlatformWindowDelegate::~MockWaylandPlatformWindowDelegate() {
-  connection_->window_manager()->RemoveObserver(this);
-}
+MockWaylandPlatformWindowDelegate::~MockWaylandPlatformWindowDelegate() =
+    default;
 
 gfx::Rect MockWaylandPlatformWindowDelegate::ConvertRectToPixels(
     const gfx::Rect& rect_in_dp) const {
@@ -45,7 +36,7 @@
     WaylandConnection* connection,
     PlatformWindowInitProperties properties) {
   auto window = WaylandWindow::Create(this, connection, std::move(properties));
-  wayland_window_ = window.get();
+  wayland_window_ = window->AsWeakPtr();
   return window;
 }
 
@@ -60,12 +51,18 @@
       old.window_scale != latest.window_scale) {
     bool origin_changed = old.bounds_dip.origin() != latest.bounds_dip.origin();
     OnBoundsChanged({origin_changed});
+    if (!wayland_window_) {
+      return -1;
+    }
   }
 
   if (!on_state_update_callback_.is_null()) {
     if (!on_state_update_callback_.Run()) {
       return -1;
     }
+    if (!wayland_window_) {
+      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 d9cbcc6..545720df 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
@@ -7,7 +7,6 @@
 
 #include "base/functional/callback.h"
 #include "base/memory/raw_ptr.h"
-#include "ui/ozone/platform/wayland/host/wayland_window_observer.h"
 #include "ui/ozone/test/mock_platform_window_delegate.h"
 
 namespace ui {
@@ -15,8 +14,7 @@
 class WaylandWindow;
 struct PlatformWindowInitProperties;
 
-class MockWaylandPlatformWindowDelegate : public MockPlatformWindowDelegate,
-                                          public WaylandWindowObserver {
+class MockWaylandPlatformWindowDelegate : public MockPlatformWindowDelegate {
  public:
   MockWaylandPlatformWindowDelegate(raw_ptr<WaylandConnection> connection);
   MockWaylandPlatformWindowDelegate(const MockWaylandPlatformWindowDelegate&) =
@@ -45,11 +43,9 @@
   }
 
  private:
-  // WaylandWindowObserver:
-  void OnWindowRemoved(WaylandWindow* window) override;
   raw_ptr<WaylandConnection> connection_ = nullptr;
 
-  raw_ptr<WaylandWindow> wayland_window_ = nullptr;
+  base::WeakPtr<WaylandWindow> wayland_window_;
 
   // |viz_seq_| is used to save an incrementing sequence point on each
   // call to InsertSequencePoint. Test code can check this value to know
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 7aea341..cca1682 100644
--- a/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
+++ b/ui/ozone/platform/wayland/host/wayland_window_unittest.cc
@@ -5467,6 +5467,41 @@
   EXPECT_FALSE(window_);
 }
 
+TEST_P(WaylandWindowTest, WaylandPopupSetBoundsUaf) {
+  MockWaylandPlatformWindowDelegate popup_delegate(connection_.get());
+  gfx::Rect popup_bounds(10, 10, 50, 50);
+  auto wayland_popup =
+      CreateWaylandWindowWithParams(PlatformWindowType::kPopup, popup_bounds,
+                                    &popup_delegate, window_->GetWidget());
+  ASSERT_TRUE(wayland_popup);
+
+  popup_delegate.set_on_state_update_callback(base::BindLambdaForTesting([&]() {
+    wayland_popup.reset();
+    return true;
+  }));
+
+  // This should not crash if the fix is applied.
+  wayland_popup->SetBoundsInDIP(gfx::Rect(15, 15, 60, 60));
+}
+
+TEST_P(WaylandWindowTest, WaylandBubbleSetBoundsUaf) {
+  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 if the fix is applied.
+  wayland_bubble->SetBoundsInDIP(gfx::Rect(15, 15, 60, 60));
+}
+
 INSTANTIATE_TEST_SUITE_P(XdgVersionStableTest,
                          WaylandWindowTest,
                          Values(wl::ServerConfig{}));
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 9ade411..f8c06cea 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
@@ -10,21 +10,12 @@
 
 namespace ui {
 
-void MockWaylandPlatformWindowDelegate::OnWindowRemoved(WaylandWindow* window) {
-  if (wayland_window_ == window) {
-    wayland_window_ = nullptr;
-  }
-}
-
 MockWaylandPlatformWindowDelegate::MockWaylandPlatformWindowDelegate(
     raw_ptr<WaylandConnection> connection)
-    : connection_(connection) {
-  connection_->window_manager()->AddObserver(this);
-}
+    : connection_(connection) {}
 
-MockWaylandPlatformWindowDelegate::~MockWaylandPlatformWindowDelegate() {
-  connection_->window_manager()->RemoveObserver(this);
-}
+MockWaylandPlatformWindowDelegate::~MockWaylandPlatformWindowDelegate() =
+    default;
 
 gfx::Rect MockWaylandPlatformWindowDelegate::ConvertRectToPixels(
     const gfx::Rect& rect_in_dp) const {
@@ -45,7 +36,7 @@
     WaylandConnection* connection,
     PlatformWindowInitProperties properties) {
   auto window = WaylandWindow::Create(this, connection, std::move(properties));
-  wayland_window_ = window.get();
+  wayland_window_ = window->AsWeakPtr();
   return window;
 }
 
@@ -60,12 +51,18 @@
       old.window_scale != latest.window_scale) {
     bool origin_changed = old.bounds_dip.origin() != latest.bounds_dip.origin();
     OnBoundsChanged({origin_changed});
+    if (!wayland_window_) {
+      return -1;
+    }
   }
 
   if (!on_state_update_callback_.is_null()) {
     if (!on_state_update_callback_.Run()) {
       return -1;
     }
+    if (!wayland_window_) {
+      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 d9cbcc6..545720df 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
@@ -7,7 +7,6 @@
 
 #include "base/functional/callback.h"
 #include "base/memory/raw_ptr.h"
-#include "ui/ozone/platform/wayland/host/wayland_window_observer.h"
 #include "ui/ozone/test/mock_platform_window_delegate.h"
 
 namespace ui {
@@ -15,8 +14,7 @@
 class WaylandWindow;
 struct PlatformWindowInitProperties;
 
-class MockWaylandPlatformWindowDelegate : public MockPlatformWindowDelegate,
-                                          public WaylandWindowObserver {
+class MockWaylandPlatformWindowDelegate : public MockPlatformWindowDelegate {
  public:
   MockWaylandPlatformWindowDelegate(raw_ptr<WaylandConnection> connection);
   MockWaylandPlatformWindowDelegate(const MockWaylandPlatformWindowDelegate&) =
@@ -45,11 +43,9 @@
   }
 
  private:
-  // WaylandWindowObserver:
-  void OnWindowRemoved(WaylandWindow* window) override;
   raw_ptr<WaylandConnection> connection_ = nullptr;
 
-  raw_ptr<WaylandWindow> wayland_window_ = nullptr;
+  base::WeakPtr<WaylandWindow> wayland_window_;
 
   // |viz_seq_| is used to save an incrementing sequence point on each
   // call to InsertSequencePoint. Test code can check this value to know
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Use-After-Free in WaylandPopup and WaylandBubble during SetBoundsInDIP

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 vulnerability exists in the Ozone Wayland platform backend of Chromium. When modifying window bounds, the base class execution can synchronously trigger state updates that destroy the platform window. The subclass implementations of SetBoundsInDIP do not perform a liveness check upon returning from the base class, leading to subsequent member access and virtual method calls on the freed instance.

Affected files:

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

Estimated timestamp from git blame: Unknown (Google3 checkout)

Description

A potential Use-After-Free (UAF) vulnerability has been identified in Chromium’s Ozone Wayland backend within the WaylandPopup and WaylandBubble classes. When window bounds are modified via SetBoundsInDIP(), the base-class call WaylandWindow::SetBoundsInDIP can synchronously notify observers of state updates. If an observer triggers widget or window destruction, the underlying platform window is synchronously deleted. Upon returning from the base-class call, the subclass implementations do not perform any liveness checks and continue executing, which results in reads, writes, and virtual method calls on the freed object.

Potential Root Cause Analysis

In ui/ozone/platform/wayland/host/wayland_popup.cc:

void WaylandPopup::SetBoundsInDIP(const gfx::Rect& bounds_dip) {
  auto* xdg_parent_window = GetXdgParentWindow();
  if (!xdg_parent_window) {
    return;
  }

  auto old_bounds_dip = GetBoundsInDIP();
  WaylandWindow::SetBoundsInDIP(bounds_dip);          // May synchronously free `this`

  // Missing liveness check here
  if (xdg_popup_ && old_bounds_dip != bounds_dip) {   // Potential UAF READ
    auto bounds_dip_in_parent =
        wl::TranslateWindowBoundsToParentDIP(this, xdg_parent_window);  // Potential UAF of `this`
    bounds_dip_in_parent.Inset(
        delegate()->CalculateInsetsInDIP(GetPlatformWindowState()));    // Virtual call on freed delegate
    ...

Similarly, in ui/ozone/platform/wayland/host/wayland_bubble.cc:

void WaylandBubble::SetBoundsInDIP(const gfx::Rect& bounds_dip) {
  auto old_bounds_dip = GetBoundsInDIP();
  WaylandWindow::SetBoundsInDIP(bounds_dip); // May synchronously free `this`

  // Missing liveness check here
  if (subsurface_ && old_bounds_dip != bounds_dip) { // Potential UAF READ
    SetSubsurfacePosition();
  }
}

The base class implementation WaylandWindow::MaybeApplyLatestStateRequest notifies delegates of state changes via delegate()->OnStateUpdate(old, latest.state). Under certain widget closure flows, the delegate handles the bounds update and synchronously destroys the platform window. Although the base class includes a weak_this liveness check to safely abort (ui/ozone/platform/wayland/host/wayland_window.cc), control returns directly to the subclass overridden methods which proceed to reference invalid members.

Potential Trigger Path

  1. An interaction (such as programmatically resizing, hovering to trigger a tooltip, or opening/positioning a popup context menu) is initiated.
  2. The views framework triggers a bounds change via WaylandPopup::SetBoundsInDIP or WaylandBubble::SetBoundsInDIP.
  3. The function delegates to WaylandWindow::SetBoundsInDIP, which requests a state change from the client side and synchronously applies it.
  4. During state application, delegate()->OnStateUpdate is called, notifying observers.
  5. An observer synchronously closes/destroys the widget, leading to the destruction of the underlying platform window.
  6. Control unwinds back to the subclass SetBoundsInDIP function, which attempts to access deallocated member pointers and invoke virtual functions on this.

Note: Since our tooling agent does not have the ability to run functional exploit code, these steps and consequences represent potential execution paths derived from static analysis of the codebase.

Impact

This issue occurs in the unsandboxed browser process on Linux platforms using Wayland. If successfully exploited, the resulting memory corruption could allow an attacker to achieve Remote Code Execution (RCE) in the context of the browser process or escape the renderer sandbox.

Suggested Fix

Both WaylandPopup and WaylandBubble implement AsWeakPtr(). Introduce a weak-pointer liveness check immediately after invoking the base class WaylandWindow::SetBoundsInDIP in both subclass implementations:

  auto weak_this = AsWeakPtr();
  WaylandWindow::SetBoundsInDIP(bounds_dip);
  if (!weak_this) {
    return;
  }

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