Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Passwords
DescriptionUse after free in Passwords
ComponentPasswords
Bug ClassUAF
Tracker521950423
Fix commit9ba399777917 (chromium/src) +20/-13
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-16

Changed Functions

FunctionChangeNotes
switch
chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller.cc
modified
if
chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller.cc
modified

Files Changed

  • chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller.cc
From 9ba399777917241b7109962cf1eab7d70e64d3c7 Mon Sep 17 00:00:00 2001
From: Mohamed Amir Yosef <[email protected]>
Date: Wed, 10 Jun 2026 07:13:24 -0700
Subject: [PATCH] [TouchToFill] Fix UAF in TouchToFillController during destruction

During the destruction of `TouchToFillController` on Android, its member
variables are destroyed in the reverse order of their declaration:

1. `weak_ptr_factory_`

2. `visibility_controller_`

3. `ttf_delegate_`

When `ttf_delegate_` is destroyed, its destructor invokes
`authenticator_->Cancel()`. On Android, this synchronously runs a
failure callback that re-enters
`TouchToFillController::ActionCompleted`. Because `ActionCompleted` was
bound using `base::Unretained(this)`, it executes and accesses
`visibility_controller_` (which was already destroyed in step 2). This
triggers a Use-After-Destruction and a subsequent Use-After-Free
vulnerability.

This CL prevents the UAF by replacing `base::Unretained(this)` with
`weak_ptr_factory_.GetWeakPtr()` when binding the `ActionCompleted`
callback. Since `weak_ptr_factory_` is destroyed first, any re-entrant
callbacks fired during the destruction of `ttf_delegate_` or other
members are safely invalidated and ignored. Explanatory comments were
also added to prevent regressions.

Fixed: 521950423

Change-Id: I5a03851dc47d9cbcd38bc5f2d4f6b411c101b88d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7914725
Commit-Queue: Mohamed Amir Yosef <[email protected]>
Reviewed-by: Ioana Treib <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1644623}
---

diff --git a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller.cc b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller.cc
index af3aa93..183b8ca 100644
--- a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller.cc
+++ b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller.cc
@@ -206,10 +206,11 @@
     password_manager::metrics_util::LogFillSuggestionGroupedMatchAccepted(
         /*grouped_match_accepted=*/false);
   }
-  // Unretained is safe here because TouchToFillController owns the delegate.
+  // A WeakPtr is necessary because the delegate may trigger this callback
+  // during or after the destruction of this controller.
   ttf_delegate_->OnCredentialSelected(
       credential, base::BindOnce(&TouchToFillController::ActionCompleted,
-                                 base::Unretained(this)));
+                                 weak_ptr_factory_.GetWeakPtr()));
 }
 
 void TouchToFillController::OnAcknowledgementBeforeFillingReceived(
@@ -222,8 +223,8 @@
 
   switch (dismiss_reason) {
     case AcknowledgeGroupedCredentialSheetBridge::DismissReason::kAccept:
-      // Unretained is safe here because TouchToFillController owns the
-      // delegate.
+      // A WeakPtr is necessary because the delegate may trigger this callback
+      // during or after the destruction of this controller.
       ttf_delegate_->OnCredentialSelected(
           credential, base::BindOnce(&TouchToFillController::ActionCompleted,
                                      weak_ptr_factory_.GetWeakPtr()));
@@ -241,24 +242,28 @@
 void TouchToFillController::OnPasskeyCredentialSelected(
     const PasskeyCredential& credential) {
   view_.reset();
-  // Unretained is safe here because TouchToFillController owns the delegate.
+  // A WeakPtr is necessary because the delegate may trigger this callback
+  // during or after the destruction of this controller.
   ttf_delegate_->OnPasskeyCredentialSelected(
       credential, base::BindOnce(&TouchToFillController::ActionCompleted,
-                                 base::Unretained(this)));
+                                 weak_ptr_factory_.GetWeakPtr()));
 }
 
 void TouchToFillController::OnManagePasswordsSelected(bool passkeys_shown) {
   view_.reset();
-  // Unretained is safe here because TouchToFillController owns the delegate.
+  // A WeakPtr is necessary because the delegate may trigger this callback
+  // during or after the destruction of this controller.
   ttf_delegate_->OnManagePasswordsSelected(
       passkeys_shown, base::BindOnce(&TouchToFillController::ActionCompleted,
-                                     base::Unretained(this)));
+                                     weak_ptr_factory_.GetWeakPtr()));
 }
 
 void TouchToFillController::OnHybridSignInSelected() {
   view_.reset();
+  // A WeakPtr is necessary because the delegate may trigger this callback
+  // during or after the destruction of this controller.
   ttf_delegate_->OnHybridSignInSelected(base::BindOnce(
-      &TouchToFillController::ActionCompleted, base::Unretained(this)));
+      &TouchToFillController::ActionCompleted, weak_ptr_factory_.GetWeakPtr()));
 }
 
 void TouchToFillController::OnShowCredManSelected() {
@@ -271,9 +276,10 @@
   if (!ttf_delegate_) {
     return;
   }
-  // Unretained is safe here because TouchToFillController owns the delegate.
+  // A WeakPtr is necessary because the delegate may trigger this callback
+  // during or after the destruction of this controller.
   ttf_delegate_->OnCredManDismissed(base::BindOnce(
-      &TouchToFillController::ActionCompleted, base::Unretained(this)));
+      &TouchToFillController::ActionCompleted, weak_ptr_factory_.GetWeakPtr()));
 }
 
 void TouchToFillController::OnDismiss() {
@@ -284,9 +290,10 @@
     // PasswordSuggestionBottomSheetV2 is launched
     return;
   }
