diff --git a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller_autofill_delegate_unittest.cc b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller_autofill_delegate_unittest.cc --- a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller_autofill_delegate_unittest.cc +++ b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller_autofill_delegate_unittest.cc @@ -223,6 +223,16 @@ class TouchToFillControllerAutofillTest return grouped_credential_sheet_helper_; } + // POC helpers: mimic ~ChromePasswordManagerClient member-destruction order + // (keyboard_replacing_surface_visibility_controller_ is declared after + // touch_to_fill_controller_, so it is destroyed first). + void DestroyVisibilityControllerForPoc() { visibility_controller_.reset(); } + void DestroyTouchToFillControllerForPoc() { + // Avoid raw_ptr dangling-detection on the cached mock pointers. + authenticator_ = nullptr; + weak_filler_ = nullptr; + touch_to_fill_controller_.reset(); + } void SetUp() override { ChromeRenderViewHostTestHarness::SetUp(); window_android_.get()->get()->AddChild(web_contents()->GetNativeView()); @@ -697,6 +707,58 @@ TEST_F(TouchToFillControllerAutofillTest, DestroyedWhileAuthRunning) { EXPECT_CALL(*authenticator(), Cancel()); } +// POC: ~TouchToFillControllerAutofillDelegate calls authenticator_->Cancel(). +// On Android, DeviceAuthenticatorAndroid::Cancel() *synchronously runs* the +// pending OnReauthCompleted callback (it does NOT just reset it, contrary to +// the comment in the delegate). The auth_successful=false branch then runs +// action_complete_, which is TouchToFillController::ActionCompleted bound with +// Unretained on the owning controller. Because ttf_delegate_ is the +// first-declared member of TouchToFillController, it is destroyed last during +// member destruction — so ActionCompleted() is re-entered after +// visibility_controller_ (a base::WeakPtr) has already had its destructor run. +// Reading the destructed WeakPtr dereferences a freed WeakReference::Flag. +// +// Under ASAN this test produces: +// heap-use-after-free in base::internal::WeakReference::IsValid() +TEST_F(TouchToFillControllerAutofillTest, + Poc_CancelDuringDestructionReentersActionCompleted) { + auto credential = + MakeUiCredential({.username = "alice", .password = "p4ssw0rd"}); + Credential credentials[] = {credential}; + + EXPECT_CALL(view(), Show).WillOnce(Return(true)); + Show(credentials, + MakeTouchToFillControllerDelegate( + SubmissionReadinessState::kNoInformation, CreateMockFiller(), + form_to_fill(), form_to_fill()->password_element_renderer_id, + TouchToFillControllerAutofillDelegate::ShowHybridOption(false)), + /*cred_man_delegate=*/nullptr); + + // Capture the OnReauthCompleted callback exactly as + // DeviceAuthenticatorAndroid::AuthenticateWithMessage stores it. + device_reauth::DeviceAuthenticator::AuthenticateCallback pending_auth_cb; + ON_CALL(client(), IsReauthBeforeFillingRequired).WillByDefault(Return(true)); + EXPECT_CALL(*authenticator(), AuthenticateWithMessage) + .WillOnce( + [&](const std::u16string&, + device_reauth::DeviceAuthenticator::AuthenticateCallback cb) { + pending_auth_cb = std::move(cb); + }); + // Mimic DeviceAuthenticatorAndroid::Cancel(): synchronously run the stored + // callback with success=false. + EXPECT_CALL(*authenticator(), Cancel()).WillOnce([&] { + if (pending_auth_cb) { + std::move(pending_auth_cb).Run(/*success=*/false); + } + }); + touch_to_fill_controller().OnCredentialSelected(credential); + + // ~ChromePasswordManagerClient member-destruction order: visibility + // controller impl is freed before the TouchToFillController. + DestroyVisibilityControllerForPoc(); + DestroyTouchToFillControllerForPoc(); // ASAN: heap-use-after-free here. +} + TEST_F(TouchToFillControllerAutofillTest, ShowWebAuthnCredential) { PasskeyCredential credential( PasskeyCredential::Source::kAndroidPhone,