CVE-2026-13786
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifui/ozone/platform/wayland/host/wayland_bubble.cc |
modified | |
ifui/ozone/platform/wayland/host/wayland_popup.cc |
modified | |
TEST_Pui/ozone/platform/wayland/host/wayland_window_unittest.cc |
modified | |
BindLambdaForTestingui/ozone/platform/wayland/host/wayland_window_unittest.cc |
modified | |
ifui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.cc |
modified | |
WaylandWindowui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.h |
modified | |
MockWaylandPlatformWindowDelegateui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.h |
modified |
Files Changed
ui/ozone/platform/wayland/host/wayland_bubble.ccui/ozone/platform/wayland/host/wayland_popup.ccui/ozone/platform/wayland/host/wayland_window_unittest.ccui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.ccui/ozone/platform/wayland/test/mock_wayland_platform_window_delegate.h
Patch
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
Regression Test / PoC
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
Original Bug Report
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.ccui/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
- An interaction (such as programmatically resizing, hovering to trigger a tooltip, or opening/positioning a popup context menu) is initiated.
- The views framework triggers a bounds change via
WaylandPopup::SetBoundsInDIPorWaylandBubble::SetBoundsInDIP. - The function delegates to
WaylandWindow::SetBoundsInDIP, which requests a state change from the client side and synchronously applies it. - During state application,
delegate()->OnStateUpdateis called, notifying observers. - An observer synchronously closes/destroys the widget, leading to the destruction of the underlying platform window.
- Control unwinds back to the subclass
SetBoundsInDIPfunction, which attempts to access deallocated member pointers and invoke virtual functions onthis.
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.