CVE-2026-12442
Overview
Background
- `TouchToFillController`
- the Android Passwords controller in
touch_to_fill_controller.ccthat mediates credential-selection actions and owns members includingttf_delegate_,visibility_controller_, andweak_ptr_factory_. - `base::Unretained(this)`
- a callback binding that passes a raw, unchecked
thispointer, 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
TouchToFillControllermethod bound as the completion callback for delegate actions such asOnCredentialSelected, which the patch says accessesvisibility_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.
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
- Trigger a Touch-To-Fill action
The user (or attacker-influenced page state) causes a delegate action such as
OnCredentialSelectedorOnPasskeyCredentialSelected, which bindsActionCompletedviabase::Unretained(this)and hands it tottf_delegate_. - Cause controller destruction
TouchToFillControlleris torn down; its members are destroyed in reverse order, sovisibility_controller_is freed beforettf_delegate_. - Re-enter through delegate teardown
Destroying
ttf_delegate_invokesauthenticator_->Cancel(), which synchronously runs a failure callback that re-entersActionCompletedon the partially destroyed controller. - Access freed member
ActionCompleteddereferences the already-destroyedvisibility_controller_, producing the use-after-destruction / use-after-free. - 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
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
| Function | Change | Notes |
|---|---|---|
switchchrome/browser/touch_to_fill/password_manager/touch_to_fill_controller.cc |
modified | |
ifchrome/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 delegatesAudit 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; preferWeakPtrwhenever destruction order is not provably safe. - Synchronous cancellation during destructionLook 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 assumptionsReview 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.
Patch
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() {
Original Bug Report
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.hchrome/browser/touch_to_fill/password_manager/touch_to_fill_controller.ccchrome/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.
- An attacker prompts the user to focus a password field on an attacker-controlled page, triggering the Touch-to-Fill bottom sheet UI.
- The user selects a credential, which triggers a biometric authentication prompt via
DeviceAuthenticatorAndroid. - While the biometric prompt is visible, the attacker’s page programmatically closes the window (e.g., via
window.close()), initiating aWebContentsteardown. - The teardown triggers the destruction of
ChromePasswordManagerClientandTouchToFillController. - During member destruction of
TouchToFillController,visibility_controller_is destructed first, freeing its underlying tracking flag. Next,ttf_delegate_is destroyed. - The delegate’s destructor calls
Cancel(), synchronously invoking the failure callback which re-entersTouchToFillController::ActionCompleted. ActionCompletedperforms a Use-After-Destruction check onvisibility_controller_and attempts to callSetShown()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 ensuresvisibility_controller_is destroyed beforettf_delegate_)chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller.cc:317-322(ActionCompletedaccessesvisibility_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.