chromium/chrome/browser/win/conflicts/incompatible_applications_updater_unittest.cc

// 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 <map>
#include <optional>
#include <string>
#include <string_view>
#include <utility>

#include "base/run_loop.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_reg_util_win.h"
#include "base/win/registry.h"
#include "chrome/browser/win/conflicts/module_info.h"
#include "chrome/browser/win/conflicts/module_list_filter.h"
#include "chrome/common/chrome_features.h"
#include "chrome/test/base/scoped_testing_local_state.h"
#include "chrome/test/base/testing_browser_process.h"
#include "content/public/common/process_type.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {

// Mocks an empty allowlist and blocklist.
class MockModuleListFilter : public ModuleListFilter {
 public:
  MockModuleListFilter() = default;

  MockModuleListFilter(const MockModuleListFilter&) = delete;
  MockModuleListFilter& operator=(const MockModuleListFilter&) = delete;

  bool IsAllowlisted(std::string_view module_basename_hash,
                     std::string_view module_code_id_hash) const override {
    return false;
  }

  std::unique_ptr<chrome::conflicts::BlocklistAction> IsBlocklisted(
      const ModuleInfoKey& module_key,
      const ModuleInfoData& module_data) const override {
    return nullptr;
  }

 private:
  ~MockModuleListFilter() override = default;
};

class MockInstalledApplications : public InstalledApplications {
 public:
  MockInstalledApplications() = default;

  MockInstalledApplications(const MockInstalledApplications&) = delete;
  MockInstalledApplications& operator=(const MockInstalledApplications&) =
      delete;

  ~MockInstalledApplications() override = default;

  void AddIncompatibleApplication(const base::FilePath& file_path,
                                  ApplicationInfo application_info) {
    applications_.insert({file_path, std::move(application_info)});
  }

  bool GetInstalledApplications(
      const base::FilePath& file,
      std::vector<ApplicationInfo>* applications) const override {
    auto range = applications_.equal_range(file);

    if (std::distance(range.first, range.second) == 0)
      return false;

    for (auto it = range.first; it != range.second; ++it)
      applications->push_back(it->second);
    return true;
  }

 private:
  std::multimap<base::FilePath, ApplicationInfo> applications_;
};

constexpr wchar_t kCertificatePath[] = L"CertificatePath";
constexpr char16_t kCertificateSubject[] = u"CertificateSubject";

constexpr wchar_t kDllPath1[] = L"c:\\path\\to\\module.dll";
constexpr wchar_t kDllPath2[] = L"c:\\some\\shellextension.dll";

// Returns a new ModuleInfoData marked as loaded into the browser process but
// otherwise empty.
ModuleInfoData CreateLoadedModuleInfoData() {
  ModuleInfoData module_data;
  module_data.module_properties |= ModuleInfoData::kPropertyLoadedModule;
  module_data.process_types |= ProcessTypeToBit(content::PROCESS_TYPE_BROWSER);
  module_data.inspection_result = std::make_optional<ModuleInspectionResult>();
  return module_data;
}

// Returns a new ModuleInfoData marked as loaded into the process with a
// CertificateInfo that matches kCertificateSubject.
ModuleInfoData CreateSignedLoadedModuleInfoData() {
  ModuleInfoData module_data = CreateLoadedModuleInfoData();

  module_data.inspection_result->certificate_info.type =
      CertificateInfo::Type::CERTIFICATE_IN_FILE;
  module_data.inspection_result->certificate_info.path =
      base::FilePath(kCertificatePath);
  module_data.inspection_result->certificate_info.subject = kCertificateSubject;

  return module_data;
}

}  // namespace

