chromium/chrome/browser/win/conflicts/module_blocklist_cache_util_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/module_blocklist_cache_util.h"

#include <memory>
#include <random>
#include <set>
#include <string_view>
#include <type_traits>
#include <utility>

#include "base/check.h"
#include "base/containers/contains.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/hash/md5.h"
#include "base/ranges/algorithm.h"
#include "base/time/time.h"
#include "chrome/browser/win/conflicts/module_list_filter.h"
#include "chrome/chrome_elf/sha1/sha1.h"
#include "chrome/chrome_elf/third_party_dlls/packed_list_format.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {

// Creates a list of unique random modules.
std::vector<third_party_dlls::PackedListModule> CreateUniqueModuleEntries(
    uint32_t entry_count,
    uint32_t min_time_date_stamp,
    uint32_t max_time_date_stamp) {
  // A pseudo-random number generator that explicitely uses the default seed to
  // make the tests deterministic.
  std::mt19937 random_engine(std::mt19937::default_seed);

  // A distribution to generate bytes and another one to generate time date
  // stamp values.
  std::uniform_int_distribution<unsigned int> byte_distribution(0u, 255u);
  std::uniform_int_distribution<unsigned int> time_date_stamp_distribution(
      min_time_date_stamp, max_time_date_stamp);

  std::vector<third_party_dlls::PackedListModule> entries(entry_count);

  for (auto& entry : entries) {
    // Fill up each bytes for both SHA1 hashes.
    for (size_t i = 0; i < elf_sha1::kSHA1Length; ++i) {
      entry.basename_hash[i] = byte_distribution(random_engine);
      entry.code_id_hash[i] = byte_distribution(random_engine);
    }
    entry.time_date_stamp = time_date_stamp_distribution(random_engine);
  }

  // Sort the entries and make sure each module is unique.
  std::sort(entries.begin(), entries.end(), internal::ModuleLess());
  CHECK(base::ranges::adjacent_find(entries, internal::ModuleEqual()) ==
        entries.end());

  return entries;
}

// Calls the |p| binary predicate for a sample of the collection.
template <typename T, typename Predicate>
void SampleBlocklistedModules(size_t count,
                              Predicate p,
                              std::vector<T>* collection) {
  size_t to_skip = collection->size() / count;
  for (size_t i = 0; i < count; ++i)
    p((*collection)[i * to_skip]);
}

}  // namespace

class ModuleBlocklistCacheUtilTest : public testing::Test {
 public:
  ModuleBlocklistCacheUtilTest(const ModuleBlocklistCacheUtilTest&) = delete;
  ModuleBlocklistCacheUtilTest& operator=(const ModuleBlocklistCacheUtilTest&) =
      delete;

 protected:
  ModuleBlocklistCacheUtilTest() = default;
  ~ModuleBlocklistCacheUtilTest() override = default;

  // The number of module entry created for each test.
  enum { kTestModuleCount = 500u };

  void SetUp() override {
    ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir());
    module_blocklist_cache_path_ = scoped_temp_dir_.GetPath().Append(
        FILE_PATH_LITERAL("ModuleBlocklistCache"));
  }

  const base::FilePath& module_blocklist_cache_path() const {
    return module_blocklist_cache_path_;
  }

 private:
  base::ScopedTempDir scoped_temp_dir_;

  base::FilePath module_blocklist_cache_path_;
};

TEST_F(ModuleBlocklistCacheUtilTest, CalculateTimeDateStamp) {
  static constexpr base::Time::Exploded kChromeBirthday = {
      .year = 2008, .month = 9, .day_of_week = 2, .day_of_month = 2};

  base::Time time;
  ASSERT_TRUE(kChromeBirthday.HasValidValues());
  ASSERT_TRUE(base::Time::FromUTCExploded(kChromeBirthday, &time));

  // Ensure that CalculateTimeDateStamp() will always return the number of
  // hours between |time| and the Windows epoch.
  EXPECT_EQ(3573552u, CalculateTimeDateStamp(time));
}

TEST_F(ModuleBlocklistCacheUtilTest, WriteEmptyCache) {
  third_party_dlls::PackedListMetadata metadata = {
      third_party_dlls::PackedListVersion::kCurrent, 0};
  std::vector<third_party_dlls::PackedListModule> blocklisted_modules;
  base::MD5Digest md5_digest;
  EXPECT_TRUE(WriteModuleBlocklistCache(module_blocklist_cache_path(), metadata,
                                        blocklisted_modules, &md5_digest));

  // Check the file's stat.
  int64_t file_size = 0;
  EXPECT_TRUE(base::GetFileSize(module_blocklist_cache_path(), &file_size));
  EXPECT_EQ(file_size, internal::CalculateExpectedFileSize(metadata));

  base::MD5Digest expected = {
      0x33, 0xCD, 0xEC, 0xCC, 0xCE, 0xBE, 0x80, 0x32,
      0x9F, 0x1F, 0xDB, 0xEE, 0x7F, 0x58, 0x74, 0xCB,
  };

  for (size_t i = 0; i < sizeof(base::MD5Digest::a); ++i) {
    EXPECT_EQ(expected.a[i], md5_digest.a[i]);
  }
}

