| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/component_updater/cros_component_installer_chromeos.h" |
| |
| #include <map> |
| #include <utility> |
| |
| #include "base/command_line.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/path_service.h" |
| #include "base/ranges/algorithm.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/task/thread_pool.h" |
| #include "chrome/browser/ash/crosapi/browser_util.h" |
| #include "chrome/browser/ash/login/demo_mode/demo_mode_dimensions.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/component_updater/component_installer_errors.h" |
| #include "chrome/browser/component_updater/metadata_table_chromeos.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chrome/common/pref_names.h" |
| #include "chromeos/ash/components/dbus/dbus_thread_manager.h" |
| #include "chromeos/ash/components/dbus/image_loader/image_loader_client.h" |
| #include "chromeos/constants/chromeos_features.h" |
| #include "components/component_updater/component_updater_paths.h" |
| #include "components/crx_file/id_util.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/version_info/version_info.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "crypto/sha2.h" |
| |
| namespace component_updater { |
| |
| // Switch that can be used for opting in to receive DCHECK-enabled binaries. If |
| // we need to expose this through chrome://flags on other platforms this can |
| // move to a shared place (but still share the prefer-dcheck name). |
| const char kPreferDcheckSwitch[] = "prefer-dcheck"; |
| const char kPreferDcheckOptIn[] = "opt-in"; |
| const char kPreferDcheckOptOut[] = "opt-out"; |
| |
| // Root path where all components are stored. |
| constexpr char kComponentsRootPath[] = "cros-components"; |
| |
| namespace { |
| |
| // All downloadable Chrome OS components. |
| const ComponentConfig kConfigs[] = { |
| {"cros-termina", ComponentConfig::PolicyType::kEnvVersion, "980.1", |
| "e9d960f84f628e1f42d05de4046bb5b3154b6f1f65c08412c6af57a29aecaffb"}, |
| {"rtanalytics-full", ComponentConfig::PolicyType::kEnvVersion, "106.0", |
| "c93c3e1013c52100a20038b405ac854d69fa889f6dc4fa6f188267051e05e444"}, |
| {"demo-mode-resources", ComponentConfig::PolicyType::kEnvVersion, "1.0", |
| "93c093ebac788581389015e9c59c5af111d2fa5174d206eb795042e6376cbd10"}, |
| {"demo-mode-app", ComponentConfig::PolicyType::kDemoApp, nullptr, |
| "b6c5ce9f03b0ce830eb5f9f92ed3016cfdb7a2327330f0187adbe9a00ddfd34d"}, |
| // NOTE: If you change the lacros component names, you must also update |
| // chrome/browser/ash/crosapi/browser_loader.cc. |
| {"lacros-dogfood-canary", ComponentConfig::PolicyType::kLacros, nullptr, |
| "7a85ffb4b316a3b89135a3f43660ef3049950a61a2f8df4237e1ec213852b848"}, |
| {"lacros-dogfood-dev", ComponentConfig::PolicyType::kLacros, nullptr, |
| "b3e1ef1780c0acd2d3fa44b4d73c657a0f1ed3ad83fd8c964a18a3502ccf5f4f"}, |
| {"lacros-dogfood-beta", ComponentConfig::PolicyType::kLacros, nullptr, |
| "7d5c1428f7f67b56f95123851adec1da105980c56b5c126352040f3b65d3e43b"}, |
| {"lacros-dogfood-stable", ComponentConfig::PolicyType::kLacros, nullptr, |
| "47f910805afac79e2d4d9117c42d5291a32ac60a4ea1a42e537fd86082c3ba48"}, |
| }; |
| |
| const char* g_ash_version_for_test = nullptr; |
| |
| // Returns the major version of the current binary, which is the ash/OS binary. |
| // For example, for ash 89.0.1234.1 returns 89. |
| uint32_t GetAshMajorVersion() { |
| base::Version ash_version = g_ash_version_for_test |
| ? base::Version(g_ash_version_for_test) |
| : version_info::GetVersion(); |
| return ash_version.components()[0]; |
| } |
| |
| const ComponentConfig* FindConfig(const std::string& name) { |
| const ComponentConfig* config = |
| base::ranges::find(kConfigs, name, &ComponentConfig::name); |
| if (config == std::end(kConfigs)) |
| return nullptr; |
| return config; |
| } |
| |
| void LogCustomUninstall(absl::optional<bool> result) {} |
| |
| void FinishCustomUninstallOnUIThread(const std::string& name) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| ash::ImageLoaderClient::Get()->UnmountComponent( |
| name, base::BindOnce(&LogCustomUninstall)); |
| } |
| |
| std::string GenerateId(const std::string& sha2hashstr) { |
| // kIdSize is the count of a pair of hex in the sha2hash array. |
| // In string representation of sha2hash, size is doubled since each hex is |
| // represented by a single char. |
| return crx_file::id_util::GenerateIdFromHex( |
| sha2hashstr.substr(0, crx_file::id_util::kIdSize * 2)); |
| } |
| |
| // Returns all installed components. |
| std::vector<ComponentConfig> GetInstalled() { |
| std::vector<ComponentConfig> configs; |
| base::FilePath root; |
| if (!base::PathService::Get(DIR_COMPONENT_USER, &root)) |
| return configs; |
| |
| root = root.Append(kComponentsRootPath); |
| for (const ComponentConfig& config : kConfigs) { |
| base::FilePath component_path = root.Append(config.name); |
| if (base::PathExists(component_path)) |
| configs.push_back(config); |
| } |
| return configs; |
| } |
| |
| } // namespace |
| |
| CrOSComponentInstallerPolicy::CrOSComponentInstallerPolicy( |
| const ComponentConfig& config, |
| CrOSComponentInstaller* cros_component_installer) |
| : cros_component_installer_(cros_component_installer), name_(config.name) { |
| if (strlen(config.sha2hash) != crypto::kSHA256Length * 2) |
| return; |
| |
| bool converted = base::HexStringToBytes(config.sha2hash, &sha2_hash_); |
| DCHECK(converted); |
| DCHECK_EQ(crypto::kSHA256Length, sha2_hash_.size()); |
| } |
| |
| CrOSComponentInstallerPolicy::~CrOSComponentInstallerPolicy() = default; |
| |
| bool CrOSComponentInstallerPolicy::SupportsGroupPolicyEnabledComponentUpdates() |
| const { |
| return true; |
| } |
| |
| bool CrOSComponentInstallerPolicy::RequiresNetworkEncryption() const { |
| return true; |
| } |
| |
| update_client::CrxInstaller::Result |
| CrOSComponentInstallerPolicy::OnCustomInstall( |
| const base::Value::Dict& manifest, |
| const base::FilePath& install_dir) { |
| cros_component_installer_->EmitInstalledSignal(GetName()); |
| |
| return update_client::CrxInstaller::Result(update_client::InstallError::NONE); |
| } |
| |
| void CrOSComponentInstallerPolicy::OnCustomUninstall() { |
| cros_component_installer_->UnregisterCompatiblePath(name_); |
| |
| content::GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce(&FinishCustomUninstallOnUIThread, name_)); |
| } |
| |
| bool CrOSComponentInstallerPolicy::VerifyInstallation( |
| const base::Value::Dict& manifest, |
| const base::FilePath& install_dir) const { |
| return true; |
| } |
| |
| base::FilePath CrOSComponentInstallerPolicy::GetRelativeInstallDir() const { |
| base::FilePath path = base::FilePath(kComponentsRootPath); |
| return path.Append(name_); |
| } |
| |
| void CrOSComponentInstallerPolicy::GetHash(std::vector<uint8_t>* hash) const { |
| *hash = sha2_hash_; |
| } |
| |
| std::string CrOSComponentInstallerPolicy::GetName() const { |
| return name_; |
| } |
| |
| EnvVersionInstallerPolicy::EnvVersionInstallerPolicy( |
| const ComponentConfig& config, |
| CrOSComponentInstaller* cros_component_installer) |
| : CrOSComponentInstallerPolicy(config, cros_component_installer), |
| env_version_(config.env_version) { |
| DCHECK(!env_version_.empty()); |
| } |
| |
| EnvVersionInstallerPolicy::~EnvVersionInstallerPolicy() = default; |
| |
| void EnvVersionInstallerPolicy::ComponentReady(const base::Version& version, |
| const base::FilePath& path, |
| base::Value::Dict manifest) { |
| std::string* min_env_version = manifest.FindString("min_env_version"); |
| if (!min_env_version) |
| return; |
| |
| if (!IsCompatible(env_version_, *min_env_version)) |
| return; |
| |
| cros_component_installer_->RegisterCompatiblePath(GetName(), path); |
| } |
| |
| update_client::InstallerAttributes |
| EnvVersionInstallerPolicy::GetInstallerAttributes() const { |
| update_client::InstallerAttributes attrs; |
| attrs["_env_version"] = env_version_; |
| return attrs; |
| } |
| |
| // static |
| bool EnvVersionInstallerPolicy::IsCompatible( |
| const std::string& env_version_str, |
| const std::string& min_env_version_str) { |
| base::Version env_version(env_version_str); |
| base::Version min_env_version(min_env_version_str); |
| return env_version.IsValid() && min_env_version.IsValid() && |
| env_version.components()[0] == min_env_version.components()[0] && |
| env_version >= min_env_version; |
| } |
| |
| LacrosInstallerPolicy::LacrosInstallerPolicy( |
| const ComponentConfig& config, |
| CrOSComponentInstaller* cros_component_installer) |
| : CrOSComponentInstallerPolicy(config, cros_component_installer) {} |
| |
| LacrosInstallerPolicy::~LacrosInstallerPolicy() = default; |
| |
| void LacrosInstallerPolicy::ComponentReady(const base::Version& version, |
| const base::FilePath& path, |
| base::Value::Dict manifest) { |
| // Each version of Lacros guarantees it will be compatible through the same |
| // major ash/OS version and -2. For example, Lacros 89 will work with ash/OS |
| // 89, 88, and 87. But it may not work with ash/OS 86 or 90. |
| // |
| // As you see we (client side) only enforces the Lacros/Ash same version |
| // check here, while the code does not check the -2 version skew requirement. |
| // This is because go/lacros-version-skew-guide mentions the restriction on |
| // lacros being too new is enforced on the Omaha server side - and the too |
| // old check is enforced client side. Supposedly this makes it easy for us to |
| // start supporting newer lacros versions by just updating the Omaha server |
| // code. |
| uint32_t lacros_major_version = version.components()[0]; |
| if (lacros_major_version < GetAshMajorVersion()) { |
| // Current lacros install is not compatible. |
| return; |
| } |
| cros_component_installer_->RegisterCompatiblePath(GetName(), path); |
| |
| // Clear the load cache for the newly installed component version to avoid |
| // loading stale components on successive loads, causing a version update |
| // restart loop (see crbug.com/1322678). |
| cros_component_installer_->RemoveLoadCacheEntry(GetName()); |
| } |
| |
| update_client::InstallerAttributes |
| LacrosInstallerPolicy::GetInstallerAttributes() const { |
| update_client::InstallerAttributes attributes; |
| auto* const cmdline = base::CommandLine::ForCurrentProcess(); |
| if (cmdline->HasSwitch(kPreferDcheckSwitch)) { |
| attributes[kPreferDcheckSwitch] = |
| cmdline->GetSwitchValueASCII(kPreferDcheckSwitch); |
| } |
| return attributes; |
| } |
| |
| // static |
| void LacrosInstallerPolicy::SetAshVersionForTest(const char* version) { |
| g_ash_version_for_test = version; |
| } |
| |
| DemoAppInstallerPolicy::DemoAppInstallerPolicy( |
| const ComponentConfig& config, |
| CrOSComponentInstaller* cros_component_installer) |
| : CrOSComponentInstallerPolicy(config, cros_component_installer) {} |
| |
| DemoAppInstallerPolicy::~DemoAppInstallerPolicy() = default; |
| |
| void DemoAppInstallerPolicy::ComponentReady(const base::Version& version, |
| const base::FilePath& path, |
| base::Value::Dict manifest) { |
| cros_component_installer_->RegisterCompatiblePath(GetName(), path); |
| } |
| |
| update_client::InstallerAttributes |
| DemoAppInstallerPolicy::GetInstallerAttributes() const { |
| update_client::InstallerAttributes demo_app_installer_attributes; |
| demo_app_installer_attributes["retailer_id"] = ash::demo_mode::RetailerName(); |
| demo_app_installer_attributes["store_id"] = ash::demo_mode::StoreNumber(); |
| demo_app_installer_attributes["demo_country"] = ash::demo_mode::Country(); |
| demo_app_installer_attributes["is_cloud_gaming_device"] = |
| ash::demo_mode::IsCloudGamingDevice() ? "true" : "false"; |
| demo_app_installer_attributes["is_feature_aware_device"] = |
| ash::demo_mode::IsFeatureAwareDevice() ? "true" : "false"; |
| return demo_app_installer_attributes; |
| } |
| |
| CrOSComponentInstaller::CrOSComponentInstaller( |
| std::unique_ptr<MetadataTable> metadata_table, |
| ComponentUpdateService* component_updater) |
| : metadata_table_(std::move(metadata_table)), |
| component_updater_(component_updater) {} |
| |
| CrOSComponentInstaller::~CrOSComponentInstaller() = default; |
| |
| void CrOSComponentInstaller::SetDelegate(Delegate* delegate) { |
| delegate_ = delegate; |
| } |
| |
| void CrOSComponentInstaller::Load(const std::string& name, |
| MountPolicy mount_policy, |
| UpdatePolicy update_policy, |
| LoadCallback load_callback) { |
| if (!IsCompatible(name) || update_policy == UpdatePolicy::kForce) { |
| // A compatible component is not installed, or forced update is requested. |
| // Start registration and installation/update process. |
| Install(name, update_policy, mount_policy, std::move(load_callback)); |
| } else if (mount_policy == MountPolicy::kMount) { |
| // A compatible component is installed, load it. |
| LoadInternal(name, std::move(load_callback)); |
| } else { |
| // A compatible component is installed, do not load it. |
| std::move(load_callback).Run(Error::NONE, base::FilePath()); |
| } |
| } |
| |
| bool CrOSComponentInstaller::Unload(const std::string& name) { |
| DispatchFailedLoads(std::move(load_cache_[name].callbacks)); |
| load_cache_.erase(name); |
| |
| const ComponentConfig* config = FindConfig(name); |
| if (!config) { |
| // Component |name| does not exist. |
| return false; |
| } |
| const std::string id = GenerateId(config->sha2hash); |
| metadata_table_->DeleteComponentForCurrentUser(name); |
| return metadata_table_->HasComponentForAnyUser(name) || |
| component_updater_->UnregisterComponent(id); |
| } |
| |
| void CrOSComponentInstaller::GetVersion( |
| const std::string& name, |
| base::OnceCallback<void(const base::Version&)> version_callback) const { |
| if (!IsCompatible(name)) { |
| // `name` does not match to any component. |
| std::move(version_callback).Run(base::Version()); |
| return; |
| } |
| |
| // Path compatible to `name` must exist. |
| CHECK(!GetCompatiblePath(name).empty()); |
| |
| ash::ImageLoaderClient::Get()->RequestComponentVersion( |
| name, |
| base::BindOnce(&CrOSComponentInstaller::FinishGetVersion, |
| weak_factory_.GetWeakPtr(), std::move(version_callback))); |
| } |
| |
| void CrOSComponentInstaller::RegisterInstalled() { |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, {base::MayBlock()}, base::BindOnce(GetInstalled), |
| base::BindOnce(&CrOSComponentInstaller::RegisterN, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void CrOSComponentInstaller::RegisterCompatiblePath( |
| const std::string& name, |
| const base::FilePath& path) { |
| compatible_components_[name] = path; |
| } |
| |
| void CrOSComponentInstaller::UnregisterCompatiblePath(const std::string& name) { |
| DispatchFailedLoads(std::move(load_cache_[name].callbacks)); |
| load_cache_.erase(name); |
| compatible_components_.erase(name); |
| } |
| |
| base::FilePath CrOSComponentInstaller::GetCompatiblePath( |
| const std::string& name) const { |
| const auto it = compatible_components_.find(name); |
| return it == compatible_components_.end() ? base::FilePath() : it->second; |
| } |
| |
| void CrOSComponentInstaller::EmitInstalledSignal(const std::string& component) { |
| if (delegate_) |
| delegate_->EmitInstalledSignal(component); |
| } |
| |
| CrOSComponentInstaller::LoadInfo::LoadInfo() = default; |
| CrOSComponentInstaller::LoadInfo::~LoadInfo() = default; |
| std::map<std::string, CrOSComponentInstaller::LoadInfo>& |
| CrOSComponentInstaller::GetLoadCacheForTesting() { |
| return load_cache_; |
| } |
| |
| void CrOSComponentInstaller::RemoveLoadCacheEntry( |
| const std::string& component_name) { |
| load_cache_.erase(component_name); |
| } |
| |
| bool CrOSComponentInstaller::IsRegisteredMayBlock(const std::string& name) { |
| base::FilePath root; |
| if (!base::PathService::Get(DIR_COMPONENT_USER, &root)) |
| return false; |
| |
| return base::PathExists(root.Append(kComponentsRootPath).Append(name)); |
| } |
| |
| void CrOSComponentInstaller::Register(const ComponentConfig& config, |
| base::OnceClosure register_callback) { |
| std::unique_ptr<CrOSComponentInstallerPolicy> policy; |
| switch (config.policy_type) { |
| case ComponentConfig::PolicyType::kEnvVersion: |
| policy = std::make_unique<EnvVersionInstallerPolicy>(config, this); |
| break; |
| case ComponentConfig::PolicyType::kLacros: |
| policy = std::make_unique<LacrosInstallerPolicy>(config, this); |
| break; |
| case ComponentConfig::PolicyType::kDemoApp: |
| policy = std::make_unique<DemoAppInstallerPolicy>(config, this); |
| break; |
| } |
| auto installer = base::MakeRefCounted<ComponentInstaller>(std::move(policy)); |
| installer->Register(component_updater_, std::move(register_callback)); |
| } |
| |
| void CrOSComponentInstaller::Install(const std::string& name, |
| UpdatePolicy update_policy, |
| MountPolicy mount_policy, |
| LoadCallback load_callback) { |
| const ComponentConfig* config = FindConfig(name); |
| if (!config) { |
| std::move(load_callback).Run(Error::UNKNOWN_COMPONENT, base::FilePath()); |
| return; |
| } |
| |
| Register( |
| *config, |
| base::BindOnce( |
| &CrOSComponentInstaller::StartInstall, weak_factory_.GetWeakPtr(), |
| name, GenerateId(config->sha2hash), update_policy, |
| base::BindOnce(&CrOSComponentInstaller::FinishInstall, |
| weak_factory_.GetWeakPtr(), name, mount_policy, |
| update_policy, std::move(load_callback)))); |
| } |
| |
| void CrOSComponentInstaller::StartInstall( |
| const std::string& name, |
| const std::string& id, |
| UpdatePolicy update_policy, |
| update_client::Callback install_callback) { |
| // Check whether an installed component was found during registration, and |
| // determine whether OnDemandUpdater should be started accordingly. |
| const bool is_compatible = IsCompatible(name); |
| if (update_policy == UpdatePolicy::kSkip || |
| (is_compatible && update_policy != UpdatePolicy::kForce)) { |
| std::move(install_callback).Run(update_client::Error::NONE); |
| return; |
| } |
| |
| const component_updater::OnDemandUpdater::Priority priority = |
| is_compatible ? component_updater::OnDemandUpdater::Priority::BACKGROUND |
| : component_updater::OnDemandUpdater::Priority::FOREGROUND; |
| component_updater_->GetOnDemandUpdater().OnDemandUpdate( |
| id, priority, std::move(install_callback)); |
| } |
| |
| void CrOSComponentInstaller::FinishInstall(const std::string& name, |
| MountPolicy mount_policy, |
| UpdatePolicy update_policy, |
| LoadCallback load_callback, |
| update_client::Error error) { |
| if (error != update_client::Error::NONE) { |
| Error err = Error::INSTALL_FAILURE; |
| if (error == update_client::Error::UPDATE_IN_PROGRESS) { |
| err = Error::UPDATE_IN_PROGRESS; |
| } |
| std::move(load_callback).Run(err, base::FilePath()); |
| } else if (!IsCompatible(name)) { |
| std::move(load_callback) |
| .Run(update_policy == UpdatePolicy::kSkip |
| ? Error::NOT_FOUND |
| : Error::COMPATIBILITY_CHECK_FAILED, |
| base::FilePath()); |
| } else if (mount_policy == MountPolicy::kMount) { |
| LoadInternal(name, std::move(load_callback)); |
| } else { |
| std::move(load_callback).Run(Error::NONE, base::FilePath()); |
| } |
| } |
| |
| void CrOSComponentInstaller::LoadInternal(const std::string& name, |
| LoadCallback load_callback) { |
| // Use the cached value if it exists. |
| auto it = load_cache_.find(name); |
| if (it != load_cache_.end()) { |
| // If the request is ongoing, queue up a callback. |
| if (!it->second.success.has_value()) { |
| it->second.callbacks.push_back(std::move(load_callback)); |
| return; |
| } |
| // Otherwise immediately dispatch. |
| DispatchLoadCallback(std::move(load_callback), it->second.path, |
| it->second.success.value()); |
| return; |
| } |
| |
| // Update the cache to indicate the request is being queued. |
| load_cache_[name].success = absl::nullopt; |
| |
| const base::FilePath path = GetCompatiblePath(name); |
| DCHECK(!path.empty()); |
| ash::ImageLoaderClient::Get()->LoadComponentAtPath( |
| name, path, |
| base::BindOnce(&CrOSComponentInstaller::FinishLoad, |
| weak_factory_.GetWeakPtr(), std::move(load_callback), |
| name)); |
| } |
| |
| void CrOSComponentInstaller::FinishLoad(LoadCallback load_callback, |
| const std::string& name, |
| absl::optional<base::FilePath> result) { |
| bool success = result.has_value(); |
| base::FilePath path; |
| if (success) |
| path = result.value(); |
| |
| DispatchLoadCallback(std::move(load_callback), path, success); |
| |
| // Update the cache. |
| auto it = load_cache_.find(name); |
| if (it != load_cache_.end()) { |
| it->second.success = success; |
| it->second.path = path; |
| |
| // Dispatch queued up callbacks. |
| for (LoadCallback& queued_callback : it->second.callbacks) { |
| DispatchLoadCallback(std::move(queued_callback), path, success); |
| } |
| it->second.callbacks.clear(); |
| } |
| } |
| |
| void CrOSComponentInstaller::FinishGetVersion( |
| base::OnceCallback<void(const base::Version&)> version_callback, |
| absl::optional<std::string> result) const { |
| std::move(version_callback).Run(base::Version(result.value_or(""))); |
| } |
| |
| void CrOSComponentInstaller::RegisterN( |
| const std::vector<ComponentConfig>& configs) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| for (const auto& config : configs) { |
| Register(config, base::OnceClosure()); |
| } |
| } |
| |
| bool CrOSComponentInstaller::IsCompatible(const std::string& name) const { |
| return compatible_components_.count(name) > 0; |
| } |
| |
| void CrOSComponentInstaller::DispatchLoadCallback(LoadCallback callback, |
| base::FilePath path, |
| bool success) { |
| Error error = success ? Error::NONE : Error::MOUNT_FAILURE; |
| std::move(callback).Run(error, std::move(path)); |
| } |
| |
| void CrOSComponentInstaller::DispatchFailedLoads( |
| std::vector<LoadCallback> callbacks) { |
| for (LoadCallback& callback : callbacks) { |
| DispatchLoadCallback(std::move(callback), base::FilePath(), |
| /*success=*/false); |
| } |
| } |
| |
| } // namespace component_updater |