class IncompatibleApplicationsUpdaterTest : public testing::Test,
                                            public ModuleDatabaseEventSource {
 public:
  IncompatibleApplicationsUpdaterTest(
      const IncompatibleApplicationsUpdaterTest&) = delete;
  IncompatibleApplicationsUpdaterTest& operator=(
      const IncompatibleApplicationsUpdaterTest&) = delete;

 protected:
  IncompatibleApplicationsUpdaterTest()
      : dll1_(kDllPath1),
        dll2_(kDllPath2),
        scoped_testing_local_state_(TestingBrowserProcess::GetGlobal()),
        module_list_filter_(base::MakeRefCounted<MockModuleListFilter>()) {
    exe_certificate_info_.type = CertificateInfo::Type::CERTIFICATE_IN_FILE;
    exe_certificate_info_.path = base::FilePath(kCertificatePath);
    exe_certificate_info_.subject = kCertificateSubject;
  }

  void SetUp() override {
    ASSERT_NO_FATAL_FAILURE(
        registry_override_manager_.OverrideRegistry(HKEY_CURRENT_USER));

    scoped_feature_list_.InitAndEnableFeature(
        features::kIncompatibleApplicationsWarning);
  }

  enum class Option {
    ADD_REGISTRY_ENTRY,
    NO_REGISTRY_ENTRY,
  };
  void AddIncompatibleApplication(const base::FilePath& injected_module_path,
                                  const std::wstring& application_name,
                                  Option option) {
    const std::wstring registry_key_path =
        L"dummy\\uninstall\\" + application_name;

    installed_applications_.AddIncompatibleApplication(
        injected_module_path, {application_name, HKEY_CURRENT_USER,
                               registry_key_path, KEY_WOW64_32KEY});

    if (option == Option::ADD_REGISTRY_ENTRY) {
      base::win::RegKey reg_key(HKEY_CURRENT_USER, registry_key_path.c_str(),
                                KEY_WOW64_32KEY | KEY_CREATE_SUB_KEY);
    }
  }

  std::unique_ptr<IncompatibleApplicationsUpdater>
  CreateIncompatibleApplicationsUpdater() {
    return std::make_unique<IncompatibleApplicationsUpdater>(
        this, exe_certificate_info_, module_list_filter_,
        installed_applications_, false);
  }

  // ModuleDatabaseEventSource:
  void AddObserver(ModuleDatabaseObserver* observer) override {}
  void RemoveObserver(ModuleDatabaseObserver* observer) override {}

  void RunLoopUntilIdle() { base::RunLoop().RunUntilIdle(); }

  const base::FilePath dll1_;
  const base::FilePath dll2_;

 private:
  content::BrowserTaskEnvironment task_environment_;
  ScopedTestingLocalState scoped_testing_local_state_;
  registry_util::RegistryOverrideManager registry_override_manager_;
  base::test::ScopedFeatureList scoped_feature_list_;

  CertificateInfo exe_certificate_info_;
  scoped_refptr<MockModuleListFilter> module_list_filter_;
  MockInstalledApplications installed_applications_;
};

// Tests that when the Local State cache is empty, no incompatible applications
// are returned.
TEST_F(IncompatibleApplicationsUpdaterTest, EmptyCache) {
  EXPECT_FALSE(IncompatibleApplicationsUpdater::HasCachedApplications());
  EXPECT_TRUE(IncompatibleApplicationsUpdater::GetCachedApplications().empty());
}

// IncompatibleApplicationsUpdater doesn't do anything when there is no
// registered installed applications.
TEST_F(IncompatibleApplicationsUpdaterTest, NoIncompatibleApplications) {
  auto incompatible_applications_updater =
      CreateIncompatibleApplicationsUpdater();

  // Simulate some arbitrary module loading into the process.
  incompatible_applications_updater->OnNewModuleFound(
      ModuleInfoKey(dll1_, 0, 0), CreateLoadedModuleInfoData());
  incompatible_applications_updater->OnModuleDatabaseIdle();
  RunLoopUntilIdle();

  EXPECT_FALSE(IncompatibleApplicationsUpdater::HasCachedApplications());
  EXPECT_TRUE(IncompatibleApplicationsUpdater::GetCachedApplications().empty());
}

TEST_F(IncompatibleApplicationsUpdaterTest, NoTiedApplications) {
  auto incompatible_applications_updater =
      CreateIncompatibleApplicationsUpdater();

  // Simulate the module loading into the process.
  ModuleInfoKey module_key(dll1_, 0, 0);
  incompatible_applications_updater->OnNewModuleFound(
      module_key, CreateLoadedModuleInfoData());
  incompatible_applications_updater->OnModuleDatabaseIdle();
  RunLoopUntilIdle();

  EXPECT_FALSE(IncompatibleApplicationsUpdater::HasCachedApplications());
  EXPECT_TRUE(IncompatibleApplicationsUpdater::GetCachedApplications().empty());

  EXPECT_EQ(
      incompatible_applications_updater->GetModuleWarningDecision(module_key),
      IncompatibleApplicationsUpdater::ModuleWarningDecision::
          kNoTiedApplication);
}