TEST_F(ModuleBlocklistCacheUtilTest, WrittenFileSize) {
  third_party_dlls::PackedListMetadata metadata = {
      third_party_dlls::PackedListVersion::kCurrent, kTestModuleCount};
  std::vector<third_party_dlls::PackedListModule> blocklisted_modules =
      CreateUniqueModuleEntries(kTestModuleCount, 0u, 4000u);
  base::MD5Digest md5_digest;
  EXPECT_TRUE(WriteModuleBlocklistCache(module_blocklist_cache_path(), metadata,
                                        blocklisted_modules, &md5_digest));

  // Check the file's stat.
  int64_t file_size = 0;
  EXPECT_TRUE(base::GetFileSize(module_blocklist_cache_path(), &file_size));
  EXPECT_EQ(file_size, internal::CalculateExpectedFileSize(metadata));
}

TEST_F(ModuleBlocklistCacheUtilTest, WriteAndRead) {
  third_party_dlls::PackedListMetadata metadata = {
      third_party_dlls::PackedListVersion::kCurrent, kTestModuleCount};
  std::vector<third_party_dlls::PackedListModule> blocklisted_modules =
      CreateUniqueModuleEntries(kTestModuleCount, 0u, 4000u);
  base::MD5Digest md5_digest;
  EXPECT_TRUE(WriteModuleBlocklistCache(module_blocklist_cache_path(), metadata,
                                        blocklisted_modules, &md5_digest));

  third_party_dlls::PackedListMetadata read_metadata;
  std::vector<third_party_dlls::PackedListModule> read_blocklisted_modules;
  base::MD5Digest read_md5_digest;
  EXPECT_EQ(
      ReadResult::kSuccess,
      ReadModuleBlocklistCache(module_blocklist_cache_path(), &read_metadata,
                               &read_blocklisted_modules, &read_md5_digest));

  EXPECT_EQ(read_metadata.version, metadata.version);
  EXPECT_EQ(read_metadata.module_count, metadata.module_count);
  ASSERT_EQ(read_blocklisted_modules.size(), blocklisted_modules.size());
  // Note: Not using PackedListModuleEquals because the time_date_stamp is also
  // verified.
  EXPECT_EQ(0, memcmp(&read_blocklisted_modules[0], &blocklisted_modules[0],
                      read_blocklisted_modules.size() *
                          sizeof(third_party_dlls::PackedListModule)));

  for (size_t i = 0; i < sizeof(base::MD5Digest::a); ++i) {
    EXPECT_EQ(md5_digest.a[i], read_md5_digest.a[i]);
  }
}

class FakeModuleListFilter : public ModuleListFilter {
 public:
  FakeModuleListFilter() = default;

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

  void AddAllowlistedModule(const third_party_dlls::PackedListModule& module) {
    allowlisted_modules_.emplace(
        std::string_view(
            reinterpret_cast<const char*>(&module.basename_hash[0]),
            std::size(module.basename_hash)),
        std::string_view(reinterpret_cast<const char*>(&module.code_id_hash[0]),
                         std::size(module.basename_hash)));
  }

  // ModuleListFilter:
  bool IsAllowlisted(std::string_view module_basename_hash,
                     std::string_view module_code_id_hash) const override {
    return base::Contains(
        allowlisted_modules_,
        std::make_pair(module_basename_hash, module_code_id_hash));
  }

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

 private:
  ~FakeModuleListFilter() override = default;

  std::set<std::pair<std::string_view, std::string_view>> allowlisted_modules_;
};

TEST_F(ModuleBlocklistCacheUtilTest, RemoveAllowlistedEntries) {
  std::vector<third_party_dlls::PackedListModule> blocklisted_modules =
      CreateUniqueModuleEntries(kTestModuleCount, 5u, 10u);

  // The number of modules that will be allowlisted in this test.
  const size_t kAllowlistedModulesCount = 5u;

  // Mark a few modules as allowlisted.
  std::vector<third_party_dlls::PackedListModule> allowlisted_modules;
  SampleBlocklistedModules(
      kAllowlistedModulesCount,
      [&allowlisted_modules](const auto& element) {
        allowlisted_modules.push_back(element);
      },
      &blocklisted_modules);
  auto module_list_filter = base::MakeRefCounted<FakeModuleListFilter>();
  for (const auto& module : allowlisted_modules)
    module_list_filter->AddAllowlistedModule(module);

  internal::RemoveAllowlistedEntries(*module_list_filter, &blocklisted_modules);

  EXPECT_EQ(kTestModuleCount - kAllowlistedModulesCount,
            blocklisted_modules.size());
  for (const auto& module : allowlisted_modules) {
    EXPECT_TRUE(base::ranges::none_of(
        blocklisted_modules, [&module](const auto& element) {
          return internal::ModuleEqual()(module, element);
        }));
  }
}

