Overview

Critical
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Passwords
DescriptionUse after free in Passwords
ComponentPasswords
Sub-componentTouchToFill
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.
Tracker521950423
Fix commit9ba399777917 (chromium/src) +20/-13
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-09 (per vendor advisory)
Fix landed2026-06-10 (public git)
Disclosed2026-06-16
Patch gap6 days — fix public in source before the advisory

Background

`TouchToFillController`
the Android Passwords controller in touch_to_fill_controller.cc that mediates credential-selection actions and owns members including ttf_delegate_, visibility_controller_, and weak_ptr_factory_.
`base::Unretained(this)`
a callback binding that passes a raw, unchecked this pointer, so the callback runs even if the bound object has since been destroyed.
`weak_ptr_factory_.GetWeakPtr()`
a binding that produces a WeakPtr; when the factory is destroyed the pointer is invalidated, so the bound method is silently skipped instead of dereferencing freed memory.
`ActionCompleted`
the TouchToFillController method bound as the completion callback for delegate actions such as OnCredentialSelected, which the patch says accesses visibility_controller_.

Root Cause Analysis

Each delegate-action call in TouchToFillController bound TouchToFillController::ActionCompleted with base::Unretained(this), on the removed comment’s assumption that “Unretained is safe here because TouchToFillController owns the delegate.” Per the commit message, member destruction runs in reverse declaration order: weak_ptr_factory_, then visibility_controller_, then ttf_delegate_; destroying ttf_delegate_ invokes authenticator_->Cancel(), which on Android synchronously fires a failure callback that re-enters ActionCompleted. Because that callback held a raw this, it executed during destruction and touched visibility_controller_, which had already been destroyed one step earlier, violating the invariant that a callback only runs while its bound object and the members it reads are still alive.

The fix replaces base::Unretained(this) with weak_ptr_factory_.GetWeakPtr() at every such binding (OnCredentialSelected, the kAccept path, OnPasskeyCredentialSelected, OnManagePasswordsSelected, OnHybridSignInSelected, OnCredManDismissed, OnDismiss); since weak_ptr_factory_ is destroyed first, any re-entrant callback during teardown is invalidated and ignored. It works because the weak pointer becomes null at the very start of destruction, closing the window before the freed members can be reached.

Key insight
The single mistake was binding a completion callback with a raw base::Unretained(this) under the false assumption that owning the delegate makes re-entrancy safe, when destroying that delegate can synchronously re-enter the callback after other members are already gone; the fix routes the callback through weak_ptr_factory_.GetWeakPtr() so it is nullified at the start of destruction.

Attack Path

  1. Trigger a Touch-To-Fill action The user (or attacker-influenced page state) causes a delegate action such as OnCredentialSelected or OnPasskeyCredentialSelected, which binds ActionCompleted via base::Unretained(this) and hands it to ttf_delegate_.
  2. Cause controller destruction TouchToFillController is torn down; its members are destroyed in reverse order, so visibility_controller_ is freed before ttf_delegate_.
  3. Re-enter through delegate teardown Destroying ttf_delegate_ invokes authenticator_->Cancel(), which synchronously runs a failure callback that re-enters ActionCompleted on the partially destroyed controller.
  4. Access freed member ActionCompleted dereferences the already-destroyed visibility_controller_, producing the use-after-destruction / use-after-free.
  5. Escalation Any read/write primitive or control-flow hijack from the freed-memory access is (inferred); the diff demonstrates only the invalid access, not a concrete exploit.

Impact Assessment

An attacker gains a use-after-free on the visibility_controller_ member during TouchToFillController destruction in the Chrome browser process on Android, per the advisory’s classification of “Use after free in Passwords” (CVE-2026-12442, bug 521950423). The precondition is a Touch-To-Fill credential/action flow that binds ActionCompleted and a controller teardown that re-enters it via authenticator_->Cancel(). Any escalation to memory corruption, information disclosure, or code execution is (inferred); the advisory states neither exploited-in-the-wild status nor a severity rating in the provided material.

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

Audit Directions

  • `base::Unretained(this)` into owned delegates
    Audit every callback bound with base::Unretained(this) that is passed to a member the controller owns, since destroying that member can synchronously re-enter the callback after other members are gone; prefer WeakPtr whenever destruction order is not provably safe.
  • Synchronous cancellation during destruction
    Look for destructors (or members’ destructors) that call Cancel(), Reset(), or similar on authenticators/bridges that may fire failure/completion callbacks synchronously, re-entering an object mid-teardown.
  • Reverse-order member destruction assumptions
    Review classes where a callback reads members declared before weak_ptr_factory_ or before the delegate whose destruction triggers re-entry, since reverse-order teardown frees those members while the raw-pointer callback can still run.
From 9ba399777917241b7109962cf1eab7d70e64d3c7 Mon Sep 17 00:00:00 2001
From: Mohamed Amir Yosef <mamir@chromium.org>
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 <mamir@chromium.org>
Reviewed-by: Ioana Treib <ioanap@chromium.org>
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 vm...@google.com

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