Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free Autofill
DescriptionUse after free Autofill
ComponentChromium
Bug ClassUAF
Tracker516907083
Fix commit21a69e5e6115 (chromium/src) +66/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-11

Changed Functions

FunctionChangeNotes
if
components/autofill/core/browser/ui/payments/autofill_progress_dialog_controller_impl.cc
modified
BindLambdaForTesting
components/autofill/core/browser/ui/payments/autofill_progress_dialog_controller_impl_unittest.cc
modified

Files Changed

  • components/autofill/core/browser/ui/payments/autofill_progress_dialog_controller_impl.cc
  • components/autofill/core/browser/ui/payments/autofill_progress_dialog_controller_impl_unittest.cc
From 21a69e5e61150e8c3ce84263083d4b7b330f3dc0 Mon Sep 17 00:00:00 2001
From: Luis Antunes <[email protected]>
Date: Mon, 08 Jun 2026 07:27:30 -0700
Subject: [PATCH] [Autofill] Use-After-Free in AutofillProgressDialogControllerImpl

Fix a UAF on macOS when mandatory reauth falls back to the passcode
prompt. The prompt spins a nested loop, during which the tab can be
closed, destroying the controller. When the prompt is dismissed,
execution resumes in OnDismissed, causing a UAF on deallocated
members.

This CL adds a base::WeakPtr self-guard to exit OnDismissed early if
the controller is destroyed during callback execution. Covers both
the reauth and cancel callback paths.

Fixed: 516907083
Change-Id: I86d2ae77c03d1e81990ac14b51979e13498f1b59
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7896024
Commit-Queue: Luis Antunes <[email protected]>
Reviewed-by: Vinny Persky <[email protected]>
Reviewed-by: Stephen McGruer <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1643184}
---

diff --git a/components/autofill/core/browser/ui/payments/autofill_progress_dialog_controller_impl.cc b/components/autofill/core/browser/ui/payments/autofill_progress_dialog_controller_impl.cc
index 5f1aed3..d1407da0 100644
--- a/components/autofill/core/browser/ui/payments/autofill_progress_dialog_controller_impl.cc
+++ b/components/autofill/core/browser/ui/payments/autofill_progress_dialog_controller_impl.cc
@@ -67,6 +67,11 @@
 
 void AutofillProgressDialogControllerImpl::OnDismissed(
     bool is_canceled_by_user) {
+  // On macOS without biometrics, the accept/cancel callbacks have the potential
+  // to destroy this controller (e.g., if the tab is closed during the nested
+  // run loop of the passcode prompt). We check a weak pointer to avoid a UAF.
+  auto weak_self = weak_ptr_factory_.GetWeakPtr();
+
   // Dialog is being dismissed so set the pointer to nullptr.
   autofill_progress_dialog_view_.reset();
   if (is_canceled_by_user) {
@@ -77,6 +82,10 @@
     }
   }
 
+  if (!weak_self) {
+    return;
+  }
+
   AutofillMetrics::LogProgressDialogResultMetric(
       is_canceled_by_user, autofill_progress_dialog_type_);
   cancel_callback_.Reset();
diff --git a/components/autofill/core/browser/ui/payments/autofill_progress_dialog_controller_impl_unittest.cc b/components/autofill/core/browser/ui/payments/autofill_progress_dialog_controller_impl_unittest.cc
index 82d408a4..71a1f412 100644
--- a/components/autofill/core/browser/ui/payments/autofill_progress_dialog_controller_impl_unittest.cc
+++ b/components/autofill/core/browser/ui/payments/autofill_progress_dialog_controller_impl_unittest.cc
@@ -5,9 +5,16 @@
 #include "components/autofill/core/browser/ui/payments/autofill_progress_dialog_controller_impl.h"
 
 #include <memory>
+#include <utility>
 
+#include "base/functional/bind.h"
+#include "base/functional/callback_helpers.h"
+#include "base/test/bind.h"
 #include "base/test/metrics/histogram_tester.h"
