blob: fb4d7a94929e598326c979689e0c75ab102bf31d [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 "chrome/windows_services/service_program/service.h"
#include <sddl.h>
#include <wrl/module.h>
#include <string_view>
#include <type_traits>
#include <utility>
#include "base/check_deref.h"
#include "base/command_line.h"
#include "base/containers/heap_array.h"
#include "base/feature_list.h"
#include "base/logging.h"
#include "base/ranges/algorithm.h"
#include "base/run_loop.h"
#include "base/task/single_thread_task_executor.h"
#include "base/win/atl.h"
#include "base/win/scoped_com_initializer.h"
#include "chrome/windows_services/service_program/process_wrl_module.h"
#include "chrome/windows_services/service_program/service_delegate.h"
#include "third_party/abseil-cpp/absl/cleanup/cleanup.h"
namespace {
Service* g_instance = nullptr;
// Command line switch "--console" runs the service interactively for debugging
// purposes.
constexpr std::string_view kConsoleSwitchName = "console";
// NOTE: this value is ignored because service type is SERVICE_WIN32_OWN_PROCESS
// please see
// https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_table_entrya#members
constexpr wchar_t kWindowsServiceName[] = L"";
} // namespace
Service::Service(ServiceDelegate& delegate) : delegate_(delegate) {
CHECK_EQ(std::exchange(g_instance, this), nullptr);
}
Service::~Service() {
CHECK_EQ(std::exchange(g_instance, nullptr), this);
}
bool Service::InitWithCommandLine(const base::CommandLine* command_line) {
const base::CommandLine::StringVector args = command_line->GetArgs();
if (!args.empty()) {
LOG(ERROR) << "No positional parameters expected.";
return false;
}
// Run interactively if needed.
if (command_line->HasSwitch(kConsoleSwitchName)) {
run_routine_ = &Service::RunInteractive;
}
// Register an empty FeatureList so that queries on it do not fail.
base::FeatureList::SetInstance(std::make_unique<base::FeatureList>());
return true;
}
// Start() is the entry point called by ServiceMain().
int Service::Start() {
return (this->*run_routine_)();
}
// When _Service gets called, it initializes COM, and then calls Run().
// Run() initializes security, then calls RegisterClassObjects().
HRESULT Service::RegisterClassObjects() {
auto& module = Microsoft::WRL::Module<Microsoft::WRL::OutOfProc>::GetModule();
// We hand-register the class factories to support unique CLSIDs for each
// Chrome channel, which is determined at runtime.
auto factories_or_error = delegate_->CreateClassFactories();
if (!factories_or_error.has_value()) {
LOG(ERROR) << "Factory creation failed; hr: " << factories_or_error.error();
return factories_or_error.error();
}
base::HeapArray<FactoryAndClsid>& factories = factories_or_error.value();
// An array to hold unowned pointers to the IClassFactory interfaces. These
// must not be Released.
auto weak_factories =
base::HeapArray<IClassFactory*>::Uninit(factories.size());
// An array to hold the CLSIDs for each factory.
auto class_ids = base::HeapArray<IID>::Uninit(factories.size());
// An array to hold the registration cookie for each factory.
cookies_ = base::HeapArray<DWORD>::Uninit(factories.size());
size_t i = 0;
for (auto& factory_and_clsid : factories) {
weak_factories[i] = factory_and_clsid.factory.Get();
class_ids[i] = factory_and_clsid.clsid;
++i;
}
HRESULT hr = module.RegisterCOMObject(
nullptr, class_ids.data(), weak_factories.data(), cookies_.data(),
static_cast<unsigned int>(factories.size()));
if (FAILED(hr)) {
LOG(ERROR) << "RegisterCOMObject failed; hr: " << hr;
return hr;
}
// Register a callback with the process's WRL module to signal the service to
// exit when the last reference is released.
SetModuleReleasedCallback(
base::BindOnce(&Service::SignalExit, base::Unretained(this)));
return hr;
}
void Service::UnregisterClassObjects() {
// Clear the callback registered with the process's WRL module.
SetModuleReleasedCallback({});
auto& module = Microsoft::WRL::Module<Microsoft::WRL::OutOfProc>::GetModule();
const HRESULT hr =
module.UnregisterCOMObject(nullptr, cookies_.data(), cookies_.size());
if (FAILED(hr)) {
LOG(ERROR) << "UnregisterCOMObject failed; hr: " << hr;
}
}
Service& Service::GetInstance() {
return CHECK_DEREF(g_instance);
}
int Service::RunAsService() {
static constexpr SERVICE_TABLE_ENTRY dispatch_table[] = {
{const_cast<LPTSTR>(kWindowsServiceName), &Service::ServiceMainEntry},
{nullptr, nullptr}};
if (!::StartServiceCtrlDispatcher(dispatch_table)) {
service_status_.dwWin32ExitCode = ::GetLastError();
PLOG(ERROR) << "Failed to connect to the service control manager";
}
return service_status_.dwWin32ExitCode;
}
void Service::ServiceMainImpl() {
service_status_handle_ = ::RegisterServiceCtrlHandler(
kWindowsServiceName, &Service::ServiceControlHandler);
if (service_status_handle_ == nullptr) {
PLOG(ERROR) << "RegisterServiceCtrlHandler failed";
return;
}
SetServiceStatus(SERVICE_RUNNING);
service_status_.dwWin32ExitCode = ERROR_SUCCESS;
service_status_.dwCheckPoint = 0;
service_status_.dwWaitHint = 0;
// Initialize COM for the current thread.
base::win::ScopedCOMInitializer com_initializer(
base::win::ScopedCOMInitializer::kMTA);
if (!com_initializer.Succeeded()) {
PLOG(ERROR) << "Failed to initialize COM";
SetServiceStatus(SERVICE_STOPPED);
return;
}
// When the Run function returns, the service has stopped.
const HRESULT hr = Run();
if (FAILED(hr)) {
service_status_.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR;
service_status_.dwServiceSpecificExitCode = hr;
}
SetServiceStatus(SERVICE_STOPPED);
}
int Service::RunInteractive() {
return Run();
}
// static
void Service::ServiceControlHandler(DWORD control) {
Service& self = Service::GetInstance();
switch (control) {
case SERVICE_CONTROL_STOP:
self.SetServiceStatus(SERVICE_STOP_PENDING);
self.SignalExit();
break;
default:
break;
}
}
// static
void WINAPI Service::ServiceMainEntry(DWORD argc, wchar_t* argv[]) {
GetInstance().ServiceMainImpl();
}
void Service::SetServiceStatus(DWORD state) {
::InterlockedExchange(&service_status_.dwCurrentState, state);
::SetServiceStatus(service_status_handle_, &service_status_);
}
HRESULT Service::Run() {
// Allow this thread to run tasks. Consider using a UI message pump if there
// is a need to respond to window messages (e.g., WM_ENDSESSION).
base::SingleThreadTaskExecutor task_executor;
base::RunLoop run_loop;
quit_closure_ = run_loop.QuitWhenIdleClosure();
delegate_->PreRun();
absl::Cleanup post_run = [&delegate = *delegate_] { delegate.PostRun(); };
HRESULT hr = InitializeComSecurity();
if (FAILED(hr)) {
return hr;
}
hr = RegisterClassObjects();
if (SUCCEEDED(hr)) {
run_loop.Run();
UnregisterClassObjects();
}
return hr;
}
// static
HRESULT Service::InitializeComSecurity() {
CDacl dacl;
constexpr auto com_rights_execute_local =
COM_RIGHTS_EXECUTE | COM_RIGHTS_EXECUTE_LOCAL;
if (!dacl.AddAllowedAce(Sids::System(), com_rights_execute_local) ||
!dacl.AddAllowedAce(Sids::Admins(), com_rights_execute_local) ||
!dacl.AddAllowedAce(Sids::Interactive(), com_rights_execute_local)) {
return E_ACCESSDENIED;
}
CSecurityDesc sd;
sd.SetDacl(dacl);
sd.MakeAbsolute();
sd.SetOwner(Sids::Admins());
sd.SetGroup(Sids::Admins());
// These are the flags being set:
// EOAC_DYNAMIC_CLOAKING: DCOM uses the thread token (if present) when
// determining the client's identity. Useful when impersonating another
// user.
// EOAC_SECURE_REFS: Authenticates distributed reference count calls to
// prevent malicious users from releasing objects that are still being used.
// EOAC_DISABLE_AAA: Causes any activation where a server process would be
// launched under the caller's identity (activate-as-activator) to fail with
// E_ACCESSDENIED.
// EOAC_NO_CUSTOM_MARSHAL: reduces the chances of executing arbitrary DLLs
// because it allows the marshaling of only CLSIDs that are implemented in
// Ole32.dll, ComAdmin.dll, ComSvcs.dll, or Es.dll, or that implement the
// CATID_MARSHALER category ID.
// RPC_C_AUTHN_LEVEL_PKT_PRIVACY: prevents replay attacks, verifies that none
// of the data transferred between the client and server has been modified,
// ensures that the data transferred can only be seen unencrypted by the
// client and the server.
return ::CoInitializeSecurity(
const_cast<SECURITY_DESCRIPTOR*>(sd.GetPSECURITY_DESCRIPTOR()), -1,
nullptr, nullptr, RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_IMP_LEVEL_IDENTIFY,
nullptr,
EOAC_DYNAMIC_CLOAKING | EOAC_DISABLE_AAA | EOAC_SECURE_REFS |
EOAC_NO_CUSTOM_MARSHAL,
nullptr);
}
void Service::SignalExit() {
if (quit_closure_) {
quit_closure_.Run();
}
}