// Copyright 2018 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/win/conflicts/incompatible_applications_updater.h"
#include <string>
#include <utility>
#include "base/base_paths.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/not_fatal_until.h"
#include "base/path_service.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "base/win/registry.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/win/conflicts/module_database.h"
#include "chrome/browser/win/conflicts/module_info.h"
#include "chrome/browser/win/conflicts/module_info_util.h"
#include "chrome/browser/win/conflicts/module_list_filter.h"
#include "chrome/browser/win/conflicts/third_party_metrics_recorder.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
namespace {
// Serializes a vector of IncompatibleApplications to JSON.
base::Value::Dict ConvertToDictionary(
const std::vector<IncompatibleApplicationsUpdater::IncompatibleApplication>&
applications) {
base::Value::Dict result;
for (const auto& application : applications) {
base::Value::Dict element;
// The registry location is necessary to quickly figure out if that
// application is still installed on the computer.
element.Set("registry_is_hkcu",
application.info.registry_root == HKEY_CURRENT_USER);
element.Set("registry_key_path",
base::WideToUTF8(application.info.registry_key_path));
element.Set("registry_wow64_access",
static_cast<int>(application.info.registry_wow64_access));
// And then the actual information needed to display a warning to the user.
element.Set("allow_load", application.blocklist_action->allow_load());
element.Set("type", application.blocklist_action->message_type());
element.Set("message_url", application.blocklist_action->message_url());
result.Set(base::WideToUTF8(application.info.name), std::move(element));
}
return result;
}
// Deserializes a IncompatibleApplication named |name| from |value|. Returns
// null if |value| is not a dict containing all required fields.
std::unique_ptr<IncompatibleApplicationsUpdater::IncompatibleApplication>
ConvertToIncompatibleApplication(const std::string& name,
const base::Value& value) {
if (!value.is_dict())
return nullptr;
const base::Value::Dict& dict = value.GetDict();
std::optional<bool> registry_is_hkcu = dict.FindBool("registry_is_hkcu");
const std::string* registry_key_path = dict.FindString("registry_key_path");
std::optional<int> registry_wow64_access =
dict.FindInt("registry_wow64_access");
std::optional<bool> allow_load = dict.FindBool("allow_load");
std::optional<int> type = dict.FindInt("type");
const std::string* message_url = dict.FindString("message_url");
// All of the above are required for a valid application.
if (!registry_is_hkcu || !registry_key_path || !registry_wow64_access ||
!allow_load || !type || !message_url) {
return nullptr;
}
InstalledApplications::ApplicationInfo application_info = {
base::UTF8ToWide(name),
*registry_is_hkcu ? HKEY_CURRENT_USER : HKEY_LOCAL_MACHINE,
base::UTF8ToWide(*registry_key_path),
static_cast<REGSAM>(*registry_wow64_access)};
auto blocklist_action =
std::make_unique<chrome::conflicts::BlocklistAction>();
blocklist_action->set_allow_load(*allow_load);
blocklist_action->set_message_type(
static_cast<chrome::conflicts::BlocklistMessageType>(*type));
blocklist_action->set_message_url(*message_url);
return std::make_unique<
IncompatibleApplicationsUpdater::IncompatibleApplication>(
std::move(application_info), std::move(blocklist_action));
}
// Returns true if |application| references an existing application in the
// registry.
//
// Used to filter out stale applications from the cache. This can happen if a
// application was uninstalled between the time it was found and Chrome was
// relaunched.
bool IsValidApplication(
const IncompatibleApplicationsUpdater::IncompatibleApplication&
application) {
return base::win::RegKey(
application.info.registry_root,
application.info.registry_key_path.c_str(),
KEY_QUERY_VALUE | application.info.registry_wow64_access)
.Valid();
}
// Clears the cache of all the applications whose name is in
// |state_application_names|.
void RemoveStaleApplications(
const std::vector<std::string>& stale_application_names) {
// Early exit because ScopedDictPrefUpdate will write to the pref even if it
// doesn't contain a value.
if (stale_application_names.empty())
return;
ScopedDictPrefUpdate update(g_browser_process->local_state(),
prefs::kIncompatibleApplications);
base::Value::Dict& existing_applications = update.Get();
for (const auto& application_name : stale_application_names) {
bool removed = existing_applications.Remove(application_name);
DCHECK(removed);
}
}
// Applies the given |function| object to each valid IncompatibleApplication
// found in the kIncompatibleApplications preference.
//
// The signature of the function must be equivalent to the following:
// bool Function(std::unique_ptr<IncompatibleApplication> application));
//
// The return value of |function| indicates if the enumeration should continue
// (true) or be stopped (false).
//
// This function takes care of removing invalid entries that are found during
// the enumeration.
template <class UnaryFunction>
void EnumerateAndTrimIncompatibleApplications(UnaryFunction function) {
std::vector<std::string> stale_application_names;
for (const auto item : g_browser_process->local_state()
->FindPreference(prefs::kIncompatibleApplications)
->GetValue()
->GetDict()) {
auto application =
ConvertToIncompatibleApplication(item.first, item.second);
if (!application || !IsValidApplication(*application)) {
// Mark every invalid application as stale so they are removed from the
// cache.
stale_application_names.push_back(item.first);
continue;
}
// Notify the caller and stop the enumeration if requested by the function.
if (!function(std::move(application)))
break;
}
RemoveStaleApplications(stale_application_names);
}
// Updates the kIncompatibleApplications pref with those contained in
// |incompatible_applications|.
void UpdateIncompatibleApplications(
bool should_clear_pref,
std::vector<IncompatibleApplicationsUpdater::IncompatibleApplication>
incompatible_applications) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// Clear pref if requested.
if (should_clear_pref) {
g_browser_process->local_state()->ClearPref(
prefs::kIncompatibleApplications);
}
// If there is no new incompatible application, there is nothing to do.
if (incompatible_applications.empty())
return;
// The conversion of the accumulated applications to a json dictionary takes
// care of eliminating duplicates.
base::Value::Dict new_applications =
ConvertToDictionary(incompatible_applications);
// Update the existing dictionary.
ScopedDictPrefUpdate update(g_browser_process->local_state(),
prefs::kIncompatibleApplications);
base::Value::Dict& existing_applications = update.Get();
for (auto&& element : new_applications) {
existing_applications.Set(std::move(element.first),
std::move(element.second));
}
}
} // namespace
// -----------------------------------------------------------------------------
// IncompatibleApplication
IncompatibleApplicationsUpdater::IncompatibleApplication::
IncompatibleApplication(
InstalledApplications::ApplicationInfo info,
std::unique_ptr<chrome::conflicts::BlocklistAction> blocklist_action)
: info(std::move(info)), blocklist_action(std::move(blocklist_action)) {}
IncompatibleApplicationsUpdater::IncompatibleApplication::
~IncompatibleApplication() = default;
IncompatibleApplicationsUpdater::IncompatibleApplication::
IncompatibleApplication(
IncompatibleApplication&& incompatible_application) = default;
IncompatibleApplicationsUpdater::IncompatibleApplication&
IncompatibleApplicationsUpdater::IncompatibleApplication::operator=(
IncompatibleApplication&& incompatible_application) = default;
// -----------------------------------------------------------------------------
// IncompatibleApplicationsUpdater
IncompatibleApplicationsUpdater::IncompatibleApplicationsUpdater(
ModuleDatabaseEventSource* module_database_event_source,
const CertificateInfo& exe_certificate_info,
scoped_refptr<ModuleListFilter> module_list_filter,
const InstalledApplications& installed_applications,
bool module_analysis_disabled)
: module_database_event_source_(module_database_event_source),
exe_certificate_info_(exe_certificate_info),
module_list_filter_(std::move(module_list_filter)),
installed_applications_(installed_applications),
module_analysis_disabled_(module_analysis_disabled) {
module_database_event_source_->AddObserver(this);
}
IncompatibleApplicationsUpdater::~IncompatibleApplicationsUpdater() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
module_database_event_source_->RemoveObserver(this);
}
// static
void IncompatibleApplicationsUpdater::RegisterLocalStatePrefs(
PrefRegistrySimple* registry) {
registry->RegisterDictionaryPref(prefs::kIncompatibleApplications);
}
// static
bool IncompatibleApplicationsUpdater::IsWarningEnabled() {
return base::FeatureList::IsEnabled(
features::kIncompatibleApplicationsWarning);
}
// static
bool IncompatibleApplicationsUpdater::HasCachedApplications() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!ModuleDatabase::IsThirdPartyBlockingPolicyEnabled() ||
!IsWarningEnabled()) {
return false;
}
bool found_valid_application = false;
EnumerateAndTrimIncompatibleApplications(
[&found_valid_application](
std::unique_ptr<IncompatibleApplication> application) {
found_valid_application = true;
// Break the enumeration.
return false;
});
return found_valid_application;
}
// static
std::vector<IncompatibleApplicationsUpdater::IncompatibleApplication>
IncompatibleApplicationsUpdater::GetCachedApplications() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(ModuleDatabase::IsThirdPartyBlockingPolicyEnabled());
DCHECK(IsWarningEnabled());
std::vector<IncompatibleApplication> valid_applications;
EnumerateAndTrimIncompatibleApplications(
[&valid_applications](
std::unique_ptr<IncompatibleApplication> application) {
valid_applications.push_back(std::move(*application));
// Continue the enumeration.
return true;
});
return valid_applications;
}
void IncompatibleApplicationsUpdater::OnNewModuleFound(
const ModuleInfoKey& module_key,
const ModuleInfoData& module_data) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// This is meant to create the element in the map if it doesn't exist yet.
ModuleWarningDecision& warning_decision =
module_warning_decisions_[module_key];
// Only consider loaded modules.
if ((module_data.module_properties & ModuleInfoData::kPropertyLoadedModule) ==
0) {
warning_decision = ModuleWarningDecision::kNotLoaded;
return;
}
// Don't check modules if they were never loaded in a process where blocking
// is enabled.
if (!IsBlockingEnabledInProcessTypes(module_data.process_types)) {
warning_decision = ModuleWarningDecision::kAllowedInProcessType;
return;
}
// New modules should not cause a warning when the module analysis is
// disabled.
if (module_analysis_disabled_) {
warning_decision = ModuleWarningDecision::kNotAnalyzed;
return;
}
// First check if this module is a part of Chrome.
// Explicitly allowlist modules whose signing cert's Subject field matches the
// one in the current executable. No attempt is made to check the validity of
// module signatures or of signing certs.
if (exe_certificate_info_->type != CertificateInfo::Type::NO_CERTIFICATE &&
exe_certificate_info_->subject ==
module_data.inspection_result->certificate_info.subject) {
warning_decision = ModuleWarningDecision::kAllowedSameCertificate;
return;
}
// Second, check if the module is seemingly signed by Microsoft. Again, no
// attempt is made to check the validity of the certificate.
if (IsMicrosoftModule(
module_data.inspection_result->certificate_info.subject)) {
warning_decision = ModuleWarningDecision::kAllowedMicrosoft;
return;
}
// allowlist modules in the same directory as the executable. This serves 2
// purposes:
// - In unsigned builds, this allowlists all of the DLL that are part of
// Chrome.
// - It avoids an issue with the simple heuristic used to determine to which
// application a DLL belongs. Without this, if an injected third-party DLL
// is first copied into Chrome's directory, Chrome will blame itself as an
// incompatible application.
base::FilePath exe_path;
if (base::PathService::Get(base::DIR_EXE, &exe_path) &&
exe_path.DirName().IsParent(module_key.module_path)) {
warning_decision = ModuleWarningDecision::kAllowedSameDirectory;
return;
}
// Skip modules allowlisted by the Module List component.
if (module_list_filter_->IsAllowlisted(module_key, module_data)) {
warning_decision = ModuleWarningDecision::kAllowedAllowlisted;
return;
}
// It is preferable to mark a allowlisted shell extension as allowed because
// it is allowlisted, not because it's a shell extension. Thus, check for the
// module type after.
if (module_data.module_properties & ModuleInfoData::kPropertyShellExtension) {
warning_decision = ModuleWarningDecision::kAllowedShellExtension;
return;
}
if (module_data.module_properties & ModuleInfoData::kPropertyIme) {
warning_decision = ModuleWarningDecision::kAllowedIME;
return;
}
// Now it has been determined that the module is unwanted. First check if it
// is going to be blocked on the next Chrome launch.
if (module_data.module_properties &
ModuleInfoData::kPropertyAddedToBlocklist) {
warning_decision = ModuleWarningDecision::kAddedToBlocklist;
return;
}
// Then check if it can be tied to an installed application on the user's
// computer.
std::vector<InstalledApplications::ApplicationInfo> associated_applications;
bool tied_to_app = installed_applications_->GetInstalledApplications(
module_key.module_path, &associated_applications);
UMA_HISTOGRAM_BOOLEAN("ThirdPartyModules.Uninstallable", tied_to_app);
if (!tied_to_app) {
warning_decision = ModuleWarningDecision::kNoTiedApplication;
return;
}
warning_decision = ModuleWarningDecision::kIncompatible;
std::unique_ptr<chrome::conflicts::BlocklistAction> blocklist_action =
module_list_filter_->IsBlocklisted(module_key, module_data);
if (!blocklist_action) {
// The default behavior is to suggest to uninstall.
blocklist_action = std::make_unique<chrome::conflicts::BlocklistAction>();
blocklist_action->set_allow_load(true);
blocklist_action->set_message_type(
chrome::conflicts::BlocklistMessageType::UNINSTALL);
blocklist_action->set_message_url(std::string());
}
for (auto&& associated_application : associated_applications) {
incompatible_applications_.emplace_back(
std::move(associated_application),
std::make_unique<chrome::conflicts::BlocklistAction>(
*blocklist_action));
}
}
void IncompatibleApplicationsUpdater::OnKnownModuleLoaded(
const ModuleInfoKey& module_key,
const ModuleInfoData& module_data) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Analyze the module again.
OnNewModuleFound(module_key, module_data);
}
void IncompatibleApplicationsUpdater::OnModuleDatabaseIdle() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Update the list of incompatible applications on the UI thread. On the first
// call to UpdateIncompatibleApplications(), the previous value must always be
// overwritten.
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&UpdateIncompatibleApplications, before_first_idle_,
std::move(incompatible_applications_)));
incompatible_applications_.clear();
before_first_idle_ = false;
}
IncompatibleApplicationsUpdater::ModuleWarningDecision
IncompatibleApplicationsUpdater::GetModuleWarningDecision(
const ModuleInfoKey& module_key) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto it = module_warning_decisions_.find(module_key);
CHECK(it != module_warning_decisions_.end(), base::NotFatalUntil::M130);
return it->second;
}
void IncompatibleApplicationsUpdater::DisableModuleAnalysis() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
module_analysis_disabled_ = true;
}