-  // Unretained is safe here because TouchToFillController owns the delegate.
+  // A WeakPtr is necessary because the delegate may trigger this callback
+  // during or after the destruction of this controller.
   ttf_delegate_->OnDismiss(base::BindOnce(
-      &TouchToFillController::ActionCompleted, base::Unretained(this)));
+      &TouchToFillController::ActionCompleted, weak_ptr_factory_.GetWeakPtr()));
 }
 
 Profile* TouchToFillController::GetProfile() {
Loading diff…

Original Bug Report

reported by [email protected]

Potential UAF and Use-After-Destruction in TouchToFillController on Android during Teardown

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 re-entrancy issue during the destruction of TouchToFillController on Android can trigger a Use-After-Destruction of its visibility controller weak pointer. This occurs because the owned delegate synchronously fires its cancellation callback during its own destruction, re-entering the parent controller. This can potentially lead to a Use-After-Free (UAF) and a controlled virtual function call in the unsandboxed browser process.

Affected files:

  • chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller.h
  • chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller.cc
  • chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller_autofill_delegate.cc

Estimated timestamp from git blame: 2023-07-12

Description

There is a potential re-entrancy vulnerability in the Android implementation of Touch-to-Fill (TTF). During the destruction of TouchToFillController, its member variables are destroyed in the reverse order of their declaration. Because the controller delegate (ttf_delegate_) is declared before the visibility controller weak pointer (visibility_controller_), the visibility controller is destroyed first, while the delegate is destroyed last.

When ttf_delegate_ is destroyed, its destructor ~TouchToFillControllerAutofillDelegate is invoked, which calls authenticator_->Cancel(). On Android, DeviceAuthenticatorAndroid::Cancel() synchronously executes any pending re-authentication callback with a failure status. This callback propagates back and synchronously runs TouchToFillController::ActionCompleted via a base::Unretained binding.

Re-entering ActionCompleted on a partially destructed TouchToFillController leads to a Use-After-Destruction of the visibility_controller_ member. Specifically, evaluating if (visibility_controller_) dereferences a freed WeakReference::Flag on the heap, and a subsequent call to SetShown() results in an indirect virtual call on an already freed KeyboardReplacingSurfaceVisibilityControllerImpl object inside the browser process.

Potential Attack Scenario

Note: These are suggested analytical steps as our tooling does not currently have the capability to run or verify proof-of-concept exploit code.

  1. An attacker prompts the user to focus a password field on an attacker-controlled page, triggering the Touch-to-Fill bottom sheet UI.
  2. The user selects a credential, which triggers a biometric authentication prompt via DeviceAuthenticatorAndroid.
  3. While the biometric prompt is visible, the attacker’s page programmatically closes the window (e.g., via window.close()), initiating a WebContents teardown.
  4. The teardown triggers the destruction of ChromePasswordManagerClient and TouchToFillController.
  5. During member destruction of TouchToFillController, visibility_controller_ is destructed first, freeing its underlying tracking flag. Next, ttf_delegate_ is destroyed.
  6. The delegate’s destructor calls Cancel(), synchronously invoking the failure callback which re-enters TouchToFillController::ActionCompleted.
  7. ActionCompleted performs a Use-After-Destruction check on visibility_controller_ and attempts to call SetShown() on the freed visibility controller object.

Affected Files and Code References

  • chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller.h:141-165 (Member declaration order ensures visibility_controller_ is destroyed before ttf_delegate_)
  • chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller.cc:317-322 (ActionCompleted accesses visibility_controller_ after its destruction)
  • chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller_autofill_delegate.cc:101-107 (Destructor synchronously cancels the authenticator)
  • chrome/browser/device_reauth/android/device_authenticator_android.cc:136-147 (Cancel() runs the callback synchronously)

Suggested Fix

To resolve this issue, avoid binding the ActionCompleted callback with base::Unretained(this). Instead, use a weak pointer from the controller’s weak_ptr_factory_. Since weak_ptr_factory_ is destroyed first during the member destruction phase of TouchToFillController, any re-entrant execution of the callback during the destruction of the delegate will be safely ignored as the weak pointer will already be invalidated.

// chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller.cc:209-212
ttf_delegate_->OnCredentialSelected(
    credential, base::BindOnce(&TouchToFillController::ActionCompleted,
                               weak_ptr_factory_.GetWeakPtr()));

Evaluated with Chrome root at commit: 3947e01999a53d4e2382e39736cb79d79c7dffcf


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