TEST_F(ModuleBlocklistCacheUtilTest, UpdateModuleBlocklistCacheTimestamps) {
  std::vector<third_party_dlls::PackedListModule> blocklisted_modules =
      CreateUniqueModuleEntries(kTestModuleCount, 5u, 10u);

  // The number of modules that will get their time_date_stamp updated.
  const size_t kModulesToRemove = 5u;

  // Pick 5 modules in the list that will have their time_date_stamp updated to
  // a new value guaranteed to be outside of the range of existing values
  // (which is [5u, 10u]).
  const uint32_t kNewTimeDateStamp = 15u;
  std::vector<third_party_dlls::PackedListModule> updated_modules;
  SampleBlocklistedModules(
      kModulesToRemove,
      [&updated_modules](const auto& element) {
        updated_modules.push_back(element);
      },
      &blocklisted_modules);
  for (auto& module : updated_modules)
    module.time_date_stamp = kNewTimeDateStamp;

  internal::UpdateModuleBlocklistCacheTimestamps(updated_modules,
                                                 &blocklisted_modules);

  EXPECT_EQ(kTestModuleCount, blocklisted_modules.size());
  // For each entires, make sure they were updated.
  for (const auto& module : updated_modules) {
    auto iter = base::ranges::find_if(
        blocklisted_modules, [&module](const auto& element) {
          return internal::ModuleEqual()(module, element);
        });
    ASSERT_NE(blocklisted_modules.end(), iter);
    EXPECT_EQ(kNewTimeDateStamp, iter->time_date_stamp);
  }
}

TEST_F(ModuleBlocklistCacheUtilTest, RemoveExpiredEntries_OnlyExpired) {
  std::vector<third_party_dlls::PackedListModule> blocklisted_modules =
      CreateUniqueModuleEntries(kTestModuleCount, 5u, 10u);

  // The number of modules to remove from the collection.
  const size_t kModulesToRemove = 5u;

  // Set the time date stamp of 5 modules to an expired value.
  const uint32_t kExpiredTimeDateStamp = 1u;
  std::vector<third_party_dlls::PackedListModule> expired_modules;
  SampleBlocklistedModules(
      kModulesToRemove,
      [&expired_modules](auto& element) {
        expired_modules.push_back(element);
        element.time_date_stamp = kExpiredTimeDateStamp;
      },
      &blocklisted_modules);

  std::sort(blocklisted_modules.begin(), blocklisted_modules.end(),
            internal::ModuleTimeDateStampGreater());

  // Set a minimum time date stamp that excludes all the entries in
  // |expired_modules|.
  const size_t kMinTimeDateStamp = kExpiredTimeDateStamp;
  // Set a maxium cache file that fits all the existing modules.
  const size_t kMaxModuleBlocklistCacheSize = kTestModuleCount;
  // Assume there is no newly blocklisted module.
  const size_t kNewlyBlocklistedModuleCount = 0u;

  internal::RemoveExpiredEntries(
      kMinTimeDateStamp, kMaxModuleBlocklistCacheSize,
      kNewlyBlocklistedModuleCount, &blocklisted_modules);

  // The 5 elements were removed.
  EXPECT_EQ(kTestModuleCount - kModulesToRemove, blocklisted_modules.size());
  for (const auto& module : expired_modules) {
    EXPECT_TRUE(base::ranges::none_of(
        blocklisted_modules, [&module](const auto& element) {
          return internal::ModuleEqual()(module, element);
        }));
  }
}