TEST_F(IncompatibleApplicationsUpdaterTest, OneIncompatibility) {
  AddIncompatibleApplication(dll1_, L"Foo", Option::ADD_REGISTRY_ENTRY);

  auto incompatible_applications_updater =
      CreateIncompatibleApplicationsUpdater();

  // Simulate the module loading into the process.
  ModuleInfoKey module_key(dll1_, 0, 0);
  incompatible_applications_updater->OnNewModuleFound(
      module_key, CreateLoadedModuleInfoData());
  incompatible_applications_updater->OnModuleDatabaseIdle();
  RunLoopUntilIdle();

  EXPECT_TRUE(IncompatibleApplicationsUpdater::HasCachedApplications());
  auto application_names =
      IncompatibleApplicationsUpdater::GetCachedApplications();
  ASSERT_EQ(1u, application_names.size());
  EXPECT_EQ(L"Foo", application_names[0].info.name);
  EXPECT_EQ(
      incompatible_applications_updater->GetModuleWarningDecision(module_key),
      IncompatibleApplicationsUpdater::ModuleWarningDecision::kIncompatible);
}

TEST_F(IncompatibleApplicationsUpdaterTest, SameModuleMultipleApplications) {
  AddIncompatibleApplication(dll1_, L"Foo", Option::ADD_REGISTRY_ENTRY);
  AddIncompatibleApplication(dll1_, L"Bar", Option::ADD_REGISTRY_ENTRY);

  auto incompatible_applications_updater =
      CreateIncompatibleApplicationsUpdater();

  // Simulate the module loading into the process.
  ModuleInfoKey module_key(dll1_, 0, 0);
  incompatible_applications_updater->OnNewModuleFound(
      module_key, CreateLoadedModuleInfoData());
  incompatible_applications_updater->OnModuleDatabaseIdle();
  RunLoopUntilIdle();

  EXPECT_TRUE(IncompatibleApplicationsUpdater::HasCachedApplications());
  auto application_names =
      IncompatibleApplicationsUpdater::GetCachedApplications();
  ASSERT_EQ(2u, application_names.size());
  EXPECT_EQ(
      incompatible_applications_updater->GetModuleWarningDecision(module_key),
      IncompatibleApplicationsUpdater::ModuleWarningDecision::kIncompatible);
}

TEST_F(IncompatibleApplicationsUpdaterTest,
       MultipleCallsToOnModuleDatabaseIdle) {
  AddIncompatibleApplication(dll1_, L"Foo", Option::ADD_REGISTRY_ENTRY);
  AddIncompatibleApplication(dll2_, L"Bar", Option::ADD_REGISTRY_ENTRY);

  auto incompatible_applications_updater =
      CreateIncompatibleApplicationsUpdater();

  // Simulate the module loading into the process.
  ModuleInfoKey module_key1(dll1_, 0, 0);
  incompatible_applications_updater->OnNewModuleFound(
      module_key1, CreateLoadedModuleInfoData());
  incompatible_applications_updater->OnModuleDatabaseIdle();
  RunLoopUntilIdle();

  // Add an additional module.
  ModuleInfoKey module_key2(dll2_, 0, 0);
  incompatible_applications_updater->OnNewModuleFound(
      module_key2, CreateLoadedModuleInfoData());
  incompatible_applications_updater->OnModuleDatabaseIdle();
  RunLoopUntilIdle();

  EXPECT_TRUE(IncompatibleApplicationsUpdater::HasCachedApplications());
  auto application_names =
      IncompatibleApplicationsUpdater::GetCachedApplications();
  ASSERT_EQ(2u, application_names.size());
  EXPECT_EQ(
      incompatible_applications_updater->GetModuleWarningDecision(module_key1),
      IncompatibleApplicationsUpdater::ModuleWarningDecision::kIncompatible);
  EXPECT_EQ(
      incompatible_applications_updater->GetModuleWarningDecision(module_key2),
      IncompatibleApplicationsUpdater::ModuleWarningDecision::kIncompatible);
}

