CVE-2026-10003
Overview
Background
- `WidgetDelegate`
- the Views object that supplies a
Widget’s configuration, including which view should receive focus first (widget_delegate.h:262). - `initially_focused_view`
- a
Paramsfield holding the widget’s initially focused view, settable only before the widget is initialized (widget_delegate.h:323-325). - `views::ViewTracker`
- a utility that holds a non-owning reference to a
Viewand reports null once that view is destroyed.
Root Cause Analysis
Pre-fix, WidgetDelegate::Params stored the initially focused view as a bare std::optional<View*> initially_focused_view (widget_delegate.h:325). WidgetDelegate::SetInitiallyFocusedView assigns the raw pointer directly with params_.initially_focused_view = initially_focused_view (widget_delegate.cc:469), and WidgetDelegate::GetInitiallyFocusedView hands it back via params_.initially_focused_view.value_or(nullptr) (widget_delegate.cc:81). The invariant these accessors assume is that the referenced View outlives the delegate’s use of the pointer, but nothing clears the pointer when the view is destroyed; because the pointer lives inside a std::optional, Chromium’s automated rewriter does not convert it to raw_ptr<View>, so it also lacks BackupRefPtr protection (per the commit message and reporter). If the view is destroyed while the delegate still holds it, GetInitiallyFocusedView returns a dangling pointer, and a later focus operation dereferences freed memory.
The fix changes the field to std::unique_ptr<ViewTracker> (widget_delegate.h:325 post-fix): SetInitiallyFocusedView now lazily allocates a ViewTracker and calls SetView(initially_focused_view) (widget_delegate.cc:468-471 post-fix), and GetInitiallyFocusedView returns params_.initially_focused_view->view() or null (widget_delegate.cc:80-81 post-fix). Because ViewTracker observes the view and nulls itself on destruction, the accessor now returns null for a freed view instead of a stale pointer, restoring the lifetime safety invariant.
ViewTracker makes the handle self-null on view destruction so no dangling pointer can be returned.Attack Path
- Configure an initial focus view
A widget (e.g. a bubble) is shown with
SetInitiallyFocusedViewpointing at one of its child views (widget_delegate.cc:467-469). - Destroy the referenced view
The child view is removed while the widget remains alive, leaving
initially_focused_viewdangling since nothing clears it (widget_delegate.h:325). (inferred that a specific download-row removal does this) - Query initial focus
A focus operation calls
GetInitiallyFocusedView, which returns the stale pointer (widget_delegate.cc:80-81). - Use freed memory The caller performs a virtual focus call on the freed view. (inferred; the sink is outside the changed files)
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifui/views/widget/widget_delegate.cc |
modified | |
TabModalConfirmDialogViewsui/views/widget/widget_delegate.h |
modified | |
TestBaseWidgetDelegateui/views/widget/widget_delegate.h |
modified | |
UpdateRecommendedMessageBoxui/views/widget/widget_delegate.h |
modified | |
ViewTrackerui/views/widget/widget_delegate.h |
modified | |
WebDialogBrowserTestui/views/widget/widget_delegate.h |
modified |
Files Changed
ui/views/widget/widget_delegate.ccui/views/widget/widget_delegate.h
Audit Directions
- Raw pointers inside `std::optional`/containersFlag
View*/object pointers wrapped so theraw_ptrrewriter skips them; they silently lose BackupRefPtr coverage. - Stored view pointers without invalidationAny long-lived
View*field must be aViewTracker/base::WeakPtror be cleared on the view’s destruction. - Set-once-use-later configuration fieldsAudit delegate/params fields set at construction and dereferenced much later for lifetime gaps between set and use.
Patch
From 5c153abc2d8518e22f3c9de4f149dcfb7308bb6b Mon Sep 17 00:00:00 2001
From: Dana Fried <dfried@chromium.org>
Date: Wed, 20 May 2026 12:26:25 -0700
Subject: [PATCH] [Views] Prevent dangling focus preference
Widget keeps an optional pointer to its initially-focused view. This
should be robust to the view going away at some later point, not just
during teardown (where it's not safe either, but less likely to get
dereferenced).
This changes the optional<View*> to unique_ptr<ViewTracker>.
Unique pointer is used to avoid importing ViewTracker, ViewObserver,
etc. into the #includes of the delegate class, which is included in
many places (and because many widgets don't bother setting this
parameter, so there's no point in allocating additional memory unless
it's needed).
Fixed: 513609324
Change-Id: Id7c393894a1b1a21584c6e43720a5f94154dab04
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7864987
Reviewed-by: Keren Zhu <kerenzhu@chromium.org>
Commit-Queue: Keren Zhu <kerenzhu@chromium.org>
Commit-Queue: Dana Fried <dfried@chromium.org>
Auto-Submit: Dana Fried <dfried@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1633754}
---
diff --git a/ui/views/widget/widget_delegate.cc b/ui/views/widget/widget_delegate.cc
index 897ec458..f80ad158 100644
--- a/ui/views/widget/widget_delegate.cc
+++ b/ui/views/widget/widget_delegate.cc
@@ -18,6 +18,7 @@
#include "ui/display/screen.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/views/view.h"
+#include "ui/views/view_tracker.h"
#include "ui/views/views_delegate.h"
#include "ui/views/widget/widget.h"
#include "ui/views/window/client_view.h"
@@ -78,11 +79,12 @@
}
View* WidgetDelegate::GetInitiallyFocusedView() {
- return params_.initially_focused_view.value_or(nullptr);
+ return params_.initially_focused_view ? params_.initially_focused_view->view()
+ : nullptr;
}
bool WidgetDelegate::HasConfiguredInitiallyFocusedView() const {
- return params_.initially_focused_view.has_value();
+ return params_.initially_focused_view != nullptr;
}
BubbleDialogDelegate* WidgetDelegate::AsBubbleDialogDelegate() {
@@ -466,7 +468,10 @@
void WidgetDelegate::SetInitiallyFocusedView(View* initially_focused_view) {
DCHECK(!GetWidget());
- params_.initially_focused_view = initially_focused_view;
+ if (!params_.initially_focused_view) {
+ params_.initially_focused_view = std::make_unique<ViewTracker>();
+ }
+ params_.initially_focused_view->SetView(initially_focused_view);
}
void WidgetDelegate::SetModalType(ui::mojom::ModalType modal_type) {
diff --git a/ui/views/widget/widget_delegate.h b/ui/views/widget/widget_delegate.h
index 07ba390..cb2d7b2f 100644
--- a/ui/views/widget/widget_delegate.h
+++ b/ui/views/widget/widget_delegate.h
@@ -53,6 +53,7 @@
class TabModalConfirmDialogViews;
class TestBaseWidgetDelegate;
class UpdateRecommendedMessageBox;
+class ViewTracker;
class WebDialogBrowserTest;
FORWARD_DECLARE_TEST(AcceleratorCommandsFullscreenBrowserTest,
ToggleFullscreen);
@@ -322,7 +323,7 @@
// The widget's initially focused view, if any. This can only be set before
// this WidgetDelegate is used to initialize a Widget.
- std::optional<View*> initially_focused_view;
+ std::unique_ptr<ViewTracker> initially_focused_view;
// This is used by modal dialogs to override and constrain desired bounds
// calculations.
Original Bug Report
Browser-process Use-After-Free in BrowserFocusControllerDelegateViews
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 the browser process due to a dangling pointer in WidgetDelegate’s initially_focused_view. This occurs because the pointer is stored in an unprotected std::optional and is not cleared when the referenced View is destroyed. Triggering accessibility shortcuts like F6 or Alt+Shift+A can result in a virtual function call on freed memory.
Affected files:
chrome/browser/ui/focus/browser_focus_controller_delegate_views.ccui/views/widget/widget_delegate.hchrome/browser/ui/views/download/bubble/download_toolbar_ui_controller.ccui/views/widget/widget_delegate.ccui/views/window/dialog_delegate.cc
Estimated timestamp from git blame: Unknown (Google3 checkout)
Description
A potential Use-After-Free (UAF) vulnerability has been identified in the browser process, specifically within BrowserFocusControllerDelegateViews::ActivateFirstInactiveBubbleForAccessibility(). The issue stems from how WidgetDelegate manages its initially_focused_view pointer, which is used to determine focus when a bubble or dialog is activated for accessibility.
Root Cause Analysis
In ui/views/widget/widget_delegate.h, the initially_focused_view field is defined as:
std::optional<View*> initially_focused_view;
Because this pointer is wrapped in std::optional, it is not converted to raw_ptr<View> by Chromium’s automated rewriter and thus lacks MiraclePtr/BackupRefPtr protection. Furthermore, there is no mechanism (such as a ViewTracker or ViewObserver) to clear this pointer if the referenced View is destroyed during the lifetime of the delegate.
In the context of the Download Bubble, this field can be set to a child view (e.g., a button within a download row). If that download row is removed while the bubble remains open, the pointer becomes dangling.
Vulnerable Sink
The vulnerability is potentially triggered when a user presses the F6 (Rotate Pane Focus) or Alt+Shift+A (Focus Inactive Popup) keyboard shortcuts. The following code path in chrome/browser/ui/focus/browser_focus_controller_delegate_views.cc retrieves the dangling pointer and performs a virtual call:
// line 112
views::View* focusable = bubble->GetInitiallyFocusedView();
// ...
if (focusable) {
focusable->RequestFocus(); // Virtual call on potentially freed memory
}
Potential Attack Scenario
While these steps are theoretical and have not been validated by a running proof-of-concept, an attacker could potentially trigger the vulnerability as follows:
- Induce the browser to show the Download Bubble in an inactive state (e.g., by initiating multiple downloads from a site).
- Cause a specific download to be removed (e.g., via interaction in another tab or automated cleanup), which destroys the associated
Viewthat was captured as theinitially_focused_view. - The user, perhaps guided by a screen reader or keyboard navigation, presses F6 or Alt+Shift+A to focus the bubble.
- The system attempts to call
RequestFocus()on the dangling pointer, potentially allowing an attacker who has groomed the heap to gain arbitrary code execution in the unsandboxed browser process.
Suggested Fix
The initially_focused_view should be managed using a views::ViewTracker or a base::WeakPtr<views::View> to ensure the pointer is invalidated when the View is destroyed. Additionally, the kAnchoredDialogKey property should be audited to ensure it does not bypass modern memory safety protections.
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
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.