CVE-2026-78983
Overview
Background
- `views::Widget`
- the top-level window object in Chrome’s Views UI toolkit. It runs in the browser process and backs windows, bubbles, menus and dialogs.
- `WidgetObserver::OnWidgetVisibilityChanged`
- an observer callback that
Widgetsends throughobservers_.Notify(...)when the widget’s visibility changes. Observer code is arbitrary and can run before the notifying function returns. - `ScopedCallStackLock`
- an RAII guard in
widget.ccthat marks theWidgetas being on the call stack. Going by the commit message and theEXPECT_CHECK_DEATHtests, destroying the widget while the guard is alive triggers aCHECKfailure instead of a silent free. - `CLIENT_OWNS_WIDGET`
- an ownership mode in which client code holds the
Widget(for example in astd::unique_ptr) and can destroy it at any time, including from inside an observer callback.
Root Cause Analysis
In the visibility-change path in ui/views/widget/widget.cc, Widget calls observers_.Notify(&WidgetObserver::OnWidgetVisibilityChanged, this, visible). Right after the notification it uses this again, through GetCompositor(), root->layer() and root->layer()->SetVisible(visible). The callers Show() and ShowInactive() also go on to run after the native show returns. If an observer synchronously destroys the widget during the notification, as the regression test’s DestroyOnVisibleObserver does by resetting the owning std::unique_ptr<Widget>, those later accesses touch freed memory. The violated invariant is that a Widget must not be destroyed while one of its own methods is still running on the stack.
The fix adds ScopedCallStackLock on_stack(this); immediately before the Notify call, so destruction during the callback now hits a deterministic CHECK failure (as both Show() and ShowInactive() tests expect) instead of a use-after-free. The enclosing function’s name does not appear in the hunk. The commit title describes it as the native show/hide visibility handling.
Widget handed control to arbitrary WidgetObserver code during a visibility change and then kept using this without guarding against being destroyed by that code. The fix wraps the notification in a ScopedCallStackLock, which turns re-entrant destruction from a use-after-free into a safe CHECK crash.Attack Path
- Widget is shown
Browser UI calls
Widget::Show()orWidget::ShowInactive()on a client-owned widget, which leads to native show and the visibility-change handler inwidget.cc. - Observer notified
The handler propagates visibility to the root view and then calls
observers_.Notify(&WidgetObserver::OnWidgetVisibilityChanged, ...). - Re-entrant destruction
An observer, directly or through code it runs synchronously, destroys the owning
std::unique_ptr<Widget>while the notification is still on the stack. - Stale `this` used
Without the guard, execution comes back into the freed
Widgetand uses it (GetCompositor(),root->layer(), and the remaining caller frames inShow()/ShowInactive()), which is a use-after-free in the browser process. - Memory corruption
With heap grooming, the freed
Widgetslot could be reclaimed with controlled data before reuse (inferred; the patch shows only the dangling use).
Impact Assessment
views::Widget in the browser process, the unsandboxed UI process. In principle it could lead to memory corruption with browser-process privileges (inferred). Triggering it needs a UI flow where some observer synchronously destroys a client-owned widget during show or hide. The patch does not show how web content or an extension could drive such a flow, so whether it can be reached remotely and whether it needs user interaction are not established (inferred). The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.3.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifui/views/widget/widget.cc |
modified | |
TEST_Fui/views/widget/widget_unittest.cc |
modified | |
DestroyOnVisibleObserverui/views/widget/widget_unittest.cc |
modified | |
ifui/views/widget/widget_unittest.cc |
modified |
Files Changed
ui/views/widget/widget_unittest.ccui/views/widget/widget.cc
Audit Directions
- Observer notification followed by `this` useIn
widget.ccand other Views classes (View,BubbleDialogDelegate,MenuController,DesktopNativeWidget*), look forobservers_.Notify(...)or delegate callbacks followed by member access with noScopedCallStackLock,WeakPtrcheck, orAutoResetdestruction guard. - Client-owned widget lifetimesReview
CLIENT_OWNS_WIDGETusers whose observers or delegates reset the owningunique_ptrfrom insideOnWidgetVisibilityChanged,OnWidgetActivationChanged,OnWidgetBoundsChangedor close callbacks, since those are the re-entrancy sources this guard now turns into crashes. - Native show/hide re-entrancyPlatform
NativeWidgetimplementations (Aura, Mac, desktop Windows/Linux) can send synchronous visibility or activation notifications from inside native show/hide calls. Check that every such entry point intoWidgetis guarded the same way.
Patch
From e9e0e0657060cf34c6bb8be63f68247bef53ed8e Mon Sep 17 00:00:00 2001 From: David Bienvenu <[email protected]> Date: Mon, 20 Jul 2026 16:32:24 -0700 Subject: [PATCH] views: Handle Widget destruction during native show/hide Use ScopedCallStackLock to handle widget destruction during show/hide. Add a unit tests that expects the CHECK on widget destruction. Bug: 519369088 Change-Id: I4653e4211ff68140487ce94689ddf5bffa3c88d3 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8112867 Reviewed-by: Allen Bauer <[email protected]> Commit-Queue: David Bienvenu <[email protected]> Cr-Commit-Position: refs/heads/main@{#1665021} --- diff --git a/ui/views/widget/widget.cc b/ui/views/widget/widget.cc index 455c727..f2592df 100644 --- a/ui/views/widget/widget.cc +++ b/ui/views/widget/widget.cc @@ -1991,6 +1991,7 @@ if (root) { root->PropagateVisibilityNotifications(nullptr, visible); } + ScopedCallStackLock on_stack(this); observers_.Notify(&WidgetObserver::OnWidgetVisibilityChanged, this, visible); if (GetCompositor() && root && root->layer()) { root->layer()->SetVisible(visible); diff --git a/ui/views/widget/widget_unittest.cc b/ui/views/widget/widget_unittest.cc index 5250ae2..9334e97 100644 --- a/ui/views/widget/widget_unittest.cc +++ b/ui/views/widget/widget_unittest.cc @@ -3148,6 +3148,49 @@ EXPECT_EQ(0, cursor_view->GetEventCount(ui::EventType::kMousewheel)); } +TEST_F(DesktopWidgetTest, ShowSurvivesWidgetDestructionInVisibilityChange) { + // Observer that synchronously destroys the owning unique_ptr when the widget + // becomes visible. + class DestroyOnVisibleObserver : public WidgetObserver { + public: + explicit DestroyOnVisibleObserver(std::unique_ptr<Widget> widget) + : widget_(std::move(widget)) { + widget_->AddObserver(this); + } + ~DestroyOnVisibleObserver() override { + if (widget_) { + widget_->RemoveObserver(this); + } + } + + void OnWidgetVisibilityChanged(Widget* widget, bool visible) override { + if (visible) { + widget->RemoveObserver(this); + widget_.reset(); + } + } + + Widget* widget() { return widget_.get(); } + + private: + std::unique_ptr<Widget> widget_; + }; + + { + DestroyOnVisibleObserver observer( + CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET, + Widget::InitParams::TYPE_WINDOW)); + EXPECT_CHECK_DEATH(observer.widget()->Show()); + } + + { + DestroyOnVisibleObserver observer( + CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET, + Widget::InitParams::TYPE_WINDOW)); + EXPECT_CHECK_DEATH(observer.widget()->ShowInactive()); + } +} + // Tests that if a scroll-begin gesture is not handled, then subsequent scroll // events are not dispatched to any view. TEST_F(WidgetTest, GestureScrollEventDispatching) {
Regression Test / PoC
diff --git a/ui/views/widget/widget_unittest.cc b/ui/views/widget/widget_unittest.cc
index 5250ae2..9334e97 100644
--- a/ui/views/widget/widget_unittest.cc
+++ b/ui/views/widget/widget_unittest.cc
@@ -3148,6 +3148,49 @@
EXPECT_EQ(0, cursor_view->GetEventCount(ui::EventType::kMousewheel));
}
+TEST_F(DesktopWidgetTest, ShowSurvivesWidgetDestructionInVisibilityChange) {
+ // Observer that synchronously destroys the owning unique_ptr when the widget
+ // becomes visible.
+ class DestroyOnVisibleObserver : public WidgetObserver {
+ public:
+ explicit DestroyOnVisibleObserver(std::unique_ptr<Widget> widget)
+ : widget_(std::move(widget)) {
+ widget_->AddObserver(this);
+ }
+ ~DestroyOnVisibleObserver() override {
+ if (widget_) {
+ widget_->RemoveObserver(this);
+ }
+ }
+
+ void OnWidgetVisibilityChanged(Widget* widget, bool visible) override {
+ if (visible) {
+ widget->RemoveObserver(this);
+ widget_.reset();
+ }
+ }
+
+ Widget* widget() { return widget_.get(); }
+
+ private:
+ std::unique_ptr<Widget> widget_;
+ };
+
+ {
+ DestroyOnVisibleObserver observer(
+ CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET,
+ Widget::InitParams::TYPE_WINDOW));
+ EXPECT_CHECK_DEATH(observer.widget()->Show());
+ }
+
+ {
+ DestroyOnVisibleObserver observer(
+ CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET,
+ Widget::InitParams::TYPE_WINDOW));
+ EXPECT_CHECK_DEATH(observer.widget()->ShowInactive());
+ }
+}
+
// Tests that if a scroll-begin gesture is not handled, then subsequent scroll
// events are not dispatched to any view.
TEST_F(WidgetTest, GestureScrollEventDispatching) {
Original Bug Report
Potential Use-After-Free in Widget::Show due to missing WeakPtr guard after native show
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 Widget::Show() and Widget::ShowInactive() due to a missing WeakPtr guard after returning from native_widget_->Show(). During synchronous Win32 window message dispatch, observer notifications can be synchronously processed, allowing a malicious observer or compromised renderer IPC to destroy the Widget. Upon return, the code continues to perform member writes and virtual method calls on the freed Widget instance.
Affected files:
ui/views/widget/widget.ccui/views/widget/widget.h
Estimated timestamp from git blame: 2025-09-04
Detailed Description
A potential Use-After-Free (UAF) vulnerability exists in Widget::Show() and Widget::ShowInactive() (ui/views/widget/widget.cc) due to the lack of safety checks after invoking native_widget_->Show().
On Windows, native_widget_->Show() triggers synchronous OS calls such as ::ShowWindow() or ::SetWindowPos() (via SetFullscreen()). These Win32 APIs synchronously dispatch window messages (e.g., WM_WINDOWPOSCHANGED) directly to the window’s procedure HWNDMessageHandler::OnWndProc on the UI thread.
When processing these messages, HWNDMessageHandler::OnWindowPosChanged invokes delegate_->HandleVisibilityChanged(true), which routes to Widget::OnNativeWidgetVisibilityChanged().
Crucially, unlike OnNativeWidgetSizeChanged(), OnNativeWidgetVisibilityChanged() does not instantiate a ScopedCallStackLock. This means the security assertion CHECK(!on_call_stack_) inside Widget::~Widget() is not triggered if destruction is initiated. If a registered WidgetObserver responds to the visibility change by synchronously invoking Widget::CloseNow(), the window is synchronously destroyed.
Because the browser window has CLIENT_OWNS_WIDGET ownership, the destruction flow synchronously deletes both the DesktopWindowTreeHostWin and the parent Widget / BrowserWidget instances.
When the synchronous message dispatch completes, control unwinds back to the in-flight execution frames of DesktopWindowTreeHostWin::Show() and Widget::Show(). Since both instances have been deallocated, this results in a cascade of Use-After-Frees:
- In
DesktopWindowTreeHostWin::Show(): Line 344 attempts to callGetWidget()->CanActivate()on the deleted host. - In
Widget::Show(): Line 1082 performs a member write (saved_show_state_ = preferred_show_state;) on the freedWidgetallocation. - In
Widget::Show(): Line 1087 callsHandleShowRequested(), which dereferences the freedsublevel_manager_unique pointer to executeEnsureOwnerSublevel()on a reclaimed pointer.
Note that since our tooling agent does not yet have the capability to execute code in a live environment, these findings represent a potential vulnerability and the trigger steps are theoretical.
Suggested Potential Trigger Steps
- An attacker-controlled extension or a compromised renderer initiates the creation of a popup window with
state: 'fullscreen'. - The browser process maps the initial show state to
ui::mojom::WindowShowState::kFullscreenand initiates the first-show of theBrowserWidget. - The attacker immediately triggers a window close action (or registers a
WidgetObserverthat reacts to the first visibility/activation notification). - During
Widget::Show(),native_widget_->Show(kFullscreen)invokes Win32 synchronous window repositioning. - The synchronous visibility change event dispatches, triggering the registered observer to synchronously call
Widget::CloseNow(). - The
Widgetand host objects are freed. The call stack returns toWidget::Show(), resulting in a member write and virtual method dereference on the freed allocations.
Proposed Fix
To remediate this issue, introduce a WeakPtr guard in both Widget::Show() and Widget::ShowInactive() immediately before calling native_widget_->Show(...), and return early if the widget was destroyed during the native call. This aligns with the sibling guard pattern used in Widget::SetFullscreen().
void Widget::Show() {
if (!native_widget_) {
return;
}
...
auto weak_ptr = GetWeakPtr();
if (non_client_view_) {
if (saved_show_state_ == ui::mojom::WindowShowState::kMaximized &&
!initial_restored_bounds_.IsEmpty() && !IsFullscreen()) {
native_widget_->Show(ui::mojom::WindowShowState::kMaximized,
initial_restored_bounds_);
} else {
native_widget_->Show(saved_show_state_, gfx::Rect());
}
} else {
native_widget_->Show(preferred_show_state, gfx::Rect());
}
if (!weak_ptr) {
return;
}
if (non_client_view_) {
saved_show_state_ = preferred_show_state;
}
HandleShowRequested();
}
Evaluated with Chrome root at commit: 87214e6721f6c34afd9181b80769a24c0c601c50
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.