blob: 20bcfc13191e5918bd6cdba2b14e2441db096d8a [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <objbase.h>
#include <windows.h>
#include <shlobj.h>
#include <wrl/client.h>
#include "base/base_switches.h"
#include "base/command_line.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/process/process.h"
#include "base/process/process_handle.h"
#include "base/strings/strcat.h"
#include "base/strings/sys_string_conversions.h"
#include "base/synchronization/waitable_event.h"
#include "base/test/gtest_util.h"
#include "base/test/multiprocess_test.h"
#include "base/types/expected.h"
#include "base/uuid.h"
#include "base/win/com_init_util.h"
#include "base/win/elevation_util.h"
#include "base/win/scoped_bstr.h"
#include "base/win/scoped_com_initializer.h"
#include "base/win/security_util.h"
#include "build/branding_buildflags.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/elevation_service/elevation_service_idl.h"
#include "chrome/elevation_service/elevator.h"
#include "chrome/windows_services/service_program/test_support/scoped_medium_integrity.h"
#include "chrome/windows_services/service_program/test_support/service_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/multiprocess_func_list.h"
namespace {
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
inline constexpr bool kExpectFullIsolation = true;
#else
inline constexpr bool kExpectFullIsolation = false;
#endif
// Relaunches the test process de-elevated; using the multi-process test
// facilitied to run the named function.
base::expected<base::Process, DWORD> RunInChildDeElevated(
std::string_view function_name) {
base::CommandLine command_line =
base::GetMultiProcessTestChildBaseCommandLine();
command_line.AppendSwitchASCII(switches::kTestChildProcess, function_name);
return base::win::RunDeElevated(command_line);
}
base::expected<Microsoft::WRL::ComPtr<IElevator2>, HRESULT> GetElevator() {
base::win::AssertComInitialized();
Microsoft::WRL::ComPtr<IElevator2> elevator;
HRESULT hr =
::CoCreateInstance(elevation_service::kTestElevatorClsid, nullptr,
CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&elevator));
if (FAILED(hr)) {
PLOG(ERROR) << "Failed to create IElevator2 instance.";
return base::unexpected(hr);
}
hr = ::CoSetProxyBlanket(
elevator.Get(), RPC_C_AUTHN_DEFAULT, RPC_C_AUTHZ_DEFAULT,
COLE_DEFAULT_PRINCIPAL, RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_DYNAMIC_CLOAKING);
if (FAILED(hr)) {
PLOG(ERROR) << "Failed to create security blanket.";
return base::unexpected(hr);
}
return elevator;
}
} // namespace
// A test harness that installs the elevated service so that tests can call it.
// The service's log output is redirected to the test process's stderr.
class ElevationServiceTest : public ::testing::Test {
protected:
static void SetUpTestSuite() {
if (!::IsUserAnAdmin()) {
GTEST_SKIP() << "Test requires admin rights";
}
service_environment_ = new ServiceEnvironment(
L"Test Elevation Service", FILE_PATH_LITERAL("elevation_service.exe"),
std::array<std::string_view, 2>{
elevation_service::switches::kAllowUntrustedPathForTesting,
elevation_service::switches::kElevatorClsIdForTestingSwitch},
elevation_service::kTestElevatorClsid, __uuidof(IElevator2));
ASSERT_TRUE(service_environment_->is_valid());
}
static void TearDownTestSuite() {
delete std::exchange(service_environment_, nullptr);
}
// Returns a handle to the service process if it is running, or an invalid
// process otherwise.
base::Process GetRunningService() {
return service_environment_->GetRunningService();
}
private:
static ServiceEnvironment* service_environment_;
};
// static
ServiceEnvironment* ElevationServiceTest::service_environment_ = nullptr;
TEST_F(ElevationServiceTest, AcceptInvitation) {
base::win::ScopedCOMInitializer com_initializer;
ASSERT_TRUE(com_initializer.Succeeded());
auto elevator = GetElevator();
ASSERT_TRUE(elevator.has_value());
auto res = elevator.value()->AcceptInvitation(L"hello");
EXPECT_EQ(res, E_NOTIMPL);
}
TEST_F(ElevationServiceTest, RunIsolatedChrome) {
auto child_or_error = RunInChildDeElevated("RunIsolatedChromeInChild");
if (!child_or_error.has_value()) {
GTEST_SKIP() << "Cannot de-elevate when UAC is disabled.";
}
int exit_code;
ASSERT_TRUE(child_or_error->WaitForExit(&exit_code));
ASSERT_EQ(exit_code, 0);
}
// This child process runs at Medium.
MULTIPROCESS_TEST_MAIN(RunIsolatedChromeInChild) {
base::win::ScopedCOMInitializer com_initializer;
EXPECT_TRUE(com_initializer.Succeeded());
const auto event_name = base::Uuid::GenerateRandomV4().AsLowercaseString();
auto elevator = GetElevator();
EXPECT_TRUE(elevator.has_value());
base::CommandLine cmd(base::PathService::CheckedGet(base::DIR_EXE)
.Append(L"elevation_service_test_child.exe"));
// Dangerous switch.
cmd.AppendSwitch(::switches::kDisableComponentUpdate);
// Safe switch. The directory used here doesn't matter since it's not going to
// be used, but allow the test to verify that switch values are passed through
// correctly.
cmd.AppendSwitchPath(::switches::kProfileDirectory,
base::PathService::CheckedGet(base::DIR_EXE));
cmd.AppendArg(event_name);
cmd.AppendArg("-invalid-switch");
cmd.AppendArg("another_arg");
base::WaitableEvent event(base::win::ScopedHandle(::CreateEventA(
/*lpEventAttributes=*/nullptr, /*bManualReset=*/FALSE,
/*bInitialState=*/FALSE, /*lpName=*/std::data(event_name))));
DWORD last_error = 0;
ULONG_PTR proc_handle;
base::win::ScopedBstr log;
auto res = elevator.value()->RunIsolatedChrome(
/*flags=*/0, std::data(cmd.GetCommandLineString()), log.Receive(),
&proc_handle, &last_error);
EXPECT_HRESULT_SUCCEEDED(res);
EXPECT_EQ(DWORD{ERROR_SUCCESS}, last_error);
base::Process process(reinterpret_cast<base::ProcessHandle>(proc_handle));
if constexpr (kExpectFullIsolation) {
HANDLE duplicate_proc_handle;
// Verify not possible to 'upgrade' the handle returned to something with
// more access.
EXPECT_FALSE(::DuplicateHandle(
/*hSourceProcessHandle=*/::GetCurrentProcess(),
/*hSourceHandle=*/process.Handle(),
/*hTargetProcessHandle=*/::GetCurrentProcess(),
/*lpTargetHandle=*/&duplicate_proc_handle,
/*dwDesiredAccess=*/PROCESS_QUERY_INFORMATION,
/*bInheritHandle=*/FALSE, /*dwOptions=*/0));
}
// If the process has terminated with a validation error then the test will
// timeout.
event.Wait();
EXPECT_TRUE(process.IsRunning());
// Verify that the child process has correctly been re-parented to the current
// process.
EXPECT_EQ(base::Process::Current().Pid(),
base::GetParentProcessId(process.Handle()));
const auto pid = process.Pid();
{
// Can always open the process read-only.
const auto read_only_process = base::Process::OpenWithAccess(
pid, PROCESS_TERMINATE | PROCESS_QUERY_LIMITED_INFORMATION);
EXPECT_TRUE(read_only_process.IsValid());
}
// Any attempt to open isolated process above PROCESS_TERMINATE, SYNCHRONIZE,
// PROCESS_QUERY_LIMITED_INFORMATION will fail.
{
const auto writeable_process = base::Process::Open(pid);
EXPECT_EQ(kExpectFullIsolation, !writeable_process.IsValid());
}
{
const auto extra_priv_process = base::Process::OpenWithExtraPrivileges(pid);
EXPECT_EQ(kExpectFullIsolation, !extra_priv_process.IsValid());
}
{
const auto maximum_allowed_process =
base::Process::OpenWithAccess(pid, MAXIMUM_ALLOWED);
EXPECT_TRUE(maximum_allowed_process.IsValid());
const auto access =
base::win::GetGrantedAccess(maximum_allowed_process.Handle());
EXPECT_TRUE(access.has_value());
const DWORD safe_masks[] = {PROCESS_TERMINATE,
PROCESS_QUERY_LIMITED_INFORMATION, SYNCHRONIZE};
const DWORD unsafe_masks[] = {
PROCESS_CREATE_PROCESS, PROCESS_CREATE_THREAD, PROCESS_DUP_HANDLE,
PROCESS_QUERY_INFORMATION, PROCESS_SET_INFORMATION, PROCESS_SET_QUOTA,
PROCESS_SUSPEND_RESUME, PROCESS_VM_OPERATION, PROCESS_VM_READ,
PROCESS_VM_WRITE};
for (const auto safe_mask : safe_masks) {
EXPECT_TRUE(*access & safe_mask);
}
for (const auto unsafe_mask : unsafe_masks) {
EXPECT_EQ(kExpectFullIsolation, !(*access & unsafe_mask));
}
}
EXPECT_TRUE(process.Terminate(0, /*wait=*/true));
return ::testing::Test::HasFailure() ? 1 : 0;
}
TEST_F(ElevationServiceTest, Failure) {
base::win::ScopedCOMInitializer com_initializer;
EXPECT_TRUE(com_initializer.Succeeded());
auto elevator = GetElevator();
EXPECT_TRUE(elevator.has_value());
DWORD last_error = 0;
ULONG_PTR proc_handle;
base::win::ScopedBstr log;
auto res = elevator.value()->RunIsolatedChrome(
/*flags=*/0, L"invalid_process_name.exe", log.Receive(), &proc_handle,
&last_error);
EXPECT_HRESULT_FAILED(res);
EXPECT_EQ(DWORD{ERROR_FILE_NOT_FOUND}, last_error);
}