CVE-2026-113316
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifui/views/widget/native_widget_aura.cc |
modified | |
SyncWidgetCloserOnVisibilityChangeui/views/widget/native_widget_aura_unittest.cc |
modified | |
ifui/views/widget/native_widget_aura_unittest.cc |
modified | |
TEST_Fui/views/widget/native_widget_aura_unittest.cc |
modified |
Files Changed
ui/views/widget/native_widget_aura_unittest.ccui/views/widget/native_widget_aura.cc
Patch
From 97aa60e7d1820d5ed079732d89147352534df7dc Mon Sep 17 00:00:00 2001
From: Thomas Lukaszewicz <tluk@chromium.org>
Date: Mon, 21 Sep 2026 11:10:16 -0700
Subject: [PATCH] [Views] Prevent potential UAF in NativeWidgetAura::Show()
Both window_->Show() and Activate() notify aura::Window observers,
which are explicitly permitted to synchronously destroy window_ during
visibility notifications. Under NATIVE_WIDGET_OWNS_WIDGET or
CLIENT_OWNS_WIDGET ownership models, OnWindowDestroyed() deletes `this`.
Continuing execution in Show() after the window is destroyed results in
a heap-use-after-free when accessing member fields or invoking further
methods (e.g. Activate(), SetInitialFocus(), Minimize()).
Use a WeakPtr to verify that `this` and `window_` remain valid after
invoking window_->Show() and Activate(), returning early if destroyed.
Add a regression test covering synchronous widget destruction during
Show().
Fixed: 553140050
Change-Id: I855479577166b018d94388c3e5a0d6326c228390
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8423564
Reviewed-by: Allen Bauer <kylixrd@chromium.org>
Commit-Queue: Thomas Lukaszewicz <tluk@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1701883}
---
diff --git a/ui/views/widget/native_widget_aura.cc b/ui/views/widget/native_widget_aura.cc
index 43c789c..4537dbb 100644
--- a/ui/views/widget/native_widget_aura.cc
+++ b/ui/views/widget/native_widget_aura.cc
@@ -795,10 +795,24 @@
disabler.emplace(window_);
}
+ // `window_->Show()` and `Activate()` below fan out to aura::Window
+ // observers, which are explicitly permitted to synchronously destroy
+ // `window_` (see Window::NotifyWindowVisibilityChangedAtReceiver()). When
+ // that happens under NATIVE_WIDGET_OWNS_WIDGET or CLIENT_OWNS_WIDGET
+ // ownership, OnWindowDestroyed() deletes `this`, so the rest of this method
+ // must not run.
+ auto weak_this = weak_factory.GetWeakPtr();
+
window_->Show();
+ if (!weak_this || !window_) {
+ return;
+ }
if (delegate_->CanActivate()) {
if (show_state != ui::mojom::WindowShowState::kInactive) {
Activate();
+ if (!weak_this || !window_) {
+ return;
+ }
}
// SetInitialFocus() should be always be called, even for
// SHOW_STATE_INACTIVE. If the window has to stay inactive, the method will
diff --git a/ui/views/widget/native_widget_aura_unittest.cc b/ui/views/widget/native_widget_aura_unittest.cc
index 0ccbf8f..3a342a0 100644
--- a/ui/views/widget/native_widget_aura_unittest.cc
+++ b/ui/views/widget/native_widget_aura_unittest.cc
@@ -1261,4 +1261,68 @@
widget->Close();
}
+namespace {
+
+// Closes (and thereby destroys) a widget's aura::Window synchronously from
+// within the window-visibility-changed observer fan-out that
+// aura::Window::Show() dispatches. Aura explicitly permits observers to
+// delete the window during this notification (see
+// Window::NotifyWindowVisibilityChangedAtReceiver). This models production
+// destroyer chains in which showing a window synchronously re-enters
+// hide/teardown logic that closes the widget (e.g. an overview-force-hidden
+// window being re-hidden by ScopedOverviewHideWindows, whose nested Hide()
+// runs WindowEventDispatcher::OnWindowHidden gesture-cancel/capture-release
+// handlers that close the widget).
+class SyncWidgetCloserOnVisibilityChange : public aura::WindowObserver {
+ public:
+ SyncWidgetCloserOnVisibilityChange(aura::Window* window, Widget* widget)
+ : widget_(widget) {
+ observation_.Observe(window);
+ }
+
+ // aura::WindowObserver:
+ void OnWindowVisibilityChanged(aura::Window* window, bool visible) override {
+ if (!visible || !widget_) {
+ return;
+ }
+ observation_.Reset();
+ Widget* widget = widget_;
+ widget_ = nullptr;
+ // Widget::CloseNow() -> NativeWidgetAura::CloseNow() -> delete window_ ->
+ // ~Window -> NativeWidgetAura::OnWindowDestroyed() -> delete this
+ // (ownership is CLIENT_OWNS_WIDGET), all synchronously while
+ // NativeWidgetAura::Show() is still on the stack.
+ widget->CloseNow();
+ }
+
+ private:
+ raw_ptr<Widget> widget_;
+ base::ScopedObservation<aura::Window, aura::WindowObserver> observation_{
+ this};
+};
+
+} // namespace
+
+// Regression test for a use-after-free in NativeWidgetAura::Show(): the
+// window-visibility observer fan-out triggered by window_->Show() may
+// synchronously destroy the aura::Window. Under NATIVE_WIDGET_OWNS_WIDGET or
+// CLIENT_OWNS_WIDGET ownership, NativeWidgetAura::OnWindowDestroyed() then
+// runs `delete this`, and the remainder of Show() (delegate_->CanActivate(),
+// Activate(), IsActive(), SetInitialFocus(), Minimize()) executes on the
+// freed NativeWidgetAura (heap-use-after-free under ASAN).
+TEST_F(NativeWidgetAuraTest, ShowWithSynchronousWindowDestruction) {
+ auto widget = std::make_unique<Widget>();
+ Widget::InitParams params(Widget::InitParams::Ownership::CLIENT_OWNS_WIDGET,
+ Widget::InitParams::TYPE_POPUP);
+ params.parent = root_window();
+ widget->Init(std::move(params));
+
+ SyncWidgetCloserOnVisibilityChange closer(widget->GetNativeView(),
+ widget.get());
+
+ // The NativeWidgetAura is deleted inside window_->Show(); the tail of
+ // NativeWidgetAura::Show() must not touch |this| afterwards.
+ widget->Show();
+}
+
} // namespace views
Regression Test / PoC
diff --git a/ui/views/widget/native_widget_aura_unittest.cc b/ui/views/widget/native_widget_aura_unittest.cc
index 0ccbf8f..3a342a0 100644
--- a/ui/views/widget/native_widget_aura_unittest.cc
+++ b/ui/views/widget/native_widget_aura_unittest.cc
@@ -1261,4 +1261,68 @@
widget->Close();
}
+namespace {
+
+// Closes (and thereby destroys) a widget's aura::Window synchronously from
+// within the window-visibility-changed observer fan-out that
+// aura::Window::Show() dispatches. Aura explicitly permits observers to
+// delete the window during this notification (see
+// Window::NotifyWindowVisibilityChangedAtReceiver). This models production
+// destroyer chains in which showing a window synchronously re-enters
+// hide/teardown logic that closes the widget (e.g. an overview-force-hidden
+// window being re-hidden by ScopedOverviewHideWindows, whose nested Hide()
+// runs WindowEventDispatcher::OnWindowHidden gesture-cancel/capture-release
+// handlers that close the widget).
+class SyncWidgetCloserOnVisibilityChange : public aura::WindowObserver {
+ public:
+ SyncWidgetCloserOnVisibilityChange(aura::Window* window, Widget* widget)
+ : widget_(widget) {
+ observation_.Observe(window);
+ }
+
+ // aura::WindowObserver:
+ void OnWindowVisibilityChanged(aura::Window* window, bool visible) override {
+ if (!visible || !widget_) {
+ return;
+ }
+ observation_.Reset();
+ Widget* widget = widget_;
+ widget_ = nullptr;
+ // Widget::CloseNow() -> NativeWidgetAura::CloseNow() -> delete window_ ->
+ // ~Window -> NativeWidgetAura::OnWindowDestroyed() -> delete this
+ // (ownership is CLIENT_OWNS_WIDGET), all synchronously while
+ // NativeWidgetAura::Show() is still on the stack.
+ widget->CloseNow();
+ }
+
+ private:
+ raw_ptr<Widget> widget_;
+ base::ScopedObservation<aura::Window, aura::WindowObserver> observation_{
+ this};
+};
+
+} // namespace
+
+// Regression test for a use-after-free in NativeWidgetAura::Show(): the
+// window-visibility observer fan-out triggered by window_->Show() may
+// synchronously destroy the aura::Window. Under NATIVE_WIDGET_OWNS_WIDGET or
+// CLIENT_OWNS_WIDGET ownership, NativeWidgetAura::OnWindowDestroyed() then
+// runs `delete this`, and the remainder of Show() (delegate_->CanActivate(),
+// Activate(), IsActive(), SetInitialFocus(), Minimize()) executes on the
+// freed NativeWidgetAura (heap-use-after-free under ASAN).
+TEST_F(NativeWidgetAuraTest, ShowWithSynchronousWindowDestruction) {
+ auto widget = std::make_unique<Widget>();
+ Widget::InitParams params(Widget::InitParams::Ownership::CLIENT_OWNS_WIDGET,
+ Widget::InitParams::TYPE_POPUP);
+ params.parent = root_window();
+ widget->Init(std::move(params));
+
+ SyncWidgetCloserOnVisibilityChange closer(widget->GetNativeView(),
+ widget.get());
+
+ // The NativeWidgetAura is deleted inside window_->Show(); the tail of
+ // NativeWidgetAura::Show() must not touch |this| afterwards.
+ widget->Show();
+}
+
} // namespace views
Original Bug Report
Potential Use-After-Free in NativeWidgetAura::Show() via Synchronous Window Deletion
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: NativeWidgetAura::Show() invokes window_->Show() and Activate() without verifying whether the NativeWidgetAura instance remains alive after re-entrant calls. Under NATIVE_WIDGET_OWNS_WIDGET or CLIENT_OWNS_WIDGET ownership models, synchronous destruction of the underlying aura::Window triggers OnWindowDestroyed(), which executes delete this. Subsequent member access in the tail of Show() executes on freed memory in the unsandboxed browser process.
Affected files:
ui/views/widget/native_widget_aura.ccui/views/widget/native_widget_aura.h
Estimated timestamp from git blame: 2018-08-15
Summary
In ui/views/widget/native_widget_aura.cc, NativeWidgetAura::Show() invokes methods that can trigger synchronous window destruction (window_->Show() at line 794 and Activate() at line 797). When the widget uses NATIVE_WIDGET_OWNS_WIDGET or CLIENT_OWNS_WIDGET ownership, synchronous window destruction causes NativeWidgetAura::OnWindowDestroyed() to execute delete this. Because NativeWidgetAura::Show() lacks a base::WeakPtr liveness check, subsequent tail code executes on a freed this pointer.
Note: The reproduction steps described below are potential/suggested steps derived from static code analysis; our automated tooling does not currently have the capability to execute an end-to-end dynamic proof of concept.
Vulnerability Details
Root Cause
In ui/views/widget/native_widget_aura.cc:771-814:
void NativeWidgetAura::Show(ui::mojom::WindowShowState show_state,
const gfx::Rect& restore_bounds) {
if (!window_) {
return;
}
...
window_->Show(); // Line 794: Re-entrant synchronous destruction possible
if (delegate_->CanActivate()) { // Line 795: UAF on freed this
if (show_state != ui::mojom::WindowShowState::kInactive) {
Activate(); // Line 797: Re-entrant synchronous destruction possible
}
SetInitialFocus(IsActive() ? show_state
: ui::mojom::WindowShowState::kInactive);
}
if (show_state == ui::mojom::WindowShowState::kMinimized) {
Minimize();
}
}
When aura::Window::Show() is called (ui/aura/window.cc:445), it executes SetVisibleInternal(true) (ui/aura/window.cc:1323). While internal delegate calls are protected by a ScopedDeleteBlocker, the subsequent observer notification NotifyWindowVisibilityChanged(this, visible) (ui/aura/window.cc:1355) executes with no delete blocker active. aura::Window explicitly documents that observers may delete the window synchronously during this notification (ui/aura/window.cc:1579-1581).
When aura::Window::~Window() executes, it invokes delegate_->OnWindowDestroyed(this) (ui/aura/window.cc:290), routing to NativeWidgetAura::OnWindowDestroyed() (ui/views/widget/native_widget_aura.cc:1227-1240):
void NativeWidgetAura::OnWindowDestroyed(aura::Window* window) {
window_ = nullptr;
bool should_delete_this =
(ownership_ == Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET) ||
(ownership_ == Widget::InitParams::CLIENT_OWNS_WIDGET);
if (delegate_) {
delegate_->OnNativeWidgetDestroyed();
}
if (should_delete_this) {
delete this;
}
}
After delete this frees the NativeWidgetAura instance, the stack unwinds to NativeWidgetAura::Show() at line 795, which immediately attempts to dereference this->delegate_ (base::WeakPtr<internal::NativeWidgetDelegate>) and perform a virtual method dispatch (CanActivate()), followed by SetInitialFocus() and Minimize().
Mitigation Analysis (MiraclePtr / BRP)
BackupRefPtr does not mitigate this UAF because:
- The dangling reference being accessed is the bare C++
thispointer on the call stack, not araw_ptr<T>field. - In
views::Widget, the reference toNativeWidgetPrivateis held asbase::WeakPtr<internal::NativeWidgetPrivate> native_widget_(ui/views/widget/widget.h:1681). Thus, there are zero activeraw_ptrreferences pinning theNativeWidgetAuraallocation when it is deleted, allowing PartitionAlloc to reclaim the memory slot immediately.
Potential Trigger Steps
- Target a ChromeOS / Desktop Aura widget with
NATIVE_WIDGET_OWNS_WIDGETorCLIENT_OWNS_WIDGETownership (such as a ChromeOS browser window or system UI widget). - Place the widget in a state where a visibility transition or activation triggers a synchronous destruction callback (for example, via
ScopedOverviewHideWindowswithforced_hidden_ == true, where a nestedHide()leads to gesture state cleanup / capture release that closes the widget, or via an activation observer cascade duringActivateWindow()). - Cause
Widget::Show()to be invoked (e.g., via extension APIs such aschrome.windows.updateor window management transitions). window_->Show()triggers synchronous destruction of theaura::Windowanddelete thisonNativeWidgetAura.- Execution resumes at
ui/views/widget/native_widget_aura.cc:795, accessing members of the freedNativeWidgetAura.
Suggested Fix
Guard NativeWidgetAura::Show() using base::WeakPtr<NativeWidgetAura> before calling destruction-capable methods:
--- a/ui/views/widget/native_widget_aura.cc
+++ b/ui/views/widget/native_widget_aura.cc
@@ -791,10 +791,18 @@ void NativeWidgetAura::Show(ui::mojom::WindowShowState show_state,
disabler.emplace(window_);
}
+ base::WeakPtr<NativeWidgetAura> weak_this = weak_factory.GetWeakPtr();
window_->Show();
+ if (!weak_this) {
+ return;
+ }
+
if (delegate_->CanActivate()) {
if (show_state != ui::mojom::WindowShowState::kInactive) {
Activate();
+ if (!weak_this) {
+ return;
+ }
}
SetInitialFocus(IsActive() ? show_state
: ui::mojom::WindowShowState::kInactive);
Evaluated with Chrome root at commit: c6718ebbf2c677c6f63691e28c7801c563f6b19c
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.