+#include "base/test/mock_callback.h"
+#include "build/buildflag.h"
 #include "components/autofill/core/browser/ui/payments/autofill_progress_dialog_view.h"
+#include "components/autofill/core/browser/ui/payments/autofill_progress_ui_type.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
@@ -46,9 +53,59 @@
     return controller_.get();
   }
 
+  void DeleteController() { controller_.reset(); }
+
+  void InitializeController(base::OnceClosure cancel_callback) {
+    controller_ = std::make_unique<AutofillProgressDialogControllerImpl>(
+        AutofillProgressUiType::kVirtualCardUnmaskProgressUi,
+        std::move(cancel_callback));
+#if BUILDFLAG(IS_IOS)
+    controller_->ShowDialog(base::BindOnce(
+        &AutofillProgressDialogControllerImplTest::CreateDialogView,
+        base::Unretained(this)));
+#else
+    controller_->ShowDialog(
+        base::BindOnce([]() -> std::unique_ptr<AutofillProgressDialogView> {
+          return std::make_unique<TestAutofillProgressDialogView>();
+        }));
+#endif
+  }
+
  private:
   std::unique_ptr<AutofillProgressDialogView> view_;
   std::unique_ptr<AutofillProgressDialogControllerImpl> controller_;
 };
 
+// Tests that a Use-After-Free (UAF) is prevented when the controller is
+// destroyed synchronously during the success callback. A UAF can occur if
+// `OnDismissed()` accesses members after the callback deletes the controller.
+TEST_F(AutofillProgressDialogControllerImplTest,
+       OnDismissed_Success_SafeSelfDestruction) {
+  base::MockCallback<base::OnceClosure> cancel_callback;
+  InitializeController(cancel_callback.Get());
+
+  base::OnceClosure no_interactive_auth_callback =
+      base::BindLambdaForTesting([&]() { DeleteController(); });
+
+  controller()->DismissDialog(/*show_confirmation_before_closing=*/false,
+                              std::move(no_interactive_auth_callback));
+
+  controller()->OnDismissed(/*is_canceled_by_user=*/false);
+
+  EXPECT_EQ(controller(), nullptr);
+}
+
+// Tests the cancellation path of the UAF fix. Ensures that if the controller
+// is destroyed synchronously during the cancel callback, the `WeakPtr` safely
+// prevents `OnDismissed()` from accessing freed memory.
+TEST_F(AutofillProgressDialogControllerImplTest,
+       OnDismissed_Canceled_SafeSelfDestruction) {
+  InitializeController(
+      base::BindLambdaForTesting([&]() { DeleteController(); }));
+
+  controller()->OnDismissed(/*is_canceled_by_user=*/true);
+
+  EXPECT_EQ(controller(), nullptr);
+}
+
 }  // namespace autofill
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/autofill/core/browser/ui/payments/autofill_progress_dialog_controller_impl_unittest.cc b/components/autofill/core/browser/ui/payments/autofill_progress_dialog_controller_impl_unittest.cc
index 82d408a4..71a1f412 100644
--- a/components/autofill/core/browser/ui/payments/autofill_progress_dialog_controller_impl_unittest.cc
+++ b/components/autofill/core/browser/ui/payments/autofill_progress_dialog_controller_impl_unittest.cc
@@ -5,9 +5,16 @@
 #include "components/autofill/core/browser/ui/payments/autofill_progress_dialog_controller_impl.h"
 
 #include <memory>
+#include <utility>
 
+#include "base/functional/bind.h"
+#include "base/functional/callback_helpers.h"
+#include "base/test/bind.h"
 #include "base/test/metrics/histogram_tester.h"
+#include "base/test/mock_callback.h"
+#include "build/buildflag.h"
 #include "components/autofill/core/browser/ui/payments/autofill_progress_dialog_view.h"
+#include "components/autofill/core/browser/ui/payments/autofill_progress_ui_type.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
@@ -46,9 +53,59 @@
     return controller_.get();
   }
 