// This is meant to test that cached incompatible applications are persisted
// through browser restarts, via the Local State file.
//
// Since this isn't really doable in a unit test, this test at least check that
// the list isn't tied to the lifetime of the IncompatibleApplicationsUpdater
// instance. It is assumed that the Local State file works as intended.
TEST_F(IncompatibleApplicationsUpdaterTest, PersistsThroughRestarts) {
  AddIncompatibleApplication(dll1_, L"Foo", Option::ADD_REGISTRY_ENTRY);

  auto incompatible_applications_updater =
      CreateIncompatibleApplicationsUpdater();

  // Simulate the module loading into the process.
  incompatible_applications_updater->OnNewModuleFound(
      ModuleInfoKey(dll1_, 0, 0), CreateLoadedModuleInfoData());
  incompatible_applications_updater->OnModuleDatabaseIdle();
  RunLoopUntilIdle();

  EXPECT_TRUE(IncompatibleApplicationsUpdater::HasCachedApplications());

  // Delete the instance.
  incompatible_applications_updater = nullptr;

  EXPECT_TRUE(IncompatibleApplicationsUpdater::HasCachedApplications());
}

// Tests that applications that do not have a registry entry are removed.
TEST_F(IncompatibleApplicationsUpdaterTest, StaleEntriesRemoved) {
  AddIncompatibleApplication(dll1_, L"Foo", Option::ADD_REGISTRY_ENTRY);
  AddIncompatibleApplication(dll2_, L"Bar", Option::NO_REGISTRY_ENTRY);

  auto incompatible_applications_updater =
      CreateIncompatibleApplicationsUpdater();

  // Simulate the modules loading into the process.
  incompatible_applications_updater->OnNewModuleFound(
      ModuleInfoKey(dll1_, 0, 0), CreateLoadedModuleInfoData());
  incompatible_applications_updater->OnNewModuleFound(
      ModuleInfoKey(dll2_, 0, 0), CreateLoadedModuleInfoData());
  incompatible_applications_updater->OnModuleDatabaseIdle();
  RunLoopUntilIdle();

  EXPECT_TRUE(IncompatibleApplicationsUpdater::HasCachedApplications());
  auto application_names =
      IncompatibleApplicationsUpdater::GetCachedApplications();
  ASSERT_EQ(1u, application_names.size());
  EXPECT_EQ(L"Foo", application_names[0].info.name);
}

TEST_F(IncompatibleApplicationsUpdaterTest, IgnoreNotLoadedModules) {
  AddIncompatibleApplication(dll1_, L"Foo", Option::ADD_REGISTRY_ENTRY);

  auto incompatible_applications_updater =
      CreateIncompatibleApplicationsUpdater();

  // Simulate the module loading into the process.
  ModuleInfoKey module_key(dll1_, 0, 0);
  ModuleInfoData module_data;
  module_data.inspection_result = std::make_optional<ModuleInspectionResult>();
  incompatible_applications_updater->OnNewModuleFound(module_key, module_data);
  incompatible_applications_updater->OnModuleDatabaseIdle();
  RunLoopUntilIdle();

  EXPECT_FALSE(IncompatibleApplicationsUpdater::HasCachedApplications());
  auto application_names =
      IncompatibleApplicationsUpdater::GetCachedApplications();
  ASSERT_EQ(0u, application_names.size());
  EXPECT_EQ(
      incompatible_applications_updater->GetModuleWarningDecision(module_key),
      IncompatibleApplicationsUpdater::ModuleWarningDecision::kNotLoaded);
}

// Tests that modules with a matching certificate subject are allowlisted.
TEST_F(IncompatibleApplicationsUpdaterTest,
       allowlistMatchingCertificateSubject) {
  AddIncompatibleApplication(dll1_, L"Foo", Option::ADD_REGISTRY_ENTRY);

  auto incompatible_applications_updater =
      CreateIncompatibleApplicationsUpdater();

  // Simulate the module loading into the process.
  ModuleInfoKey module_key(dll1_, 0, 0);
  incompatible_applications_updater->OnNewModuleFound(
      module_key, CreateSignedLoadedModuleInfoData());
  incompatible_applications_updater->OnModuleDatabaseIdle();
  RunLoopUntilIdle();

  EXPECT_FALSE(IncompatibleApplicationsUpdater::HasCachedApplications());
  auto application_names =
      IncompatibleApplicationsUpdater::GetCachedApplications();
  ASSERT_EQ(0u, application_names.size());
  EXPECT_EQ(
      incompatible_applications_updater->GetModuleWarningDecision(module_key),
      IncompatibleApplicationsUpdater::ModuleWarningDecision::
          kAllowedSameCertificate);
}

