blob: b403bd73649ac4174742ce8fd9a634b297df3df6 [file] [log] [blame]
// Copyright 2019 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 <windows.h>
#include <atomic>
#include <map>
#include <string>
#include <utility>
#include "base/bind.h"
#include "base/callback.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/scoped_temp_dir.h"
#include "base/logging.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/synchronization/waitable_event.h"
#include "base/task/post_task.h"
#include "base/task/task_traits.h"
#include "base/test/task_environment.h"
#include "base/test/test_timeouts.h"
#include "base/threading/thread_restrictions.h"
#include "base/win/win_util.h"
#include "base/win/windows_version.h"
#include "chrome/chrome_cleaner/engines/broker/interface_metadata_observer.h"
#include "chrome/chrome_cleaner/engines/target/engine_file_requests_proxy.h"
#include "chrome/chrome_cleaner/engines/target/sandboxed_test_helpers.h"
#include "chrome/chrome_cleaner/ipc/mojo_task_runner.h"
#include "chrome/chrome_cleaner/mojom/engine_file_requests.mojom.h"
#include "chrome/chrome_cleaner/os/inheritable_event.h"
#include "chrome/chrome_cleaner/strings/string16_embedded_nulls.h"
#include "chrome/chrome_cleaner/test/test_executables.h"
#include "chrome/chrome_cleaner/test/test_native_reg_util.h"
#include "chrome/chrome_cleaner/test/test_scoped_service_handle.h"
#include "chrome/chrome_cleaner/test/test_task_scheduler.h"
#include "components/chrome_cleaner/test/test_name_helper.h"
#include "sandbox/win/src/sid.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/multiprocess_func_list.h"
namespace chrome_cleaner {
namespace {
constexpr char kSharedEventHandleSwitch[] = "shared-event-handle";
constexpr char kTestRequestNameSwitch[] = "test-request-name";
constexpr char kTestFilePathSwitch[] = "test-file-path";
constexpr char kTestRegKeySwitch[] = "test-reg-key";
constexpr char kTestProcessIdSwitch[] = "test-process-id";
// An InterfaceMetadataObserver that introduces a pause into the first call
// that's observed, to simulate a slow operation.
class SlowMetadataObserver : public InterfaceMetadataObserver {
public:
explicit SlowMetadataObserver(base::WaitableEvent* event) : event_(event) {}
~SlowMetadataObserver() override = default;
// InterfaceMetadataObserver
void ObserveCall(const LogInformation& log_information,
const std::map<std::string, std::string>& params) override {
// Ignore the params.
ObserveCall(log_information);
}
void ObserveCall(const LogInformation& log_information) override {
bool was_first_call = is_first_call_.exchange(false);
if (was_first_call) {
// Wait for longer than TestTimeouts::action_timeout() to avoid a race
// condition if the second call times out at the same time as this. The
// child process will signal |event_| to abort the wait when the test is
// done.
base::ScopedAllowBaseSyncPrimitivesForTesting allow_sync;
event_->TimedWait(TestTimeouts::action_max_timeout());
}
}
private:
std::atomic<bool> is_first_call_{true};
base::WaitableEvent* event_;
};
scoped_refptr<SandboxChildProcess> SetupSandboxedChildProcess() {
scoped_refptr<MojoTaskRunner> mojo_task_runner = MojoTaskRunner::Create();
auto child_process =
base::MakeRefCounted<SandboxChildProcess>(mojo_task_runner);
child_process->LowerToken();
return child_process;
}
// Execute |closure| on a different sequence since it could block and we don't
// want to block on the Mojo thread.
void InvokeOnOtherSequence(base::OnceClosure closure) {
base::PostTask(FROM_HERE,
{base::ThreadPool(), base::WithBaseSyncPrimitives()},
std::move(closure));
}
} // namespace
class TestEngineRequestInvoker {
public:
TestEngineRequestInvoker(SandboxChildProcess* child_process,
const base::FilePath& test_file_path,
const String16EmbeddedNulls& test_native_reg_key,
base::ProcessId test_process_id)
: file_requests_proxy_(child_process->GetFileRequestsProxy()),
engine_requests_proxy_(child_process->GetEngineRequestsProxy()),
cleaner_requests_proxy_(child_process->GetCleanerEngineRequestsProxy()),
test_file_path_(test_file_path),
test_native_reg_key_(test_native_reg_key),
test_process_id_(test_process_id) {}
// Invokes a request on the proxy object. The proxy will send the request
// over Mojo to the parent process, where the request's impl may pause for a
// long time. After the response is received over Mojo, |result_closure| is
// called on another sequence to avoid blocking the Mojo thread. The
// request's actual result is ignored since this test only cares about the
// timing when it's received.
void InvokeRequest(const std::string& request_name,
base::OnceClosure result_closure) {
using base::BindOnce;
using base::IgnoreResult;
using FileProxy = EngineFileRequestsProxy;
using EngineProxy = EngineRequestsProxy;
using CleanerProxy = CleanerEngineRequestsProxy;
// Functions called with these parameters may fail but that's ok as long as
// they don't block while they're failing.
constexpr FindFileHandle kDummyFindFileHandle = -1; // INVALID_HANDLE_VALUE
// This is static because it needs to exist for the entire life of an async
// request.
static const sandbox::Sid kDummySid(WinSelfSid);
if (request_name == "FindFirstFile") {
file_requests_proxy_->task_runner()->PostTask(
FROM_HERE, BindOnce(IgnoreResult(&FileProxy::SandboxFindFirstFile),
file_requests_proxy_, test_file_path_,
BindOnce(&FindFirstFileCallback,
std::move(result_closure))));
} else if (request_name == "FindNextFile") {
file_requests_proxy_->task_runner()->PostTask(
FROM_HERE,
BindOnce(IgnoreResult(&FileProxy::SandboxFindNextFile),
file_requests_proxy_, kDummyFindFileHandle,
BindOnce(&FindNextFileCallback, std::move(result_closure))));
} else if (request_name == "FindClose") {
file_requests_proxy_->task_runner()->PostTask(
FROM_HERE,
BindOnce(IgnoreResult(&FileProxy::SandboxFindClose),
file_requests_proxy_, kDummyFindFileHandle,
BindOnce(&FindCloseCallback, std::move(result_closure))));
} else if (request_name == "OpenReadOnlyFile") {
file_requests_proxy_->task_runner()->PostTask(
FROM_HERE, BindOnce(IgnoreResult(&FileProxy::SandboxOpenReadOnlyFile),
file_requests_proxy_, test_file_path_, 0,
BindOnce(&OpenReadOnlyFileCallback,
std::move(result_closure))));
} else if (request_name == "GetFileAttributes") {
engine_requests_proxy_->task_runner()->PostTask(
FROM_HERE,
BindOnce(
IgnoreResult(&EngineProxy::SandboxGetFileAttributes),
engine_requests_proxy_, test_file_path_,
BindOnce(&GetFileAttributesCallback, std::move(result_closure))));
} else if (request_name == "GetKnownFolderPath") {
engine_requests_proxy_->task_runner()->PostTask(
FROM_HERE,
BindOnce(IgnoreResult(&EngineProxy::SandboxGetKnownFolderPath),
engine_requests_proxy_, mojom::KnownFolder::kWindows,
BindOnce(&GetKnownFolderPathCallback,
std::move(result_closure))));
} else if (request_name == "GetProcesses") {
engine_requests_proxy_->task_runner()->PostTask(
FROM_HERE,
BindOnce(IgnoreResult(&EngineProxy::SandboxGetProcesses),
engine_requests_proxy_,
BindOnce(&GetProcessesCallback, std::move(result_closure))));
} else if (request_name == "GetTasks") {
engine_requests_proxy_->task_runner()->PostTask(
FROM_HERE,
BindOnce(IgnoreResult(&EngineProxy::SandboxGetTasks),
engine_requests_proxy_,
BindOnce(&GetTasksCallback, std::move(result_closure))));
} else if (request_name == "GetProcessImagePath") {
engine_requests_proxy_->task_runner()->PostTask(
FROM_HERE,
BindOnce(IgnoreResult(&EngineProxy::SandboxGetProcessImagePath),
engine_requests_proxy_, test_process_id_,
BindOnce(&GetProcessImagePathCallback,
std::move(result_closure))));
} else if (request_name == "GetLoadedModules") {
engine_requests_proxy_->task_runner()->PostTask(
FROM_HERE,
BindOnce(
IgnoreResult(&EngineProxy::SandboxGetLoadedModules),
engine_requests_proxy_, test_process_id_,
BindOnce(&GetLoadedModulesCallback, std::move(result_closure))));
} else if (request_name == "GetProcessCommandLine") {
engine_requests_proxy_->task_runner()->PostTask(
FROM_HERE,
BindOnce(IgnoreResult(&EngineProxy::SandboxGetProcessCommandLine),
engine_requests_proxy_, test_process_id_,
BindOnce(&GetProcessCommandLineCallback,
std::move(result_closure))));
} else if (request_name == "GetUserInfoFromSID") {
engine_requests_proxy_->task_runner()->PostTask(
FROM_HERE,
BindOnce(IgnoreResult(&EngineProxy::SandboxGetUserInfoFromSID),
engine_requests_proxy_,
static_cast<SID*>(kDummySid.GetPSID()),
BindOnce(&GetUserInfoFromSIDCallback,
std::move(result_closure))));
} else if (request_name == "OpenReadOnlyRegistry") {
engine_requests_proxy_->task_runner()->PostTask(
FROM_HERE,
BindOnce(IgnoreResult(&EngineProxy::SandboxOpenReadOnlyRegistry),
engine_requests_proxy_, HKEY_CURRENT_USER,
L"Software\\Dummy", 0,
BindOnce(&OpenReadOnlyRegistryCallback,
std::move(result_closure))));
} else if (request_name == "NtOpenReadOnlyRegistry") {
engine_requests_proxy_->task_runner()->PostTask(
FROM_HERE,
BindOnce(IgnoreResult(&EngineProxy::SandboxNtOpenReadOnlyRegistry),
engine_requests_proxy_, nullptr, test_native_reg_key_, 0,
BindOnce(&NtOpenReadOnlyRegistryCallback,
std::move(result_closure))));
} else if (request_name == "DeleteFile") {
cleaner_requests_proxy_->task_runner()->PostTask(
FROM_HERE,
BindOnce(IgnoreResult(&CleanerProxy::SandboxDeleteFile),
cleaner_requests_proxy_, test_file_path_,
BindOnce(&DeleteFileCallback, std::move(result_closure))));
} else if (request_name == "DeleteFilePostReboot") {
cleaner_requests_proxy_->task_runner()->PostTask(
FROM_HERE,
BindOnce(IgnoreResult(&CleanerProxy::SandboxDeleteFilePostReboot),
cleaner_requests_proxy_, test_file_path_,
BindOnce(&DeleteFilePostRebootCallback,
std::move(result_closure))));
} else if (request_name == "NtDeleteRegistryKey") {
cleaner_requests_proxy_->task_runner()->PostTask(
FROM_HERE,
BindOnce(IgnoreResult(&CleanerProxy::SandboxNtDeleteRegistryKey),
cleaner_requests_proxy_, test_native_reg_key_,
BindOnce(&NtDeleteRegistryKeyCallback,
std::move(result_closure))));
} else if (request_name == "NtDeleteRegistryValue") {
// Reuse the key name for the value. It's an arbitrary string since the
// key doesn't really exist.
cleaner_requests_proxy_->task_runner()->PostTask(
FROM_HERE,
BindOnce(IgnoreResult(&CleanerProxy::SandboxNtDeleteRegistryValue),
cleaner_requests_proxy_, test_native_reg_key_,
/*value_name=*/test_native_reg_key_,
BindOnce(&NtDeleteRegistryValueCallback,
std::move(result_closure))));
} else if (request_name == "NtChangeRegistryValue") {
// Reuse the key name for the value. It's an arbitrary string since the
// key doesn't really exist.
cleaner_requests_proxy_->task_runner()->PostTask(
FROM_HERE,
BindOnce(IgnoreResult(&CleanerProxy::SandboxNtChangeRegistryValue),
cleaner_requests_proxy_, test_native_reg_key_,
/*value_name=*/test_native_reg_key_,
/*new_value=*/test_native_reg_key_,
BindOnce(&NtChangeRegistryValueCallback,
std::move(result_closure))));
} else if (request_name == "DeleteService") {
// The broker should reject the empty string so we won't risk deleting a
// real service.
const base::string16 empty_service_name;
cleaner_requests_proxy_->task_runner()->PostTask(
FROM_HERE, BindOnce(IgnoreResult(&CleanerProxy::SandboxDeleteService),
cleaner_requests_proxy_, empty_service_name,
BindOnce(&DeleteServiceCallback,
std::move(result_closure))));
} else if (request_name == "DeleteTask") {
cleaner_requests_proxy_->task_runner()->PostTask(
FROM_HERE,
BindOnce(IgnoreResult(&CleanerProxy::SandboxDeleteTask),
cleaner_requests_proxy_, L"NonExistentTaskName",
BindOnce(&DeleteTaskCallback, std::move(result_closure))));
} else if (request_name == "TerminateProcess") {
cleaner_requests_proxy_->task_runner()->PostTask(
FROM_HERE,
BindOnce(
IgnoreResult(&CleanerProxy::SandboxTerminateProcess),
cleaner_requests_proxy_, test_process_id_,
BindOnce(&TerminateProcessCallback, std::move(result_closure))));
} else {
NOTREACHED() << "Unrecognized request: " << request_name;
}
}
private:
static void FindFirstFileCallback(
base::OnceClosure closure,
uint32_t /*result*/,
chrome_cleaner::mojom::FindFileDataPtr /*win32_find_data*/,
chrome_cleaner::mojom::FindHandlePtr /*find_handle*/) {
InvokeOnOtherSequence(std::move(closure));
}
static void FindNextFileCallback(
base::OnceClosure closure,
uint32_t /*result*/,
chrome_cleaner::mojom::FindFileDataPtr /*win32_find_data*/) {
InvokeOnOtherSequence(std::move(closure));
}
static void FindCloseCallback(base::OnceClosure closure,
uint32_t /*result*/) {
InvokeOnOtherSequence(std::move(closure));
}
static void OpenReadOnlyFileCallback(base::OnceClosure closure,
mojo::ScopedHandle /*handle*/) {
InvokeOnOtherSequence(std::move(closure));
}
static void GetFileAttributesCallback(base::OnceClosure closure,
uint32_t /*result*/,
uint32_t /*attributes*/) {
InvokeOnOtherSequence(std::move(closure));
}
static void GetKnownFolderPathCallback(base::OnceClosure closure,
bool /*result*/,
const base::FilePath& /*path*/) {
InvokeOnOtherSequence(std::move(closure));
}
static void GetProcessesCallback(
base::OnceClosure closure,
bool /*result*/,
const std::vector<base::ProcessId>& /*processes*/) {
InvokeOnOtherSequence(std::move(closure));
}
static void GetTasksCallback(base::OnceClosure closure,
bool /*result*/,
std::vector<mojom::ScheduledTaskPtr> /*tasks*/) {
InvokeOnOtherSequence(std::move(closure));
}
static void GetProcessImagePathCallback(base::OnceClosure closure,
bool /*result*/,
const base::FilePath& /*path*/) {
InvokeOnOtherSequence(std::move(closure));
}
static void GetLoadedModulesCallback(
base::OnceClosure closure,
bool /*result*/,
const std::vector<base::string16>& /*modules*/) {
InvokeOnOtherSequence(std::move(closure));
}
static void GetProcessCommandLineCallback(
base::OnceClosure closure,
bool /*result*/,
const base::string16& /*command_line*/) {
InvokeOnOtherSequence(std::move(closure));
}
static void GetUserInfoFromSIDCallback(base::OnceClosure closure,
bool /*result*/,
mojom::UserInformationPtr /*info*/) {
InvokeOnOtherSequence(std::move(closure));
}
static void OpenReadOnlyRegistryCallback(base::OnceClosure closure,
uint32_t /*result*/,
HANDLE /*handle*/) {
InvokeOnOtherSequence(std::move(closure));
}
static void NtOpenReadOnlyRegistryCallback(base::OnceClosure closure,
uint32_t /*result*/,
HANDLE /*handle*/) {
InvokeOnOtherSequence(std::move(closure));
}
static void DeleteFileCallback(base::OnceClosure closure, bool /*result*/) {
InvokeOnOtherSequence(std::move(closure));
}
static void DeleteFilePostRebootCallback(base::OnceClosure closure,
bool /*result*/) {
InvokeOnOtherSequence(std::move(closure));
}
static void NtDeleteRegistryKeyCallback(base::OnceClosure closure,
bool /*result*/) {
InvokeOnOtherSequence(std::move(closure));
}
static void NtDeleteRegistryValueCallback(base::OnceClosure closure,
bool /*result*/) {
InvokeOnOtherSequence(std::move(closure));
}
static void NtChangeRegistryValueCallback(base::OnceClosure closure,
bool /*result*/) {
InvokeOnOtherSequence(std::move(closure));
}
static void DeleteServiceCallback(base::OnceClosure closure,
bool /*result*/) {
InvokeOnOtherSequence(std::move(closure));
}
static void DeleteTaskCallback(base::OnceClosure closure, bool /*result*/) {
InvokeOnOtherSequence(std::move(closure));
}
static void TerminateProcessCallback(base::OnceClosure closure,
bool /*result*/) {
InvokeOnOtherSequence(std::move(closure));
}
scoped_refptr<EngineFileRequestsProxy> file_requests_proxy_;
scoped_refptr<EngineRequestsProxy> engine_requests_proxy_;
scoped_refptr<CleanerEngineRequestsProxy> cleaner_requests_proxy_;
base::FilePath test_file_path_;
String16EmbeddedNulls test_native_reg_key_;
base::ProcessId test_process_id_;
};
MULTIPROCESS_TEST_MAIN(EngineRequestsNoBlocking) {
// COM can't be initialized inside the sandbox.
base::test::TaskEnvironment task_environment(
base::test::TaskEnvironment::ThreadPoolCOMEnvironment::NONE);
auto child_process = SetupSandboxedChildProcess();
if (!child_process)
return 1;
auto* command_line = base::CommandLine::ForCurrentProcess();
std::string request_name =
command_line->GetSwitchValueASCII(kTestRequestNameSwitch);
base::string16 test_native_reg_key_str =
command_line->GetSwitchValueNative(kTestRegKeySwitch);
// Include the final null.
String16EmbeddedNulls test_native_reg_key(test_native_reg_key_str.data(),
test_native_reg_key_str.size() + 1);
unsigned test_process_id;
CHECK(base::StringToUint(
command_line->GetSwitchValueNative(kTestProcessIdSwitch),
&test_process_id));
TestEngineRequestInvoker invoker(
child_process.get(),
command_line->GetSwitchValuePath(kTestFilePathSwitch),
test_native_reg_key, base::ProcessId(test_process_id));
// Make a call, which will go across the Mojo interface to the parent
// process. Since this is the first call observed by the MetadataObserver in
// the parent process, it should block in that process for longer than
// action_timeout.
base::WaitableEvent blocking_event;
invoker.InvokeRequest(request_name,
base::BindOnce(&base::WaitableEvent::Signal,
base::Unretained(&blocking_event)));
base::ScopedAllowBaseSyncPrimitivesForTesting allow_sync;
// Pause to be sure the call has started and is blocking. (Avoids a race
// condition if calls are serviced in separate threads in the parent
// process.)
base::WaitableEvent pause_event;
pause_event.TimedWait(TestTimeouts::tiny_timeout());
// Ensure a second call works, even though the first hasn't completed.
// When this call executes it will signal the event, which causes TimedWait to
// return true. If the wait times out without executing the call, TimedWait
// will return false.
base::WaitableEvent nonblocking_event;
invoker.InvokeRequest(request_name,
base::BindOnce(&base::WaitableEvent::Signal,
base::Unretained(&nonblocking_event)));
bool called_without_timeout =
nonblocking_event.TimedWait(TestTimeouts::action_timeout());
EXPECT_FALSE(blocking_event.IsSignaled())
<< "First Mojo call finished unexpectedly.";
EXPECT_TRUE(called_without_timeout)
<< "First Mojo call seems to be blocking the thread.";
// Cancel the blocking call and wait until it's cleaned up. This avoids a
// race condition: after |shared_event| is signaled, the blocking call in the
// broker process will return and the response is written over the mojo pipe,
// which triggers the callback that signals |blocking_event|. We need to wait
// until that response is received before we return from this function and
// tear down the mojo pipe.
uint32_t handle_value = 0;
CHECK(base::StringToUint(
command_line->GetSwitchValueNative(kSharedEventHandleSwitch),
&handle_value));
base::WaitableEvent shared_event(
base::win::ScopedHandle(base::win::Uint32ToHandle(handle_value)));
shared_event.Signal();
blocking_event.Wait();
return ::testing::Test::HasFailure();
}
using TestParentProcess = MaybeSandboxedParentProcess<SandboxedParentProcess>;
class EngineRequestsNoBlockingTest
: public ::testing::TestWithParam<const char*> {};
TEST_P(EngineRequestsNoBlockingTest, TestRequest) {
base::test::TaskEnvironment task_environment;
// This event will be shared between the parent and child processes. The
// parent will wait on the event to simulate a long-running function call.
// The child can signal the event to cancel the wait once the test is
// finished.
auto shared_event =
CreateInheritableEvent(base::WaitableEvent::ResetPolicy::MANUAL,
base::WaitableEvent::InitialState::NOT_SIGNALED);
SlowMetadataObserver slow_metadata_observer(shared_event.get());
scoped_refptr<TestParentProcess> parent_process =
base::MakeRefCounted<TestParentProcess>(
MojoTaskRunner::Create(),
TestParentProcess::CallbacksToSetup::kCleanupRequests,
&slow_metadata_observer);
parent_process->AppendSwitchHandleToShare(kSharedEventHandleSwitch,
shared_event->handle());
parent_process->AppendSwitch(kTestRequestNameSwitch, GetParam());
// Create test resources that can't be created or cleaned up from the
// sandbox.
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
parent_process->AppendSwitchPath(
kTestFilePathSwitch, temp_dir.GetPath().Append(L"dummy-file.txt"));
chrome_cleaner_sandbox::ScopedTempRegistryKey temp_reg_key;
parent_process->AppendSwitchNative(
kTestRegKeySwitch,
base::StrCat({temp_reg_key.FullyQualifiedPath(), L"\\dummy-subkey"}));
base::Process test_process = LongRunningProcess(/*command_line=*/nullptr);
parent_process->AppendSwitchNative(
kTestProcessIdSwitch, base::NumberToString16(test_process.Pid()));
// Install a test task scheduler so we don't accidentally delete a real task
// when servicing the DeleteTask request.
TestTaskScheduler test_task_scheduler;
int32_t exit_code = -1;
EXPECT_TRUE(parent_process->LaunchConnectedChildProcess(
"EngineRequestsNoBlocking", TestTimeouts::action_max_timeout(),
&exit_code));
EXPECT_EQ(0, exit_code) << "Child process returned a failure - probably an "
"engine request blocked";
test_process.Terminate(0, true);
}
INSTANTIATE_TEST_SUITE_P(All,
EngineRequestsNoBlockingTest,
testing::Values("FindFirstFile",
"FindNextFile",
"FindClose",
"OpenReadOnlyFile",
"GetFileAttributes",
"GetKnownFolderPath",
"GetProcesses",
"GetTasks",
"GetProcessImagePath",
"GetLoadedModules",
"GetProcessCommandLine",
"GetUserInfoFromSID",
"OpenReadOnlyRegistry",
"NtOpenReadOnlyRegistry",
#if 0
// Calls using FileRemover still block.
"DeleteFile",
"DeleteFilePostReboot",
#endif
"NtDeleteRegistryKey",
"NtDeleteRegistryValue",
"NtChangeRegistryValue",
"DeleteService",
"DeleteTask",
"TerminateProcess"),
GetParamNameForTest());
} // namespace chrome_cleaner