+  void DeleteController() { controller_.reset(); }
+
+  void InitializeController(base::OnceClosure cancel_callback) {
+    controller_ = std::make_unique<AutofillProgressDialogControllerImpl>(
+        AutofillProgressUiType::kVirtualCardUnmaskProgressUi,
+        std::move(cancel_callback));
+#if BUILDFLAG(IS_IOS)
+    controller_->ShowDialog(base::BindOnce(
+        &AutofillProgressDialogControllerImplTest::CreateDialogView,
+        base::Unretained(this)));
+#else
+    controller_->ShowDialog(
+        base::BindOnce([]() -> std::unique_ptr<AutofillProgressDialogView> {
+          return std::make_unique<TestAutofillProgressDialogView>();
+        }));
+#endif
+  }
+
  private:
   std::unique_ptr<AutofillProgressDialogView> view_;
   std::unique_ptr<AutofillProgressDialogControllerImpl> controller_;
 };
 
+// Tests that a Use-After-Free (UAF) is prevented when the controller is
+// destroyed synchronously during the success callback. A UAF can occur if
+// `OnDismissed()` accesses members after the callback deletes the controller.
+TEST_F(AutofillProgressDialogControllerImplTest,
+       OnDismissed_Success_SafeSelfDestruction) {
+  base::MockCallback<base::OnceClosure> cancel_callback;
+  InitializeController(cancel_callback.Get());
+
+  base::OnceClosure no_interactive_auth_callback =
+      base::BindLambdaForTesting([&]() { DeleteController(); });
+
+  controller()->DismissDialog(/*show_confirmation_before_closing=*/false,
+                              std::move(no_interactive_auth_callback));
+
+  controller()->OnDismissed(/*is_canceled_by_user=*/false);
+
+  EXPECT_EQ(controller(), nullptr);
+}
+
+// Tests the cancellation path of the UAF fix. Ensures that if the controller
+// is destroyed synchronously during the cancel callback, the `WeakPtr` safely
+// prevents `OnDismissed()` from accessing freed memory.
+TEST_F(AutofillProgressDialogControllerImplTest,
+       OnDismissed_Canceled_SafeSelfDestruction) {
+  InitializeController(
+      base::BindLambdaForTesting([&]() { DeleteController(); }));
+
+  controller()->OnDismissed(/*is_canceled_by_user=*/true);
+
+  EXPECT_EQ(controller(), nullptr);
+}
+
 }  // namespace autofill
Loading diff…

Original Bug Report

reported by [email protected]

Potential Browser Process UAF in AutofillProgressDialogControllerImpl::OnDismissed on macOS

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 browser process on macOS within AutofillProgressDialogControllerImpl::OnDismissed. During non-interactive autofill unmasking on devices without biometrics, executing a device authentication callback spins a nested CFRunLoop to display the system password prompt. If the tab is closed during this nested loop, the controller is destroyed, causing a UAF when the prompt is closed and execution resumes.

Affected files:

  • components/autofill/core/browser/ui/payments/autofill_progress_dialog_controller_impl.cc

Estimated timestamp from git blame: 2023-05-10

Root Cause Analysis

A potential Use-After-Free (UAF) vulnerability exists in the browser process on macOS due to unsafe member access after a nested event loop executes in AutofillProgressDialogControllerImpl::OnDismissed located in components/autofill/core/browser/ui/payments/autofill_progress_dialog_controller_impl.cc.

void AutofillProgressDialogControllerImpl::OnDismissed(
    bool is_canceled_by_user) {
  // 1. View is reset here
  autofill_progress_dialog_view_.reset();
  if (is_canceled_by_user) {
    std::move(cancel_callback_).Run();
  } else {
    if (no_interactive_authentication_callback_) {
      // 2. Callback runs and blocks synchronously inside nested loop
      std::move(no_interactive_authentication_callback_).Run();
    }
  }

  // 4. Execution resumes here after destruction, leading to UAF
  AutofillMetrics::LogProgressDialogResultMetric(
      is_canceled_by_user, autofill_progress_dialog_type_);
  cancel_callback_.Reset();
}

