CVE-2026-13038
Overview
Background
- `PopupViewViews`
- the
views::Viewsubclass that renders the Autofill suggestion popup in the Chrome browser UI process. - `TrackAndRun`
- a template helper in
popup_view_utils.hthat wraps aviews::ViewTrackeraround aviews::View, runs the supplied callables, and returns abool aliveindicating whether the tracked view still exists afterward. - `MaybeA11yFocusInformationalSuggestion`
- a
PopupViewViewsmethod that, when the popup contains a singlePopupWarningViewrow, fires platform accessibility focus/selection events on that row viaTrackAndRun. - Nested message loop / a11y reentrancy
- on Windows, dispatching a platform accessibility event (e.g.
ax::mojom::Event::kFocus) can synchronously spin a nested message loop that closes the widget and destroys the popupthis, per the commit description.
Root Cause Analysis
The pre-patch MaybeA11yFocusInformationalSuggestion() returned void and invoked TrackAndRun(view_ptr, ...) while discarding its bool return value, even though TrackAndRun exists specifically to report whether the tracked view survived firing the accessibility events (NotifyAXSelection(*view_ptr) and NotifyAccessibilityEventDeprecated(ax::mojom::Event::kFocus, true)). Because a11y event dispatch on Windows can enter a nested message loop that destroys the popup, the method could return to callers after this had already been freed. Those callers — identified in the commit message as Show() and OnSuggestionsChanged() (inferred: their names do not appear in the @@ hunk headers) — then continued to touch this: Show() evaluated return !CanActivate() || (GetWidget() && GetWidget()->IsActive()); and OnSuggestionsChanged() called ShowIPHFeaturePromos();, each a use-after-free on the destroyed view.
The fix changes MaybeA11yFocusInformationalSuggestion() to return bool (propagating TrackAndRun’s aliveness result, returning true on the non-firing paths), and makes both callers return early when the method reports the view was destroyed, so no member of freed this is accessed. Marking TrackAndRun [[nodiscard]] and the method [[nodiscard]] guarantees the aliveness result can no longer be silently dropped.
TrackAndRun’s alive return value — the exact signal that this may have been destroyed — and then dereferencing this anyway; the fix threads that boolean up through MaybeA11yFocusInformationalSuggestion() into Show()/OnSuggestionsChanged() and forces both to bail out before any post-free access.Attack Path
- Present a single-warning popup
Cause the Autofill popup to be shown with exactly one row that is a
PopupWarningView, e.g. via aSuggestionType::kMixedFormMessagesuggestion, so thatrows_.size() == 1and thewarning_viewbranch inMaybeA11yFocusInformationalSuggestion()is taken (matching the regression testWarningOnShow_DestroyOnA11yFocus). - Trigger accessibility event dispatch
With platform accessibility active on Windows,
TrackAndRunfiresNotifyAXSelection(*view_ptr)/NotifyAccessibilityEventDeprecated(...), which per the commit description can spin a nested message loop. - Destroy the popup during the nested loop
Within that loop the widget is closed and the popup is destroyed (the test models this by overriding
NotifyAXSelectionto callGetWidget()->CloseNow()), freeingthiswhile it is still on the call stack ofShow()/OnSuggestionsChanged(). - Use the freed view
Control returns to the caller, which (pre-patch) dereferences freed
this—Show()callingCanActivate()/GetWidget()orOnSuggestionsChanged()callingShowIPHFeaturePromos()— producing the use-after-free. - Escalate from the UAF
Corrupting or reading through the freed
PopupViewViewsallocation could be leveraged toward controlled memory access or code execution(inferred); the diff itself demonstrates only the crash/UAF, not a specific read/write primitive or RCE.
Impact Assessment
PopupViewViews object in the browser process. The preconditions are the Windows a11y-reentrancy path and a page that induces the qualifying warning suggestion; the advisory classifies this as a use-after-free in Autofill (CVE-2026-13038, bug 523740781) and reports no in-the-wild exploitation. Any escalation from the UAF to a controlled primitive, sandbox-context compromise, or remote code execution is (inferred) and not established by the patch.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/views/autofill/popup/popup_view_views_unittest.cc |
modified | |
PopupViewViewsTestchrome/browser/ui/views/autofill/popup/popup_view_views_unittest.cc |
modified | |
TEST_Fchrome/browser/ui/views/autofill/popup/popup_view_views_unittest.cc |
modified |
Files Changed
chrome/browser/ui/views/autofill/popup/popup_view_utils.hchrome/browser/ui/views/autofill/popup/popup_view_views.ccchrome/browser/ui/views/autofill/popup/popup_view_views.hchrome/browser/ui/views/autofill/popup/popup_view_views_unittest.cc
Audit Directions
- Dropped lifetime-tracker return valuesSearch for other call sites of
TrackAndRun(and anyViewTracker-based helper) whoseboolresult is discarded, and confirm nothing touches the tracked view orthisafterward. - Post-a11y-dispatch member accessAudit any code that fires platform accessibility events (
NotifyAXSelection,NotifyAccessibilityEventDeprecated, focus/selection notifications) and then continues to accessthis, its members, or itsviews::Widget, especially on Windows where such calls can spin nested message loops. - Reentrancy-freed `this` in views callersReview popup/menu/bubble view methods that call helper functions capable of destroying the widget mid-call and then run further statements, ensuring destruction is signaled to and honored by every caller in the chain.
Patch
From f7faaf0f60da100d09c0556ff173159c3dce5faa Mon Sep 17 00:00:00 2001
From: Jan Keitel <jkeitel@google.com>
Date: Mon, 15 Jun 2026 08:01:58 -0700
Subject: [PATCH] Autofill: Fix UAF in PopupViewViews on Windows
Accessibility notifications can trigger a nested message loop that
destroys the view, but the calling functions continue to access the
freed `this` pointer.
This CL fixes the issue by:
1. Modifying `MaybeA11yFocusInformationalSuggestion()` to return a bool
indicating whether the view survived the accessibility calls.
2. Updating callers `Show()` and `OnSuggestionsChanged()` to check this
return value and return early if the view was destroyed.
3. Marking `TrackAndRun` as `[[nodiscard]]` to prevent future ignored
return values.
4. Adding a unit test to verify the fix.
Note that this fix is not terribly robust against future code changes.
The linked bug contains a reference to a more fundamental solution
to popup lifetime issues.
TAG=agy
CONV=b8251de8-7593-4cd8-9dfa-d130c55f5101
Fixed: 523740781
Change-Id: Id478d96267f47e4e2b25e39dce3990f80c4c77d9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7941589
Reviewed-by: Christoph Schwering <schwering@google.com>
Commit-Queue: Jan Keitel <jkeitel@google.com>
Cr-Commit-Position: refs/heads/main@{#1646809}
---
diff --git a/chrome/browser/ui/views/autofill/popup/popup_view_utils.h b/chrome/browser/ui/views/autofill/popup/popup_view_utils.h
index 1b24b95..eaa89037 100644
--- a/chrome/browser/ui/views/autofill/popup/popup_view_utils.h
+++ b/chrome/browser/ui/views/autofill/popup/popup_view_utils.h
@@ -198,8 +198,27 @@
// This is typically used to prevent Use-After-Free (UAF) vulnerabilities on
// Windows when firing platform accessibility events (e.g., focus or selection
// changes).
+// Note that when `view` is `this`, the return value must be handled and must be
+// propagated:
+//
+// [[nodiscard]] bool PopupBaseView::Foo() {
+// if (!TrackAndRun(this, callable)) {
+// return false;
+// }
+// DoSomethingElse();
+// }
+//
+// // You may only drop the return value if you can guarantee that nothing
+// // accesses `this` or members of `this` after calling `Bar();`.
+// [[nodiscard]] bool PopupBaseView::Bar() {
+// if (!Foo()) { return false; }
+// DoSomethingElse();
+// return true;
+// }
+//
+// TODO(crbug.com/524084900): Migrate to a more robust pattern.
template <typename... Callables>
-bool TrackAndRun(views::View* view, Callables&&... callbacks) {
+[[nodiscard]] bool TrackAndRun(views::View* view, Callables&&... callbacks) {
CHECK(view);
views::ViewTracker tracker(view);
bool alive = true;
diff --git a/chrome/browser/ui/views/autofill/popup/popup_view_views.cc b/chrome/browser/ui/views/autofill/popup/popup_view_views.cc
index c4b19c7..7d12e225 100644
--- a/chrome/browser/ui/views/autofill/popup/popup_view_views.cc
+++ b/chrome/browser/ui/views/autofill/popup/popup_view_views.cc
@@ -323,7 +323,9 @@
MaybeAnnounceCurrentTabAndFootnote();
MaybeAnnouncePasswordRecoveryPopup();
MaybeAnnounceLoadingState();
- MaybeA11yFocusInformationalSuggestion();
+ if (!MaybeA11yFocusInformationalSuggestion()) {
+ return false;
+ }
return !CanActivate() || (GetWidget() && GetWidget()->IsActive());
}
@@ -811,7 +813,9 @@
MaybeAutoSelectSuggestion();
MaybeAnnouncePasswordRecoveryPopup();
MaybeAnnounceLoadingState();
- MaybeA11yFocusInformationalSuggestion();
+ if (!MaybeA11yFocusInformationalSuggestion()) {
+ return;
+ }
ShowIPHFeaturePromos();
}
@@ -1690,21 +1694,23 @@
return true;
}
-void PopupViewViews::MaybeA11yFocusInformationalSuggestion() {
+bool PopupViewViews::MaybeA11yFocusInformationalSuggestion() {
if (rows_.size() != 1) {
- return;
+ return true;
}
if (auto* warning_view = std::get_if<PopupWarningView*>(&rows_[0]);
warning_view && *warning_view) {
PopupWarningView* view_ptr = *warning_view;
- TrackAndRun(
+ return TrackAndRun(
view_ptr, [this, view_ptr]() { NotifyAXSelection(*view_ptr); },
[view_ptr]() {
view_ptr->NotifyAccessibilityEventDeprecated(ax::mojom::Event::kFocus,
true);
});
}
+
+ return true;
}
base::WeakPtr<AutofillPopupView> PopupViewViews::GetWeakPtr() {
diff --git a/chrome/browser/ui/views/autofill/popup/popup_view_views.h b/chrome/browser/ui/views/autofill/popup/popup_view_views.h
index f984cc3a..0eb5a3b 100644
--- a/chrome/browser/ui/views/autofill/popup/popup_view_views.h
+++ b/chrome/browser/ui/views/autofill/popup/popup_view_views.h
@@ -329,7 +329,9 @@
// the suggestion's message is being announced to the user by focusing the row
// view (which must be selectable). Currently, only `PopupWarningView` is
// supported.
- void MaybeA11yFocusInformationalSuggestion();
+ // Returns true if the popup survived the accessibility event dispatch, false
+ // if it was destroyed. DO NOT access the popup if it has been destroyed.
+ [[nodiscard]] bool MaybeA11yFocusInformationalSuggestion();
// Controller for this view.
base::WeakPtr<AutofillPopupController> controller_ = nullptr;
diff --git a/chrome/browser/ui/views/autofill/popup/popup_view_views_unittest.cc b/chrome/browser/ui/views/autofill/popup/popup_view_views_unittest.cc
index 5f07448..c0ae055 100644
--- a/chrome/browser/ui/views/autofill/popup/popup_view_views_unittest.cc
+++ b/chrome/browser/ui/views/autofill/popup/popup_view_views_unittest.cc
@@ -154,6 +154,16 @@
std::move(callback);
}
+ void NotifyAXSelection(views::View& view) override {
+ if (destroy_on_notify_ax_selection_) {
+ GetWidget()->CloseNow();
+ } else {
+ PopupViewViews::NotifyAXSelection(view);
+ }
+ }
+
+ void DestroyOnNotifyAxSelection() { destroy_on_notify_ax_selection_ = true; }
+
protected:
gfx::Rect GetOptimalPositionAndPlaceArrowOnPopup(
const gfx::Rect& element_bounds,
@@ -173,6 +183,7 @@
private:
GetOptimalPositionAndPlaceArrowOnPopupOverride
get_optimal_position_and_place_arrow_on_popup_override_;
+ bool destroy_on_notify_ax_selection_ = false;
};
class PopupViewViewsTest : public ChromeViewsTestBase {
@@ -2750,6 +2761,15 @@
EXPECT_EQ(1, counter.GetCount(ax::mojom::Event::kFocus, *row_view));
}
+TEST_F(PopupViewViewsTest, WarningOnShow_DestroyOnA11yFocus) {
+ CreateView();
+ controller().set_suggestions({SuggestionType::kMixedFormMessage});
+ view().DestroyOnNotifyAxSelection();
+
+ // This should not crash!
+ ShowView(&view(), widget());
+}
+
TEST_F(PopupViewViewsTest, Show_A11yAnnouncesPasswordRecovery) {
const std::vector<Suggestion> suggestions = {
Suggestion(SuggestionType::kBackupPasswordEntry),
Regression Test / PoC
diff --git a/chrome/browser/ui/views/autofill/popup/popup_view_views_unittest.cc b/chrome/browser/ui/views/autofill/popup/popup_view_views_unittest.cc
index 5f07448..c0ae055 100644
--- a/chrome/browser/ui/views/autofill/popup/popup_view_views_unittest.cc
+++ b/chrome/browser/ui/views/autofill/popup/popup_view_views_unittest.cc
@@ -154,6 +154,16 @@
std::move(callback);
}
+ void NotifyAXSelection(views::View& view) override {
+ if (destroy_on_notify_ax_selection_) {
+ GetWidget()->CloseNow();
+ } else {
+ PopupViewViews::NotifyAXSelection(view);
+ }
+ }
+
+ void DestroyOnNotifyAxSelection() { destroy_on_notify_ax_selection_ = true; }
+
protected:
gfx::Rect GetOptimalPositionAndPlaceArrowOnPopup(
const gfx::Rect& element_bounds,
@@ -173,6 +183,7 @@
private:
GetOptimalPositionAndPlaceArrowOnPopupOverride
get_optimal_position_and_place_arrow_on_popup_override_;
+ bool destroy_on_notify_ax_selection_ = false;
};
class PopupViewViewsTest : public ChromeViewsTestBase {
@@ -2750,6 +2761,15 @@
EXPECT_EQ(1, counter.GetCount(ax::mojom::Event::kFocus, *row_view));
}
+TEST_F(PopupViewViewsTest, WarningOnShow_DestroyOnA11yFocus) {
+ CreateView();
+ controller().set_suggestions({SuggestionType::kMixedFormMessage});
+ view().DestroyOnNotifyAxSelection();
+
+ // This should not crash!
+ ShowView(&view(), widget());
+}
+
TEST_F(PopupViewViewsTest, Show_A11yAnnouncesPasswordRecovery) {
const std::vector<Suggestion> suggestions = {
Suggestion(SuggestionType::kBackupPasswordEntry),
Original Bug Report
Potential Use-After-Free in PopupViewViews via synchronous accessibility events
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 Use-After-Free (UAF) vulnerability exists in PopupViewViews on Windows when handling informational suggestions. Accessibility notifications can trigger a nested message loop that destroys the view, but the calling functions continue to access the freed this pointer.
Affected files:
chrome/browser/ui/views/autofill/popup/popup_view_views.ccchrome/browser/ui/views/autofill/popup/popup_view_views.h
Estimated timestamp from git blame: Unknown (Google3 checkout)
Description
A potential Use-After-Free (UAF) vulnerability exists in PopupViewViews, the Views-based implementation of the Autofill popup.
The issue occurs because PopupViewViews::MaybeA11yFocusInformationalSuggestion() triggers synchronous accessibility events on Windows but fails to propagate the destruction status of the view back to its callers. As a result, callers like PopupViewViews::Show() and PopupViewViews::OnSuggestionsChanged() will continue executing and access the freed this pointer if a concurrent task destroys the popup during the event dispatch.
Root Cause Analysis
When MaybeA11yFocusInformationalSuggestion() is called and the popup contains a single PopupWarningView (e.g., for mixed forms or insecure contexts), it calls NotifyAXSelection() for that child view. This execution is wrapped in a TrackAndRun utility to detect if the view is destroyed during the process.
However, on Windows, if an accessibility client (like a screen reader) is active, firing native events eventually calls ::NotifyWinEvent(). This Windows API can operate synchronously and spin a nested COM message loop. Chromium’s MessagePumpForUI processes pending tasks during this nested loop. If a queued task (such as an IPC triggered by a web page calling window.close() or blurring the input) closes the Autofill popup, the PopupViewViews instance is synchronously destroyed.
While TrackAndRun correctly detects the destruction of the child view and returns false, MaybeA11yFocusInformationalSuggestion() is declared as a void function. It completely ignores this return value. Consequently, control returns to the caller.
In PopupViewViews::Show() (around line 328):
MaybeA11yFocusInformationalSuggestion();
return !CanActivate() || (GetWidget() && GetWidget()->IsActive());
If this was destroyed, the subsequent virtual call to CanActivate() dereferences the freed this pointer.
Impact
Because the this pointer in Show() and OnSuggestionsChanged() is a raw C++ pointer residing on the stack, its memory is not protected by MiraclePtr/BackupRefPtr (which relies on reference counting of raw_ptr<T> members).
If an attacker successfully grooms the browser process heap, the freed memory block can be replaced with attacker-controlled data. The virtual call to CanActivate() can then be hijacked via a forged vtable, potentially leading to arbitrary Remote Code Execution (RCE) and a full Sandbox Escape in the browser process.
Potential Steps to Reproduce
Note: These are suggested theoretical steps based on code analysis, as our tooling does not currently have the capability to run code to produce a working PoC.
- The attacker creates a malicious webpage that grooms the browser process heap.
- The page interacts with an input field to trigger an Autofill popup guaranteed to show a single informational row (e.g., triggering a mixed-form warning).
- Concurrently, the attacker’s script triggers an action that queues an IPC task to close the browser window or hide the popup (e.g., navigating away, calling
window.close(), or rapidly blurring the input). - The browser process attempts to show the popup and fires the accessibility event.
::NotifyWinEventspins a nested message loop on Windows, processing the attacker’s queued close task.- The popup is destroyed, the memory is freed, and the attacker’s heap grooming takes over the memory block.
- Execution returns to
PopupViewViews::Show(), where the forged vtable is dereferenced, achieving RCE.
Suggested Fix
- Modify
MaybeA11yFocusInformationalSuggestion()to return aboolindicating whether thePopupViewViewsinstance survived the accessibility calls (by returning the result ofTrackAndRun). - Update callers like
PopupViewViews::Show()andPopupViewViews::OnSuggestionsChanged()to check this return value and return early if the view was destroyed. - Alternatively, implement
views::ViewTrackeror checkweak_ptr_factory_.GetWeakPtr()in the calling methods to verify the object is still alive before accessingthisagain.
Evaluated with Chrome root at commit: 65b3256311f3ab6fb9870eaa522de7e6dd2663bb
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.