blob: b499b8eb449d19089526164e9b52161199138009 [file]
// 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 <objbase.h>
#include <windows.h>
#include <shlobj.h>
#include <wrl/client.h>
#include <string_view>
#include <utility>
#include "base/base_switches.h"
#include "base/command_line.h"
#include "base/containers/span.h"
#include "base/process/process.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/multiprocess_test.h"
#include "base/test/task_environment.h"
#include "base/test/test_timeouts.h"
#include "base/types/expected.h"
#include "base/win/elevation_util.h"
#include "base/win/scoped_com_initializer.h"
#include "base/win/win_util.h"
#include "chrome/windows_services/elevated_tracing_service/system_tracing_session.h"
#include "chrome/windows_services/elevated_tracing_service/tracing_service_idl.h"
#include "chrome/windows_services/elevated_tracing_service/with_child_test.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 "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/platform/named_platform_channel.h"
#include "mojo/public/cpp/system/invitation.h"
#include "mojo/public/cpp/system/message_pipe.h"
#include "mojo/public/cpp/system/simple_watcher.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/multiprocess_func_list.h"
#include "third_party/abseil-cpp/absl/cleanup/cleanup.h"
namespace {
void TestGetServiceProcessHandle() {
Microsoft::WRL::ComPtr<IUnknown> unknown;
ASSERT_HRESULT_SUCCEEDED(::CoCreateInstance(
elevated_tracing_service::kTestSystemTracingSessionClsid, nullptr,
CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&unknown)));
Microsoft::WRL::ComPtr<ISystemTraceSession> trace_session;
ASSERT_HRESULT_SUCCEEDED(unknown.As(&trace_session));
unknown.Reset();
ASSERT_HRESULT_SUCCEEDED(::CoSetProxyBlanket(
trace_session.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));
mojo::NamedPlatformChannel channel(mojo::NamedPlatformChannel::Options{});
mojo::OutgoingInvitation invitation;
invitation.set_extra_flags(MOJO_SEND_INVITATION_FLAG_ELEVATED);
mojo::ScopedMessagePipeHandle pipe = invitation.AttachMessagePipe(/*name=*/0);
DWORD pid = base::kNullProcessId;
ASSERT_HRESULT_SUCCEEDED(
trace_session->AcceptInvitation(channel.GetServerName().c_str(), &pid));
ASSERT_NE(pid, base::kNullProcessId);
mojo::OutgoingInvitation::Send(std::move(invitation),
/*target_process=*/base::kNullProcessHandle,
channel.TakeServerEndpoint());
// Wait for the service to connect to the pipe.
mojo::SimpleWatcher pipe_watcher(FROM_HERE,
mojo::SimpleWatcher::ArmingPolicy::MANUAL);
base::RunLoop run_loop;
ASSERT_EQ(
pipe_watcher.Watch(
pipe.get(),
MOJO_HANDLE_SIGNAL_WRITABLE | MOJO_HANDLE_SIGNAL_PEER_CLOSED,
MOJO_TRIGGER_CONDITION_SIGNALS_SATISFIED,
base::BindLambdaForTesting(
[quit_closure = run_loop.QuitClosure()](
MojoResult result, const mojo::HandleSignalsState& state) {
EXPECT_EQ(result, MOJO_RESULT_OK);
EXPECT_EQ(state.satisfied_signals, MOJO_HANDLE_SIGNAL_WRITABLE);
quit_closure.Run();
})),
MOJO_RESULT_OK);
pipe_watcher.ArmOrNotify();
run_loop.Run();
// Releasing `trace_session` will trigger termination in the service.
}
// Tests that AcceptInvitation refuses a second invitation from a client.
void TestOnlyOneInvitation() {
// Create a session and activate it by sending an invitation.
Microsoft::WRL::ComPtr<IUnknown> unknown;
ASSERT_HRESULT_SUCCEEDED(::CoCreateInstance(
elevated_tracing_service::kTestSystemTracingSessionClsid, nullptr,
CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&unknown)));
Microsoft::WRL::ComPtr<ISystemTraceSession> trace_session;
ASSERT_HRESULT_SUCCEEDED(unknown.As(&trace_session));
unknown.Reset();
ASSERT_HRESULT_SUCCEEDED(::CoSetProxyBlanket(
trace_session.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));
mojo::ScopedMessagePipeHandle pipe;
{
mojo::NamedPlatformChannel channel(mojo::NamedPlatformChannel::Options{});
mojo::OutgoingInvitation invitation;
invitation.set_extra_flags(MOJO_SEND_INVITATION_FLAG_ELEVATED);
pipe = invitation.AttachMessagePipe(/*name=*/0);
DWORD pid = base::kNullProcessId;
ASSERT_HRESULT_SUCCEEDED(
trace_session->AcceptInvitation(channel.GetServerName().c_str(), &pid));
ASSERT_NE(pid, base::kNullProcessId);
mojo::OutgoingInvitation::Send(std::move(invitation),
/*target_process=*/base::kNullProcessHandle,
channel.TakeServerEndpoint());
}
{
mojo::NamedPlatformChannel channel(mojo::NamedPlatformChannel::Options{});
mojo::OutgoingInvitation invitation;
invitation.set_extra_flags(MOJO_SEND_INVITATION_FLAG_ELEVATED);
mojo::ScopedMessagePipeHandle pipe2 =
invitation.AttachMessagePipe(/*name=*/0);
DWORD pid = base::kNullProcessId;
ASSERT_EQ(
trace_session->AcceptInvitation(channel.GetServerName().c_str(), &pid),
kErrorSessionAlreadyActive);
}
}
// Tests that a second session is refused while a first is active.
void TestOnlyOneSession() {
// Create a session and activate it by sending an invitation.
Microsoft::WRL::ComPtr<IUnknown> unknown;
ASSERT_HRESULT_SUCCEEDED(::CoCreateInstance(
elevated_tracing_service::kTestSystemTracingSessionClsid, nullptr,
CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&unknown)));
Microsoft::WRL::ComPtr<ISystemTraceSession> trace_session;
ASSERT_HRESULT_SUCCEEDED(unknown.As(&trace_session));
unknown.Reset();
ASSERT_HRESULT_SUCCEEDED(::CoSetProxyBlanket(
trace_session.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));
mojo::ScopedMessagePipeHandle pipe;
{
mojo::NamedPlatformChannel channel(mojo::NamedPlatformChannel::Options{});
mojo::OutgoingInvitation invitation;
invitation.set_extra_flags(MOJO_SEND_INVITATION_FLAG_ELEVATED);
pipe = invitation.AttachMessagePipe(/*name=*/0);
DWORD pid = base::kNullProcessId;
ASSERT_HRESULT_SUCCEEDED(
trace_session->AcceptInvitation(channel.GetServerName().c_str(), &pid));
ASSERT_NE(pid, base::kNullProcessId);
mojo::OutgoingInvitation::Send(std::move(invitation),
/*target_process=*/base::kNullProcessHandle,
channel.TakeServerEndpoint());
}
// Now a second client is refused.
ASSERT_HRESULT_SUCCEEDED(::CoCreateInstance(
elevated_tracing_service::kTestSystemTracingSessionClsid, nullptr,
CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&unknown)));
Microsoft::WRL::ComPtr<ISystemTraceSession> trace_session2;
ASSERT_HRESULT_SUCCEEDED(unknown.As(&trace_session2));
unknown.Reset();
ASSERT_HRESULT_SUCCEEDED(::CoSetProxyBlanket(
trace_session2.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));
mojo::NamedPlatformChannel channel(mojo::NamedPlatformChannel::Options{});
mojo::OutgoingInvitation invitation;
invitation.set_extra_flags(MOJO_SEND_INVITATION_FLAG_ELEVATED);
mojo::ScopedMessagePipeHandle pipe2 =
invitation.AttachMessagePipe(/*name=*/0);
DWORD pid = base::kNullProcessId;
ASSERT_EQ(
trace_session2->AcceptInvitation(channel.GetServerName().c_str(), &pid),
kErrorSessionInProgress);
}
MULTIPROCESS_TEST_MAIN(GetServiceProcessHandleInChild) {
// base::debug::WaitForDebugger(30, /*silent=*/false);
base::win::ScopedCOMInitializer com_initializer;
if (!com_initializer.Succeeded()) {
return 1;
}
base::test::SingleThreadTaskEnvironment task_environment;
TestGetServiceProcessHandle();
return ::testing::Test::HasFailure() ? 1 : 0;
}
MULTIPROCESS_TEST_MAIN(OnlyOneInvitationInChild) {
// base::debug::WaitForDebugger(30, /*silent=*/false);
base::win::ScopedCOMInitializer com_initializer;
if (!com_initializer.Succeeded()) {
return 1;
}
TestOnlyOneInvitation();
return ::testing::Test::HasFailure() ? 1 : 0;
}
MULTIPROCESS_TEST_MAIN(OnlyOneSessionInChild) {
// base::debug::WaitForDebugger(30, /*silent=*/false);
base::win::ScopedCOMInitializer com_initializer;
if (!com_initializer.Succeeded()) {
return 1;
}
TestOnlyOneSession();
return ::testing::Test::HasFailure() ? 1 : 0;
}
// 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);
}
} // namespace
// A test harness that installs the tracing service so that tests can call into
// it. The service's log output is redirected to the test process's stderr.
class TracingServiceTest : public elevated_tracing_service::WithChildTest {
protected:
static void SetUpTestSuite() {
if (!::IsUserAnAdmin()) {
GTEST_SKIP() << "Test requires admin rights";
}
service_environment_ = new ServiceEnvironment(
L"Test Tracing Service",
FILE_PATH_LITERAL("elevated_tracing_service.exe"),
base::span_from_ref(
std::string_view(elevated_tracing_service::switches::
kSystemTracingClsIdForTestingSwitch)),
elevated_tracing_service::kTestSystemTracingSessionClsid,
__uuidof(ISystemTraceSession));
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* TracingServiceTest::service_environment_ = nullptr;
// Tests calling into the service from the main test process itself after
// dropping to medium integrity level.
TEST_F(TracingServiceTest, GetServiceProcessHandle) {
base::win::ScopedCOMInitializer com_initializer;
ASSERT_TRUE(com_initializer.Succeeded());
ScopedMediumIntegrity medium_integrity;
ASSERT_TRUE(medium_integrity.Succeeded());
TestGetServiceProcessHandle();
}
// Tests calling into the service from the main test process itself after
// dropping to medium integrity level.
TEST_F(TracingServiceTest, OnlyOneInvitation) {
base::win::ScopedCOMInitializer com_initializer;
ASSERT_TRUE(com_initializer.Succeeded());
ScopedMediumIntegrity medium_integrity;
ASSERT_TRUE(medium_integrity.Succeeded());
TestOnlyOneInvitation();
}
// Tests calling into the service from the main test process itself after
// dropping to medium integrity level.
TEST_F(TracingServiceTest, OnlyOneSession) {
base::win::ScopedCOMInitializer com_initializer;
ASSERT_TRUE(com_initializer.Succeeded());
ScopedMediumIntegrity medium_integrity;
ASSERT_TRUE(medium_integrity.Succeeded());
TestOnlyOneSession();
}
// Tests calling into the service from a de-elevated child process. This test is
// skipped if UAC is disabled.
TEST_F(TracingServiceTest, GetServiceProcessHandleInChild) {
// Pro tip: Add --show-error-dialogs to the test's command line and put
// base::debug::WaitForDebugger(30, /*silent=*/false); in
// `GetServiceProcessHandleInChild()` to more easily debug in the child
// process.
auto child_or_error = RunInChildDeElevated("GetServiceProcessHandleInChild");
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);
}
// Tests calling into the service from a de-elevated child process. This test is
// skipped if UAC is disabled.
TEST_F(TracingServiceTest, OnlyOneInvitationInChild) {
// Pro tip: Add --show-error-dialogs to the test's command line and put
// base::debug::WaitForDebugger(30, /*silent=*/false); in
// `GetServiceProcessHandleInChild()` to more easily debug in the child
// process.
auto child_or_error = RunInChildDeElevated("OnlyOneInvitationInChild");
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);
}
// Tests calling into the service from a de-elevated child process. This test is
// skipped if UAC is disabled.
TEST_F(TracingServiceTest, OnlyOneSessionInChild) {
// Pro tip: Add --show-error-dialogs to the test's command line and put
// base::debug::WaitForDebugger(30, /*silent=*/false); in
// `GetServiceProcessHandleInChild()` to more easily debug in the child
// process.
auto child_or_error = RunInChildDeElevated("OnlyOneSessionInChild");
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);
}
MULTIPROCESS_TEST_MAIN(ConnectToServiceAndTerminateInChild) {
base::win::ScopedCOMInitializer com_initializer;
if (!com_initializer.Succeeded()) {
return 1;
}
// Create a session and activate it by sending an invitation.
Microsoft::WRL::ComPtr<IUnknown> unknown;
CHECK(SUCCEEDED(::CoCreateInstance(
elevated_tracing_service::kTestSystemTracingSessionClsid, nullptr,
CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&unknown))));
Microsoft::WRL::ComPtr<ISystemTraceSession> trace_session;
CHECK(SUCCEEDED(unknown.As(&trace_session)));
unknown.Reset();
CHECK(SUCCEEDED(::CoSetProxyBlanket(
trace_session.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)));
mojo::ScopedMessagePipeHandle pipe;
{
mojo::NamedPlatformChannel channel(mojo::NamedPlatformChannel::Options{});
mojo::OutgoingInvitation invitation;
invitation.set_extra_flags(MOJO_SEND_INVITATION_FLAG_ELEVATED);
pipe = invitation.AttachMessagePipe(/*name=*/0);
DWORD pid = base::kNullProcessId;
CHECK(SUCCEEDED(trace_session->AcceptInvitation(
channel.GetServerName().c_str(), &pid)));
CHECK_NE(pid, base::kNullProcessId);
mojo::OutgoingInvitation::Send(std::move(invitation),
/*target_process=*/base::kNullProcessHandle,
channel.TakeServerEndpoint());
}
// A session is now active. Tell the test that the child has started. The test
// will monitor the service and then release the child.
elevated_tracing_service::WithChildTest::SignalChildStart();
// Wait for the test to release the child for termination.
elevated_tracing_service::WithChildTest::WaitForChildTermination();
base::Process::TerminateCurrentProcessImmediately(1);
}
// Tests that the service terminates promptly when the child process terminates
// while holding a session.
TEST_F(TracingServiceTest, ServiceTerminatesOnChildTermination) {
auto child =
SpawnChildWithEventHandles("ConnectToServiceAndTerminateInChild");
ASSERT_TRUE(child.IsValid());
absl::Cleanup child_stopper = [this] { SignalChildTermination(); };
// Wait for the child to attach to the service.
WaitForChildStart();
// Get a handle to the service process.
auto service_process = GetRunningService();
ASSERT_TRUE(service_process.IsValid());
// Tell the child to terminate.
std::move(child_stopper).Invoke();
// Wait for the service to terminate.
ASSERT_EQ(::WaitForSingleObject(
service_process.Handle(),
TestTimeouts::action_max_timeout().InMilliseconds()),
WAIT_OBJECT_0);
}