Overview

Critical
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Autofill
DescriptionUse after free in Autofill
ComponentAutofill
Sub-componentAutofill
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.
Tracker523740781
Fix commitf7faaf0f60da (chromium/src) +54/-7
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-06-14 (per vendor advisory)
Fix landed2026-06-15 (public git)
Disclosed2026-06-23
Patch gap8 days — fix public in source before the advisory

Background

`PopupViewViews`
the views::View subclass that renders the Autofill suggestion popup in the Chrome browser UI process.
`TrackAndRun`
a template helper in popup_view_utils.h that wraps a views::ViewTracker around a views::View, runs the supplied callables, and returns a bool alive indicating whether the tracked view still exists afterward.
`MaybeA11yFocusInformationalSuggestion`
a PopupViewViews method that, when the popup contains a single PopupWarningView row, fires platform accessibility focus/selection events on that row via TrackAndRun.
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 popup this, 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.

Key insight
The single mistake was dropping 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

  1. Present a single-warning popup Cause the Autofill popup to be shown with exactly one row that is a PopupWarningView, e.g. via a SuggestionType::kMixedFormMessage suggestion, so that rows_.size() == 1 and the warning_view branch in MaybeA11yFocusInformationalSuggestion() is taken (matching the regression test WarningOnShow_DestroyOnA11yFocus).
  2. Trigger accessibility event dispatch With platform accessibility active on Windows, TrackAndRun fires NotifyAXSelection(*view_ptr) / NotifyAccessibilityEventDeprecated(...), which per the commit description can spin a nested message loop.
  3. 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 NotifyAXSelection to call GetWidget()->CloseNow()), freeing this while it is still on the call stack of Show()/OnSuggestionsChanged().
  4. Use the freed view Control returns to the caller, which (pre-patch) dereferences freed this — Show() calling CanActivate()/GetWidget() or OnSuggestionsChanged() calling ShowIPHFeaturePromos() — producing the use-after-free.
  5. Escalate from the UAF Corrupting or reading through the freed PopupViewViews allocation 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

An attacker who can present a mixed-form (or similar single-warning) Autofill popup to a victim running Chrome on Windows with platform accessibility active obtains a use-after-free of the 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

FunctionChangeNotes
if
chrome/browser/ui/views/autofill/popup/popup_view_views_unittest.cc
modified
PopupViewViewsTest
chrome/browser/ui/views/autofill/popup/popup_view_views_unittest.cc
modified
TEST_F
chrome/browser/ui/views/autofill/popup/popup_view_views_unittest.cc
modified

Files Changed

  • chrome/browser/ui/views/autofill/popup/popup_view_utils.h
  • chrome/browser/ui/views/autofill/popup/popup_view_views.cc
  • chrome/browser/ui/views/autofill/popup/popup_view_views.h
  • chrome/browser/ui/views/autofill/popup/popup_view_views_unittest.cc

Audit Directions

  • Dropped lifetime-tracker return values
    Search for other call sites of TrackAndRun (and any ViewTracker-based helper) whose bool result is discarded, and confirm nothing touches the tracked view or this afterward.
  • Post-a11y-dispatch member access
    Audit any code that fires platform accessibility events (NotifyAXSelection, NotifyAccessibilityEventDeprecated, focus/selection notifications) and then continues to access this, its members, or its views::Widget, especially on Windows where such calls can spin nested message loops.
  • Reentrancy-freed `this` in views callers
    Review 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.
From f7faaf0f60da100d09c0556ff173159c3dce5faa Mon Sep 17 00:00:00 2001
From: Jan Keitel <[email protected]>
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 <[email protected]>
Commit-Queue: Jan Keitel <[email protected]>
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),
Loading diff…

Regression Test / PoC

shipped with the fix
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),
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • chrome/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.

  1. The attacker creates a malicious webpage that grooms the browser process heap.
  2. 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).
  3. 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).
  4. The browser process attempts to show the popup and fires the accessibility event.
  5. ::NotifyWinEvent spins a nested message loop on Windows, processing the attacker’s queued close task.
  6. The popup is destroyed, the memory is freed, and the attacker’s heap grooming takes over the memory block.
  7. Execution returns to PopupViewViews::Show(), where the forged vtable is dereferenced, achieving RCE.

Suggested Fix

  1. Modify MaybeA11yFocusInformationalSuggestion() to return a bool indicating whether the PopupViewViews instance survived the accessibility calls (by returning the result of TrackAndRun).
  2. Update callers like PopupViewViews::Show() and PopupViewViews::OnSuggestionsChanged() to check this return value and return early if the view was destroyed.
  3. Alternatively, implement views::ViewTracker or check weak_ptr_factory_.GetWeakPtr() in the calling methods to verify the object is still alive before accessing this again.

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.

View on issue tracker