When a user authenticates on a macOS device without biometrics configured (e.g., Touch ID disabled), the device authenticator falls back to screen-lock passcode verification. This triggers AuthenticatorMac::AuthenticateUserWithNonBiometrics which calls password_manager_util_mac::AuthenticateUser. This function calls the blocking macOS API base::mac::GetAuthorizationRightsWithPrompt to prompt the user for their system login credentials.

While this system-modal dialog is active, a nested CFRunLoop is spun on the UI thread to keep the browser UI responsive. If the web page initiates a tab or popup closure (e.g., via window.close()) during this nested event loop, the corresponding WebContents is destroyed. This triggers the destruction of ChromeAutofillClient, its member ChromePaymentsAutofillClient, and finally the AutofillProgressDialogControllerImpl instance itself.

Bypassing the Safety Guard

Normally, the controller’s destructor has a safety mechanism to prevent UAF if the tab is closed:

AutofillProgressDialogControllerImpl::~AutofillProgressDialogControllerImpl() {
  if (autofill_progress_dialog_view_) {
    autofill_progress_dialog_view_->InvalidateControllerForCallbacks();
    OnDismissed(/*is_canceled_by_user=*/true);
    autofill_progress_dialog_view_ = nullptr;
  }
}

However, in the described scenario, OnDismissed was already invoked by the view manager. Crucially, the very first statement of OnDismissed is autofill_progress_dialog_view_.reset(). Since autofill_progress_dialog_view_ is already nullptr when the destructor executes during the nested run loop, this safety conditional block is entirely bypassed, and this is safely deallocated while the execution frame remains on the stack.

Once the user cancels or completes the macOS prompt, the nested run loop exits and execution resumes in OnDismissed directly after the callback. Accessing autofill_progress_dialog_type_ and resetting cancel_callback_ (which is a base::OnceClosure holding a reference-counted BindStateBase) on a deallocated this pointer results in a Use-After-Free. This allows an attacker to control execution flow if they can successfully groom the heap during the nested event loop.

Potential Trigger Path

Note: These are potential steps reconstructed through static analysis. Our tooling does not currently have the capability to execute live code or verify this dynamically with a proof-of-concept.

  1. A user visits a page with a credit card form on a macOS device without biometrics configured (mandatory reauth enabled).
  2. The user selects a card from Autofill, triggering a progress dialog.
  3. The Payments server returns a successful response without requiring interactive challenges (kNoAuthenticationRequired).
  4. CreditCardAccessManager triggers CloseAutofillProgressDialog, binding StartDeviceAuthenticationForFilling to no_interactive_authentication_callback_.
  5. The progress dialog is closed synchronously, and OnDismissed(false) is invoked.
  6. no_interactive_authentication_callback_ runs, invoking the system-modal passcode prompt which spins a nested CFRunLoop.
  7. While the OS dialog is active, the opener page closes the popup. This destroys the WebContents and frees the AutofillProgressDialogControllerImpl.
  8. Once the user closes the OS dialog, the nested loop exits, and OnDismissed resumes and accesses member fields on the freed this object.

Suggested Remediation

To remediate this issue, use a base::WeakPtr of this to check if the controller is still alive before executing any further member access or callback resets after the synchronous callback is run:

void AutofillProgressDialogControllerImpl::OnDismissed(
    bool is_canceled_by_user) {
  autofill_progress_dialog_view_.reset();
  if (is_canceled_by_user) {
    std::move(cancel_callback_).Run();
  } else {
    if (no_interactive_authentication_callback_) {
      base::WeakPtr<AutofillProgressDialogControllerImpl> weak_this = 
          weak_ptr_factory_.GetWeakPtr();
      std::move(no_interactive_authentication_callback_).Run();
      if (!weak_this) {
        return;
      }
    }
  }

  AutofillMetrics::LogProgressDialogResultMetric(
      is_canceled_by_user, autofill_progress_dialog_type_);
  cancel_callback_.Reset();
}

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.

View on issue tracker