CVE-2026-95277
Overview
Background
- `Widget`
- the Views-layer object that owns a top-level window’s non-client frame and delegates platform operations to a
NativeWidget; it lives in the browser process. - `native_widget_`
- the
Widget’s pointer to its platform-specific backend (NativeWidget/aura window) that actually maps, shows, and hides the OS window. - Synchronous re-entrancy
- visibility changes sent to
native_widget_can fire window-observer callbacks, focus changes, or capture-loss handlers inline, which may close the window and free the owningWidgetbefore the delegating call returns. - `GetWeakPtr()` / `WeakPtr`
- returns a weak reference to
thisthat automatically becomes null when the object is destroyed, letting a method detect thatthiswas freed out from under it.
Root Cause Analysis
Widget::Show(), Widget::Hide(), and Widget::ShowInactive() delegate visibility changes to native_widget_ (via native_widget_->Show(...), native_widget_->Hide(), and SetBounds(...)) and then continue to touch this — writing saved_show_state_, calling HandleShowRequested(), or passing this to OnAnyWidgetHidden(this). The unstated invariant is that this survives those delegated calls, but the delegate can synchronously run observer callbacks, focus changes, or capture loss that destroy the aura window and free the Widget instance. When that happens the post-delegation statements read and write freed memory, producing a use-after-free.
The fix captures auto weak_this = GetWeakPtr() before each delegating call and inserts CHECK(weak_this) immediately after it and before any further use of this, so a destruction turns into a deterministic abort instead of continued access to freed memory. Because each CHECK is placed ahead of the subsequent this-write or this-pass, the guard runs before the sink is reached.
this remains valid across a call that delegates to native_widget_, when that call can synchronously destroy the Widget; the fix detects the destruction with a WeakPtr and CHECKs before any post-call use of this, converting a use-after-free into a safe crash.Attack Path
- Attach a destructive observer
Arrange for a window observer, focus-change handler, or capture-loss handler that closes/destroys the widget’s window (and thus frees the
Widget) when visibility changes. - Trigger a visibility change
Cause
Widget::Show(),Widget::Hide(), orWidget::ShowInactive()to run, which delegates tonative_widget_->Show()/Hide()(orSetBounds()). - Free the Widget mid-call
The delegated call synchronously invokes the destructive callback, destroying the aura window and freeing the
Widgetwhile the method’s stack frame is still executing. - Use the freed instance
Control returns to the method, which writes
saved_show_state_, callsHandleShowRequested(), or passesthistoOnAnyWidgetHidden(this), all operating on freed memory (the use-after-free the pre-patch code allowed). - Escalate from the UAF
Grooming the freed
Widgetallocation to obtain a controlled read/write primitive and code execution in the browser process (inferred).
Impact Assessment
Widget object in the browser process, potentially leading to memory corruption and code execution (inferred). Preconditions are the ability to trigger the visibility change and to arrange the synchronous destruction (e.g., via an observer, focus, or capture-loss path). The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 9.6.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifui/views/widget/widget.cc |
modified |
Files Changed
ui/views/widget/widget.cc
Audit Directions
- Delegation-then-self-useAudit other
Widget/NativeWidgetmethods that call intonative_widget_(or other platform backends) and then read or write member state or passthisonward, since those calls can re-enter and free the object. - Synchronous observer/focus/capture callbacksLook for code paths where visibility, focus, activation, or capture changes fire observer notifications inline, and check whether any caller assumes the notifying object outlives the notification.
- Missing WeakPtr guards around reentrant sinksSearch for methods lacking a
GetWeakPtr()/CHECK(or early-return) pattern around calls that are known to be capable of destroying the current object mid-method.
Patch
From 6080f65e48f96ee710060f982eb0a4a006919ac5 Mon Sep 17 00:00:00 2001 From: David Yeung <[email protected]> Date: Sun, 13 Sep 2026 21:10:56 -0700 Subject: [PATCH] Guard against widget destruction during show and hide Delegating window visibility changes to the native widget in Widget::Show(), Widget::ShowInactive(), and Widget::Hide() can synchronously trigger window observer callbacks, focus changes, or capture loss that destroy the underlying aura window and free the Widget instance before the call returns. Fix is to acquire a WeakPtr and check if the widget was destroyed to prevent any UAFs. Fixed: 553136141 Change-Id: Ie27e7d9b46a23df033045ac6f6603362f16b5734 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8391999 Commit-Queue: Keren Zhu <[email protected]> Reviewed-by: Keren Zhu <[email protected]> Cr-Commit-Position: refs/heads/main@{#1697039} --- diff --git a/ui/views/widget/widget.cc b/ui/views/widget/widget.cc index 41f7e82c..8bae166 100644 --- a/ui/views/widget/widget.cc +++ b/ui/views/widget/widget.cc @@ -1120,6 +1120,7 @@ ui::mojom::WindowShowState preferred_show_state = CanActivate() ? ui::mojom::WindowShowState::kNormal : ui::mojom::WindowShowState::kInactive; + auto weak_this = GetWeakPtr(); if (non_client_view_) { // While initializing, the kiosk mode will go to full screen before the // widget gets shown. In that case we stay in full screen mode, regardless @@ -1131,12 +1132,14 @@ } else { native_widget_->Show(saved_show_state_, gfx::Rect()); } + CHECK(weak_this); // |saved_show_state_| only applies the first time the window is shown. // If we don't reset the value the window may be shown maximized every time // it is subsequently shown after being hidden. saved_show_state_ = preferred_show_state; } else { native_widget_->Show(preferred_show_state, gfx::Rect()); + CHECK(weak_this); } HandleShowRequested(); @@ -1146,7 +1149,9 @@ if (!native_widget_) { return; } + auto weak_this = GetWeakPtr(); native_widget_->Hide(); + CHECK(weak_this); internal::AnyWidgetObserverSingleton::GetInstance()->OnAnyWidgetHidden(this); } @@ -1154,6 +1159,7 @@ if (!native_widget_) { return; } + auto weak_this = GetWeakPtr(); // If this gets called with saved_show_state_ == // ui::mojom::WindowShowState::kMaximized, call SetBounds()with the restored // bounds to set the correct size. This normally should not happen, but if it @@ -1161,9 +1167,11 @@ if (saved_show_state_ == ui::mojom::WindowShowState::kMaximized && !initial_restored_bounds_.IsEmpty()) { SetBounds(initial_restored_bounds_); + CHECK(weak_this); saved_show_state_ = ui::mojom::WindowShowState::kNormal; } native_widget_->Show(ui::mojom::WindowShowState::kInactive, gfx::Rect()); + CHECK(weak_this); HandleShowRequested(); }