Medium watch CVSS 8.3 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Views
DescriptionUse after free in Views
ComponentViews
Sub-componentViews
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker519369088
Fix commite9e0e0657060 (chromium/src) +44/-0
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-06-03 (per vendor advisory)
Fix landed2026-07-20 (public git)
Disclosed2026-08-25
Patch gap36 days — fix public in source before the advisory

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 Widget sends through observers_.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.cc that marks the Widget as being on the call stack. Going by the commit message and the EXPECT_CHECK_DEATH tests, destroying the widget while the guard is alive triggers a CHECK failure instead of a silent free.
`CLIENT_OWNS_WIDGET`
an ownership mode in which client code holds the Widget (for example in a std::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.

Key insight
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

  1. Widget is shown Browser UI calls Widget::Show() or Widget::ShowInactive() on a client-owned widget, which leads to native show and the visibility-change handler in widget.cc.
  2. Observer notified The handler propagates visibility to the root view and then calls observers_.Notify(&WidgetObserver::OnWidgetVisibilityChanged, ...).
  3. 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.
  4. Stale `this` used Without the guard, execution comes back into the freed Widget and uses it (GetCompositor(), root->layer(), and the remaining caller frames in Show() / ShowInactive()), which is a use-after-free in the browser process.
  5. Memory corruption With heap grooming, the freed Widget slot could be reclaimed with controlled data before reuse (inferred; the patch shows only the dangling use).

Impact Assessment

The bug is a use-after-free of a 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

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_unittest.cc
  • ui/views/widget/widget.cc

Audit Directions

  • Observer notification followed by `this` use
    In widget.cc and other Views classes (View, BubbleDialogDelegate, MenuController, DesktopNativeWidget*), look for observers_.Notify(...) or delegate callbacks followed by member access with no ScopedCallStackLock, WeakPtr check, or AutoReset destruction guard.
  • Client-owned widget lifetimes
    Review CLIENT_OWNS_WIDGET users whose observers or delegates reset the owning unique_ptr from inside OnWidgetVisibilityChanged, OnWidgetActivationChanged, OnWidgetBoundsChanged or close callbacks, since those are the re-entrancy sources this guard now turns into crashes.
  • Native show/hide re-entrancy
    Platform NativeWidget implementations (Aura, Mac, desktop Windows/Linux) can send synchronous visibility or activation notifications from inside native show/hide calls. Check that every such entry point into Widget is guarded the same way.
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) {
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 [email protected]

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