CVE-2026-79226
Overview
Background
- Default search engine (DSE) propagation
- when Chrome forks/creates a new profile (e.g., during Dice sign-in interception), it copies the source profile’s DSE choice data into the new profile so both share the same default search.
- `ChoiceData`
- a struct capturing the DSE choice (timestamp, Chrome version, and
TemplateURLDatafor the default engine) to be replayed into the new profile. - `default_search_provider_source()`
- reports where the active default search provider came from:
FROM_USER,FROM_FALLBACK,FROM_EXTENSION,FROM_POLICY, orFROM_POLICY_RECOMMENDED. - Extension/policy-controlled DSE
- a default search engine imposed by an installed extension or an enterprise policy rather than chosen by the user.
Root Cause Analysis
SearchEngineChoiceDialogService::GetChoiceDataFromProfile captured the active default search engine from the source profile unconditionally, returning a ChoiceData regardless of whether the default was set by the user, an extension, a policy, or the built-in fallback. SearchEngineChoiceDialogService::UpdateProfileFromChoiceData then wrote that captured engine into the newly created profile via SetUserSelectedDefaultSearchProvider, effectively converting an extension- or policy-scoped default into a user-selected default in a different profile. This broke the privilege/scope boundary: an extension’s or a specific profile’s policy-imposed search engine choice should not silently follow the user into a freshly forked profile where that extension/policy may not apply.
The fix makes GetChoiceDataFromProfile switch on default_search_provider_source() and return std::nullopt for FROM_FALLBACK, FROM_EXTENSION, FROM_POLICY, and FROM_POLICY_RECOMMENDED, propagating only genuine FROM_USER choices; UpdateProfileFromChoiceData now takes std::optional<ChoiceData> and early-returns when it is empty, so nothing is written when propagation is skipped.
default_search_provider_source() == FROM_USER and returns std::nullopt otherwise.Attack Path
- Impose a non-user default
An extension installed in the source profile (or an enterprise policy) sets the profile’s default search engine, so
default_search_provider_source()isFROM_EXTENSION(orFROM_POLICY). - Trigger profile forking
The user signs in such that Dice web-signin interception or
TurnSyncOnHelper::CreateNewSignedInProfilecreates a new profile and captures the source profile’s choice data. - Capture the imposed default
Pre-patch,
GetChoiceDataFromProfilereturns aChoiceDataholding the extension/policy-imposed engine without recording its source. - Persist as user-selected in the new profile
UpdateProfileFromChoiceDatacallsSetUserSelectedDefaultSearchProvideron the new profile, installing the imposed engine there as a user choice beyond the scope of the original extension/policy.
Impact Assessment
Search.ChoiceDebug.PropagatedDataOutcome histogram was added to measure the change.Changed Functions
| Function | Change | Notes |
|---|---|---|
GetChoiceDataFromProfilechrome/browser/search_engine_choice/search_engine_choice_dialog_service.cc |
modified | |
ifchrome/browser/search_engine_choice/search_engine_choice_dialog_service.cc |
modified |
Files Changed
tools/metrics/histograms/metadata/search/enums.xmlchrome/browser/ui/webui/signin/turn_sync_on_helper.ccchrome/browser/signin/dice_web_signin_interceptor.ccchrome/browser/signin/dice_web_signin_interceptor.hchrome/browser/search_engine_choice/search_engine_choice_dialog_service.cctools/metrics/histograms/metadata/search/histograms.xmlchrome/browser/search_engine_choice/search_engine_choice_dialog_service.hchrome/browser/search_engine_choice/search_engine_choice_dialog_service_unittest.ccchrome/browser/ui/webui/signin/turn_sync_on_helper.h
Audit Directions
- Provenance-blind state copyingLook for code that copies user-settings state (search engine, homepage, permissions) between profiles or contexts without checking whether the setting was user-chosen versus extension/policy/fallback-imposed.
- Managed/extension-controlled prefs crossing boundariesAudit places that read an “active” value (default search, proxy, etc.) and persist it elsewhere as user-selected, bypassing the original controlling scope.
- Optional-return refactors as security fixesReview callers of
Get*FromProfile-style helpers to ensure a skipped/empty capture is handled with an early return rather than a default-constructed value being written downstream.
Patch
From 2e421e49757bf4231412a3f46d756df41df71272 Mon Sep 17 00:00:00 2001 From: Nicolas Dossou-Gbete <[email protected]> Date: Fri, 03 Jul 2026 09:21:44 -0700 Subject: [PATCH] Reland "Avoid propagating extension or policy choice across profiles" This is a reland of commit 1c6acb95514ada701e107ff963b8689281503d71 Fixes the test-specific compilation failure in ChromeForTesting builds, un-guarding one of the test helper functions that the new tests start using in CFT builds. Original change's description: > Avoid propagating extension or policy choice across profiles > > When capturing default search engine (DSE) choice data to propagate to > forked profiles, skip the propagation for the cases where the active DSE > is not set by the user (e.g. managed by extension or policies, or > fallback) > > Adds the dedicated UMA histogram > `Search.ChoiceDebug.PropagatedDataOutcome` to track the impact of this > change. > > Bug: 513607252 > Change-Id: I86c1fdfb6cbe392e4cd7bb8d48dc179f5b07fcd8 > Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7986107 > Commit-Queue: Nicolas Dossou-Gbété <[email protected]> > Reviewed-by: David Roger <[email protected]> > Reviewed-by: James Lee <[email protected]> > Auto-Submit: Nicolas Dossou-Gbété <[email protected]> > Cr-Commit-Position: refs/heads/main@{#1656537} Bug: 513607252 Change-Id: I1b6cb0ec5fef5d6a1960ffb876a15064febad439 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8034248 Reviewed-by: James Lee <[email protected]> Auto-Submit: Nicolas Dossou-Gbété <[email protected]> Reviewed-by: David Roger <[email protected]> Commit-Queue: David Roger <[email protected]> Cr-Commit-Position: refs/heads/main@{#1656620} --- diff --git a/chrome/browser/search_engine_choice/search_engine_choice_dialog_service.cc b/chrome/browser/search_engine_choice/search_engine_choice_dialog_service.cc index ad63ef3f..072359d2 100644 --- a/chrome/browser/search_engine_choice/search_engine_choice_dialog_service.cc +++ b/chrome/browser/search_engine_choice/search_engine_choice_dialog_service.cc @@ -292,49 +292,82 @@ } // static -search_engines::ChoiceData +std::optional<search_engines::ChoiceData> SearchEngineChoiceDialogService::GetChoiceDataFromProfile(Profile& profile) { PrefService* pref_service = profile.GetPrefs(); TemplateURLService* template_url_service = TemplateURLServiceFactory::GetForProfile(&profile); CHECK(template_url_service); + + switch (template_url_service->default_search_provider_source()) { + case DefaultSearchManager::FROM_FALLBACK: + base::UmaHistogramEnumeration( + "Search.ChoiceDebug.PropagatedDataOutcome", + CurrentDefaultPropagationOutcome::kSkippedIsFallback); + return std::nullopt; + + case DefaultSearchManager::FROM_EXTENSION: + base::UmaHistogramEnumeration( + "Search.ChoiceDebug.PropagatedDataOutcome", + CurrentDefaultPropagationOutcome::kSkippedIsExtension); + return std::nullopt; + + case DefaultSearchManager::FROM_POLICY: + case DefaultSearchManager::FROM_POLICY_RECOMMENDED: + base::UmaHistogramEnumeration( + "Search.ChoiceDebug.PropagatedDataOutcome", + CurrentDefaultPropagationOutcome::kSkippedDueToPolicies); + return std::nullopt; + + case DefaultSearchManager::FROM_USER: + break; // Current default eligible for propagation. + } + + CHECK(template_url_service->GetDefaultSearchProvider()); const TemplateURLData& default_search_engine = template_url_service->GetDefaultSearchProvider()->data(); - return {.timestamp = pref_service->GetInt64( - prefs::kDefaultSearchProviderChoiceScreenCompletionTimestamp), - .chrome_version = pref_service->GetString( - prefs::kDefaultSearchProviderChoiceScreenCompletionVersion), - .default_search_engine = default_search_engine}; + base::UmaHistogramEnumeration( + "Search.ChoiceDebug.PropagatedDataOutcome", + CurrentDefaultPropagationOutcome::kPropagatedCurrentDefault); + + return search_engines::ChoiceData{ + .timestamp = pref_service->GetInt64( + prefs::kDefaultSearchProviderChoiceScreenCompletionTimestamp), + .chrome_version = pref_service->GetString( + prefs::kDefaultSearchProviderChoiceScreenCompletionVersion), + .default_search_engine = default_search_engine}; } // static void SearchEngineChoiceDialogService::UpdateProfileFromChoiceData( Profile& profile, - const search_engines::ChoiceData& choice_data) { - PrefService* pref_service = profile.GetPrefs(); - if (choice_data.timestamp != 0) { - pref_service->SetInt64( - prefs::kDefaultSearchProviderChoiceScreenCompletionTimestamp, - choice_data.timestamp); - } - - if (!choice_data.chrome_version.empty()) { - pref_service->SetString( - prefs::kDefaultSearchProviderChoiceScreenCompletionVersion, - choice_data.chrome_version); + const std::optional<search_engines::ChoiceData>& choice_data) { + if (!choice_data.has_value()) { + return; } const TemplateURLData& default_search_engine = - choice_data.default_search_engine; - if (!default_search_engine.keyword().empty() && - !default_search_engine.url().empty()) { - TemplateURLService* template_url_service = - TemplateURLServiceFactory::GetForProfile(&profile); - CHECK(template_url_service); - TemplateURL template_url(default_search_engine); - template_url_service->SetUserSelectedDefaultSearchProvider(&template_url); + choice_data->default_search_engine; + + PrefService* pref_service = profile.GetPrefs(); + if (choice_data->timestamp != 0) { + pref_service->SetInt64( + prefs::kDefaultSearchProviderChoiceScreenCompletionTimestamp, + choice_data->timestamp); } + + if (!choice_data->chrome_version.empty()) { + pref_service->SetString( + prefs::kDefaultSearchProviderChoiceScreenCompletionVersion, + choice_data->chrome_version); + } + + TemplateURLService* template_url_service = + TemplateURLServiceFactory::GetForProfile(&profile); + CHECK(template_url_service); + TemplateURL template_url(default_search_engine); + template_url_service->SetUserSelectedDefaultSearchProvider(&template_url); } TemplateURL::TemplateURLVector diff --git a/chrome/browser/search_engine_choice/search_engine_choice_dialog_service.h b/chrome/browser/search_engine_choice/search_engine_choice_dialog_service.h index 6b83b61..9b9a9482 100644 --- a/chrome/browser/search_engine_choice/search_engine_choice_dialog_service.h +++ b/chrome/browser/search_engine_choice/search_engine_choice_dialog_service.h @@ -5,6 +5,7 @@ #ifndef CHROME_BROWSER_SEARCH_ENGINE_CHOICE_SEARCH_ENGINE_CHOICE_DIALOG_SERVICE_H_ #define CHROME_BROWSER_SEARCH_ENGINE_CHOICE_SEARCH_ENGINE_CHOICE_DIALOG_SERVICE_H_ +#include <optional> #include <string> #include "base/containers/flat_map.h" @@ -152,12 +153,28 @@ static void SetDialogDisabledForTests(bool dialog_disabled); // Returns a copy of the `ChoiceData` specific to `profile`. - static search_engines::ChoiceData GetChoiceDataFromProfile(Profile& profile); + // LINT.IfChange(CurrentDefaultPropagationOutcome) + enum class CurrentDefaultPropagationOutcome { + kPropagatedCurrentDefault = 0, + kSkippedDueToPolicies = 1, + kSkippedIsFallback = 2, + kSkippedIsExtension = 3, + kMaxValue = kSkippedIsExtension, + }; + // LINT.ThenChange(/tools/metrics/histograms/metadata/search/enums.xml:CurrentDefaultPropagationOutcome) + + // Returns a copy of the `ChoiceData` specific to `profile`, or `std::nullopt` + // if there is no default search engine to propagate. This function ignores + // any extension-provided default search engine to capture the underlying user + // choice (or system default), and skips propagation entirely if the default + // search is managed by an enterprise policy. + static std::optional<search_engines::ChoiceData> GetChoiceDataFromProfile( + Profile& profile); // Updates `profile` with the values from `choice_data`. static void UpdateProfileFromChoiceData( Profile& profile, - const search_engines::ChoiceData& choice_data); + const std::optional<search_engines::ChoiceData>& choice_data); private: friend class SearchEngineChoiceDialogServiceFactory; diff --git a/chrome/browser/signin/dice_web_signin_interceptor.cc b/chrome/browser/signin/dice_web_signin_interceptor.cc index 00c14b72..c896aa38 100644 --- a/chrome/browser/signin/dice_web_signin_interceptor.cc +++ b/chrome/browser/signin/dice_web_signin_interceptor.cc @@ -413,6 +413,11 @@ DiceWebSigninInterceptor::ProfilePresets::ProfilePresets(SkColor profile_color) : profile_color(profile_color) {} +DiceWebSigninInterceptor::ProfilePresets::ProfilePresets(ProfilePresets&&) = + default; +DiceWebSigninInterceptor::ProfilePresets& +DiceWebSigninInterceptor::ProfilePresets::operator=(ProfilePresets&&) = default; + DiceWebSigninInterceptor::ProfilePresets::~ProfilePresets() = default; // static diff --git a/chrome/browser/signin/dice_web_signin_interceptor.h b/chrome/browser/signin/dice_web_signin_interceptor.h index 22cc634..d81c43738 100644 --- a/chrome/browser/signin/dice_web_signin_interceptor.h +++ b/chrome/browser/signin/dice_web_signin_interceptor.h @@ -237,14 +237,14 @@ ~ProfilePresets(); - ProfilePresets(ProfilePresets&&) = default; - ProfilePresets& operator=(ProfilePresets&&) = default; + ProfilePresets(ProfilePresets&&); + ProfilePresets& operator=(ProfilePresets&&); ProfilePresets(const ProfilePresets&) = delete; ProfilePresets& operator=(ProfilePresets&) = delete; SkColor profile_color = SK_ColorTRANSPARENT; - search_engines::ChoiceData search_engine_choice_data; + std::optional<search_engines::ChoiceData> search_engine_choice_data; }; // Cancels any current signin interception and resets the interceptor to its diff --git a/chrome/browser/ui/webui/signin/turn_sync_on_helper.cc b/chrome/browser/ui/webui/signin/turn_sync_on_helper.cc index f86ec517..5f96c22 100644 --- a/chrome/browser/ui/webui/signin/turn_sync_on_helper.cc +++ b/chrome/browser/ui/webui/signin/turn_sync_on_helper.cc @@ -389,7 +389,7 @@ void TurnSyncOnHelper::CreateNewSignedInProfile() { // Use the same the default search engine in the new profile. - search_engines::ChoiceData search_engine_choice_data = + std::optional<search_engines::ChoiceData> search_engine_choice_data = SearchEngineChoiceDialogService::GetChoiceDataFromProfile(*profile_); base::OnceCallback<void(Profile*)> profile_created_callback = base::BindOnce( @@ -412,7 +412,7 @@ } void TurnSyncOnHelper::OnNewSignedInProfileCreated( - search_engines::ChoiceData search_engine_choice_data, + std::optional<search_engines::ChoiceData> search_engine_choice_data, Profile* new_profile) { DCHECK(dice_signed_in_profile_creator_); dice_signed_in_profile_creator_.reset(); diff --git a/chrome/browser/ui/webui/signin/turn_sync_on_helper.h b/chrome/browser/ui/webui/signin/turn_sync_on_helper.h index 7b6a4b0..cc0a5d4 100644 --- a/chrome/browser/ui/webui/signin/turn_sync_on_helper.h +++ b/chrome/browser/ui/webui/signin/turn_sync_on_helper.h @@ -6,6 +6,7 @@ #define CHROME_BROWSER_UI_WEBUI_SIGNIN_TURN_SYNC_ON_HELPER_H_ #include <memory> +#include <optional> #include <string> #include "base/callback_list.h" @@ -226,7 +227,7 @@ // Called when the new profile is created. void OnNewSignedInProfileCreated( - search_engines::ChoiceData search_engine_choice_data, + std::optional<search_engines::ChoiceData> search_engine_choice_data, Profile* new_profile); // Returns the SyncService, or nullptr if sync is not allowed. diff --git a/tools/metrics/histograms/metadata/search/enums.xml b/tools/metrics/histograms/metadata/search/enums.xml index 85abe60f..ff4c655e 100644 --- a/tools/metrics/histograms/metadata/search/enums.xml +++ b/tools/metrics/histograms/metadata/search/enums.xml @@ -214,6 +214,17 @@ <int value="3" label="Not seen, from Long-press"/> </enum> +<!-- LINT.IfChange(CurrentDefaultPropagationOutcome) --> + +<enum name="CurrentDefaultPropagationOutcome"> + <int value="0" label="PropagatedCurrentDefault"/> + <int value="1" label="SkippedDueToPolicies"/> + <int value="2" label="SkippedIsFallback"/> + <int value="3" label="SkippedIsExtension"/> +</enum> + +<!-- LINT.ThenChange(//chrome/browser/search_engine_choice/search_engine_choice_dialog_service.h:CurrentDefaultPropagationOutcome) --> + <enum name="DonateResult"> <int value="0" label="No favicons available"/> <int value="1" label="Failed"/> diff --git a/tools/metrics/histograms/metadata/search/histograms.xml b/tools/metrics/histograms/metadata/search/histograms.xml index cca1f13..749fba3b 100644 --- a/tools/metrics/histograms/metadata/search/histograms.xml +++ b/tools/metrics/histograms/metadata/search/histograms.xml @@ -386,6 +386,17 @@ </summary> </histogram> +<histogram name="Search.ChoiceDebug.PropagatedDataOutcome" + enum="CurrentDefaultPropagationOutcome" expires_after="2026-12-01"> + <owner>[email protected]</owner> + <owner>[email protected]</owner> + <summary> + Records the outcome of attempting to propagate the search engine choice data + from an existing profile to a newly created one. Recorded each time a + profile forks or creates a new profile while capturing preferences. + </summary> +</histogram> + <histogram name="Search.ChoiceDebug.ScrollStateWithMoreButtonClick" enum="SearchEngineChoiceScrollState" expires_after="2026-08-19"> <owner>[email protected]</owner> diff --git a/chrome/browser/search_engine_choice/search_engine_choice_dialog_service_unittest.cc b/chrome/browser/search_engine_choice/search_engine_choice_dialog_service_unittest.cc index c24152a..bf9330a5 100644 --- a/chrome/browser/search_engine_choice/search_engine_choice_dialog_service_unittest.cc +++ b/chrome/browser/search_engine_choice/search_engine_choice_dialog_service_unittest.cc @@ -19,6 +19,7 @@ #include "chrome/browser/search_engine_choice/search_engine_choice_service_factory.h" #include "chrome/browser/search_engines/template_url_service_factory.h" #include "chrome/common/webui_url_constants.h" +#include "chrome/test/base/search_test_utils.h" #include "chrome/test/base/testing_browser_process.h" #include "chrome/test/base/testing_profile_manager.h" #include "components/country_codes/country_codes.h" @@ -31,9 +32,11 @@ #include "components/search_engines/search_engines_pref_names.h" #include "components/search_engines/search_engines_switches.h" #include "components/search_engines/search_engines_test_util.h" +#include "components/search_engines/template_url_data_util.h" #include "components/search_engines/template_url_prepopulate_data.h" #include "components/search_engines/template_url_service.h" #include "components/signin/public/base/signin_switches.h" +#include "components/sync_preferences/testing_pref_service_syncable.h" #include "content/public/test/browser_task_environment.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" @@ -63,7 +66,6 @@ constexpr CountryId kBelgiumCountryId("BE"); -#if !BUILDFLAG(CHROME_FOR_TESTING) void SetUserSelectedDefaultSearchProvider( TemplateURLService* template_url_service, bool created_by_policy) { @@ -89,6 +91,7 @@ template_url_service->SetUserSelectedDefaultSearchProvider(template_url); } +#if !BUILDFLAG(CHROME_FOR_TESTING) struct TestParam { std::string test_suffix; std::optional<regional_capabilities::SearchEngineCountryListOverride> @@ -549,3 +552,141 @@ ASSERT_EQ(search_engine_choice_dialog_service, nullptr); } #endif // !BUILDFLAG(CHROME_FOR_TESTING) + +TEST_F(SearchEngineChoiceDialogServiceTest, + GetChoiceDataFromProfilePropagatesCurrentDefault) { + base::HistogramTester histogram_tester; + TemplateURLService* source_template_url_service = + TemplateURLServiceFactory::GetForProfile(profile()); + search_test_utils::WaitForTemplateURLServiceToLoad( + source_template_url_service); + + // Set a user-selected default search provider to ensure the source is + // FROM_USER. + SetUserSelectedDefaultSearchProvider(source_template_url_service, + /*created_by_policy=*/false); + + const TemplateURL* underlying_default = + source_template_url_service->GetDefaultSearchProvider(); + ASSERT_TRUE(underlying_default); + const std::string underlying_default_url = underlying_default->url(); + + std::optional<search_engines::ChoiceData> choice_data = + SearchEngineChoiceDialogService::GetChoiceDataFromProfile(*profile()); + ASSERT_TRUE(choice_data.has_value()); + EXPECT_EQ(choice_data->default_search_engine.url(), underlying_default_url); + + histogram_tester.ExpectUniqueSample( + "Search.ChoiceDebug.PropagatedDataOutcome", + SearchEngineChoiceDialogService::CurrentDefaultPropagationOutcome:: + kPropagatedCurrentDefault, + 1); +} + +TEST_F(SearchEngineChoiceDialogServiceTest, + GetChoiceDataFromProfileIgnoresExtensionProvidedDefault) { + base::HistogramTester histogram_tester; ... (file diff truncated)
Regression Test / PoC
diff --git a/chrome/browser/search_engine_choice/search_engine_choice_dialog_service_unittest.cc b/chrome/browser/search_engine_choice/search_engine_choice_dialog_service_unittest.cc
index c24152a..bf9330a5 100644
--- a/chrome/browser/search_engine_choice/search_engine_choice_dialog_service_unittest.cc
+++ b/chrome/browser/search_engine_choice/search_engine_choice_dialog_service_unittest.cc
@@ -19,6 +19,7 @@
#include "chrome/browser/search_engine_choice/search_engine_choice_service_factory.h"
#include "chrome/browser/search_engines/template_url_service_factory.h"
#include "chrome/common/webui_url_constants.h"
+#include "chrome/test/base/search_test_utils.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "components/country_codes/country_codes.h"
@@ -31,9 +32,11 @@
#include "components/search_engines/search_engines_pref_names.h"
#include "components/search_engines/search_engines_switches.h"
#include "components/search_engines/search_engines_test_util.h"
+#include "components/search_engines/template_url_data_util.h"
#include "components/search_engines/template_url_prepopulate_data.h"
#include "components/search_engines/template_url_service.h"
#include "components/signin/public/base/signin_switches.h"
+#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -63,7 +66,6 @@
constexpr CountryId kBelgiumCountryId("BE");
-#if !BUILDFLAG(CHROME_FOR_TESTING)
void SetUserSelectedDefaultSearchProvider(
TemplateURLService* template_url_service,
bool created_by_policy) {
@@ -89,6 +91,7 @@
template_url_service->SetUserSelectedDefaultSearchProvider(template_url);
}
+#if !BUILDFLAG(CHROME_FOR_TESTING)
struct TestParam {
std::string test_suffix;
std::optional<regional_capabilities::SearchEngineCountryListOverride>
@@ -549,3 +552,141 @@
ASSERT_EQ(search_engine_choice_dialog_service, nullptr);
}
#endif // !BUILDFLAG(CHROME_FOR_TESTING)
+
+TEST_F(SearchEngineChoiceDialogServiceTest,
+ GetChoiceDataFromProfilePropagatesCurrentDefault) {
+ base::HistogramTester histogram_tester;
+ TemplateURLService* source_template_url_service =
+ TemplateURLServiceFactory::GetForProfile(profile());
+ search_test_utils::WaitForTemplateURLServiceToLoad(
+ source_template_url_service);
+
+ // Set a user-selected default search provider to ensure the source is
+ // FROM_USER.
+ SetUserSelectedDefaultSearchProvider(source_template_url_service,
+ /*created_by_policy=*/false);
+
+ const TemplateURL* underlying_default =
+ source_template_url_service->GetDefaultSearchProvider();
+ ASSERT_TRUE(underlying_default);
+ const std::string underlying_default_url = underlying_default->url();
+
+ std::optional<search_engines::ChoiceData> choice_data =
+ SearchEngineChoiceDialogService::GetChoiceDataFromProfile(*profile());
+ ASSERT_TRUE(choice_data.has_value());
+ EXPECT_EQ(choice_data->default_search_engine.url(), underlying_default_url);
+
+ histogram_tester.ExpectUniqueSample(
+ "Search.ChoiceDebug.PropagatedDataOutcome",
+ SearchEngineChoiceDialogService::CurrentDefaultPropagationOutcome::
+ kPropagatedCurrentDefault,
+ 1);
+}
+
+TEST_F(SearchEngineChoiceDialogServiceTest,
+ GetChoiceDataFromProfileIgnoresExtensionProvidedDefault) {
+ base::HistogramTester histogram_tester;
+ TemplateURLService* source_template_url_service =
+ TemplateURLServiceFactory::GetForProfile(profile());
+ search_test_utils::WaitForTemplateURLServiceToLoad(
+ source_template_url_service);
+
+ // Install an extension-provided default search engine in the source profile.
+ std::unique_ptr<TemplateURLData> extension_data =
+ GenerateDummyTemplateURLData("extension");
+ source_template_url_service->Add(std::make_unique<TemplateURL>(
+ *extension_data, TemplateURL::NORMAL_CONTROLLED_BY_EXTENSION,
+ "extension_id", base::Time(), /*wants_to_be_default_engine=*/true));
+ SetExtensionDefaultSearchInPrefs(profile()->GetTestingPrefService(),
+ *extension_data);
+ ASSERT_TRUE(
+ source_template_url_service->IsExtensionControlledDefaultSearch());
+
+ // The choice data captured for propagation to a new profile should be absent
+ // for extensions under the new logic.
+ std::optional<search_engines::ChoiceData> choice_data =
+ SearchEngineChoiceDialogService::GetChoiceDataFromProfile(*profile());
+ EXPECT_EQ(choice_data, std::nullopt);
+
+ histogram_tester.ExpectUniqueSample(
+ "Search.ChoiceDebug.PropagatedDataOutcome",
+ SearchEngineChoiceDialogService::CurrentDefaultPropagationOutcome::
+ kSkippedIsExtension,
+ 1);
+
+ // Propagating absent choice data should leave the destination profile's
+ // default search engine unchanged from its own default.
+ TestingProfile* destination_profile =
+ profile_manager()->CreateTestingProfile("Profile 2");
+ TemplateURLServiceFactory::GetInstance()->SetTestingFactory(
+ destination_profile,
+ base::BindRepeating(&TemplateURLServiceFactory::BuildInstanceFor));
+ TemplateURLService* destination_template_url_service =
+ TemplateURLServiceFactory::GetForProfile(destination_profile);
+ search_test_utils::WaitForTemplateURLServiceToLoad(
+ destination_template_url_service);
+
+ const std::string destination_default_url =
+ destination_template_url_service->GetDefaultSearchProvider()->url();
+
+ SearchEngineChoiceDialogService::UpdateProfileFromChoiceData(
+ *destination_profile, choice_data);
+ EXPECT_FALSE(
+ destination_template_url_service->IsExtensionControlledDefaultSearch());
+ EXPECT_EQ(destination_template_url_service->GetDefaultSearchProvider()->url(),
+ destination_default_url);
+}
+
+TEST_F(SearchEngineChoiceDialogServiceTest,
+ GetChoiceDataFromProfileSkipsIfNoUnderlyingDefault) {
+ base::HistogramTester histogram_tester;
+ TemplateURLService* source_template_url_service =
+ TemplateURLServiceFactory::GetForProfile(profile());
+ search_test_utils::WaitForTemplateURLServiceToLoad(
+ source_template_url_service);
+
+ // When no user choice is made, a fresh profile uses the fallback search
+ // engine, which skips under the new FROM_FALLBACK policy.
+ std::optional<search_engines::ChoiceData> choice_data =
+ SearchEngineChoiceDialogService::GetChoiceDataFromProfile(*profile());
+ EXPECT_EQ(choice_data, std::nullopt);
+
+ histogram_tester.ExpectUniqueSample(
+ "Search.ChoiceDebug.PropagatedDataOutcome",
+ SearchEngineChoiceDialogService::CurrentDefaultPropagationOutcome::
+ kSkippedIsFallback,
+ 1);
+}
+
+TEST_F(SearchEngineChoiceDialogServiceTest,
+ GetChoiceDataFromProfileSkipsIfManagedByPolicy) {
+ base::HistogramTester histogram_tester;
+ TemplateURLService* source_template_url_service =
+ TemplateURLServiceFactory::GetForProfile(profile());
+ search_test_utils::WaitForTemplateURLServiceToLoad(
+ source_template_url_service);
+
+ TemplateURLData data;
+ data.SetShortName(u"policy");
+ data.SetKeyword(u"policy");
+ data.SetURL("https://policy/url?bar={searchTerms}");
+ profile()->GetTestingPrefService()->SetManagedPref(
+ DefaultSearchManager::kDefaultSearchProviderDataPrefName,
+ TemplateURLDataToDictionary(data));
+
+ ASSERT_TRUE(source_template_url_service->is_default_search_managed());
+
+ std::optional<search_engines::ChoiceData> choice_data =
+ SearchEngineChoiceDialogService::GetChoiceDataFromProfile(*profile());
+ EXPECT_EQ(choice_data, std::nullopt);
+
+ histogram_tester.ExpectUniqueSample(
+ "Search.ChoiceDebug.PropagatedDataOutcome",
+ SearchEngineChoiceDialogService::CurrentDefaultPropagationOutcome::
+ kSkippedDueToPolicies,
+ 1);
+
+ // Clear for subsequent tests.
+ profile()->GetTestingPrefService()->RemoveManagedPref(
+ DefaultSearchManager::kDefaultSearchProviderDataPrefName);
+}