CVE-2026-13808
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forios/chrome/app/credential_provider_migrator_app_agent.mm |
modified | |
ifios/chrome/app/credential_provider_migrator_app_agent.mm |
modified |
Files Changed
ios/chrome/app/BUILD.gnios/chrome/app/credential_provider_migrator_app_agent.mm
Patch
From 99afbdce24d30bd74c1e88a52c139eefd6b413c2 Mon Sep 17 00:00:00 2001
From: Alexis Hétu <sugoi@chromium.org>
Date: Wed, 06 May 2026 06:46:35 -0700
Subject: [PATCH] [iOS] Filter credentials by GAIA ID and serialize migration
This CL updates the Credential Provider Migrator to ensure that
credentials are only migrated to the profile they belong to, and
that migration across multiple profiles happens sequentially.
Changes:
1) CredentialProviderMigrator:
Added a `gaiaID` parameter to the constructor and implemented
filtering logic. Any credential in the temporal store that has a
GAIA ID not matching the target profile's GAIA ID is now skipped
2) CredentialProviderMigratorAppAgent:
- Updated to retrieve the primary identity's GAIA ID via
AuthenticationService and pass it to the migrator
- Implemented a serialization queue (std::deque) to manage
migration for multiple loaded profiles. Migration for a
profile now strictly waits for the previous profile's migration
and cleanup to complete
- Added necessary sign-in model dependencies to the build target
Also added new test cases to verify that credentials are correctly
filtered by GAIA ID and that nil GAIA IDs are handled appropriately.
Bug: 504221510
Change-Id: I2f23f3881c46f0e531b13dbeef0a406cbdb0b0bb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7803815
Reviewed-by: Mark Cogan <marq@chromium.org>
Commit-Queue: Alexis Hétu <sugoi@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1626133}
---
diff --git a/ios/chrome/app/BUILD.gn b/ios/chrome/app/BUILD.gn
index 1ecac5f..3330126 100644
--- a/ios/chrome/app/BUILD.gn
+++ b/ios/chrome/app/BUILD.gn
@@ -376,6 +376,8 @@
"//ios/chrome/browser/shared/model/application_context",
"//ios/chrome/browser/shared/model/profile",
"//ios/chrome/browser/shared/model/profile:features",
+ "//ios/chrome/browser/signin/model:authentication_service",
+ "//ios/chrome/browser/signin/model:authentication_service_factory",
"//ios/chrome/browser/sync/model",
"//ios/chrome/browser/webauthn/model",
"//ios/chrome/common/app_group",
diff --git a/ios/chrome/app/credential_provider_migrator_app_agent.mm b/ios/chrome/app/credential_provider_migrator_app_agent.mm
index e07244be..d0ee7a9 100644
--- a/ios/chrome/app/credential_provider_migrator_app_agent.mm
+++ b/ios/chrome/app/credential_provider_migrator_app_agent.mm
@@ -4,6 +4,8 @@
#import "ios/chrome/app/credential_provider_migrator_app_agent.h"
+#import <algorithm>
+#import <deque>
#import <map>
#import "base/functional/bind.h"
@@ -30,6 +32,8 @@
#import "ios/chrome/browser/shared/model/profile/features.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/profile/profile_manager_ios.h"
+#import "ios/chrome/browser/signin/model/authentication_service.h"
+#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/sync/model/sync_service_factory.h"
#import "ios/chrome/browser/webauthn/model/ios_passkey_model_factory.h"
#import "ios/chrome/common/app_group/app_group_constants.h"
@@ -76,6 +80,9 @@
// Maps profile name to the CredentialProviderMigrator responsible for the
// profile's migration.
std::map<std::string, CredentialProviderMigrator*, std::less<>> _migratorMap;
+
+ // Queue of profile names waiting for migration.
+ std::deque<std::string> _pendingMigrationProfileNames;
}
#pragma mark - SceneObservingAppAgent
@@ -127,13 +134,7 @@
});
if (iter != loadedProfiles.end()) {
- NSString* key = AppGroupUserDefaultsCredentialProviderNewCredentials();
- NSUserDefaults* userDefaults = app_group::GetGroupUserDefaults();
-
- [self migrateCredentialForProfile:*iter
- passKeyModel:passkeyModel
- key:key
- userDefaults:userDefaults];
+ [self migrateNextProfile];
}
}
@@ -199,42 +200,56 @@
return;
}
- NSString* key = AppGroupUserDefaultsCredentialProviderNewCredentials();
- NSUserDefaults* userDefaults = app_group::GetGroupUserDefaults();
-
const std::vector<ProfileIOS*> loadedProfiles =
GetApplicationContext()->GetProfileManager()->GetLoadedProfiles();
for (ProfileIOS* profile : loadedProfiles) {
- webauthn::PasskeyModel* passkeyModel =
- IOSPasskeyModelFactory::GetForProfile(profile);
-
- [self migrateCredentialForProfile:profile
- passKeyModel:passkeyModel
- key:key
- userDefaults:userDefaults];
+ std::string profileName = profile->GetProfileName();
+ if (_migratorMap.contains(profileName)) {
+ continue;
+ }
+ if (std::ranges::find(_pendingMigrationProfileNames, profileName) !=
+ _pendingMigrationProfileNames.end()) {
+ continue;
+ }
+ _pendingMigrationProfileNames.push_back(profileName);
}
+ [self migrateNextProfile];
}
-// Migrate the credential for the given profile and model.
-- (void)migrateCredentialForProfile:(ProfileIOS*)profile
- passKeyModel:(webauthn::PasskeyModel*)passkeyModel
- key:(NSString*)key
- userDefaults:(NSUserDefaults*)userDefaults {
+// Starts the next pending migration if possible.
+- (void)migrateNextProfile {
// Only attempt to start migrations while the app is foregrounded or fully
// initialized.
if (![self canMigrate]) {
return;
}
- CHECK(profile);
- // Do nothing if the migration for the profile already started.
- if (_migratorMap.contains(profile->GetProfileName())) {
+ // If a migration is already running, wait for it to finish.
+ if (!_migratorMap.empty()) {
return;
}
- // If the passkey model isn't ready, delay the migration of passkeys until
- // it is ready.
+ if (_pendingMigrationProfileNames.empty()) {
+ return;
+ }
+
+ std::string profileName = _pendingMigrationProfileNames.front();
+ ProfileIOS* profile =
+ GetApplicationContext()->GetProfileManager()->GetProfileWithName(
+ profileName);
+
+ if (!profile) {
+ _pendingMigrationProfileNames.pop_front();
+ [self migrateNextProfile];
+ return;
+ }
+
+ webauthn::PasskeyModel* passkeyModel =
+ IOSPasskeyModelFactory::GetForProfile(profile);
+
+ // If the passkey model isn't ready, delay the migration until it is ready.
+ // The profile remains at the head of the queue.
if (passkeyModel && !passkeyModel->IsReady()) {
if (![self isObservingPasskeyModel:passkeyModel]) {
[self addObserverForPasskeyModel:passkeyModel];
@@ -242,6 +257,18 @@
return;
}
+ _pendingMigrationProfileNames.pop_front();
+ [self migrateProfile:profile passkeyModel:passkeyModel];
+}
+
+// Migrates a specific profile.
+- (void)migrateProfile:(ProfileIOS*)profile
+ passkeyModel:(webauthn::PasskeyModel*)passkeyModel {
+ CHECK(profile);
+
+ NSString* key = AppGroupUserDefaultsCredentialProviderNewCredentials();
+ NSUserDefaults* userDefaults = app_group::GetGroupUserDefaults();
+
password_manager::PasswordForm::Store defaultStore =
password_manager::features_util::IsAccountStorageActive(
SyncServiceFactory::GetForProfile(profile))
@@ -254,9 +281,15 @@
: IOSChromeProfilePasswordStoreFactory::GetForProfile(
profile, ServiceAccessType::IMPLICIT_ACCESS);
+ AuthenticationService* authService =
+ AuthenticationServiceFactory::GetForProfile(profile);
+ id<SystemIdentity> identity = authService->GetPrimaryIdentity();
+ NSString* gaiaID = identity ? identity.gaiaId.ToNSString() : nil;
+
CredentialProviderMigrator* migrator =
Regression Test / PoC
diff --git a/ios/chrome/browser/credential_provider/model/credential_provider_migrator_unittest.mm b/ios/chrome/browser/credential_provider/model/credential_provider_migrator_unittest.mm
index 0ad2c1e..da6c8a1 100644
--- a/ios/chrome/browser/credential_provider/model/credential_provider_migrator_unittest.mm
+++ b/ios/chrome/browser/credential_provider/model/credential_provider_migrator_unittest.mm
@@ -5,10 +5,10 @@
#import "ios/chrome/browser/credential_provider/model/credential_provider_migrator.h"
#import "base/strings/sys_string_conversions.h"
-#import "base/test/ios/wait_util.h"
#import "base/test/metrics/histogram_tester.h"
#import "base/test/scoped_feature_list.h"
#import "base/test/task_environment.h"
+#import "base/test/test_future.h"
#import "components/password_manager/core/browser/password_form.h"
#import "components/password_manager/core/browser/password_store/mock_password_store_interface.h"
#import "components/webauthn/core/browser/test_passkey_model.h"
@@ -24,9 +24,10 @@
constexpr int64_t kJan1st2024 = 1704085200;
+NSString* const kMatchingGaia = @"123456";
+NSString* const kMismatchingGaia = @"654321";
+
using ::base::SysNSStringToUTF8;
-using ::base::test::ios::kWaitForFileOperationTimeout;
-using ::base::test::ios::WaitUntilConditionOrTimeout;
using ::password_manager::MockPasswordStoreInterface;
using ::password_manager::PasswordForm;
using ::testing::_;
@@ -39,14 +40,14 @@
return [NSData dataWithBytes:str.data() length:str.length()];
}
-ArchivableCredential* TestPasswordCredential() {
+ArchivableCredential* TestPasswordCredential(NSString* gaia = nil) {
NSString* username = @"username_value";
NSString* password = @"qwerty123";
NSString* url = @"http://www.alpha.example.com/path/and?args=8";
NSString* recordIdentifier = @"recordIdentifier";
NSString* note = @"note";
return [[ArchivableCredential alloc] initWithFavicon:nil
- gaia:nil
+ gaia:gaia
password:password
rank:1
recordIdentifier:recordIdentifier
@@ -58,17 +59,17 @@
lastUsedTime:0];
}
-ArchivableCredential* TestPasskeyCredential(bool valid = true) {
+ArchivableCredential* TestPasskeyCredential(NSString* rpId, NSString* gaia) {
return [[ArchivableCredential alloc]
initWithFavicon:nil
- gaia:nil
+ gaia:gaia
recordIdentifier:@"recordIdentifier"
syncId:StringToData("syncIdOfLength16")
username:@"username"
userDisplayName:@"userDisplayName"
userId:StringToData("userId")
credentialId:StringToData("credentialId_16_")
- rpId:valid ? @"rpId" : nil
+ rpId:rpId
privateKey:StringToData("privateKey")
encrypted:StringToData("encrypted")
creationTime:kJan1st2024
@@ -94,7 +95,7 @@
// Mocking time is required for password notes since they are created with the
// creation_date metadata, which is compared in AddLogin() call expectations.
base::test::SingleThreadTaskEnvironment task_environment_{
- base::test::TaskEnvironment::MainThreadType::IO,
+ base::test::TaskEnvironment::MainThreadType::UI,
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
};
@@ -116,6 +117,7 @@
CredentialProviderMigrator* migrator =
[[CredentialProviderMigrator alloc] initWithUserDefaults:user_defaults_
key:store_key_
+ gaia:nil
passwordStore:mock_store_
passkeyStore:nil];
ASSERT_TRUE(migrator);
@@ -123,16 +125,15 @@
// Start migration.
PasswordForm expected = PasswordFormFromCredential(credential);
EXPECT_CALL(*mock_store_, AddLogin(expected, _));
- __block BOOL blockWaitCompleted = false;
+ base::test::TestFuture<BOOL, NSError*> future;
+ auto* future_ptr = &future;
[migrator startMigrationWithCompletion:^(BOOL success, NSError* error) {
- EXPECT_TRUE(success);
- EXPECT_TRUE(error == nil)
- << SysNSStringToUTF8([error localizedDescription]);
- blockWaitCompleted = true;
+ future_ptr->SetValue(success, error);
}];
- EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForFileOperationTimeout, ^bool {
- return blockWaitCompleted;
- }));
+ auto [migration_success, migration_error] = future.Take();
+ EXPECT_TRUE(migration_success);
+ EXPECT_TRUE(migration_error == nil)
+ << SysNSStringToUTF8([migration_error localizedDescription]);
// Reload temp store.
store =
@@ -148,7 +149,7 @@
UserDefaultsCredentialStore* store =
[[UserDefaultsCredentialStore alloc] initWithUserDefaults:user_defaults_
key:store_key_];
- id<Credential> credential = TestPasskeyCredential();
+ id<Credential> credential = TestPasskeyCredential(@"rpId", kMatchingGaia);
[store addCredential:credential];
[store saveDataWithCompletion:^(NSError* error) {
EXPECT_TRUE(error == nil)
@@ -160,6 +161,7 @@
CredentialProviderMigrator* migrator = [[CredentialProviderMigrator alloc]
initWithUserDefaults:user_defaults_
key:store_key_
+ gaia:kMatchingGaia
passwordStore:mock_store_
passkeyStore:&test_passkey_model_];
ASSERT_TRUE(migrator);
@@ -170,16 +172,15 @@
// Start migration.
sync_pb::WebauthnCredentialSpecifics expected =
PasskeyFromCredential(credential);
- __block BOOL blockWaitCompleted = false;
+ base::test::TestFuture<BOOL, NSError*> future;
+ auto* future_ptr = &future;
[migrator startMigrationWithCompletion:^(BOOL success, NSError* error) {
- EXPECT_TRUE(success);
- EXPECT_TRUE(error == nil)
- << SysNSStringToUTF8([error localizedDescription]);
- blockWaitCompleted = true;
+ future_ptr->SetValue(success, error);
}];
- EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForFileOperationTimeout, ^bool {
- return blockWaitCompleted;
- }));
+ auto [migration_success, migration_error] = future.Take();
+ EXPECT_TRUE(migration_success);
+ EXPECT_TRUE(migration_error == nil)
+ << SysNSStringToUTF8([migration_error localizedDescription]);
histogram_tester_.ExpectBucketCount(
"Passkeys.IOSMigration", PasskeysMigrationStatus::kPasskeyCreated, 1);
@@ -217,23 +218,23 @@
histogram_tester_.ExpectBucketCount(
"Passkeys.IOSMigration", PasskeysMigrationStatus::kPasskeyUpdated, 0);
- blockWaitCompleted = false;
+ base::test::TestFuture<BOOL, NSError*> update_future;
+ auto* update_future_ptr = &update_future;
migrator = [[CredentialProviderMigrator alloc]
initWithUserDefaults:user_defaults_
key:store_key_
+ gaia:kMatchingGaia
passwordStore:mock_store_
passkeyStore:&test_passkey_model_];
[migrator startMigrationWithCompletion:^(BOOL success, NSError* error) {
- EXPECT_TRUE(success);
- EXPECT_TRUE(error == nil)
- << SysNSStringToUTF8([error localizedDescription]);
- blockWaitCompleted = true;
+ update_future_ptr->SetValue(success, error);
}];
- EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForFileOperationTimeout, ^bool {
- return blockWaitCompleted;
- }));
+ auto [update_success, update_error] = update_future.Take();
+ EXPECT_TRUE(update_success);
+ EXPECT_TRUE(update_error == nil)
+ << SysNSStringToUTF8([update_error localizedDescription]);
histogram_tester_.ExpectBucketCount(
"Passkeys.IOSMigration", PasskeysMigrationStatus::kPasskeyUpdated, 1);
@@ -260,7 +261,8 @@
UserDefaultsCredentialStore* store =
[[UserDefaultsCredentialStore alloc] initWithUserDefaults:user_defaults_
key:store_key_];
- id<Credential> invalidCredential = TestPasskeyCredential(/*valid=*/false);
+ id<Credential> invalidCredential =
+ TestPasskeyCredential(/*rpId=*/nil, kMatchingGaia);
[store addCredential:invalidCredential];
[store saveDataWithCompletion:^(NSError* error) {
@@ -273,6 +275,7 @@
CredentialProviderMigrator* migrator = [[CredentialProviderMigrator alloc]
initWithUserDefaults:user_defaults_
key:store_key_
+ gaia:kMatchingGaia
passwordStore:mock_store_
passkeyStore:&test_passkey_model_];
ASSERT_TRUE(migrator);
@@ -281,16 +284,15 @@
"Passkeys.IOSMigration", PasskeysMigrationStatus::kInvalidPasskey, 0);
// Start migration.
- __block BOOL blockWaitCompleted = false;
+ base::test::TestFuture<BOOL, NSError*> future;
+ auto* future_ptr = &future;
[migrator startMigrationWithCompletion:^(BOOL success, NSError* error) {
- EXPECT_TRUE(success);
- EXPECT_TRUE(error == nil)
- << SysNSStringToUTF8([error localizedDescription]);
- blockWaitCompleted = true;
+ future_ptr->SetValue(success, error);
}];
- EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForFileOperationTimeout, ^bool {
- return blockWaitCompleted;
- }));
+ auto [migration_success, migration_error] = future.Take();
+ EXPECT_TRUE(migration_success);
+ EXPECT_TRUE(migration_error == nil)
+ << SysNSStringToUTF8([migration_error localizedDescription]);
histogram_tester_.ExpectBucketCount(
"Passkeys.IOSMigration", PasskeysMigrationStatus::kInvalidPasskey, 1);
@@ -326,7 +328,8 @@
key:store_key_];
// `TestPasskeyCredential()` is created with hidden = NO.
- ArchivableCredential* credential = TestPasskeyCredential();
+ ArchivableCredential* credential =
+ TestPasskeyCredential(@"rpId", kMatchingGaia);
[store addCredential:credential];
[store saveDataWithCompletion:^(NSError* error) {
EXPECT_TRUE(error == nil)
@@ -338,21 +341,21 @@
CredentialProviderMigrator* migrator = [[CredentialProviderMigrator alloc]
initWithUserDefaults:user_defaults_
key:store_key_
+ gaia:kMatchingGaia
passwordStore:mock_store_
passkeyStore:&test_passkey_model_];
ASSERT_TRUE(migrator);
// Start initial migration.
- __block BOOL blockWaitCompleted = false;
+ base::test::TestFuture<BOOL, NSError*> future;
+ auto* future_ptr = &future;
[migrator startMigrationWithCompletion:^(BOOL success, NSError* error) {
- EXPECT_TRUE(success);
- EXPECT_TRUE(error == nil)
- << SysNSStringToUTF8([error localizedDescription]);
- blockWaitCompleted = true;
+ future_ptr->SetValue(success, error);
}];
- EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForFileOperationTimeout, ^bool {
- return blockWaitCompleted;
- }));
+ auto [migration_success, migration_error] = future.Take();
+ EXPECT_TRUE(migration_success);
+ EXPECT_TRUE(migration_error == nil)
+ << SysNSStringToUTF8([migration_error localizedDescription]);
// Verify the passkey was migrated and is not hidden.
std::vector<sync_pb::WebauthnCredentialSpecifics> passkeys =
@@ -377,23 +380,23 @@
EXPECT_EQ(store.credentials.count, 1u);
// Start migration again.
- blockWaitCompleted = false;
+ base::test::TestFuture<BOOL, NSError*> update_future;
+ auto* update_future_ptr = &update_future;
migrator = [[CredentialProviderMigrator alloc]
initWithUserDefaults:user_defaults_
key:store_key_
+ gaia:kMatchingGaia
passwordStore:mock_store_
passkeyStore:&test_passkey_model_];
[migrator startMigrationWithCompletion:^(BOOL success, NSError* error) {
- EXPECT_TRUE(success);
- EXPECT_TRUE(error == nil)
- << SysNSStringToUTF8([error localizedDescription]);
- blockWaitCompleted = true;
+ update_future_ptr->SetValue(success, error);
}];
- EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForFileOperationTimeout, ^bool {
- return blockWaitCompleted;
- }));
+ auto [update_success, update_error] = update_future.Take();
+ EXPECT_TRUE(update_success);
+ EXPECT_TRUE(update_error == nil)
+ << SysNSStringToUTF8([update_error localizedDescription]);
// Verify temporal store is empty again.
store =
@@ -414,7 +417,8 @@
UserDefaultsCredentialStore* store =
[[UserDefaultsCredentialStore alloc] initWithUserDefaults:user_defaults_
key:store_key_];
- ArchivableCredential* credential = TestPasskeyCredential();
... (truncated)
Original Bug Report
Potential cross-profile credential leak in iOS Credential Provider migration
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 without the Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.
Overview: In Chrome for iOS, a potential vulnerability exists where credentials saved via the Credential Provider Extension (CPE) may be inadvertently imported into all loaded profiles. This is caused by the migration logic ignoring the credential’s GAIA ID and a race condition during shared storage cleanup. Consequently, personal credentials could be leaked to managed enterprise profiles.
Affected files:
ios/chrome/app/credential_provider_migrator_app_agent.mmios/chrome/browser/credential_provider/model/credential_provider_migrator.mmios/chrome/browser/credential_provider/model/archivable_credential+password_form.mmios/chrome/common/credential_provider/archivable_credential+passkey.mmios/chrome/common/credential_provider/user_defaults_credential_store.mmios/chrome/common/credential_provider/memory_credential_store.mm
Estimated timestamp from git blame: 2025-12-11
Summary
A potential cross-profile data leak has been identified in the iOS Chrome Credential Provider Extension (CPE) migration logic. When a user saves a password or passkey via the CPE, it is written to a shared App-Group NSUserDefaults store. When Chrome is foregrounded, it attempts to import these credentials into the browser’s internal stores.
However, due to a missing validation check and an asynchronous race condition, the credentials can be deterministically imported into every loaded profile (e.g., both personal and work profiles), rather than just the profile that created them.
Note: These findings are based on static code analysis by an AI tooling agent and represent a potential vulnerability; no live proof-of-concept has been executed.
Technical Details
The issue stems from two intersecting flaws in the codebase:
-
Missing Profile Validation (
gaiatag is ignored): When the CPE saves a credential, it creates anArchivableCredentialtagged with the active profile’sgaiaidentifier. However, during migration, the conversion functions (PasswordFormFromCredentialinarchivable_credential+password_form.mmandPasskeyFromCredentialinarchivable_credential+passkey.mm) entirely ignore thisgaiaproperty. TheCredentialProviderMigratorunconditionally imports the credential into the profile it was instantiated for, regardless of the credential’s intended account. -
Asynchronous Clearing Race Condition: When Chrome enters the foreground,
CredentialProviderMigratorAppAgentloops through all loaded profiles synchronously on the main thread, instantiating aCredentialProviderMigratorand callingstartMigrationWithCompletion:for each. When a migrator finishes processing a credential, it callsremoveCredentialWithRecordIdentifier:andsaveDataWithCompletion:. These operations are dispatched asynchronously on a backgroundworkingQueue(viaMemoryCredentialStoreandUserDefaultsCredentialStore). Because the main thread does not wait for this asynchronous cleanup to finish, it immediately instantiates the next profile’s migrator, which synchronously reads the unmodified sharedNSUserDefaultsstorage. Thus, subsequent profiles process and import the exact same credentials.
Impact
In a multi-profile environment (e.g., a personal profile and a managed/work profile), a user saving a personal password or passkey from outside of Chrome will have that credential silently imported into their work profile. If the managed profile has Sync enabled, this plaintext credential will be uploaded to enterprise servers, violating the profile isolation security boundary.
Suggested Steps to Reproduce
- Configure Chrome on an iOS device with two profiles: Profile A (Personal) and Profile B (Managed).
- Ensure both profiles are loaded into memory (e.g., via iPadOS Split View or recent profile switching).
- From a third-party app or Safari, use the Chrome Credential Provider Extension to save a new password or passkey. (This writes to the shared App-Group storage).
- Bring the Chrome app to the foreground.
- Check
chrome://password-managerin both Profile A and Profile B. The newly saved credential will potentially appear in both profiles.
Suggested Fix
- Enforce Profile Routing: Update
CredentialProviderMigratorto check thegaiaproperty of theArchivableCredential. If thegaiaID does not match the GAIA ID of the current profile undergoing migration, the migrator should ignore the credential. - Fix the Race Condition: Either serialize the migration process so that a profile’s migration strictly waits for the previous profile’s cleanup to complete before starting, or handle the
NSUserDefaultsreading/clearing centrally inCredentialProviderMigratorAppAgentbefore distributing the credentials to the appropriate profile migrators.
Evaluated with Chrome root at commit: 7353d249d9cacf9c7218e1d7b8a39cf39c72d646
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.