blob: 582ae940df68eeb61eeabd2dffd57ab174b60736 [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/browser/safe_browsing/chrome_cleaner/reporter_runner_win.h"
#include "base/base_switches.h"
#include "base/command_line.h"
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/logging.h"
#include "base/process/launch.h"
#include "base/process/process.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/synchronization/waitable_event.h"
#include "base/test/multiprocess_test.h"
#include "base/test/task_environment.h"
#include "base/test/test_timeouts.h"
#include "base/time/time.h"
#include "base/win/win_util.h"
#include "chrome/browser/safe_browsing/chrome_cleaner/sw_reporter_invocation_win.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/multiprocess_func_list.h"
#include <windows.h>
using ::testing::AssertionFailure;
using ::testing::AssertionResult;
using ::testing::AssertionSuccess;
namespace safe_browsing {
namespace {
constexpr char kParentHandleSwitch[] = "parent-handle";
std::string LastSystemErrorString() {
return logging::SystemErrorCodeToString(logging::GetLastSystemErrorCode());
}
// Duplicates |src_handle| and gives |target_process| access to it.
//
// To give a process that doesn't exist yet access to the handle, use the
// current process as |target_process| and add the resulting handle to
// LaunchOptions::handles_to_inherit_vector.
HANDLE DuplicateHandleIntoProcess(const base::Process& target_process,
HANDLE src_handle) {
HANDLE target_handle;
if (!::DuplicateHandle(/*src_process=*/::GetCurrentProcess(), src_handle,
target_process.Handle(), &target_handle,
/*desired_access=*/0, /*inherit_handle=*/true,
/*flags=*/DUPLICATE_SAME_ACCESS)) {
return INVALID_HANDLE_VALUE;
}
return target_handle;
}
// Allows a test child to spawn a test grandchild without endless recursion.
base::CommandLine GetNestedTestChildBaseCommandLine() {
base::CommandLine command_line =
base::GetMultiProcessTestChildBaseCommandLine();
// Since we are already be in a child process, remove the test child process
// switch. It will be re-added with the grandchild test name by
// SpawnMultiprocessTestChild.
command_line.RemoveSwitch(switches::kTestChildProcess);
return command_line;
}
// Gives the parent process access to |grandchild|'s process handle. Called in
// the child process.
void GiveGrandchildHandleToParent(const base::Process& grandchild) {
// Get the parent process handle from the commandline.
unsigned int raw_handle;
CHECK(base::StringToUint(
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
kParentHandleSwitch),
&raw_handle));
base::Process parent(base::win::Uint32ToHandle(raw_handle));
// Give the parent access to the grandchild process handle and write it to
// stdout.
base::File write_pipe(::GetStdHandle(STD_OUTPUT_HANDLE));
uint32_t grandchild_handle = base::win::HandleToUint32(
DuplicateHandleIntoProcess(parent, grandchild.Handle()));
constexpr int handle_size = sizeof(grandchild_handle);
CHECK_EQ(write_pipe.WriteAtCurrentPos(
reinterpret_cast<char*>(&grandchild_handle), handle_size),
handle_size);
}
// Reporter delegate that implements LaunchReporterProcess to spawn a
// multiprocess test child.
class LaunchReporterDelegate : public internal::SwReporterTestingDelegate {
public:
LaunchReporterDelegate() = default;
~LaunchReporterDelegate() override = default;
base::Process LaunchReporterProcess(
const SwReporterInvocation& invocation,
const base::LaunchOptions& options) override {
// Launch a multiprocess test child, getting the name from the invocation
// commandline.
base::Process grandchild = base::SpawnMultiProcessTestChild(
invocation.command_line().GetProgram().AsUTF8Unsafe(),
GetNestedTestChildBaseCommandLine(), options);
GiveGrandchildHandleToParent(grandchild);
return grandchild;
}
int WaitForReporterExit(const base::Process& process) const override {
int exit_code;
process.WaitForExit(&exit_code);
return exit_code;
}
base::Time Now() const override {
NOTREACHED();
return base::Time::Now();
}
base::TaskRunner* BlockingTaskRunner() const override {
NOTREACHED();
return nullptr;
}
ChromeCleanerController* GetCleanerController() override {
NOTREACHED();
return nullptr;
}
void CreateChromeCleanerDialogController() override { NOTREACHED(); }
};
// Spawns a test child process that runs |child_function|. This process must
// spawn a grandchild process and write its handle to stdout.
//
// On success |child| will be set to the child process object and |grandchild|
// will be set to the grandchild process object. Both of these processes will
// be valid and running.
AssertionResult CreateRunningProcesseses(const std::string& child_function,
base::Process* child,
base::Process* grandchild) {
base::CommandLine child_command_line =
base::GetMultiProcessTestChildBaseCommandLine();
base::LaunchOptions options;
// Give the child a handle to the current process. It will need this to pass
// the grandchild handle back to us.
// ::GetCurrentProcess returns a pseudo-handle that always refers to the
// current process. Duplicate it to get a real handle to pass to the child.
HANDLE pseudo_handle = ::GetCurrentProcess();
HANDLE parent_handle =
DuplicateHandleIntoProcess(base::Process::Current(), pseudo_handle);
if (parent_handle == INVALID_HANDLE_VALUE) {
return AssertionFailure()
<< "Failed to duplicate parent handle: " << LastSystemErrorString();
}
child_command_line.AppendSwitchASCII(
kParentHandleSwitch,
base::NumberToString(base::win::HandleToUint32(parent_handle)));
options.handles_to_inherit.push_back(parent_handle);
// Create a pipe to read the grandchild handle back from the child.
// (Adapted from
// https://cs.chromium.org/chromium/src/base/process/process_util_unittest.cc?rcl=51b17c51acc7dbf5fb812371d5724b2564578661&l=1294)
HANDLE read_handle, write_handle;
if (!::CreatePipe(&read_handle, &write_handle, nullptr, 0)) {
return AssertionFailure()
<< "Failed to create pipe: " << LastSystemErrorString();
}
base::File read_pipe(read_handle);
base::File write_pipe(write_handle);
options.stdin_handle = INVALID_HANDLE_VALUE;
options.stdout_handle = write_handle;
options.stderr_handle = ::GetStdHandle(STD_ERROR_HANDLE);
options.handles_to_inherit.push_back(write_handle);
*child = base::SpawnMultiProcessTestChild(child_function, child_command_line,
options);
if (!child->IsValid()) {
return AssertionFailure() << "Failed to spawn child process";
}
// Read the grandchild handle from the child.
uint32_t grandchild_handle;
constexpr int handle_size = sizeof(grandchild_handle);
int bytes_read = read_pipe.ReadAtCurrentPos(
reinterpret_cast<char*>(&grandchild_handle), handle_size);
if (bytes_read != handle_size) {
return AssertionFailure() << "Failure reading grandchild handle. Expected "
<< handle_size << " bytes, read " << bytes_read;
}
*grandchild = base::Process(base::win::Uint32ToHandle(grandchild_handle));
if (!grandchild->IsValid()) {
return AssertionFailure()
<< "Child wrote invalid grandchild handle: " << grandchild_handle;
}
// All the subprocesses should now be running. Wait a moment before verifying
// this since if something went wrong it will take a few msec for them exit.
base::WaitableEvent pause;
pause.TimedWait(TestTimeouts::tiny_timeout());
if (!child->IsRunning()) {
return AssertionFailure() << "Child process died unexpectedly.";
}
if (!grandchild->IsRunning()) {
return AssertionFailure() << "Grandchild process died before child did.";
}
return AssertionSuccess();
}
void WaitUntilSlain() {
base::test::SingleThreadTaskEnvironment task_environment;
base::RunLoop run_loop;
run_loop.Run();
}
// Main function for the grandchild process in all tests.
MULTIPROCESS_TEST_MAIN(InfiniteSubprocess) {
WaitUntilSlain();
return 0;
}
// ValidateDefaultBehaviour test
//
// The TerminateWhileRunning test assumes that, when a child process exits, the
// test framework doesn't immediately kill its subprocesses. This makes sure
// that assumption continues to be true.
// Main function for the child process.
MULTIPROCESS_TEST_MAIN(DefaultBehaviourChild) {
base::LaunchOptions options;
base::Process grandchild = base::SpawnMultiProcessTestChild(
"InfiniteSubprocess", GetNestedTestChildBaseCommandLine(), options);
GiveGrandchildHandleToParent(grandchild);
WaitUntilSlain();
return 0;
}
// Parent process.
TEST(ReporterRunnerLaunchTest, ValidateDefaultBehaviour) {
if (!safe_browsing::internal::ReporterTerminatesOnBrowserExit()) {
// No point testing the default behaviour if we won't run the
// TerminateWhileRunning test.
return;
}
base::Process child, grandchild;
ASSERT_TRUE(
CreateRunningProcesseses("DefaultBehaviourChild", &child, &grandchild));
ASSERT_TRUE(child.Terminate(/*exit_code=*/0, /*wait=*/true));
// Expect the grandchild process is still running even though the child is
// not. Wait a moment before checking this because it will take a few msec
// for it to exit.
base::WaitableEvent pause;
pause.TimedWait(TestTimeouts::tiny_timeout());
EXPECT_TRUE(grandchild.IsRunning());
grandchild.Terminate(/*exit_code=*/0, /*wait=*/false);
}
// TerminateWhileRunning test
//
// This makes sure that the software reporter process doesn't outlive its
// parent if the parent is terminated while the reporter is still running.
// Main function for the child process.
MULTIPROCESS_TEST_MAIN(SwReporterChild) {
LaunchReporterDelegate delegate;
SetSwReporterTestingDelegate(&delegate);
// Stuff the multiprocess test name for the grandchild into a CommandLine
// object by pretending it's a program path.
const SwReporterInvocation invocation(base::CommandLine(
base::FilePath(FILE_PATH_LITERAL("InfiniteSubprocess"))));
return safe_browsing::internal::LaunchAndWaitForExit(invocation);
}
// Parent process.
TEST(ReporterRunnerLaunchTest, TerminateWhileRunning) {
if (!safe_browsing::internal::ReporterTerminatesOnBrowserExit()) {
// Skip the test since the code being tested isn't enabled in this
// configuration.
return;
}
base::Process child, grandchild;
ASSERT_TRUE(CreateRunningProcesseses("SwReporterChild", &child, &grandchild));
ASSERT_TRUE(child.Terminate(/*exit_code=*/0, /*wait=*/true));
// The child process called safe_browsing::internal::LaunchReporter which
// should set things up so that the grandchild is automatically killed when
// the child exits. Wait a moment before checking this because it will take a
// few msec for it to exit.
base::WaitableEvent pause;
pause.TimedWait(TestTimeouts::tiny_timeout());
EXPECT_FALSE(grandchild.IsRunning());
}
} // namespace
} // namespace safe_browsing