blob: 8ca1e9ed521fdd3c1e35d6cadac183ae4c2f7f23 [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 "chrome/chrome_cleaner/engines/common/registry_util.h"
#include <windows.h>
#include <algorithm>
#include <memory>
#include <sstream>
#include <utility>
#include "base/base_paths.h"
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/numerics/safe_conversions.h"
#include "base/path_service.h"
#include "base/strings/string_util.h"
#include "base/synchronization/waitable_event.h"
#include "base/win/win_util.h"
#include "chrome/chrome_cleaner/ipc/ipc_test_util.h"
#include "chrome/chrome_cleaner/ipc/mojo_task_runner.h"
#include "chrome/chrome_cleaner/mojom/windows_handle.mojom.h"
#include "chrome/chrome_cleaner/os/pre_fetched_paths.h"
#include "chrome/chrome_cleaner/test/test_native_reg_util.h"
#include "mojo/core/embedder/embedder.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "sandbox/win/src/win_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/multiprocess_func_list.h"
using base::WaitableEvent;
using chrome_cleaner::MojoTaskRunner;
using chrome_cleaner::WStringEmbeddedNulls;
using chrome_cleaner::mojom::TestWindowsHandle;
namespace chrome_cleaner_sandbox {
namespace {
class TestFile : public base::File {
public:
TestFile()
: base::File(
chrome_cleaner::PreFetchedPaths::GetInstance()->GetExecutablePath(),
FLAG_OPEN) {}
};
class TestWindowsHandleImpl : public TestWindowsHandle {
public:
explicit TestWindowsHandleImpl(
mojo::PendingReceiver<TestWindowsHandle> receiver)
: receiver_(this, std::move(receiver)) {}
// TestWindowsHandle
void EchoHandle(HANDLE handle, EchoHandleCallback callback) override {
std::move(callback).Run(handle);
}
void EchoRawHandle(mojo::PlatformHandle handle,
EchoRawHandleCallback callback) override {
std::move(callback).Run(std::move(handle));
}
private:
mojo::Receiver<TestWindowsHandle> receiver_;
};
class SandboxParentProcess : public chrome_cleaner::ParentProcess {
public:
explicit SandboxParentProcess(scoped_refptr<MojoTaskRunner> mojo_task_runner)
: ParentProcess(std::move(mojo_task_runner)) {}
protected:
void CreateImpl(mojo::ScopedMessagePipeHandle mojo_pipe) override {
mojo::PendingReceiver<TestWindowsHandle> receiver(std::move(mojo_pipe));
test_windows_handle_impl_ =
std::make_unique<TestWindowsHandleImpl>(std::move(receiver));
}
void DestroyImpl() override { test_windows_handle_impl_.reset(); }
private:
~SandboxParentProcess() override = default;
std::unique_ptr<TestWindowsHandleImpl> test_windows_handle_impl_;
};
class SandboxChildProcess : public chrome_cleaner::ChildProcess {
public:
explicit SandboxChildProcess(scoped_refptr<MojoTaskRunner> mojo_task_runner)
: ChildProcess(mojo_task_runner),
test_windows_handle_(
std::make_unique<mojo::Remote<TestWindowsHandle>>()) {}
void BindToPipe(mojo::ScopedMessagePipeHandle mojo_pipe,
WaitableEvent* event) {
test_windows_handle_->Bind(
mojo::PendingRemote<TestWindowsHandle>(std::move(mojo_pipe), 0));
event->Signal();
}
HANDLE EchoHandle(HANDLE input_handle) {
HANDLE output_handle;
WaitableEvent event(WaitableEvent::ResetPolicy::MANUAL,
WaitableEvent::InitialState::NOT_SIGNALED);
auto callback = base::BindOnce(
[](HANDLE* handle_holder, WaitableEvent* event, HANDLE handle) {
*handle_holder = handle;
event->Signal();
},
&output_handle, &event);
mojo_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(
[](mojo::Remote<TestWindowsHandle>* remote, HANDLE handle,
TestWindowsHandle::EchoHandleCallback callback) {
(*remote)->EchoHandle(std::move(handle), std::move(callback));
},
base::Unretained(test_windows_handle_.get()), input_handle,
std::move(callback)));
event.Wait();
return output_handle;
}
HANDLE EchoRawHandle(HANDLE input_handle) {
mojo::PlatformHandle scoped_handle((base::win::ScopedHandle(input_handle)));
mojo::PlatformHandle output_handle;
WaitableEvent event(WaitableEvent::ResetPolicy::MANUAL,
WaitableEvent::InitialState::NOT_SIGNALED);
auto callback = base::BindOnce(
[](mojo::PlatformHandle* handle_holder, WaitableEvent* event,
mojo::PlatformHandle handle) {
*handle_holder = std::move(handle);
event->Signal();
},
&output_handle, &event);
mojo_task_runner_->PostTask(
FROM_HERE, base::BindOnce(
[](mojo::Remote<TestWindowsHandle>* remote,
mojo::PlatformHandle handle,
TestWindowsHandle::EchoRawHandleCallback callback) {
(*remote)->EchoRawHandle(std::move(handle),
std::move(callback));
},
base::Unretained(test_windows_handle_.get()),
std::move(scoped_handle), std::move(callback)));
event.Wait();
return output_handle.ReleaseHandle();
}
private:
~SandboxChildProcess() override {
mojo_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(
[](std::unique_ptr<mojo::Remote<TestWindowsHandle>> remote) {
remote.reset();
},
std::move(test_windows_handle_)));
}
std::unique_ptr<mojo::Remote<TestWindowsHandle>> test_windows_handle_;
};
std::wstring HandlePath(HANDLE handle) {
std::wstring full_path;
// The size parameter of GetFinalPathNameByHandle does NOT include the null
// terminator.
DWORD result = ::GetFinalPathNameByHandleW(
handle, base::WriteInto(&full_path, MAX_PATH), MAX_PATH - 1, 0);
if (result > MAX_PATH) {
result = ::GetFinalPathNameByHandle(
handle, base::WriteInto(&full_path, result), result - 1, 0);
}
if (!result) {
PLOG(ERROR) << "Could not get full path for handle " << handle;
return std::wstring();
}
return full_path;
}
::testing::AssertionResult HandlesAreEqual(HANDLE handle1, HANDLE handle2) {
// The best way to check this is CompareObjectHandles, but it isn't available
// until Windows 10. So just check that both refer to the same path.
std::wstring path1 = HandlePath(handle1);
std::wstring path2 = HandlePath(handle2);
if (path1.empty() || path2.empty() || path1 != path2) {
auto format_message = [](HANDLE handle, const std::wstring& path) {
std::ostringstream s;
s << handle;
if (path.empty())
s << " has no valid path";
else
s << " has path " << path;
return s.str();
};
return ::testing::AssertionFailure()
<< format_message(handle1, path1) << ", "
<< format_message(handle2, path2);
}
return ::testing::AssertionSuccess();
}
} // namespace
MULTIPROCESS_TEST_MAIN(HandleWrappingIPCMain) {
auto mojo_task_runner = MojoTaskRunner::Create();
auto child_process =
base::MakeRefCounted<SandboxChildProcess>(mojo_task_runner);
auto message_pipe_handle = child_process->CreateMessagePipeFromCommandLine();
WaitableEvent event(WaitableEvent::ResetPolicy::MANUAL,
WaitableEvent::InitialState::NOT_SIGNALED);
mojo_task_runner->PostTask(
FROM_HERE, base::BindOnce(&SandboxChildProcess::BindToPipe, child_process,
std::move(message_pipe_handle), &event));
event.Wait();
// Check that this test is actually testing what it thinks it is: when
// passing a mojo::ScopedHandle to another process, Mojo attempts to
// duplicate and close the original handle. This fails if the ScopedHandle is
// wrapping a Windows pseudo-handle, and nullptr gets passed instead. The
// WindowsHandle wrapper avoids this.
//
// However, if the mojo::ScopedHandle is passed over a Mojo connection that
// isn't going to another process, pseudo-handles are passed correctly. So
// make sure the test connection actually causes the error with
// mojo::ScopedHandle. If not, the tests of WindowsHandle will trivially
// succeed without demonstrating that WindowsHandle avoids the error.
CHECK_EQ(nullptr, child_process->EchoRawHandle(HKEY_CLASSES_ROOT));
CHECK_EQ(INVALID_HANDLE_VALUE,
child_process->EchoHandle(INVALID_HANDLE_VALUE));
CHECK_EQ(nullptr, child_process->EchoHandle(nullptr));
CHECK_EQ(HKEY_CLASSES_ROOT, child_process->EchoHandle(HKEY_CLASSES_ROOT));
CHECK_EQ(HKEY_CURRENT_CONFIG, child_process->EchoHandle(HKEY_CURRENT_CONFIG));
CHECK_EQ(HKEY_CURRENT_USER, child_process->EchoHandle(HKEY_CURRENT_USER));
CHECK_EQ(HKEY_LOCAL_MACHINE, child_process->EchoHandle(HKEY_LOCAL_MACHINE));
CHECK_EQ(HKEY_USERS, child_process->EchoHandle(HKEY_USERS));
// mojo::ScopedHandle CHECKS if given an invalid handle, so pass this handle
// raw and ensure it is marked as invalid.
HANDLE fake_handle = base::win::Uint32ToHandle(0x12345678);
CHECK_EQ(nullptr, child_process->EchoRawHandle(fake_handle));
TestFile test_file;
HANDLE test_handle = test_file.GetPlatformFile();
CHECK(HandlesAreEqual(test_handle, child_process->EchoHandle(test_handle)));
return 0;
}
TEST(SandboxUtil, HandleWrappingIPC) {
auto mojo_task_runner = MojoTaskRunner::Create();
auto parent_process =
base::MakeRefCounted<SandboxParentProcess>(mojo_task_runner);
int32_t exit_code = -1;
EXPECT_TRUE(parent_process->LaunchConnectedChildProcess(
"HandleWrappingIPCMain", &exit_code));
EXPECT_EQ(0, exit_code);
}
TEST(SandboxUtil, NativeQueryValueKey) {
std::vector<wchar_t> key_name{L'a', L'b', L'c', L'\0'};
WStringEmbeddedNulls value_name1{L'f', L'o', L'o', L'1', L'\0'};
WStringEmbeddedNulls value_name2{L'f', L'o', L'o', L'2', L'\0'};
WStringEmbeddedNulls value_name3{L'f', L'o', L'o', L'3', L'\0'};
WStringEmbeddedNulls value_name4{L'f', L'o', L'o', L'4', L'\0'};
WStringEmbeddedNulls value{L'b', L'a', L'r', L'\0'};
struct TestCases {
const WStringEmbeddedNulls& value_name;
ULONG reg_type;
} test_cases[] = {
{value_name1, REG_SZ},
{value_name2, REG_EXPAND_SZ},
{value_name3, REG_DWORD},
{value_name4, REG_BINARY},
};
ScopedTempRegistryKey temp_key;
ULONG disposition = 0;
HANDLE subkey_handle = INVALID_HANDLE_VALUE;
EXPECT_EQ(STATUS_SUCCESS, NativeCreateKey(temp_key.Get(), &key_name,
&subkey_handle, &disposition));
EXPECT_EQ(static_cast<ULONG>(REG_CREATED_NEW_KEY), disposition);
for (auto test_case : test_cases) {
EXPECT_EQ(STATUS_SUCCESS,
NativeSetValueKey(subkey_handle, test_case.value_name,
test_case.reg_type, value));
DWORD actual_reg_type = 0;
WStringEmbeddedNulls actual_value;
EXPECT_TRUE(NativeQueryValueKey(subkey_handle, test_case.value_name,
&actual_reg_type, &actual_value));
EXPECT_EQ(test_case.reg_type, actual_reg_type);
EXPECT_EQ(value, actual_value);
}
EXPECT_EQ(STATUS_SUCCESS, NativeDeleteKey(subkey_handle));
EXPECT_TRUE(::CloseHandle(subkey_handle));
}
TEST(SandboxUtil, ValidateRegistryValueChange) {
WStringEmbeddedNulls eq{L'a', L'b', L'\0', L'c'};
EXPECT_TRUE(ValidateRegistryValueChange(eq, eq));
WStringEmbeddedNulls subset1{L'a', L'b', L'\0', L'c'};
WStringEmbeddedNulls subset2{L'a', L'\0', L'c'};
EXPECT_TRUE(ValidateRegistryValueChange(subset1, subset2));
WStringEmbeddedNulls prefix1{L'a', L'b', L'\0', L'c'};
WStringEmbeddedNulls prefix2{L'b', L'\0', L'c'};
EXPECT_TRUE(ValidateRegistryValueChange(prefix1, prefix2));
WStringEmbeddedNulls suffix1{L'a', L'b', L'\0', L'c'};
WStringEmbeddedNulls suffix2{L'a', L'b', L'\0'};
EXPECT_TRUE(ValidateRegistryValueChange(suffix1, suffix2));
WStringEmbeddedNulls empty1{L'a', L'b', L'\0', L'c'};
WStringEmbeddedNulls empty2;
EXPECT_TRUE(ValidateRegistryValueChange(empty1, empty2));
WStringEmbeddedNulls super_empty1;
WStringEmbeddedNulls super_empty2{L'a', L'b', L'\0', L'c'};
EXPECT_FALSE(ValidateRegistryValueChange(super_empty1, super_empty2));
WStringEmbeddedNulls superset1{L'a', L'\0', L'c'};
WStringEmbeddedNulls superset2{L'a', L'b', L'\0', L'c'};
EXPECT_FALSE(ValidateRegistryValueChange(superset1, superset2));
WStringEmbeddedNulls bad_prefix1{L'b', L'\0', L'c'};
WStringEmbeddedNulls bad_prefix2{L'a', L'b', L'\0', L'c'};
EXPECT_FALSE(ValidateRegistryValueChange(bad_prefix1, bad_prefix2));
WStringEmbeddedNulls bad_suffix1{L'a', L'b', L'\0'};
WStringEmbeddedNulls bad_suffix2{L'a', L'b', L'\0', L'c'};
EXPECT_FALSE(ValidateRegistryValueChange(bad_suffix1, bad_suffix2));
WStringEmbeddedNulls different1{L'a', L'b', L'\0', L'c'};
WStringEmbeddedNulls different2{L'd', L'e', L'f'};
EXPECT_FALSE(ValidateRegistryValueChange(different1, different2));
}
TEST(SandboxUtil, ValidateFailureOfNtSetValueKeyOnNull) {
// The documentation for NtSetValueKey is incorrect, calling it with a
// NULL pointer returns access denied. This test ensures that the observed
// behaviour remains consistent.
std::vector<wchar_t> key_name{L'a', L'b', L'c', L'\0'};
std::vector<wchar_t> value{L'b', L'a', L'r', L'\0'};
ScopedTempRegistryKey temp_key;
ULONG disposition = 0;
HANDLE subkey_handle = INVALID_HANDLE_VALUE;
EXPECT_EQ(STATUS_SUCCESS, NativeCreateKey(temp_key.Get(), &key_name,
&subkey_handle, &disposition));
EXPECT_EQ(static_cast<ULONG>(REG_CREATED_NEW_KEY), disposition);
static NtSetValueKeyFunction NtSetValueKey = nullptr;
if (!NtSetValueKey)
ResolveNTFunctionPtr("NtSetValueKey", &NtSetValueKey);
NTSTATUS status =
NtSetValueKey(subkey_handle, nullptr, /*TitleIndex=*/0, REG_SZ,
reinterpret_cast<void*>(value.data()),
base::checked_cast<ULONG>(value.size() * sizeof(wchar_t)));
EXPECT_EQ(static_cast<NTSTATUS>(STATUS_ACCESS_VIOLATION), status);
EXPECT_EQ(STATUS_SUCCESS, NativeDeleteKey(subkey_handle));
EXPECT_TRUE(::CloseHandle(subkey_handle));
}
TEST(SandboxUtil, ValidateFailureOfNtQueryValueKeyOnNull) {
// The documentation for NtQueryValueKey is incorrect, calling it with a
// NULL pointer returns access denied. This test ensures that the observed
// behaviour remains consistent.
std::vector<wchar_t> key_name{L'a', L'b', L'c', L'\0'};
WStringEmbeddedNulls value{L'b', L'a', L'r', L'\0'};
ScopedTempRegistryKey temp_key;
ULONG disposition = 0;
HANDLE subkey_handle = INVALID_HANDLE_VALUE;
EXPECT_EQ(STATUS_SUCCESS, NativeCreateKey(temp_key.Get(), &key_name,
&subkey_handle, &disposition));
EXPECT_EQ(static_cast<ULONG>(REG_CREATED_NEW_KEY), disposition);
// Create a default value.
EXPECT_EQ(STATUS_SUCCESS,
NativeSetValueKey(subkey_handle, WStringEmbeddedNulls(nullptr),
REG_SZ, value));
static NtQueryValueKeyFunction NtQueryValueKey = nullptr;
if (!NtQueryValueKey)
ResolveNTFunctionPtr("NtQueryValueKey", &NtQueryValueKey);
DWORD size_needed = 0;
NTSTATUS status =
NtQueryValueKey(subkey_handle, nullptr, KeyValueFullInformation, nullptr,
0, &size_needed);
EXPECT_EQ(static_cast<NTSTATUS>(STATUS_ACCESS_VIOLATION), status);
EXPECT_EQ(STATUS_SUCCESS, NativeDeleteKey(subkey_handle));
EXPECT_TRUE(::CloseHandle(subkey_handle));
}
} // namespace chrome_cleaner_sandbox