CVE-2026-110158
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/password_manager/core/browser/credential_manager_impl_unittest.cc |
modified |
Files Changed
components/password_manager/core/browser/credential_manager_pending_request_task.ccchrome/browser/password_manager/credential_manager_browsertest.cccomponents/password_manager/core/browser/credential_manager_impl_unittest.cccomponents/password_manager/core/browser/credential_manager_pending_request_task.h
Patch
From e4afb48bc920df1b3291804ba90fcd2e29532688 Mon Sep 17 00:00:00 2001 From: Adem Derinel <[email protected]> Date: Fri, 18 Sep 2026 05:57:17 -0700 Subject: [PATCH] [Passwords] Fix UAF in CredentialManager task When CredentialManagerPendingRequestTask prompts the user to choose credentials, it passes a callback bound to Unretained(CredentialManagerImpl). If the Mojo pipe disconnects while the chooser dialog is open, CredentialManagerImpl drops the task, but the UI controller retains the stale callback. A subsequent Get() request with kOptional mediation triggers auto sign-in, which synchronously closes the old chooser dialog and runs its callback. That callback deletes the active pending_request_ while its ProcessForms() method is still executing on the stack, causing a use-after-free. Fix this by binding the chooser callback to a WeakPtr on the task, and guarding against reentrancy when callbacks are cancelled. TAG=agy CONV=50e40463-fcb0-472e-9e9a-873e75681685 Fixed: 562242421 Change-Id: Id4f903a9b678da301f1d76984d23cc5afb838abe Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8424659 Reviewed-by: Friedrich Hauser <[email protected]> Commit-Queue: Adem Derinel <[email protected]> Cr-Commit-Position: refs/heads/main@{#1700684} --- diff --git a/components/password_manager/core/browser/credential_manager_pending_request_task.cc b/components/password_manager/core/browser/credential_manager_pending_request_task.cc index 1ae9e355..989106b8 100644 --- a/components/password_manager/core/browser/credential_manager_pending_request_task.cc +++ b/components/password_manager/core/browser/credential_manager_pending_request_task.cc @@ -273,21 +273,24 @@ return; } - auto split_send_callback = base::SplitOnceCallback(std::move(send_callback_)); if (!delegate_->client()->PromptUserToChooseCredentials( std::move(results), origin_, - base::BindOnce( - &CredentialManagerPendingRequestTaskDelegate::SendPasswordForm, - base::Unretained(delegate_), std::move(split_send_callback.first), - mediation_))) { + base::BindOnce(&CredentialManagerPendingRequestTask::SendPasswordForm, + weak_ptr_factory_.GetWeakPtr()))) { // Since PromptUserToChooseCredentials() does not invoke the callback when - // returning false, `repeating_send_callback` has not been run in this - // branch yet. + // returning false, `send_callback_` has not been run in this branch yet. LogCredentialManagerGetResult( metrics_util::CredentialManagerGetResult::kNone, mediation_); - delegate_->SendCredential(std::move(split_send_callback.second), - CredentialInfo()); + delegate_->SendCredential(std::move(send_callback_), CredentialInfo()); } } +void CredentialManagerPendingRequestTask::SendPasswordForm( + const PasswordForm* form) { + // `delegate_->SendPasswordForm()` invokes CredentialManagerImpl::SendCredential(), + // which resets `pending_request_` and deletes `this`. Do not access any member + // after this call. + delegate_->SendPasswordForm(std::move(send_callback_), mediation_, form); +} + } // namespace password_manager diff --git a/components/password_manager/core/browser/credential_manager_pending_request_task.h b/components/password_manager/core/browser/credential_manager_pending_request_task.h index 1c0aa6c..64cad8cf 100644 --- a/components/password_manager/core/browser/credential_manager_pending_request_task.h +++ b/components/password_manager/core/browser/credential_manager_pending_request_task.h @@ -12,6 +12,7 @@ #include "base/functional/callback_forward.h" #include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" #include "components/password_manager/core/browser/form_fetcher.h" #include "components/password_manager/core/browser/password_form_digest.h" #include "components/password_manager/core/common/credential_manager_types.h" @@ -74,6 +75,7 @@ void OnFetchCompleted() override; void ProcessForms(std::vector<std::unique_ptr<PasswordForm>> results); + void SendPasswordForm(const PasswordForm* form); raw_ptr<CredentialManagerPendingRequestTaskDelegate> delegate_; // Weak; SendCredentialCallback send_callback_; @@ -82,6 +84,9 @@ const bool include_passwords_; std::set<std::string> federations_; std::unique_ptr<FormFetcher> form_fetcher_; + + base::WeakPtrFactory<CredentialManagerPendingRequestTask> weak_ptr_factory_{ + this}; }; } // namespace password_manager diff --git a/chrome/browser/password_manager/credential_manager_browsertest.cc b/chrome/browser/password_manager/credential_manager_browsertest.cc index 461ca1b..532f5dc 100644 --- a/chrome/browser/password_manager/credential_manager_browsertest.cc +++ b/chrome/browser/password_manager/credential_manager_browsertest.cc @@ -1,12 +1,15 @@ // Copyright 2016 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include <optional> + #include "base/command_line.h" #include "base/memory/raw_ptr.h" #include "base/strings/strcat.h" #include "base/strings/stringprintf.h" #include "base/strings/utf_string_conversions.h" #include "base/test/scoped_feature_list.h" +#include "base/test/test_future.h" #include "chrome/browser/password_manager/chrome_password_manager_client.h" #include "chrome/browser/password_manager/password_manager_test_base.h" #include "chrome/browser/password_manager/password_manager_test_util.h" @@ -23,6 +26,8 @@ #include "components/password_manager/core/browser/password_store/test_password_store.h" #include "components/password_manager/core/browser/password_string.h" #include "components/password_manager/core/common/password_manager_features.h" +#include "components/password_manager/core/common/password_manager_pref_names.h" +#include "components/prefs/pref_service.h" #include "content/public/browser/back_forward_cache.h" #include "content/public/browser/browser_task_traits.h" #include "content/public/browser/browser_thread.h" @@ -34,6 +39,7 @@ #include "content/public/test/browser_test.h" #include "content/public/test/browser_test_utils.h" #include "content/public/test/prerender_test_util.h" +#include "mojo/public/cpp/bindings/remote.h" #include "net/dns/mock_host_resolver.h" namespace { @@ -1259,4 +1265,72 @@ EXPECT_EQ(avatar_request_counter(), 2u); } +// Regression test for crbug.com/562242421: A disconnected CredentialManager +// pipe with an active account chooser must not leave a stale callback in +// ManagePasswordsUIController that deletes a subsequent auto-signin request +// during ProcessForms(). +IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest, + AutoSigninAfterDisconnectWithPendingChooserDoesNotUAF) { + scoped_refptr<password_manager::TestPasswordStore> password_store = + GetDefaultPasswordStore(browser()->GetProfile()); + + NavigateToFile("/password/simple_password.html"); + GURL url = WebContents()->GetLastCommittedURL(); + + password_manager::PasswordForm form; + form.signon_realm = url::Origin::Create(url).GetURL().spec(); + form.url = url::Origin::Create(url).GetURL(); + form.username_value = u"user1"; + form.password_value = PasswordString(u"abcdef"); + form.skip_zero_click = false; + password_store->AddLogin(password_manager::FromPasswordForm(form)); + WaitForPasswordStore(); + + browser()->GetProfile()->GetPrefs()->SetBoolean( + password_manager::prefs::kWasAutoSignInFirstRunExperienceShown, true); + + ChromePasswordManagerClient* client = + ChromePasswordManagerClient::FromWebContents(WebContents()); + ASSERT_TRUE(client); + + // 1. Bind first CredentialManager pipe and call Get(kRequired) to open the + // account chooser. + mojo::Remote<blink::mojom::CredentialManager> remote1; + client->GetContentCredentialManager()->BindRequest( + WebContents()->GetPrimaryMainFrame(), + remote1.BindNewPipeAndPassReceiver()); + remote1->Get(password_manager::CredentialMediationRequirement::kRequired, + /*include_passwords=*/true, + /*federations=*/{}, base::DoNothing()); + + BubbleObserver(WebContents()).WaitForAccountChooser(); + EXPECT_TRUE(IsShowingAccountChooser()); + + // 2. Disconnect the Mojo pipe while the account chooser is showing. + remote1.reset(); + base::RunLoop().RunUntilIdle(); + + // 3. Bind a new CredentialManager pipe and call Get(kOptional), triggering + // auto sign-in and NotifyUserAutoSignin(), which destroys the old chooser. + mojo::Remote<blink::mojom::CredentialManager> remote2; + client->GetContentCredentialManager()->BindRequest( + WebContents()->GetPrimaryMainFrame(), + remote2.BindNewPipeAndPassReceiver()); + + base::test::TestFuture<password_manager::CredentialManagerError, + std::optional<password_manager::CredentialInfo>> + future; + remote2->Get(password_manager::CredentialMediationRequirement::kOptional, + /*include_passwords=*/true, + /*federations=*/{}, + future.GetCallback< + password_manager::CredentialManagerError, + const std::optional<password_manager::CredentialInfo>&>()); + + ASSERT_TRUE(future.Wait()); + EXPECT_EQ(password_manager::CredentialManagerError::SUCCESS, future.Get<0>()); + ASSERT_TRUE(future.Get<1>().has_value()); + EXPECT_EQ(u"user1", future.Get<1>()->id); +} + } // namespace diff --git a/components/password_manager/core/browser/credential_manager_impl_unittest.cc b/components/password_manager/core/browser/credential_manager_impl_unittest.cc index c7627444..aa12007 100644 --- a/components/password_manager/core/browser/credential_manager_impl_unittest.cc +++ b/components/password_manager/core/browser/credential_manager_impl_unittest.cc @@ -184,10 +184,14 @@ const url::Origin& origin, CredentialsCallback callback) override { EXPECT_FALSE(local_forms.empty()); - const PasswordForm* form = local_forms[0].get(); - base::SequencedTaskRunner::GetCurrentDefault()->PostTask( - FROM_HERE, base::BindOnce(std::move(callback), - base::Owned(new PasswordForm(*form)))); + if (should_store_callback_) { + stored_choose_callback_ = std::move(callback); + } else { + const PasswordForm* form = local_forms[0].get(); + base::SequencedTaskRunner::GetCurrentDefault()->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), + base::Owned(new PasswordForm(*form)))); + } PromptUserToChooseCredentialsPtr( base::ToVector(local_forms, &std::unique_ptr<PasswordForm>::get), origin, base::DoNothing()); @@ -198,9 +202,21 @@ std::vector<std::unique_ptr<PasswordForm>> local_forms, const url::Origin& origin) override { EXPECT_FALSE(local_forms.empty()); + if (stored_choose_callback_) { + // Simulate ManagePasswordsUIController::OnAutoSignin calling + // DestroyPopups() -> TransitionToState(), which synchronously runs the + // stale chooser callback before reading `origin`. + std::move(stored_choose_callback_).Run(nullptr); + url::Origin origin_copy = origin; + EXPECT_FALSE(origin_copy.opaque()); + } NotifyUserAutoSigninPtr(); } + // Captures the CredentialsCallback instead of posting it asynchronously to + // simulate an open chooser dialog. + void capture_callback() { should_store_callback_ = true; } + void set_zero_click_enabled(bool zero_click_enabled) { auto_sign_in_enabled_ = zero_click_enabled; } @@ -221,6 +237,8 @@ NiceMock<MockPasswordManager> password_manager_; GURL last_committed_url_{kTestWebOrigin}; bool auto_sign_in_enabled_ = true; + bool should_store_callback_ = false; + CredentialsCallback stored_choose_callback_; }; // Callbacks from CredentialManagerImpl methods @@ -2138,4 +2156,58 @@ EXPECT_FALSE(called); } +// Regression test for crbug.com/562242421: When a Get() request with kRequired +// opens the account chooser, and the Mojo pipe disconnects before the chooser +// is answered, ResetAfterDisconnecting() drops the pending request while the +// chooser callback survives in the UI controller. If a subsequent Get() request +// with kOptional triggers auto sign-in, NotifyUserAutoSignin() closes the old +// chooser and runs its stale callback. That stale callback must not delete the +// active `pending_request_` while ProcessForms() is still executing on the +// stack. +TEST_P(CredentialManagerImplTest, + AutoSigninAfterDisconnectWithPendingChooserDoesNotUAF) { + client_->set_zero_click_enabled(true); + client_->set_first_run_seen(true); + client_->capture_callback(); + form_.skip_zero_click = false; + store_->AddLogin(password_manager::FromPasswordForm(form_)); + + EXPECT_CALL(*client_, PromptUserToChooseCredentialsPtr); + EXPECT_CALL(*client_, NotifyUserAutoSigninPtr); + + // 1. First Get() request with kRequired opens the account chooser and stores + // the callback in the UI controller. + bool called1 = false; + CredentialManagerError error1 = CredentialManagerError::UNKNOWN; + std::optional<CredentialInfo> credential1; + CallGet( + CredentialMediationRequirement::kRequired, /*include_passwords=*/true, + /*federations=*/{}, + base::BindOnce(&GetCredentialCallback, &called1, &error1, &credential1)); + RunAllPendingTasks(); + EXPECT_FALSE(called1); + + // 2. The renderer disconnects the Mojo pipe. + cm_service_impl_->ResetAfterDisconnecting(); + + // 3. A new pipe calls Get() with kOptional, which chooses auto sign-in and + // calls NotifyUserAutoSignin(), synchronously closing the old chooser and + // running its stale callback. + bool called2 = false; + CredentialManagerError error2 = CredentialManagerError::UNKNOWN; + std::optional<CredentialInfo> credential2; + CallGet( + CredentialMediationRequirement::kOptional, /*include_passwords=*/true, + /*federations=*/{}, + base::BindOnce(&GetCredentialCallback, &called2, &error2, &credential2)); + RunAllPendingTasks(); + + EXPECT_FALSE(called1); + EXPECT_TRUE(called2); + EXPECT_EQ(CredentialManagerError::SUCCESS, error2); + ASSERT_TRUE(credential2.has_value()); + EXPECT_EQ(CredentialType::CREDENTIAL_TYPE_PASSWORD, credential2->type); + EXPECT_EQ(form_.username_value, credential2->id); +} + } // namespace password_manager
Regression Test / PoC
diff --git a/chrome/browser/password_manager/credential_manager_browsertest.cc b/chrome/browser/password_manager/credential_manager_browsertest.cc
index 461ca1b..532f5dc 100644
--- a/chrome/browser/password_manager/credential_manager_browsertest.cc
+++ b/chrome/browser/password_manager/credential_manager_browsertest.cc
@@ -1,12 +1,15 @@
// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#include <optional>
+
#include "base/command_line.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
+#include "base/test/test_future.h"
#include "chrome/browser/password_manager/chrome_password_manager_client.h"
#include "chrome/browser/password_manager/password_manager_test_base.h"
#include "chrome/browser/password_manager/password_manager_test_util.h"
@@ -23,6 +26,8 @@
#include "components/password_manager/core/browser/password_store/test_password_store.h"
#include "components/password_manager/core/browser/password_string.h"
#include "components/password_manager/core/common/password_manager_features.h"
+#include "components/password_manager/core/common/password_manager_pref_names.h"
+#include "components/prefs/pref_service.h"
#include "content/public/browser/back_forward_cache.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
@@ -34,6 +39,7 @@
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/prerender_test_util.h"
+#include "mojo/public/cpp/bindings/remote.h"
#include "net/dns/mock_host_resolver.h"
namespace {
@@ -1259,4 +1265,72 @@
EXPECT_EQ(avatar_request_counter(), 2u);
}
+// Regression test for crbug.com/562242421: A disconnected CredentialManager
+// pipe with an active account chooser must not leave a stale callback in
+// ManagePasswordsUIController that deletes a subsequent auto-signin request
+// during ProcessForms().
+IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest,
+ AutoSigninAfterDisconnectWithPendingChooserDoesNotUAF) {
+ scoped_refptr<password_manager::TestPasswordStore> password_store =
+ GetDefaultPasswordStore(browser()->GetProfile());
+
+ NavigateToFile("/password/simple_password.html");
+ GURL url = WebContents()->GetLastCommittedURL();
+
+ password_manager::PasswordForm form;
+ form.signon_realm = url::Origin::Create(url).GetURL().spec();
+ form.url = url::Origin::Create(url).GetURL();
+ form.username_value = u"user1";
+ form.password_value = PasswordString(u"abcdef");
+ form.skip_zero_click = false;
+ password_store->AddLogin(password_manager::FromPasswordForm(form));
+ WaitForPasswordStore();
+
+ browser()->GetProfile()->GetPrefs()->SetBoolean(
+ password_manager::prefs::kWasAutoSignInFirstRunExperienceShown, true);
+
+ ChromePasswordManagerClient* client =
+ ChromePasswordManagerClient::FromWebContents(WebContents());
+ ASSERT_TRUE(client);
+
+ // 1. Bind first CredentialManager pipe and call Get(kRequired) to open the
+ // account chooser.
+ mojo::Remote<blink::mojom::CredentialManager> remote1;
+ client->GetContentCredentialManager()->BindRequest(
+ WebContents()->GetPrimaryMainFrame(),
+ remote1.BindNewPipeAndPassReceiver());
+ remote1->Get(password_manager::CredentialMediationRequirement::kRequired,
+ /*include_passwords=*/true,
+ /*federations=*/{}, base::DoNothing());
+
+ BubbleObserver(WebContents()).WaitForAccountChooser();
+ EXPECT_TRUE(IsShowingAccountChooser());
+
+ // 2. Disconnect the Mojo pipe while the account chooser is showing.
+ remote1.reset();
+ base::RunLoop().RunUntilIdle();
+
+ // 3. Bind a new CredentialManager pipe and call Get(kOptional), triggering
+ // auto sign-in and NotifyUserAutoSignin(), which destroys the old chooser.
+ mojo::Remote<blink::mojom::CredentialManager> remote2;
+ client->GetContentCredentialManager()->BindRequest(
+ WebContents()->GetPrimaryMainFrame(),
+ remote2.BindNewPipeAndPassReceiver());
+
+ base::test::TestFuture<password_manager::CredentialManagerError,
+ std::optional<password_manager::CredentialInfo>>
+ future;
+ remote2->Get(password_manager::CredentialMediationRequirement::kOptional,
+ /*include_passwords=*/true,
+ /*federations=*/{},
+ future.GetCallback<
+ password_manager::CredentialManagerError,
+ const std::optional<password_manager::CredentialInfo>&>());
+
+ ASSERT_TRUE(future.Wait());
+ EXPECT_EQ(password_manager::CredentialManagerError::SUCCESS, future.Get<0>());
+ ASSERT_TRUE(future.Get<1>().has_value());
+ EXPECT_EQ(u"user1", future.Get<1>()->id);
+}
+
} // namespace
diff --git a/components/password_manager/core/browser/credential_manager_impl_unittest.cc b/components/password_manager/core/browser/credential_manager_impl_unittest.cc
index c7627444..aa12007 100644
--- a/components/password_manager/core/browser/credential_manager_impl_unittest.cc
+++ b/components/password_manager/core/browser/credential_manager_impl_unittest.cc
@@ -184,10 +184,14 @@
const url::Origin& origin,
CredentialsCallback callback) override {
EXPECT_FALSE(local_forms.empty());
- const PasswordForm* form = local_forms[0].get();
- base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
- FROM_HERE, base::BindOnce(std::move(callback),
- base::Owned(new PasswordForm(*form))));
+ if (should_store_callback_) {
+ stored_choose_callback_ = std::move(callback);
+ } else {
+ const PasswordForm* form = local_forms[0].get();
+ base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
+ FROM_HERE, base::BindOnce(std::move(callback),
+ base::Owned(new PasswordForm(*form))));
+ }
PromptUserToChooseCredentialsPtr(
base::ToVector(local_forms, &std::unique_ptr<PasswordForm>::get),
origin, base::DoNothing());
@@ -198,9 +202,21 @@
std::vector<std::unique_ptr<PasswordForm>> local_forms,
const url::Origin& origin) override {
EXPECT_FALSE(local_forms.empty());
+ if (stored_choose_callback_) {
+ // Simulate ManagePasswordsUIController::OnAutoSignin calling
+ // DestroyPopups() -> TransitionToState(), which synchronously runs the
+ // stale chooser callback before reading `origin`.
+ std::move(stored_choose_callback_).Run(nullptr);
+ url::Origin origin_copy = origin;
+ EXPECT_FALSE(origin_copy.opaque());
+ }
NotifyUserAutoSigninPtr();
}
+ // Captures the CredentialsCallback instead of posting it asynchronously to
+ // simulate an open chooser dialog.
+ void capture_callback() { should_store_callback_ = true; }
+
void set_zero_click_enabled(bool zero_click_enabled) {
auto_sign_in_enabled_ = zero_click_enabled;
}
@@ -221,6 +237,8 @@
NiceMock<MockPasswordManager> password_manager_;
GURL last_committed_url_{kTestWebOrigin};
bool auto_sign_in_enabled_ = true;
+ bool should_store_callback_ = false;
+ CredentialsCallback stored_choose_callback_;
};
// Callbacks from CredentialManagerImpl methods
@@ -2138,4 +2156,58 @@
EXPECT_FALSE(called);
}
+// Regression test for crbug.com/562242421: When a Get() request with kRequired
+// opens the account chooser, and the Mojo pipe disconnects before the chooser
+// is answered, ResetAfterDisconnecting() drops the pending request while the
+// chooser callback survives in the UI controller. If a subsequent Get() request
+// with kOptional triggers auto sign-in, NotifyUserAutoSignin() closes the old
+// chooser and runs its stale callback. That stale callback must not delete the
+// active `pending_request_` while ProcessForms() is still executing on the
+// stack.
+TEST_P(CredentialManagerImplTest,
+ AutoSigninAfterDisconnectWithPendingChooserDoesNotUAF) {
+ client_->set_zero_click_enabled(true);
+ client_->set_first_run_seen(true);
+ client_->capture_callback();
+ form_.skip_zero_click = false;
+ store_->AddLogin(password_manager::FromPasswordForm(form_));
+
+ EXPECT_CALL(*client_, PromptUserToChooseCredentialsPtr);
+ EXPECT_CALL(*client_, NotifyUserAutoSigninPtr);
+
+ // 1. First Get() request with kRequired opens the account chooser and stores
+ // the callback in the UI controller.
+ bool called1 = false;
+ CredentialManagerError error1 = CredentialManagerError::UNKNOWN;
+ std::optional<CredentialInfo> credential1;
+ CallGet(
+ CredentialMediationRequirement::kRequired, /*include_passwords=*/true,
+ /*federations=*/{},
+ base::BindOnce(&GetCredentialCallback, &called1, &error1, &credential1));
+ RunAllPendingTasks();
+ EXPECT_FALSE(called1);
+
+ // 2. The renderer disconnects the Mojo pipe.
+ cm_service_impl_->ResetAfterDisconnecting();
+
+ // 3. A new pipe calls Get() with kOptional, which chooses auto sign-in and
+ // calls NotifyUserAutoSignin(), synchronously closing the old chooser and
+ // running its stale callback.
+ bool called2 = false;
+ CredentialManagerError error2 = CredentialManagerError::UNKNOWN;
+ std::optional<CredentialInfo> credential2;
+ CallGet(
+ CredentialMediationRequirement::kOptional, /*include_passwords=*/true,
+ /*federations=*/{},
+ base::BindOnce(&GetCredentialCallback, &called2, &error2, &credential2));
+ RunAllPendingTasks();
+
+ EXPECT_FALSE(called1);
+ EXPECT_TRUE(called2);
+ EXPECT_EQ(CredentialManagerError::SUCCESS, error2);
+ ASSERT_TRUE(credential2.has_value());
+ EXPECT_EQ(CredentialType::CREDENTIAL_TYPE_PASSWORD, credential2->type);
+ EXPECT_EQ(form_.username_value, credential2->id);
+}
+
} // namespace password_manager
Original Bug Report
Browser-process use-after-free in CredentialManagerPendingRequestTask::ProcessForms()
Security Bug
VULNERABILITY DETAILS
Summary. A compromised renderer can make the browser process delete a password_manager::CredentialManagerPendingRequestTask (168 bytes) while that object’s own ProcessForms() is still running on the UI thread. ProcessForms() then reads four members from the freed memory and makes two calls through them. No user interaction at attack time.
Preconditions.
- A compromised renderer (the PoC uses MojoJS as the stand-in).
- One saved password for the attacker’s origin.
- The user once dismissed Chrome’s “Sign in automatically to sites?” prompt with “OK, got it” (synced pref
profile.was_auto_sign_in_first_run_experience_shown). Without it the same steps only show a second chooser. Incognito is not affected.
How it happens.
CredentialManager.Get()with mediationrequiredshows the account chooser and stores a callback bound toUnretained(CredentialManagerImpl).- The renderer closes its
CredentialManagerpipe.ResetAfterDisconnecting()drops the pending request; the chooser and its callback survive. - On a new pipe the renderer calls
Get()with mediationoptional. In the password-store reply,ProcessForms()(credential_manager_pending_request_task.cc:213-227) chooses auto sign-in and callsNotifyUserAutoSignin(). - That closes the old chooser (
ManagePasswordsUIController::OnAutoSignin()→DestroyPopups()→TransitionToState()), which runs the stale callback. It callsCredentialManagerImpl::SendCredential(), whosepending_request_.reset()deletes the task whoseProcessForms()is still on the stack. ProcessForms()lines 224 and 226 then readorigin_,mediation_,delegate_andsend_callback_from the freed slot and callSendCredential()through them. The PoC refills the slot with its own 168-byte string, so release builds fault on page-chosen values.
Not MiraclePtr-protected. The five files involved are byte-identical in 152.0.7977.64, 153.0.8010.25 and 155.0.8038.0. Line-by-line detail: attached DETAILS.md.
VERSION
Chrome Version: tested on Chromium 155.0.8038.0, Linux x86-64: ASan dcheck_always_on=false 7/7; ASan dcheck_always_on=true 7/7 stop at a DCHECK first (see gn args below); is_official_build=true 4/5 SIGSEGV on page-chosen values + 1/5 SIGABRT from the allocator quarantine (same bug, see README); UUI=on 2/2. Google Chrome for Testing 152.0.7977.83 linux64: 2/2. 153: by source only, not executed.
Operating System: Linux (tested, headless). Windows / ChromeOS / macOS: by source only, not executed.
REPRODUCTION CASE
Attachments (save into one directory): repro.py, page.html, README.md, DETAILS.md. Python 3 standard library only; no clicks.
python3 repro.py /path/to/chrome # expected: RESULT: BROWSER CRASHED (ASan heap-use-after-free READ 2 in ManagePasswordsState::OnAutoSignin <- … <- ProcessForms); report in ./asan.txt
FRE=0 python3 repro.py /path/to/chrome # control, first-run pref absent: RESULT: NO CRASH (exit 2)
ENVIRONMENT USED
- Linux x86-64, no desktop. The script runs Chrome headless (
--headless=new --ozone-platform=headless). No window placement is involved, so X11 vs Wayland does not matter. - Chromium 155.0.8038.0 (d81900a3939b) built from source; Google Chrome for Testing 152.0.7977.83.
- gn args: ASan
is_asan=true is_debug=false dcheck_always_on=false is_component_build=false symbol_level=1; releaseis_official_build=true is_chrome_branded=false use_thin_lto=true is_cfi=false dcheck_always_on=false symbol_level=1. Please keepdcheck_always_on=false: with DCHECKs on, the run stops inside the buggy delete itself, where generated Mojo code asserts “CredentialManager::GetCallback was destroyed without first either being run…” (credential_manager.mojom.cc) because the task’s reply callback is destroyed unanswered. Release builds have no such check. - Non-default switches:
--enable-blink-features=MojoJS,MojoJSTest(stands in for a compromised renderer) and--disable-features=CredentialManagementUnifiedUi(branded Chrome’s default auto sign-in bubble;UUI=onkeeps Chromium’s toast, same bug). Otherwise a fresh--user-data-dir,--no-first-run,--no-default-browser-check,--password-store=basic, offline--host-resolver-rules. - Profile: the script writes the first-run pref into
Default/Preferences(stands in for the past “OK, got it” click) and saves one password viachrome.passwordsPrivate.addPasswordover DevTools, then detaches (stands in for a saved password). Page served by the script on127.0.0.1/localhost. - Run time: 10–30 s on ASan, about 7 s on release.
- If it does not reproduce, please send
chrome://versionand the script’sRESULT:andpage …lines.
FOR CRASHES, PLEASE INCLUDE THE FOLLOWING ADDITIONAL INFORMATION
Type of crash: browser process, main (UI) thread.
Chromium 155.0.8038.0 ASan (dcheck_always_on=false)
heap-use-after-free READ of size 2, thread T0 (chrome)
#0 url::SchemeHostPort::operator=
#1 url::Origin::operator=
#2 ManagePasswordsState::OnAutoSignin
#3 ManagePasswordsUIController::OnAutoSignin
#4 ChromePasswordManagerClient::NotifyUserAutoSignin
#5 password_manager::CredentialManagerPendingRequestTask::ProcessForms
48 bytes inside of 168-byte region, freed by thread T0:
operator delete <- CredentialManagerImpl::SendCredential <- SendPasswordForm <- …
<- ManagePasswordsState::TransitionToState <- ManagePasswordsUIController::OnAutoSignin
<- NotifyUserAutoSignin <- ProcessForms (same call as the use above)
MiraclePtr Status: NOT PROTECTED
Official 155 (4/5): SEGV_MAPERR 0x414141414158 at CredentialManagerImpl::SendCredential()+0x1a (mov 0x20(%rdi),%rdi), rdi = 0x414141414138, rsi = 0x434343434340, both page-chosen. 1/5: SIGABRT from the allocator quarantine.
Chrome for Testing 152.0.7977.83 (2/2): SEGV_MAPERR 414141414158, di: 0000414141414138 si: 0000434343434340, pc chrome+0xd885e7e.
RELATED REPORT
None of our earlier reports is involved. The silent-save precondition is adjacent to crbug 498301853.
FULL EXPLOIT CHAIN
In our Linux lab this bug also served as the final, browser-process stage of a one-URL chain behind a compromised renderer (front as in b/559726956 → b/559726782, with b/559793873 supplying browser-heap addresses), reaching command execution in the browser process with the browser surviving. The attached PoC needs no other bug.
CREDIT INFORMATION
Reporter credit: Xinyang Ge
Assisted-by: Claude