Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Views
DescriptionUse after free in Views
ComponentViews
Bug ClassUAF
Tracker519369088
Fix commite9e0e0657060 (chromium/src) +44/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
if
ui/views/widget/widget.cc
modified
TEST_F
ui/views/widget/widget_unittest.cc
modified
DestroyOnVisibleObserver
ui/views/widget/widget_unittest.cc
modified
if
ui/views/widget/widget_unittest.cc
modified

Files Changed

  • ui/views/widget/widget.cc
  • ui/views/widget/widget_unittest.cc
From e9e0e0657060cf34c6bb8be63f68247bef53ed8e Mon Sep 17 00:00:00 2001
From: David Bienvenu <davidbienvenu@chromium.org>
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 <kylixrd@chromium.org>
Commit-Queue: David Bienvenu <davidbienvenu@chromium.org>
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) {
Loading diff…

Regression Test / PoC

shipped with the fix
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) {
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • ui/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:

  1. In DesktopWindowTreeHostWin::Show(): Line 344 attempts to call GetWidget()->CanActivate() on the deleted host.
  2. In Widget::Show(): Line 1082 performs a member write (saved_show_state_ = preferred_show_state;) on the freed Widget allocation.
  3. In Widget::Show(): Line 1087 calls HandleShowRequested(), which dereferences the freed sublevel_manager_ unique pointer to execute EnsureOwnerSublevel() 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

  1. An attacker-controlled extension or a compromised renderer initiates the creation of a popup window with state: 'fullscreen'.
  2. The browser process maps the initial show state to ui::mojom::WindowShowState::kFullscreen and initiates the first-show of the BrowserWidget.
  3. The attacker immediately triggers a window close action (or registers a WidgetObserver that reacts to the first visibility/activation notification).
  4. During Widget::Show(), native_widget_->Show(kFullscreen) invokes Win32 synchronous window repositioning.
  5. The synchronous visibility change event dispatches, triggering the registered observer to synchronously call Widget::CloseNow().
  6. The Widget and host objects are freed. The call stack returns to Widget::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.

View on issue tracker