// Registered modules are defined as either a shell extension or an IME.
TEST_F(IncompatibleApplicationsUpdaterTest, IgnoreRegisteredModules) {
  AddIncompatibleApplication(dll1_, L"Shell Extension",
                             Option::ADD_REGISTRY_ENTRY);
  AddIncompatibleApplication(dll2_, L"Input Method Editor",
                             Option::ADD_REGISTRY_ENTRY);

  auto incompatible_applications_updater =
      CreateIncompatibleApplicationsUpdater();

  // Set the respective bit for registered modules.
  ModuleInfoKey module_key1(dll1_, 0, 0);
  auto module_data1 = CreateLoadedModuleInfoData();
  module_data1.module_properties |= ModuleInfoData::kPropertyShellExtension;

  ModuleInfoKey module_key2(dll2_, 0, 0);
  auto module_data2 = CreateLoadedModuleInfoData();
  module_data2.module_properties |= ModuleInfoData::kPropertyIme;

  // Simulate the modules loading into the process.
  incompatible_applications_updater->OnNewModuleFound(module_key1,
                                                      module_data1);
  incompatible_applications_updater->OnNewModuleFound(module_key2,
                                                      module_data2);
  incompatible_applications_updater->OnModuleDatabaseIdle();
  RunLoopUntilIdle();

  EXPECT_FALSE(IncompatibleApplicationsUpdater::HasCachedApplications());
  auto application_names =
      IncompatibleApplicationsUpdater::GetCachedApplications();
  ASSERT_EQ(0u, application_names.size());
  EXPECT_EQ(
      incompatible_applications_updater->GetModuleWarningDecision(module_key1),
      IncompatibleApplicationsUpdater::ModuleWarningDecision::
          kAllowedShellExtension);
  EXPECT_EQ(
      incompatible_applications_updater->GetModuleWarningDecision(module_key2),
      IncompatibleApplicationsUpdater::ModuleWarningDecision::kAllowedIME);
}

TEST_F(IncompatibleApplicationsUpdaterTest, IgnoreModulesAddedToTheBlocklist) {
  AddIncompatibleApplication(dll1_, L"Blocklisted Application",
                             Option::ADD_REGISTRY_ENTRY);

  auto incompatible_applications_updater =
      CreateIncompatibleApplicationsUpdater();

  // Set the respective bit for the module.
  auto module_data = CreateLoadedModuleInfoData();
  module_data.module_properties |= ModuleInfoData::kPropertyAddedToBlocklist;

  // Simulate the module loading into the process.
  incompatible_applications_updater->OnNewModuleFound(
      ModuleInfoKey(dll1_, 0, 0), module_data);
  incompatible_applications_updater->OnModuleDatabaseIdle();
  RunLoopUntilIdle();

  EXPECT_FALSE(IncompatibleApplicationsUpdater::HasCachedApplications());
  auto application_names =
      IncompatibleApplicationsUpdater::GetCachedApplications();
  ASSERT_EQ(0u, application_names.size());
}

TEST_F(IncompatibleApplicationsUpdaterTest, DisableModuleAnalysis) {
  AddIncompatibleApplication(dll1_, L"Foo", Option::ADD_REGISTRY_ENTRY);

  auto incompatible_applications_updater =
      CreateIncompatibleApplicationsUpdater();

  incompatible_applications_updater->DisableModuleAnalysis();

  // Simulate the module loading into the process.
  ModuleInfoKey module_key(dll1_, 0, 0);
  incompatible_applications_updater->OnNewModuleFound(
      module_key, CreateLoadedModuleInfoData());
  incompatible_applications_updater->OnModuleDatabaseIdle();
  RunLoopUntilIdle();

  // The module does not cause a warning.
  EXPECT_FALSE(IncompatibleApplicationsUpdater::HasCachedApplications());
  EXPECT_TRUE(IncompatibleApplicationsUpdater::GetCachedApplications().empty());
  EXPECT_EQ(
      incompatible_applications_updater->GetModuleWarningDecision(module_key),
      IncompatibleApplicationsUpdater::ModuleWarningDecision::kNotAnalyzed);
}