// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <windows.h>
#include <memory>
#include <string>
#include "base/process/process_info.h"
#include "base/task/lazy_thread_pool_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/single_thread_task_runner_thread_mode.h"
#include "base/task/thread_pool.h"
#include "base/test/task_environment.h"
#include "base/win/scoped_process_information.h"
#include "sandbox/win/src/broker_services.h"
#include "sandbox/win/tests/common/controller.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace sandbox {
// BrokerServicesDelegate common implementation.
class TestBrokerServicesDelegateBase : public BrokerServicesDelegate {
public:
bool ParallelLaunchEnabled() override { return true; }
void ParallelLaunchPostTaskAndReplyWithResult(
const base::Location& from_here,
base::OnceCallback<CreateTargetResult()> task,
base::OnceCallback<void(CreateTargetResult)> reply) override {
base::ThreadPool::PostTaskAndReplyWithResult(
from_here,
{base::MayBlock(), base::TaskPriority::USER_BLOCKING,
base::TaskShutdownBehavior::BLOCK_SHUTDOWN},
std::move(task), std::move(reply));
}
void AfterTargetProcessCreateOnCreationThread(const void* trace_id,
DWORD process_id) override {}
};
class ParallelLaunchTest : public testing::Test {
public:
void SetUp() override {
task_environment_ = std::make_unique<base::test::TaskEnvironment>();
}
static void FinishSpawnTargetAsync(CreateTargetResult* spawn_result,
base::RunLoop* run_loop,
int* launches_remaining_count,
base::win::ScopedProcessInformation target,
DWORD last_error,
ResultCode result) {
spawn_result->process_info = std::move(target);
spawn_result->last_error = last_error;
spawn_result->result_code = result;
if (--*launches_remaining_count == 0) {
run_loop->Quit();
}
}
private:
std::unique_ptr<base::test::TaskEnvironment> task_environment_;
};
class SingleLaunch_TestBrokerServicesDelegate
: public TestBrokerServicesDelegateBase {
public:
void BeforeTargetProcessCreateOnCreationThread(
const void* trace_id) override {
creation_thread_id_ = ::GetCurrentThreadId();
}
DWORD creation_thread_id_ = 0;
};
// Launches a single child with parallel launching enabled. The child process
// will be created on the thread pool.
TEST_F(ParallelLaunchTest, SingleLaunch) {
BrokerServices* broker = GetBroker();
ASSERT_TRUE(broker);
auto* delegate = new SingleLaunch_TestBrokerServicesDelegate();
static_cast<BrokerServicesBase*>(broker)->SetBrokerServicesDelegateForTesting(
std::unique_ptr<BrokerServicesDelegate>(delegate));
// Get the path to the sandboxed app.
wchar_t prog_name[MAX_PATH];
GetModuleFileNameW(nullptr, prog_name, MAX_PATH);
std::wstring arguments(L"\"");
arguments += prog_name;
arguments += L"\" -child 0 wait"; // Don't care about the "state" argument.
auto policy = broker->CreatePolicy();
EXPECT_EQ(SBOX_ALL_OK, policy->GetConfig()->SetTokenLevel(USER_INTERACTIVE,
USER_LOCKDOWN));
CreateTargetResult spawn_result;
spawn_result.result_code = SBOX_ERROR_GENERIC;
base::RunLoop run_loop;
int launches_remaining_count = 1;
ResultCode result_code = broker->SpawnTargetAsync(
prog_name, arguments.c_str(), std::move(policy),
base::BindOnce(&FinishSpawnTargetAsync, base::Unretained(&spawn_result),
base::Unretained(&run_loop),
base::Unretained(&launches_remaining_count)));
EXPECT_EQ(result_code, SBOX_ALL_OK);
run_loop.Run();
// Target creation should happen on a different thread.
EXPECT_NE(delegate->creation_thread_id_, 0u);
EXPECT_NE(delegate->creation_thread_id_, GetCurrentThreadId());
EXPECT_EQ(SBOX_ALL_OK, spawn_result.result_code);
EXPECT_EQ(1u, ::ResumeThread(spawn_result.process_info.thread_handle()));
EXPECT_EQ(
static_cast<DWORD>(WAIT_TIMEOUT),
::WaitForSingleObject(spawn_result.process_info.process_handle(), 2000));
EXPECT_TRUE(
::TerminateProcess(spawn_result.process_info.process_handle(), 0));
::WaitForSingleObject(spawn_result.process_info.process_handle(), INFINITE);
}
class ParallelLaunch_TestBrokerServicesDelegate
: public TestBrokerServicesDelegateBase {
public:
void BeforeTargetProcessCreateOnCreationThread(
const void* trace_id) override {
if (first_launch_) {
first_creation_thread_id_ = ::GetCurrentThreadId();
first_trace_id_ = reinterpret_cast<uintptr_t>(trace_id);
first_launch_ = false;
EXPECT_TRUE(::SetEvent(reached_first_creation_event_));
::WaitForSingleObject(first_block_event_, INFINITE);
} else {
second_creation_thread_id_ = ::GetCurrentThreadId();
second_trace_id_ = reinterpret_cast<uintptr_t>(trace_id);
EXPECT_TRUE(::SetEvent(first_block_event_));
}
}
bool first_launch_ = true;
HANDLE reached_first_creation_event_;
HANDLE first_block_event_;
DWORD first_creation_thread_id_;
DWORD second_creation_thread_id_;
uintptr_t first_trace_id_;
uintptr_t second_trace_id_;
};
// This test launches two processes and synchronizes the target creation threads
// to run at the same time.
TEST_F(ParallelLaunchTest, ParallelLaunch) {
BrokerServices* broker = GetBroker();
ASSERT_TRUE(broker);
auto* delegate = new ParallelLaunch_TestBrokerServicesDelegate();
static_cast<BrokerServicesBase*>(broker)->SetBrokerServicesDelegateForTesting(
std::unique_ptr<BrokerServicesDelegate>(delegate));
// Get the path to the sandboxed app.
wchar_t prog_name[MAX_PATH];
GetModuleFileNameW(nullptr, prog_name, MAX_PATH);
std::wstring arguments(L"\"");
arguments += prog_name;
arguments += L"\" -child 0 wait"; // Don't care about the "state" argument.
base::RunLoop run_loop;
int launches_remaining_count = 2;
// Launch the first process. This will block on the creation thread and wait
// for the second process launch to unblock it.
CreateTargetResult first_spawn_result;
first_spawn_result.result_code = SBOX_ERROR_GENERIC;
delegate->reached_first_creation_event_ =
::CreateEvent(nullptr, FALSE, FALSE, nullptr);
delegate->first_block_event_ = ::CreateEvent(nullptr, FALSE, FALSE, nullptr);
{
auto policy = broker->CreatePolicy();
EXPECT_EQ(SBOX_ALL_OK, policy->GetConfig()->SetTokenLevel(USER_INTERACTIVE,
USER_LOCKDOWN));
ResultCode result_code = broker->SpawnTargetAsync(
prog_name, arguments.c_str(), std::move(policy),
base::BindOnce(&FinishSpawnTargetAsync,
base::Unretained(&first_spawn_result),
base::Unretained(&run_loop),
base::Unretained(&launches_remaining_count)));
EXPECT_EQ(result_code, SBOX_ALL_OK);
}
::WaitForSingleObject(delegate->reached_first_creation_event_, INFINITE);
// Launch the second process.
CreateTargetResult second_spawn_result;
second_spawn_result.result_code = SBOX_ERROR_GENERIC;
{
auto policy = broker->CreatePolicy();
EXPECT_EQ(SBOX_ALL_OK, policy->GetConfig()->SetTokenLevel(USER_INTERACTIVE,
USER_LOCKDOWN));
ResultCode result_code = broker->SpawnTargetAsync(
prog_name, arguments.c_str(), std::move(policy),
base::BindOnce(&FinishSpawnTargetAsync,
base::Unretained(&second_spawn_result),
base::Unretained(&run_loop),
base::Unretained(&launches_remaining_count)));
EXPECT_EQ(result_code, SBOX_ALL_OK);
}
run_loop.Run();
// Targets should be created on different threads.
EXPECT_NE(delegate->first_creation_thread_id_,
delegate->second_creation_thread_id_);
EXPECT_NE(delegate->first_trace_id_, delegate->second_trace_id_);
EXPECT_EQ(SBOX_ALL_OK, first_spawn_result.result_code);
EXPECT_EQ(SBOX_ALL_OK, second_spawn_result.result_code);
EXPECT_EQ(1u,
::ResumeThread(first_spawn_result.process_info.thread_handle()));
EXPECT_EQ(1u,
::ResumeThread(second_spawn_result.process_info.thread_handle()));
HANDLE handles[2] = {first_spawn_result.process_info.process_handle(),
second_spawn_result.process_info.process_handle()};
EXPECT_EQ(static_cast<DWORD>(WAIT_TIMEOUT),
::WaitForMultipleObjects(2, handles, /*bWaitAll=*/TRUE, 2000));
EXPECT_TRUE(
::TerminateProcess(first_spawn_result.process_info.process_handle(), 0));
EXPECT_TRUE(
::TerminateProcess(second_spawn_result.process_info.process_handle(), 0));
::WaitForMultipleObjects(2, handles, /*bWaitAll=*/TRUE, INFINITE);
}
} // namespace sandbox