blob: f269914082af69c92b3771c2e785c9b0acc5f2f9 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/chrome_cleaner/test/test_util.h"
#include <stdint.h>
#include <windows.h>
#include <algorithm>
#include <iterator>
#include <memory>
#include <set>
#include <utility>
#include "base/base_paths.h"
#include "base/bind.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/process/launch.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/launcher/unit_test_launcher.h"
#include "base/test/test_suite.h"
#include "base/test/test_switches.h"
#include "base/test/test_timeouts.h"
#include "base/time/time.h"
#include "base/win/scoped_com_initializer.h"
#include "base/win/windows_version.h"
#include "chrome/chrome_cleaner/buildflags.h"
#include "chrome/chrome_cleaner/constants/chrome_cleaner_switches.h"
#include "chrome/chrome_cleaner/crash/crash_client.h"
#include "chrome/chrome_cleaner/ipc/sandbox.h"
#include "chrome/chrome_cleaner/logging/scoped_logging.h"
#include "chrome/chrome_cleaner/os/disk_util.h"
#include "chrome/chrome_cleaner/os/initializer.h"
#include "chrome/chrome_cleaner/os/post_reboot_registration.h"
#include "chrome/chrome_cleaner/os/pre_fetched_paths.h"
#include "chrome/chrome_cleaner/os/rebooter.h"
#include "chrome/chrome_cleaner/os/scoped_disable_wow64_redirection.h"
#include "chrome/chrome_cleaner/os/system_util_cleaner.h"
#include "chrome/chrome_cleaner/os/task_scheduler.h"
#include "chrome/chrome_cleaner/os/whitelisted_directory.h"
#include "chrome/chrome_cleaner/proto/shared_pup_enums.pb.h"
#include "chrome/chrome_cleaner/settings/settings_types.h"
#include "chrome/chrome_cleaner/strings/string_util.h"
#include "chrome/chrome_cleaner/test/test_file_util.h"
#include "chrome/chrome_cleaner/test/test_strings.h"
#include "chrome/chrome_cleaner/test/test_uws_catalog.h"
#include "sandbox/win/src/sandbox_factory.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace chrome_cleaner {
namespace {
bool IsSandboxedProcess() {
static const bool is_sandboxed_process =
(sandbox::SandboxFactory::GetTargetServices() != nullptr);
return is_sandboxed_process;
// base::TestSuite's Initialize method initializes logging differently than we
// do. This subclass ensures logging is properly initialized using ScopedLogging
// after base::TestSuite::Initialize has run.
class ChromeCleanerTestSuite : public base::TestSuite {
// Inherit constructors.
using base::TestSuite::TestSuite;
void Initialize() override {
scoped_logging.reset(new chrome_cleaner::ScopedLogging(
IsSandboxedProcess() ? chrome_cleaner::kSandboxLogFileSuffix
: nullptr));
std::unique_ptr<chrome_cleaner::ScopedLogging> scoped_logging;
} // namespace
bool SetupTestConfigs() {
return SetupTestConfigsWithCatalogs({&TestUwSCatalog::GetInstance()});
bool SetupTestConfigsWithCatalogs(const PUPData::UwSCatalogs& catalogs) {
if (!InitializeOSUtils())
return false;
return true;
int RunChromeCleanerTestSuite(int argc,
char** argv,
const PUPData::UwSCatalogs& catalogs) {
ChromeCleanerTestSuite test_suite(argc, argv);
if (!chrome_cleaner::SetupTestConfigsWithCatalogs(catalogs))
return 1;
// Make sure tests will not end up in an infinite reboot loop.
if (chrome_cleaner::Rebooter::IsPostReboot())
return 0;
// The tests will run with the internal engine, which takes longer.
// IS_INTERNAL_CHROME_CLEANER_BUILD is only set on the Chrome Cleaner
// builders, not the chromium builders, so this will not slow down the
// general commit queue.
constexpr base::TimeDelta kInternalTimeout = base::TimeDelta::FromMinutes(10);
// ScopedCOMInitializer keeps COM initialized in a specific scope. We don't
// want to initialize it for sandboxed processes, so manage its lifetime with
// a unique_ptr, which will call ScopedCOMInitializer's destructor when it
// goes out of scope below.
std::unique_ptr<base::win::ScopedCOMInitializer> scoped_com_initializer;
if (!IsSandboxedProcess()) {
scoped_com_initializer = std::make_unique<base::win::ScopedCOMInitializer>(
bool success = chrome_cleaner::InitializeCOMSecurity();
DCHECK(success) << "InitializeCOMSecurity() failed.";
success = chrome_cleaner::TaskScheduler::Initialize();
DCHECK(success) << "TaskScheduler::Initialize() failed.";
// Crash reporting must be initialized only once, so it cannot be
// initialized by individual tests or fixtures. Also, since crashpad does
// not actually enable uploading of crash reports in non-official builds
// (unless forced to by the --enable-crash-reporting flag) we don't need to
// disable crash reporting.
// Some tests spawn sandbox targets using job objects. Windows 7 doesn't
// support nested job objects, so don't use them in the test suite. Otherwise
// all sandbox tests will fail as they try to create a second job object.
bool use_job_objects = base::win::GetVersion() >= base::win::Version::WIN8;
// Some tests will fail if two tests try to launch test_process.exe
// simultaneously, so run the tests serially. This will still shard them and
// distribute the shards to different swarming bots, but tests will run
// serially on each bot.
const int result = base::LaunchUnitTestsWithOptions(
argc, argv,
/*parallel_jobs=*/1U, // Like LaunchUnitTestsSerially
/*default_batch_limit=*/10, // Like LaunchUnitTestsSerially
base::BindOnce(&base::TestSuite::Run, base::Unretained(&test_suite)));
if (!IsSandboxedProcess())
return result;
ScopedIsPostReboot::ScopedIsPostReboot() {
// This switch will be removed by scoped_command_line_'s destructor when it
// goes out of scope.
bool RunOnceCommandLineContains(const base::string16& product_shortname,
const wchar_t* sub_string) {
PostRebootRegistration post_reboot(product_shortname);
base::string16 run_once_value = post_reboot.RunOnceOnRestartRegisteredValue();
return String16ContainsCaseInsensitive(run_once_value, sub_string);
bool RunOnceOverrideCommandLineContains(const std::string& cleanup_id,
const wchar_t* sub_string) {
base::string16 reg_value;
base::win::RegKey run_once_key(
PostRebootRegistration::GetPostRebootSwitchKeyPath().c_str(), KEY_READ);
if (run_once_key.Valid()) {
// There is no need to check the return value, since ReadValue will leave
// |reg_value| empty on error.
run_once_key.ReadValue(base::UTF8ToUTF16(cleanup_id).c_str(), &reg_value);
return String16ContainsCaseInsensitive(reg_value, sub_string);
bool RegisterTestTask(TaskScheduler* task_scheduler,
TaskScheduler::TaskInfo* task_info) {
const wchar_t name[] = L"Chrome Cleaner Test task";
const wchar_t description[] =
L"Chrome Cleaner Test Task Used Just For Testing";
const base::FilePath exe_path =
TaskScheduler::TaskExecAction action{
exe_path, base::FilePath(), L"argument",
base::CommandLine command_line(action.application_path);
if (!task_scheduler->RegisterTask(
name, description, command_line,
chrome_cleaner::TaskScheduler::TRIGGER_TYPE_HOURLY, false)) {
LOG(ERROR) << "Failed to register test task";
return false;
task_info->name = name;
task_info->description = description;
return true;
void AppendTestSwitches(const base::ScopedTempDir& temp_dir,
base::CommandLine* command_line) {
// Some tests spawn an executable which spawns a sandboxed process. All of
// these logs should go to a temporary directory so we can ensure they are
// deleted after the test.
command_line->AppendSwitchPath(kTestLoggingPathSwitch, temp_dir.GetPath());
void ExpectDiskFootprint(const PUPData::PUP& pup,
const base::FilePath& expected_path) {
for (const auto& path : pup.expanded_disk_footprints.file_paths()) {
if (PathEqual(expected_path, path))
ADD_FAILURE() << "Expected file: " << expected_path.value();
// Expect the scheduled task footprint to be found in |pup|.
void ExpectScheduledTaskFootprint(const PUPData::PUP& pup,
const wchar_t* task_name) {
for (const auto& footprint : pup.expanded_scheduled_tasks) {
if (footprint == task_name)
ADD_FAILURE() << "Expected schedule task not found: '" << task_name << "'.";
bool StringContainsCaseInsensitive(const std::string& value,
const std::string& substring) {
return std::search(
value.begin(), value.end(), substring.begin(), substring.end(),
base::CaseInsensitiveCompareASCII<std::string::value_type>()) !=
LoggingOverride::LoggingOverride() {
active_logging_messages_ = &logging_messages_;
LoggingOverride::~LoggingOverride() {
DCHECK_EQ(active_logging_messages_, &logging_messages_);
active_logging_messages_ = nullptr;
bool LoggingOverride::LoggingMessagesContain(const std::string& sub_string) {
for (const auto& line : logging_messages_) {
if (StringContainsCaseInsensitive(line, sub_string))
return true;
return false;
bool LoggingOverride::LoggingMessagesContain(const std::string& sub_string1,
const std::string& sub_string2) {
for (const auto& line : logging_messages_) {
if (StringContainsCaseInsensitive(line, sub_string1) &&
StringContainsCaseInsensitive(line, sub_string2))
return true;
return false;
std::vector<std::string>* LoggingOverride::active_logging_messages_ = nullptr;
bool IsSubsetOf(const FilePathSet& set1,
const FilePathSet& set2,
const std::string& unexpected_path_log_message) {
bool is_subset = true;
for (const base::FilePath& file_path : set1.file_paths()) {
if (!set2.Contains(file_path)) {
LOG(ERROR) << unexpected_path_log_message << ": '" << file_path.value()
<< "'";
is_subset = false;
return is_subset;
void ExpectEqualFilePathSets(const FilePathSet& matched_files,
const FilePathSet& expected_files) {
EXPECT_TRUE(IsSubsetOf(matched_files, expected_files,
"Unexpected file in matched footprints"));
IsSubsetOf(expected_files, matched_files, "Missing expected footprint"));
base::FilePath GetWow64RedirectedSystemPath() {
std::vector<wchar_t> buffer;
size_t size_needed = MAX_PATH;
while (size_needed > buffer.size()) {
size_needed = ::GetSystemWow64DirectoryW(, buffer.size());
if (size_needed == 0) {
PLOG(ERROR) << "Could not get system Wow64 directory";
return base::FilePath();
return base::FilePath(;
base::FilePath GetSampleDLLPath() {
// The sample DLL should be next to the executable because it is generated at
// build time.
base::FilePath exe_dir;
base::PathService::Get(base::DIR_EXE, &exe_dir);
return exe_dir.Append(L"empty_dll.dll");
base::FilePath GetSignedSampleDLLPath() {
// The signed sample DLL should be next to the executable because it's copied
// there at build time.
base::FilePath exe_dir;
base::PathService::Get(base::DIR_EXE, &exe_dir);
return exe_dir.Append(L"signed_empty_dll.dll");
ScopedTempDirNoWow64::ScopedTempDirNoWow64() = default;
ScopedTempDirNoWow64::~ScopedTempDirNoWow64() {
// Since the temp dir was created with Wow64 disabled, it must be deleted
// with Wow64 disabled.
ScopedDisableWow64Redirection disable_wow64_redirection;
// The parent's destructor will call Delete again, without disabling Wow64,
// which could delete a directory with the same name in SysWOW64. So make
// sure the path is cleared even if the Delete call above failed.
bool ScopedTempDirNoWow64::CreateUniqueSystem32TempDir() {
ScopedDisableWow64Redirection disable_wow64_redirection;
base::FilePath system_path;
if (!base::PathService::Get(base::DIR_SYSTEM, &system_path)) {
PLOG(ERROR) << "Unable to get system32 path";
return false;
// Note that on 32-bit Windows this check will always succeed because
// GetWow64RedirectedSystemPath returns an empty string. This is correct
// because Wow64 redirection isn't supported on 32-bit Windows, so the system
// path is guaranteed not to be redirected.
DCHECK(system_path != GetWow64RedirectedSystemPath());
return CreateUniqueTempDirUnderPath(system_path);
bool ScopedTempDirNoWow64::CreateEmptyFileInUniqueSystem32TempDir(
const base::string16& file_name) {
if (!CreateUniqueSystem32TempDir())
return false;
ScopedDisableWow64Redirection disable_wow64_redirection;
return CreateEmptyFile(GetPath().Append(file_name));
bool CheckTestPrivileges() {
// Check for administrator privileges, unless running in the sandbox.
const bool is_sandboxed_process =
(sandbox::SandboxFactory::GetTargetServices() != nullptr);
if (!is_sandboxed_process && !chrome_cleaner::HasAdminRights()) {
LOG(ERROR) << "Some Chrome Cleanup tests need administrator privileges.";
return false;
// All programs run under the msys git shell have debug privileges (!), which
// breaks the assumptions of some of the tests. So drop that privilege unless
// actually running under a debugger.
if (!::IsDebuggerPresent() &&
!chrome_cleaner::ReleaseDebugRightsPrivileges()) {
PLOG(ERROR) << "Failed to release debug privileges";
return false;
return true;
bool ResetAclForUcrtbase() {
base::FilePath exe_path;
if (!base::PathService::Get(base::BasePathKey::DIR_EXE, &exe_path)) {
LOG(ERROR) << "Failed to get directory path.";
return false;
base::FilePath abs_path = base::MakeAbsoluteFilePath(exe_path);
#ifdef NDEBUG
base::FilePath ucrt_path = abs_path.Append(L"ucrtbase.dll");
base::FilePath ucrt_path = abs_path.Append(L"ucrtbased.dll");
base::CommandLine cmd({L"icacls"});
base::Process process =
base::LaunchProcess(cmd, base::LaunchOptionsForTest());
int exit_code = 0;
if (!process.WaitForExitWithTimeout(TestTimeouts::action_timeout(),
&exit_code)) {
LOG(ERROR) << "Failed to reset acl for file " << ucrt_path.AsUTF16Unsafe();
return false;
if (exit_code) {
LOG(ERROR) << "Failed to reset acl for file " << ucrt_path.AsUTF16Unsafe()
<< " with exit code " << exit_code;
return !exit_code;
} // namespace chrome_cleaner