CVE-2026-10901
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/device_reauth/mac/device_authenticator_mac.mm |
modified | |
TEST_Pchrome/browser/device_reauth/mac/device_authenticator_mac_unittest.mm |
modified |
Files Changed
chrome/browser/device_reauth/mac/device_authenticator_mac.hchrome/browser/device_reauth/mac/device_authenticator_mac.mmchrome/browser/device_reauth/mac/device_authenticator_mac_unittest.mm
Patch
From 2abda6c797ed977b2247d63484168e1d8ea5108f Mon Sep 17 00:00:00 2001 From: Viktor Semeniuk <[email protected]> Date: Wed, 27 May 2026 04:41:46 -0700 Subject: [PATCH] Post AuthenticateUserWithNonBiometrics call Fixed: 516957738, 516878683 Change-Id: I19a660927c166c8d0eee855ad5a82efad866e692 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7876622 Commit-Queue: Viktor Semeniuk <[email protected]> Reviewed-by: Ioana Treib <[email protected]> Cr-Commit-Position: refs/heads/main@{#1636852} --- diff --git a/chrome/browser/device_reauth/mac/device_authenticator_mac.h b/chrome/browser/device_reauth/mac/device_authenticator_mac.h index 24db5a70..265939d 100644 --- a/chrome/browser/device_reauth/mac/device_authenticator_mac.h +++ b/chrome/browser/device_reauth/mac/device_authenticator_mac.h @@ -48,6 +48,9 @@ // Called when the authentication completes with the result |success|. void OnAuthenticationCompleted(bool success); + // Triggers non-biometric authentication asynchronously. + void AuthenticateWithNonBiometricsAsync(const std::u16string& message); + // Callback to be executed after the authentication completes. AuthenticateCallback callback_; diff --git a/chrome/browser/device_reauth/mac/device_authenticator_mac.mm b/chrome/browser/device_reauth/mac/device_authenticator_mac.mm index c9cb06d..c3da5198 100644 --- a/chrome/browser/device_reauth/mac/device_authenticator_mac.mm +++ b/chrome/browser/device_reauth/mac/device_authenticator_mac.mm @@ -8,6 +8,7 @@ #include "base/memory/ptr_util.h" #include "base/metrics/histogram_functions.h" #include "base/notreached.h" +#include "base/task/sequenced_task_runner.h" #include "chrome/browser/browser_process.h" #include "chrome/browser/device_reauth/mac/authenticator_mac.h" #include "chrome/browser/password_manager/password_manager_util_mac.h" @@ -81,20 +82,16 @@ return; } callback_ = std::move(callback); - // Always use CanAuthenticateWithBiometrics() before invoking the biometrics - // API, and if it fails use password_manager_util_mac::AuthenticateUser() - // instead, until crbug.com/40236979 is fixed. if (!CanAuthenticateWithBiometrics()) { - // AuthenticateUserWithNonBiometrics runs a dialog with a nested run loop, - // so protect against this page disappearing within that nested run loop. - // https://crbug.com/508289938 - auto weak_this = weak_ptr_factory_.GetWeakPtr(); - bool success = authenticator_->AuthenticateUserWithNonBiometrics( - l10n_util::GetStringFUTF16(IDS_PASSWORDS_AUTHENTICATION_PROMPT_PREFIX, - message)); - if (weak_this) { - weak_this->OnAuthenticationCompleted(success); - } + // AuthenticateUserWithNonBiometrics runs a dialog with a nested run loop. + // Post a task to run it asynchronously, so that it doesn't block the + // current call stack. This prevents UaF in the callers if the page is + // closed during the nested run loop. https://crbug.com/508289938 + base::SequencedTaskRunner::GetCurrentDefault()->PostTask( + FROM_HERE, + base::BindOnce( + &DeviceAuthenticatorMac::AuthenticateWithNonBiometricsAsync, + weak_ptr_factory_.GetWeakPtr(), message)); return; } @@ -105,6 +102,17 @@ weak_ptr_factory_.GetWeakPtr())); } +void DeviceAuthenticatorMac::AuthenticateWithNonBiometricsAsync( + const std::u16string& message) { + auto weak_this = weak_ptr_factory_.GetWeakPtr(); + bool success = authenticator_->AuthenticateUserWithNonBiometrics( + l10n_util::GetStringFUTF16(IDS_PASSWORDS_AUTHENTICATION_PROMPT_PREFIX, + message)); + if (weak_this) { + weak_this->OnAuthenticationCompleted(success); + } +} + void DeviceAuthenticatorMac::OnAuthenticationCompleted(bool success) { touch_id_auth_context_ = nullptr; DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); diff --git a/chrome/browser/device_reauth/mac/device_authenticator_mac_unittest.mm b/chrome/browser/device_reauth/mac/device_authenticator_mac_unittest.mm index bd67286..36112da7 100644 --- a/chrome/browser/device_reauth/mac/device_authenticator_mac_unittest.mm +++ b/chrome/browser/device_reauth/mac/device_authenticator_mac_unittest.mm @@ -8,8 +8,8 @@ #include "base/functional/callback_helpers.h" #include "base/memory/raw_ptr.h" #include "base/test/metrics/histogram_tester.h" -#include "base/test/mock_callback.h" #include "base/test/task_environment.h" +#include "base/test/test_future.h" #include "base/time/time.h" #include "chrome/browser/device_reauth/chrome_device_authenticator_factory.h" #include "chrome/browser/device_reauth/mac/authenticator_mac.h" @@ -23,8 +23,6 @@ namespace { -using MockAuthResultCallback = - base::MockCallback<DeviceAuthenticatorMac::AuthenticateCallback>; using device_reauth::ReauthResult; constexpr base::TimeDelta kAuthValidityPeriod = base::Seconds(60); @@ -104,8 +102,6 @@ return &touch_id_test_environment_; } - MockAuthResultCallback& result_callback() { return result_callback_; } - base::HistogramTester& histogram_tester() { return histogram_tester_; } private: @@ -119,22 +115,20 @@ .metadata_secret = "TestMetadataSecret"}; device::fido::mac::ScopedTouchIdTestEnvironment touch_id_test_environment_{ config_}; - MockAuthResultCallback result_callback_; base::HistogramTester histogram_tester_; // This is owned by the authenticator. raw_ptr<MockSystemAuthenticator> system_authenticator_ = nullptr; }; -// If time that passed since the last successful authentication is smaller than -// kAuthValidityPeriod, no reauthentication is needed. TEST_P(DeviceAuthenticatorMacTest, NoReauthenticationIfLessThan60Seconds) { SimulateReauthSuccess(); - EXPECT_CALL(result_callback(), Run(/*success=*/true)); + base::test::TestFuture<bool> future_1; authenticator()->AuthenticateWithMessage( /*message=*/u"Chrome is trying to show passwords.", - result_callback().Get()); + future_1.GetCallback()); + EXPECT_TRUE(future_1.Get()); // Since the delay is smaller than kAuthValidityPeriod there shouldn't be // another prompt, so the auth should be reported as successful. If there is a @@ -142,21 +136,21 @@ // since there is no prompt expected. task_environment().FastForwardBy(kAuthValidityPeriod / 2); - EXPECT_CALL(result_callback(), Run(/*success=*/true)); + base::test::TestFuture<bool> future_2; authenticator()->AuthenticateWithMessage( /*message=*/u"Chrome is trying to show passwords.", - result_callback().Get()); + future_2.GetCallback()); + EXPECT_TRUE(future_2.Get()); } -// If the time since the last reauthentication is greater than -// kAuthValidityPeriod or the authentication failed, reauthentication is needed. TEST_P(DeviceAuthenticatorMacTest, ReauthenticationIfMoreThan60Seconds) { SimulateReauthSuccess(); - EXPECT_CALL(result_callback(), Run(/*success=*/true)); + base::test::TestFuture<bool> future_1; authenticator()->AuthenticateWithMessage( /*message=*/u"Chrome is trying to show passwords.", - result_callback().Get()); + future_1.GetCallback()); + EXPECT_TRUE(future_1.Get()); // Make the reauth prompt auth fail. SimulateReauthFailure(); @@ -165,36 +159,36 @@ // authentication. task_environment().FastForwardBy(kAuthValidityPeriod * 2); - EXPECT_CALL(result_callback(), Run(/*success=*/false)); + base::test::TestFuture<bool> future_2; authenticator()->AuthenticateWithMessage( /*message=*/u"Chrome is trying to show passwords.", - result_callback().Get()); + future_2.GetCallback()); + EXPECT_FALSE(future_2.Get()); } -// If previous authentication failed kAuthValidityPeriod isn't started and -// reauthentication will be needed. TEST_P(DeviceAuthenticatorMacTest, ReauthenticationIfPreviousFailed) { SimulateReauthFailure(); // First authentication fails, no last_good_auth_timestamp_ should be // recorded, which fill force reauthentication. - EXPECT_CALL(result_callback(), Run(/*success=*/false)); + base::test::TestFuture<bool> future_1; authenticator()->AuthenticateWithMessage( /*message=*/u"Chrome is trying to show passwords.", - result_callback().Get()); + future_1.GetCallback()); + EXPECT_FALSE(future_1.Get());
Regression Test / PoC
diff --git a/chrome/browser/device_reauth/mac/device_authenticator_mac_unittest.mm b/chrome/browser/device_reauth/mac/device_authenticator_mac_unittest.mm
index bd67286..36112da7 100644
--- a/chrome/browser/device_reauth/mac/device_authenticator_mac_unittest.mm
+++ b/chrome/browser/device_reauth/mac/device_authenticator_mac_unittest.mm
@@ -8,8 +8,8 @@
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/test/metrics/histogram_tester.h"
-#include "base/test/mock_callback.h"
#include "base/test/task_environment.h"
+#include "base/test/test_future.h"
#include "base/time/time.h"
#include "chrome/browser/device_reauth/chrome_device_authenticator_factory.h"
#include "chrome/browser/device_reauth/mac/authenticator_mac.h"
@@ -23,8 +23,6 @@
namespace {
-using MockAuthResultCallback =
- base::MockCallback<DeviceAuthenticatorMac::AuthenticateCallback>;
using device_reauth::ReauthResult;
constexpr base::TimeDelta kAuthValidityPeriod = base::Seconds(60);
@@ -104,8 +102,6 @@
return &touch_id_test_environment_;
}
- MockAuthResultCallback& result_callback() { return result_callback_; }
-
base::HistogramTester& histogram_tester() { return histogram_tester_; }
private:
@@ -119,22 +115,20 @@
.metadata_secret = "TestMetadataSecret"};
device::fido::mac::ScopedTouchIdTestEnvironment touch_id_test_environment_{
config_};
- MockAuthResultCallback result_callback_;
base::HistogramTester histogram_tester_;
// This is owned by the authenticator.
raw_ptr<MockSystemAuthenticator> system_authenticator_ = nullptr;
};
-// If time that passed since the last successful authentication is smaller than
-// kAuthValidityPeriod, no reauthentication is needed.
TEST_P(DeviceAuthenticatorMacTest, NoReauthenticationIfLessThan60Seconds) {
SimulateReauthSuccess();
- EXPECT_CALL(result_callback(), Run(/*success=*/true));
+ base::test::TestFuture<bool> future_1;
authenticator()->AuthenticateWithMessage(
/*message=*/u"Chrome is trying to show passwords.",
- result_callback().Get());
+ future_1.GetCallback());
+ EXPECT_TRUE(future_1.Get());
// Since the delay is smaller than kAuthValidityPeriod there shouldn't be
// another prompt, so the auth should be reported as successful. If there is a
@@ -142,21 +136,21 @@
// since there is no prompt expected.
task_environment().FastForwardBy(kAuthValidityPeriod / 2);
- EXPECT_CALL(result_callback(), Run(/*success=*/true));
+ base::test::TestFuture<bool> future_2;
authenticator()->AuthenticateWithMessage(
/*message=*/u"Chrome is trying to show passwords.",
- result_callback().Get());
+ future_2.GetCallback());
+ EXPECT_TRUE(future_2.Get());
}
-// If the time since the last reauthentication is greater than
-// kAuthValidityPeriod or the authentication failed, reauthentication is needed.
TEST_P(DeviceAuthenticatorMacTest, ReauthenticationIfMoreThan60Seconds) {
SimulateReauthSuccess();
- EXPECT_CALL(result_callback(), Run(/*success=*/true));
+ base::test::TestFuture<bool> future_1;
authenticator()->AuthenticateWithMessage(
/*message=*/u"Chrome is trying to show passwords.",
- result_callback().Get());
+ future_1.GetCallback());
+ EXPECT_TRUE(future_1.Get());
// Make the reauth prompt auth fail.
SimulateReauthFailure();
@@ -165,36 +159,36 @@
// authentication.
task_environment().FastForwardBy(kAuthValidityPeriod * 2);
- EXPECT_CALL(result_callback(), Run(/*success=*/false));
+ base::test::TestFuture<bool> future_2;
authenticator()->AuthenticateWithMessage(
/*message=*/u"Chrome is trying to show passwords.",
- result_callback().Get());
+ future_2.GetCallback());
+ EXPECT_FALSE(future_2.Get());
}
-// If previous authentication failed kAuthValidityPeriod isn't started and
-// reauthentication will be needed.
TEST_P(DeviceAuthenticatorMacTest, ReauthenticationIfPreviousFailed) {
SimulateReauthFailure();
// First authentication fails, no last_good_auth_timestamp_ should be
// recorded, which fill force reauthentication.
- EXPECT_CALL(result_callback(), Run(/*success=*/false));
+ base::test::TestFuture<bool> future_1;
authenticator()->AuthenticateWithMessage(
/*message=*/u"Chrome is trying to show passwords.",
- result_callback().Get());
+ future_1.GetCallback());
+ EXPECT_FALSE(future_1.Get());
// Although it passed less than kAuthValidityPeriod no valid authentication
// should be recorded as reauth will fail.
SimulateReauthFailure();
task_environment().FastForwardBy(kAuthValidityPeriod / 2);
- EXPECT_CALL(result_callback(), Run(/*success=*/false));
+ base::test::TestFuture<bool> future_2;
authenticator()->AuthenticateWithMessage(
/*message=*/u"Chrome is trying to show passwords.",
- result_callback().Get());
+ future_2.GetCallback());
+ EXPECT_FALSE(future_2.Get());
}
-// If pending authentication can be canceled.
TEST_P(DeviceAuthenticatorMacTest, CancelPendingAuthentication) {
// Non-biometric reauth is modal, and hence cannot be requested twice.
if (!is_biometric_available()) {
@@ -203,14 +197,14 @@
touch_id_environment()->SimulateTouchIdPromptSuccess();
touch_id_environment()->DoNotResolveNextPrompt();
+ base::test::TestFuture<bool> future;
authenticator()->AuthenticateWithMessage(
- /*message=*/u"Chrome is trying to show passwords.",
- result_callback().Get());
+ /*message=*/u"Chrome is trying to show passwords.", future.GetCallback());
// Authentication should fail as it will take 10 seconds to authenticate, and
// there will be a cancellation in the meantime.
- EXPECT_CALL(result_callback(), Run(/*success=*/false));
authenticator()->Cancel();
+ EXPECT_FALSE(future.Get());
}
TEST_P(DeviceAuthenticatorMacTest, BiometricAuthenticationAvailability) {
@@ -240,8 +234,10 @@
TEST_P(DeviceAuthenticatorMacTest, RecordSuccessAuthHistogram) {
SimulateReauthSuccess();
+ base::test::TestFuture<bool> future;
authenticator()->AuthenticateWithMessage(
- /*message=*/u"Chrome is trying to show passwords.", base::DoNothing());
+ /*message=*/u"Chrome is trying to show passwords.", future.GetCallback());
+ EXPECT_TRUE(future.Get());
histogram_tester().ExpectUniqueSample(kHistogramName, ReauthResult::kSuccess,
1);
@@ -250,10 +246,17 @@
TEST_P(DeviceAuthenticatorMacTest, RecordSkippedAuthHistogram) {
SimulateReauthSuccess();
+ base::test::TestFuture<bool> future_1;
authenticator()->AuthenticateWithMessage(
- /*message=*/u"Chrome is trying to show passwords.", base::DoNothing());
+ /*message=*/u"Chrome is trying to show passwords.",
+ future_1.GetCallback());
+ EXPECT_TRUE(future_1.Get());
+
+ base::test::TestFuture<bool> future_2;
authenticator()->AuthenticateWithMessage(
- /*message=*/u"Chrome is trying to show passwords.", base::DoNothing());
+ /*message=*/u"Chrome is trying to show passwords.",
+ future_2.GetCallback());
+ EXPECT_TRUE(future_2.Get());
histogram_tester().ExpectBucketCount(kHistogramName, ReauthResult::kSuccess,
1);
@@ -264,8 +267,10 @@
TEST_P(DeviceAuthenticatorMacTest, RecordFailAuthHistogram) {
SimulateReauthFailure();
+ base::test::TestFuture<bool> future;
authenticator()->AuthenticateWithMessage(
- /*message=*/u"Chrome is trying to show passwords.", base::DoNothing());
+ /*message=*/u"Chrome is trying to show passwords.", future.GetCallback());
+ EXPECT_FALSE(future.Get());
histogram_tester().ExpectUniqueSample(kHistogramName, ReauthResult::kFailure,
1);
Original Bug Report
Potential Use-After-Free in PasswordAutofillManager::DidAcceptSuggestion 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 PasswordAutofillManager on macOS during fallback device authentication. If the containing frame is destroyed while the synchronous macOS password modal dialog is active, the manager is deleted but its member variables are subsequently dereferenced upon dialog dismissal. This could potentially allow arbitrary code execution inside the unsandboxed browser process.
Affected files:
components/password_manager/core/browser/password_autofill_manager.cc
Estimated timestamp from git blame: 2022-08-26
Description
A potential Use-After-Free (UAF) vulnerability exists in PasswordAutofillManager::DidAcceptSuggestion on macOS. This issue stems from a synchronous nested run loop run during non-biometric fallback authentication.
While mitigations exist in DeviceAuthenticatorMac using a weak pointer guard to protect its own lifetime, the calling frame PasswordAutofillManager::DidAcceptSuggestion lacks lifetime tracking after the synchronous authentication flow returns, leading to a potential UAF on the this pointer.
Root Cause Analysis
- In
PasswordAutofillManager::DidAcceptSuggestion(components/password_manager/core/browser/password_autofill_manager.ccline 288), accepting a standard password suggestion invokesOnPasswordCredentialSuggestionAccepted(line 427). - If device re-authentication is required but biometric authentication (Touch ID) is currently unavailable (e.g., due to clamshell mode or biometric lockout),
DeviceAuthenticatorMac::AuthenticateWithMessage(chrome/browser/device_reauth/mac/device_authenticator_mac.mmline 71) falls back to non-biometric authentication viaAuthenticateUserWithNonBiometrics(line 92). - This eventually calls
password_manager_util_mac::AuthenticateUser(chrome/browser/password_manager/password_manager_util_mac.mmline 73) which invokesbase::mac::GetAuthorizationRightsWithPrompt(line 88). - This displays a system modal dialog (
AuthorizationCopyRightswithkAuthorizationFlagInteractionAllowed) that runs a synchronous nestedCFRunLoopon the main UI thread to keep the application responsive while the prompt is active. - During this nested run loop, the attacker-controlled page can trigger a navigation or close the frame, which causes
RenderFrameDeletedto fire. ContentPasswordManagerDriverFactory::RenderFrameDeleted(components/password_manager/content/browser/content_password_manager_driver_factory.ccline 116) erases the driver fromframe_driver_map_, immediately destroying theContentPasswordManagerDriverand its inlinePasswordAutofillManagermember (components/password_manager/content/browser/content_password_manager_driver.hline 215).- When the user dismisses the OS modal dialog, the nested run loop finishes and execution unwinds back to
PasswordAutofillManager::DidAcceptSuggestion(line 433). - The code immediately dereferences member fields of
this(such aspassword_client_andpassword_manager_driver_) to determine if it should enter a loading state. Since thePasswordAutofillManagerinstance has been destroyed, this results in a Use-After-Free.
MiraclePtr (BackupRefPtr) Status
Because PasswordAutofillManager is allocated inline within ContentPasswordManagerDriver and there are no external raw_ptr<PasswordAutofillManager> pointers keeping its refcount active at this point in the call stack, the memory is immediately deallocated back to PartitionAlloc and is reclaimable, potentially bypassing MiraclePtr protections.
Potential Steps to Trigger the Vulnerability
Note: These steps are theoretical/suggested based on static analysis; our tooling does not currently have the capability to run code or execute a proof of concept.
- Run Google Chrome on macOS with a locked-out or unavailable Touch ID configuration (such as clamshell mode) and a device screen lock configured.
- Navigate to an attacker-controlled page containing a password form and trigger the autofill suggestions dropdown.
- Accept a password suggestion to trigger the OS authentication prompt.
- While the modal authentication prompt is displayed, have the page automatically close the containing frame or perform a cross-origin navigation.
- Dismiss the authentication prompt. This returns control to
DidAcceptSuggestion, which should result in a browser process crash or unexpected memory access.
Suggested Remediation
Add a base::WeakPtr guard in PasswordAutofillManager::DidAcceptSuggestion to track the lifetime of this across the reauthentication flow. For example:
base::WeakPtr<PasswordAutofillManager> weak_this = weak_ptr_factory_.GetWeakPtr();
// ... Call to OnPasswordCredentialSuggestionAccepted ...
if (!weak_this) {
return;
}
// Safely resume post-authentication logic using weak_this
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.