TEST_F(ModuleBlocklistCacheUtilTest, RemoveExpiredEntries_NewlyBlocklisted) {
  std::vector<third_party_dlls::PackedListModule> blocklisted_modules =
      CreateUniqueModuleEntries(kTestModuleCount, 10u, 10000u);

  std::sort(blocklisted_modules.begin(), blocklisted_modules.end(),
            internal::ModuleTimeDateStampGreater());

  // The number of modules to remove from the collection.
  const size_t kModulesToRemove = 10u;

  // Set a minimum time date stamp that doesn't exclude any entry.
  const size_t kMinTimeDateStamp = 0u;
  // Set a maxium cache file that fits all the existing modules.
  const size_t kMaxModuleBlocklistCacheSize = kTestModuleCount;
  // Remove the kModulesToRemove oldest entries by pretending there is
  // kModulesToRemove newly blocklisted modules.
  const size_t kNewlyBlocklistedModuleCount = kModulesToRemove;

  // Get a copy of the 10 oldest modules to make sure they were removed. They
  // should at the end of the vector.
  std::vector<third_party_dlls::PackedListModule> excess_modules(
      blocklisted_modules.end() - kNewlyBlocklistedModuleCount,
      blocklisted_modules.end());

  internal::RemoveExpiredEntries(
      kMinTimeDateStamp, kMaxModuleBlocklistCacheSize,
      kNewlyBlocklistedModuleCount, &blocklisted_modules);

  // Enough elements were removed.
  EXPECT_EQ(kTestModuleCount - kNewlyBlocklistedModuleCount,
            blocklisted_modules.size());
  for (const auto& module : excess_modules) {
    EXPECT_TRUE(base::ranges::none_of(
        blocklisted_modules, [&module](const auto& element) {
          return internal::ModuleEqual()(module, element);
        }));
  }
}

TEST_F(ModuleBlocklistCacheUtilTest, RemoveExpiredEntries_MaxSize) {
  std::vector<third_party_dlls::PackedListModule> blocklisted_modules =
      CreateUniqueModuleEntries(kTestModuleCount, 10u, 10000u);

  std::sort(blocklisted_modules.begin(), blocklisted_modules.end(),
            internal::ModuleTimeDateStampGreater());

  // The number of modules to remove from the collection.
  const size_t kModulesToRemove = 15u;

  // Set a minimum time date stamp that doesn't exclude any entry.
  const size_t kMinTimeDateStamp = 0u;
  // Pick a max size that ensures that kModulesToRemove entries are removed.
  const size_t kMaxModuleBlocklistCacheSize =
      kTestModuleCount - kModulesToRemove;
  // Assume there is no newly blocklisted module.
  const size_t kNewlyBlocklistedModuleCount = 0u;

  // Get a copy of the last kModulesToRemove entries to make sure they are the
  // one that are removed.
  std::vector<third_party_dlls::PackedListModule> excess_modules(
      blocklisted_modules.end() - kModulesToRemove, blocklisted_modules.end());

  // The collection contains too many entries at first.
  EXPECT_NE(kMaxModuleBlocklistCacheSize, blocklisted_modules.size());

  internal::RemoveExpiredEntries(
      kMinTimeDateStamp, kMaxModuleBlocklistCacheSize,
      kNewlyBlocklistedModuleCount, &blocklisted_modules);

  // Enough elements were removed.
  EXPECT_EQ(kMaxModuleBlocklistCacheSize, blocklisted_modules.size());
  for (const auto& module : excess_modules) {
    EXPECT_TRUE(base::ranges::none_of(
        blocklisted_modules, [&module](const auto& element) {
          return internal::ModuleEqual()(module, element);
        }));
  }
}

TEST_F(ModuleBlocklistCacheUtilTest, RemoveDuplicateEntries) {
  std::vector<third_party_dlls::PackedListModule> blocklisted_modules =
      CreateUniqueModuleEntries(kTestModuleCount, 10u, 10000u);

  // The number of modules to duplicate in this test.
  const size_t kDuplicateCount = 15u;

  // Create kDuplicateCount duplicate modules and set their time_date_stamp to a
  // greater value so that the duplicate is kept instead of the original.
  std::vector<third_party_dlls::PackedListModule> duplicated_modules;
  SampleBlocklistedModules(
      kDuplicateCount,
      [&duplicated_modules](auto& element) {
        duplicated_modules.push_back(element);
      },
      &blocklisted_modules);
  for (auto& module : duplicated_modules)
    module.time_date_stamp += 10u;

  // Insert at the beginning because RemoveDuplicateEntries() keeps the first
  // duplicate in the list.
  blocklisted_modules.insert(blocklisted_modules.begin(),
                             duplicated_modules.begin(),
                             duplicated_modules.end());

  // Stable sort to keep the relative order between duplicates.
  std::stable_sort(blocklisted_modules.begin(), blocklisted_modules.end(),
                   internal::ModuleLess());

  EXPECT_EQ(kTestModuleCount + kDuplicateCount, blocklisted_modules.size());

  internal::RemoveDuplicateEntries(&blocklisted_modules);

  EXPECT_EQ(kTestModuleCount, blocklisted_modules.size());
  for (const auto& module : duplicated_modules) {
    EXPECT_TRUE(base::ranges::any_of(
        blocklisted_modules, [&module](const auto& element) {
          return internal::ModuleEqual()(module, element) &&
                 module.time_date_stamp == element.time_date_stamp;
        }));
  }
}