CVE-2026-11636
Overview
Background
- Autofill popup views
- the Views-based UI (
PopupBaseView,PopupRowView,PopupBnplFootnoteView, and siblings) that renders autofill suggestions in the browser process. - COM reentrancy on Windows
- firing a platform accessibility event (e.g. via
NotifyAccessibilityEventDeprecated,NotifyAXSelection) can synchronously spin a COM message pump when assistive technology is active, letting queued UI-thread tasks run mid-method. - `views::ViewTracker`
- a helper that holds a non-owning, auto-nulling reference to a
View, so it can report whether that view was destroyed while a nested operation ran. - `TrackAndRun`
- the new helper the patch introduces (and the regression test exercises): it tracks a view, runs a sequence of callbacks, and returns
false(so callersreturn) if the tracked view is destroyed between callbacks.
Root Cause Analysis
Several autofill popup view methods fired a sequence of accessibility notifications and then continued to touch this and other member views (parent_widget_, content_view_, a11y_selection_delegate_, etc.). On Windows, any of those accessibility calls can synchronously spin a COM message pump, during which a queued task can rebuild/destroy the popup view hierarchy; the subsequent member accesses in the same method then operate on freed views — a use-after-free. The invariant violated is that this and its sibling views must remain alive across each accessibility call before being reused.
The fix wraps each such sequence in TrackAndRun(this, cb1, cb2, ...), which runs the callbacks under a views::ViewTracker and returns false if the tracked view is destroyed; every call site checks the result and returns early (if (!TrackAndRun(...)) { return; }) so no member is touched after a reentrant destruction. The regression test TrackAndRun_Basic confirms that if the first callback destroys the tracked view, TrackAndRun returns false and the remaining callbacks do not run.
TrackAndRun interleaves a ViewTracker liveness check between the event-firing callbacks and aborts the method the instant the view dies.Attack Path
- Assistive tech active The victim runs on Windows with an assistive technology that installs a COM message pump reachable from accessibility event firing.
- Show/navigate the popup
Attacker-influenced autofill UI (a suggestion popup) is shown and a selection/focus/close transition fires an accessibility event such as
kMenuStart,kFocus, orkSelectedChildrenChanged. - Reenter and destroy The event synchronously spins the COM pump, running a queued task that rebuilds/destroys the popup view hierarchy while the original method is still on the stack.
- Use after free
Control returns and the method dereferences the now-freed view members (e.g.
content_view_,parent_widget_,this), reading/writing freed memory (inferred downstream consequence; the patch shows only that the guard now aborts here).
Impact Assessment
(inferred) and not demonstrated by the diff.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/views/autofill/popup/popup_base_view.cc |
modified | |
ifchrome/browser/ui/views/autofill/popup/popup_row_view.cc |
modified |
Files Changed
chrome/browser/ui/views/autofill/popup/popup_base_view.ccchrome/browser/ui/views/autofill/popup/popup_bnpl_footnote_view.ccchrome/browser/ui/views/autofill/popup/popup_row_view.cc
Audit Directions
- Accessibility-event firing followed by member useaudit Views code (especially menu/popup/row views) that calls
NotifyAccessibilityEventDeprecated/NotifyAXSelectionand then touchesthisor sibling views, and require aViewTracker/TrackAndRunguard. - COM-reentrant sinks on Windowstreat any call that can pump messages when AT is active (accessibility, drag, clipboard, modal) as a yield point and revalidate object lifetime afterward.
- Multi-step view sequenceswhere several mutations run in order on related views, verify liveness between steps rather than only at entry, since an early step can destroy objects used by a later one.
Patch
From c2df61a54db1d495380fbd7dca9062e292b7d223 Mon Sep 17 00:00:00 2001
From: Bruno Braga <brunobraga@google.com>
Date: Fri, 29 May 2026 06:37:52 -0700
Subject: [PATCH] [Autofill] Fix UAF in popup views due to COM reentrancy on Windows
Fix potential Use-After-Free (UAF) vulnerabilities in Autofill popup
views on Windows.
On Windows, firing accessibility events (such as focus or selection
changes) can synchronously spin a COM message pump if assistive
technologies are active. This reentrancy window allows queued tasks
on the browser UI thread to run, which can destroy the executing
view hierarchy (e.g., by rebuilding the popup). Subsequent member
accesses on the deleted views result in UAF.
This CL introduces `views::ViewTracker` to track the lifetime of the
executing views across calls that trigger accessibility events and
aborts execution early if the view is synchronously destroyed.
Touched files:
- popup_row_with_button_view.cc: Guard key press handlers.
- popup_row_view.cc: Guard selected cell updates.
- popup_bnpl_footnote_view.cc: Guard settings link focus.
- popup_view_views.cc: Guard SetSelectedCell path.
TAG=agy
CONV=d11a46e3-2dd7-4aea-9644-0f62932c301c
Bug: 517014512, 516938039, 517023053
Change-Id: I93ce0650d32f477d5bd9a0b0d23ef20018a3e523
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7878305
Reviewed-by: Christoph Schwering <schwering@google.com>
Commit-Queue: Bruno Braga <brunobraga@google.com>
Cr-Commit-Position: refs/heads/main@{#1638448}
---
diff --git a/chrome/browser/ui/views/autofill/popup/popup_base_view.cc b/chrome/browser/ui/views/autofill/popup/popup_base_view.cc
index 5c9c09d1..77555e50 100644
--- a/chrome/browser/ui/views/autofill/popup/popup_base_view.cc
+++ b/chrome/browser/ui/views/autofill/popup/popup_base_view.cc
@@ -329,19 +329,29 @@
// navigates into the menu, otherwise some screen readers will ignore
// any focus events outside of the menu, including a focus event on
// the form control itself.
- NotifyAccessibilityEventDeprecated(ax::mojom::Event::kMenuPopupEnd, true);
- NotifyAccessibilityEventDeprecated(ax::mojom::Event::kMenuEnd, true);
- GetViewAccessibility().EndPopupFocusOverride();
-
- // Also fire an accessible focus event on what currently has focus,
- // typically the widget associated with this popup.
- if (parent_widget_) {
- if (views::FocusManager* focus_manager =
- parent_widget_->GetFocusManager()) {
- if (View* focused_view = focus_manager->GetFocusedView()) {
- focused_view->GetViewAccessibility().FireFocusAfterMenuClose();
- }
- }
+ if (!TrackAndRun(
+ this,
+ [this]() {
+ NotifyAccessibilityEventDeprecated(
+ ax::mojom::Event::kMenuPopupEnd, true);
+ },
+ [this]() {
+ NotifyAccessibilityEventDeprecated(ax::mojom::Event::kMenuEnd,
+ true);
+ },
+ [this]() { GetViewAccessibility().EndPopupFocusOverride(); },
+ [this]() {
+ if (parent_widget_) {
+ if (views::FocusManager* focus_manager =
+ parent_widget_->GetFocusManager()) {
+ if (View* focused_view = focus_manager->GetFocusedView()) {
+ focused_view->GetViewAccessibility()
+ .FireFocusAfterMenuClose();
+ }
+ }
+ }
+ })) {
+ return;
}
}
@@ -369,8 +379,18 @@
// readers that the focus is only changing temporarily, and the screen
// reader will restore the focus back to the appropriate textfield when the
// menu closes.
- NotifyAccessibilityEventDeprecated(ax::mojom::Event::kMenuStart, true);
- NotifyAccessibilityEventDeprecated(ax::mojom::Event::kMenuPopupStart, true);
+ if (!TrackAndRun(
+ this,
+ [this]() {
+ NotifyAccessibilityEventDeprecated(ax::mojom::Event::kMenuStart,
+ true);
+ },
+ [this]() {
+ NotifyAccessibilityEventDeprecated(
+ ax::mojom::Event::kMenuPopupStart, true);
+ })) {
+ return;
+ }
is_ax_menu_start_event_fired_ = true;
}
diff --git a/chrome/browser/ui/views/autofill/popup/popup_bnpl_footnote_view.cc b/chrome/browser/ui/views/autofill/popup/popup_bnpl_footnote_view.cc
index 4f29d09..0fc1b86 100644
--- a/chrome/browser/ui/views/autofill/popup/popup_bnpl_footnote_view.cc
+++ b/chrome/browser/ui/views/autofill/popup/popup_bnpl_footnote_view.cc
@@ -120,9 +120,16 @@
is_settings_link_selected_ = true;
link->SetBorder(views::CreateSolidBorder(kSelectionBorderThickness,
ui::kColorFocusableBorderFocused));
- a11y_selection_delegate_->NotifyAXSelection(*this);
- this->NotifyAccessibilityEventDeprecated(ax::mojom::Event::kFocus, true);
- this->GetViewAccessibility().SetIsSelected(true);
+ if (!TrackAndRun(
+ this,
+ [this]() { a11y_selection_delegate_->NotifyAXSelection(*this); },
+ [this]() {
+ NotifyAccessibilityEventDeprecated(ax::mojom::Event::kFocus,
+ true);
+ },
+ [this]() { GetViewAccessibility().SetIsSelected(true); })) {
+ return;
+ }
announce_callback_.Run(std::u16string(link->GetText()), /*polite=*/false);
}
}
diff --git a/chrome/browser/ui/views/autofill/popup/popup_row_view.cc b/chrome/browser/ui/views/autofill/popup/popup_row_view.cc
index 333fb65..0634e7b 100644
--- a/chrome/browser/ui/views/autofill/popup/popup_row_view.cc
+++ b/chrome/browser/ui/views/autofill/popup/popup_row_view.cc
@@ -366,7 +366,11 @@
// If the previous cell was content, set it as unselected.
if (selected_cell_ == CellType::kContent) {
content_view_->UpdateStyle(/*selected=*/false);
- content_view_->GetViewAccessibility().SetIsSelected(false);
+ if (!TrackAndRun(this, [this]() {
+ content_view_->GetViewAccessibility().SetIsSelected(false);
+ })) {
+ return;
+ }
controller_->UnselectSuggestion();
}
@@ -377,18 +381,34 @@
// is required for a11y focus working on a non-activatable popup. Consider
// moving `SetIsSelected()` into `NotifyAXSelection()` (and rename it) to
// hide this API complexity from clients.
- GetA11ySelectionDelegate().NotifyAXSelection(*this);
- GetViewAccessibility().SetIsSelected(true);
- NotifyAccessibilityEventDeprecated(
- ax::mojom::Event::kSelectedChildrenChanged, true);
+ if (!TrackAndRun(
+ this,
+ [this]() { GetA11ySelectionDelegate().NotifyAXSelection(*this); },
+ [this]() { GetViewAccessibility().SetIsSelected(true); },
+ [this]() {
+ NotifyAccessibilityEventDeprecated(
+ ax::mojom::Event::kSelectedChildrenChanged, true);
+ })) {
+ return;
+ }
selected_cell_ = new_cell;
} else if (new_cell == CellType::kContent) {
controller_->SelectSuggestion(line_number_);
content_view_->UpdateStyle(/*selected=*/true);
- GetA11ySelectionDelegate().NotifyAXSelection(*content_view_);
- content_view_->GetViewAccessibility().SetIsSelected(true);
- NotifyAccessibilityEventDeprecated(
- ax::mojom::Event::kSelectedChildrenChanged, true);
+ if (!TrackAndRun(
+ this,
+ [this]() {
+ GetA11ySelectionDelegate().NotifyAXSelection(*content_view_);
+ },
+ [this]() {
+ content_view_->GetViewAccessibility().SetIsSelected(true);
+ },
+ [this]() {
+ NotifyAccessibilityEventDeprecated(
+ ax::mojom::Event::kSelectedChildrenChanged, true);
+ })) {
+ return;
+ }
selected_cell_ = new_cell;
} else {
// Set the selected cell to none in case an invalid choice was made (e.g.
@@ -396,8 +416,13 @@
// explicitly with `std::nullopt`.
selected_cell_ = std::nullopt;
- GetViewAccessibility().SetIsSelected(false);
- content_view_->GetViewAccessibility().SetIsSelected(false);
+ if (!TrackAndRun(
+ this, [this]() { GetViewAccessibility().SetIsSelected(false); },
+ [this]() {
+ content_view_->GetViewAccessibility().SetIsSelected(false);
+ })) {
+ return;
Regression Test / PoC
diff --git a/chrome/browser/ui/views/autofill/popup/popup_view_utils_unittest.cc b/chrome/browser/ui/views/autofill/popup/popup_view_utils_unittest.cc
index 1e92c102..c6ae677 100644
--- a/chrome/browser/ui/views/autofill/popup/popup_view_utils_unittest.cc
+++ b/chrome/browser/ui/views/autofill/popup/popup_view_utils_unittest.cc
@@ -4,11 +4,13 @@
#include "chrome/browser/ui/views/autofill/popup/popup_view_utils.h"
+#include <memory>
#include <vector>
#include "chrome/browser/ui/views/autofill/popup/popup_base_view.h"
#include "components/autofill/core/browser/ui/popup_open_enums.h"
#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/views/view.h"
namespace autofill {
@@ -544,4 +546,16 @@
gfx::Rect(50, 50, 100, 100), {non_overlapping_popup, kBasePopup}));
}
+TEST(PopupViewsUtilsTest, TrackAndRun_Basic) {
+ auto view = std::make_unique<views::View>();
+ bool cb2_called = false;
+
+ bool survived = TrackAndRun(
+ view.get(), [&view]() { view.reset(); },
+ [&cb2_called]() { cb2_called = true; });
+
+ EXPECT_FALSE(survived);
+ EXPECT_FALSE(cb2_called);
+}
+
} // namespace autofill
Original Bug Report
Potential UAF in PopupViewViews::SetSelectedCell via synchronous Windows accessibility event pumping
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 (UAF) vulnerability exists in the desktop Autofill popup UI on Windows. When updating the selected cell, firing native accessibility events synchronously pumps the STA COM message loop. If a queued UI thread task executes during this pump and updates the suggestions, the underlying row view is deleted, leading to a use-after-free when the function resumes execution.
Affected files:
chrome/browser/ui/views/autofill/popup/popup_view_views.ccchrome/browser/ui/views/autofill/popup/popup_row_view.cc
Estimated timestamp from git blame: 2023-02-16
Description
A potential Use-After-Free (UAF) vulnerability has been identified in PopupViewViews::SetSelectedCell (located in chrome/browser/ui/views/autofill/popup/popup_view_views.cc) on Windows.
When updating the selected cell of the Autofill popup, the code obtains a bare C++ reference to the target PopupRowView on the stack. It then invokes SetSelectedCell on this row, which eventually calls NotifyAXSelection to fire native Windows accessibility (MSAA/UIA) events. Firing these native events on Windows synchronously pumps the Single-Threaded Apartment (STA) COM message loop.
If a queued UI thread task (such as an IPC task from the renderer containing updated suggestions) is processed during this synchronous pump, it can invoke OnSuggestionsChanged(). This synchronously invokes CreateSuggestionViews(), which clears and deletes all existing child row views via RemoveAllChildViews(). When the nested event pump returns, the stack reference to the deleted PopupRowView is dereferenced to call ScrollViewToVisible(), resulting in a Use-After-Free on a bare reference that is not protected by MiraclePtr/BackupRefPtr.
Potential Step-by-Step Trigger Sequence
- User Interaction: A Windows user with an active accessibility utility (such as Narrator or NVDA) triggers an Autofill popup and navigates it (e.g., using keyboard arrow keys).
- Selection Initiation:
PopupViewViews::SetSelectedCellis invoked, and retrieves a bare reference to the row:PopupRowView& new_selected_row = GetPopupRowViewAt(...). - Event Dispatching:
new_selected_row.SetSelectedCell(...)is called, which callsNotifyAXSelectionto fire native accessibility events. - STA Message Pump: The Windows accessibility subsystem synchronously pumps the message loop on the UI thread to dispatch the COM messages.
- Re-entrant Deletion: A queued UI task runs re-entrantly during the pump, executing
OnSuggestionsChanged()and deleting all existingPopupRowViewelements. - Use-After-Free: After the pump completes,
new_selected_row.ScrollViewToVisible()is executed on the deleted reference, initiating a virtual method dispatch via the deleted object’s vtable.
Note: These steps represent a potential execution flow based on static analysis; our tooling does not currently have the capability to run code or execute a live proof of concept.
Suggested Fix
To prevent this re-entrant Use-After-Free, track the lifetime of the new_selected_row view across synchronous accessibility calls using a views::ViewTracker. If the view is destroyed during the call, execution should return early:
// chrome/browser/ui/views/autofill/popup/popup_view_views.cc
// Wrap the view in a tracker to detect re-entrant destruction
views::ViewTracker row_tracker(&new_selected_row);
new_selected_row.SetSelectedCell(cell_index->second);
// Check if the row view was destroyed during the synchronous call
if (!row_tracker.view()) {
return;
}
new_selected_row.ScrollViewToVisible();
Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8
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.