| // 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 <atomic> |
| #include <string_view> |
| #include <type_traits> |
| #include <utility> |
| |
| #include "base/auto_reset.h" |
| #include "base/check.h" |
| #include "base/check_deref.h" |
| #include "base/command_line.h" |
| #include "base/containers/heap_array.h" |
| #include "base/debug/crash_logging.h" |
| #include "base/logging.h" |
| #include "base/ranges/algorithm.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/synchronization/waitable_event.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" |
| |
| namespace { |
| |
| std::atomic<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""; |
| |
| void WINAPI SpuriousServiceControlHandler(DWORD) {} |
| |
| // A service main function that logs the state of its service and then stops it. |
| void HandleSpuriousServiceMain(DWORD argc, const wchar_t* const* argv) { |
| using ServiceHandle = |
| std::unique_ptr<SC_HANDLE__, decltype(&::CloseServiceHandle)>; |
| |
| // The first argument is the name of the service. |
| const wchar_t* service_name = argc && *argv ? *argv : kWindowsServiceName; |
| |
| if (auto scm_raw = ::OpenSCManager( |
| /*lpMachineName=*/nullptr, |
| /*lpDatabaseName=*/SERVICES_ACTIVE_DATABASE, SC_MANAGER_CONNECT)) { |
| ServiceHandle scm(scm_raw, &::CloseServiceHandle); |
| if (auto svc_raw = |
| ::OpenService(scm.get(), service_name, SERVICE_QUERY_STATUS)) { |
| ServiceHandle svc(svc_raw, &::CloseServiceHandle); |
| SERVICE_STATUS_PROCESS status = {}; |
| DWORD bytes_needed = 0; |
| if (::QueryServiceStatusEx(svc.get(), SC_STATUS_PROCESS_INFO, |
| reinterpret_cast<unsigned char*>(&status), |
| sizeof(status), &bytes_needed)) { |
| LOG(ERROR) << "Spurious start for " << service_name |
| << ". Current state: " << status.dwCurrentState |
| << "; pid: " << status.dwProcessId; |
| } else { |
| PLOG(ERROR) << "Failed to query " << service_name; |
| } |
| } else { |
| PLOG(ERROR) << "Failed to open " << service_name; |
| } |
| } else { |
| PLOG(ERROR) << "Failed to connect to SCM"; |
| } |
| |
| if (auto service_status_handle = ::RegisterServiceCtrlHandler( |
| service_name, &SpuriousServiceControlHandler)) { |
| SERVICE_STATUS service_status{.dwServiceType = SERVICE_WIN32_OWN_PROCESS, |
| .dwCurrentState = SERVICE_STOPPED}; |
| ::SetServiceStatus(service_status_handle, &service_status); |
| } else { |
| PCHECK(false); |
| } |
| } |
| |
| } // namespace |
| |
| Service::Service(ServiceDelegate& delegate) : delegate_(delegate) { |
| Service* expected = nullptr; |
| CHECK(g_instance.compare_exchange_strong(expected, this, |
| std::memory_order_relaxed)); |
| } |
| |
| Service::~Service() { |
| Service* expected = this; |
| CHECK(g_instance.compare_exchange_strong(expected, nullptr, |
| std::memory_order_relaxed)); |
| } |
| |
| 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; |
| exit_routine_ = &Service::StopInteractive; |
| } |
| |
| return true; |
| } |
| |
| // Start() is the entry point called by `ServiceProgramMain()`. |
| int Service::Start() { |
| delegate_implements_run_ = delegate_->PreRun(); |
| |
| const auto result = (this->*run_routine_)(); |
| |
| delegate_->PostRun(); |
| |
| return result; |
| } |
| |
| // When _Service gets called, it initializes COM, and then calls Run(). |
| // Run() initializes security, then calls RegisterClassObjects(). |
| // static |
| HRESULT Service::RegisterClassObjects(ServiceDelegate& delegate, |
| base::OnceClosure on_module_released, |
| base::HeapArray<DWORD>& cookies) { |
| 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. |
| auto new_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; |
| } |
| |
| // Register a callback with the process's WRL module to signal the service to |
| // exit when the last reference is released. |
| SetModuleReleasedCallback(std::move(on_module_released)); |
| |
| HRESULT hr = module.RegisterCOMObject( |
| nullptr, class_ids.data(), weak_factories.data(), new_cookies.data(), |
| static_cast<unsigned int>(factories.size())); |
| if (FAILED(hr)) { |
| LOG(ERROR) << "RegisterCOMObject failed; hr: " << hr; |
| SetModuleReleasedCallback({}); |
| return hr; |
| } |
| |
| // Return the cookies on success. |
| cookies = std::move(new_cookies); |
| return hr; |
| } |
| |
| // static |
| void Service::UnregisterClassObjects(base::HeapArray<DWORD>& cookies) { |
| if (!cookies.empty()) { |
| // Clear the callback registered with the process's WRL module. |
| SetModuleReleasedCallback({}); |
| |
| const HRESULT hr = |
| Microsoft::WRL::Module<Microsoft::WRL::OutOfProc>::GetModule() |
| .UnregisterCOMObject(nullptr, cookies.data(), cookies.size()); |
| if (FAILED(hr)) { |
| LOG(ERROR) << "UnregisterCOMObject failed; hr: 0x" << std::hex << hr; |
| } |
| cookies = base::HeapArray<DWORD>(); |
| } |
| } |
| |
| Service& Service::GetInstance() { |
| return CHECK_DEREF(g_instance.load(std::memory_order_relaxed)); |
| } |
| |
| int Service::RunAsService() { |
| static constexpr SERVICE_TABLE_ENTRY dispatch_table[] = { |
| {const_cast<LPTSTR>(kWindowsServiceName), &Service::ServiceMainEntry}, |
| {nullptr, nullptr}}; |
| |
| // This thread becomes the service control dispatcher thread for the process |
| // upon the call to `::StartServiceCtrlDispatcher()`. |
| |
| // Upon this call, processing will continue on the service main thread in |
| // `ServiceMainEntry()`. Processing will resume here when the service is |
| // stopped. This same thread will process calls to `ServiceControlHandler()`. |
| if (!::StartServiceCtrlDispatcher(dispatch_table)) { |
| const auto error = ::GetLastError(); |
| |
| // MSDN States: "If StartServiceCtrlDispatcher succeeds, it connects the |
| // calling thread to the service control manager and does not return until |
| // all running services in the process have entered the SERVICE_STOPPED |
| // state." Despite that, https://crbug.com/380943791 is a case where a |
| // service main thread is executing `ServiceMainEntry()` after the service |
| // control dispatcher returns. Put the error code from a failure to start |
| // the dispatcher into a crash key so that it is included in such crashes. |
| static auto* const crash_key = base::debug::AllocateCrashKeyString( |
| "Service-DispatcherError", base::debug::CrashKeySize::Size32); |
| base::debug::SetCrashKeyString(crash_key, base::NumberToString(error)); |
| |
| PLOG(ERROR) << "Failed to connect to the service control manager"; |
| return error; |
| } |
| |
| // Take the lock both for the sake of accessing service_status_ and to wait |
| // for the thread in `OnModuleReleased()` to complete the call. |
| base::AutoLock lock(lock_); |
| return service_status_.dwWin32ExitCode; |
| } |
| |
| void Service::StopService() { |
| // This will cause the service control dispatcher to exit on the main thread. |
| // Processing will continue in `RunAsService()`. After this call, the SCM will |
| // launch a new process to handle inbound requests even if this process takes |
| // time to clean up and terminate. |
| SetServiceStatus(SERVICE_STOPPED); |
| } |
| |
| void Service::ServiceMainImpl(const base::CommandLine& command_line) { |
| base::AutoLock lock(lock_); |
| |
| service_status_handle_ = ::RegisterServiceCtrlHandler( |
| kWindowsServiceName, &Service::ServiceControlHandler); |
| if (service_status_handle_ == nullptr) { |
| PLOG(ERROR) << "RegisterServiceCtrlHandler failed"; |
| return; |
| } |
| |
| // Initialize this thread into the process's MTA. |
| base::win::ScopedCOMInitializer com_initializer( |
| base::win::ScopedCOMInitializer::kMTA); |
| HRESULT hr = com_initializer.hr(); |
| if (SUCCEEDED(hr)) { |
| // Tell the service control manager that the service is now running, and |
| // will accept a stop request. |
| SetServiceStatus(SERVICE_RUNNING); |
| // Start the service. |
| hr = Run(command_line); |
| } else { |
| PLOG(ERROR) << "Failed to initialize COM; hr = 0x" << std::hex << hr; |
| } |
| |
| if (FAILED(hr)) { |
| // Shut down immediately in case of error. |
| service_status_.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR; |
| service_status_.dwServiceSpecificExitCode = hr; |
| SetServiceStatus(SERVICE_STOPPED); |
| } else if (delegate_implements_run_) { |
| // Shut down immediately if the service provided its own `Run()`. |
| SetServiceStatus(SERVICE_STOPPED); |
| } |
| } |
| |
| int Service::RunInteractive() { |
| base::WaitableEvent exit_event; |
| base::AutoReset<raw_ptr<base::WaitableEvent>> reset_stop_event( |
| &interactive_stop_event_, &exit_event); |
| |
| base::AutoLock lock(lock_); |
| if (HRESULT hr = Run(*base::CommandLine::ForCurrentProcess()); |
| FAILED(hr) || delegate_implements_run_) { |
| // Return immediately on error or if the service provided its own `Run()`. |
| return hr; |
| } |
| |
| { |
| base::AutoUnlock unlock(lock_); |
| // Wait for StopInteractive to be called. |
| exit_event.Wait(); |
| } |
| |
| return S_OK; |
| } |
| |
| void Service::StopInteractive() { |
| CHECK_DEREF(interactive_stop_event_.get()).Signal(); |
| } |
| |
| // static |
| void Service::ServiceControlHandler(DWORD control) { |
| if (control == SERVICE_CONTROL_STOP) { |
| GetInstance().OnStopRequested(); |
| } |
| } |
| |
| // static |
| void WINAPI Service::ServiceMainEntry(DWORD argc, wchar_t* argv[]) { |
| if (Service* instance = g_instance.load(std::memory_order_relaxed)) { |
| instance->ServiceMainImpl(base::CommandLine(argc, argv)); |
| } else { |
| // There are cases where this function is called when there is no active |
| // Service instance; see https://crbug.com/380943791. |
| HandleSpuriousServiceMain(argc, argv); |
| } |
| } |
| |
| void Service::SetServiceStatus(DWORD state) { |
| if (service_status_handle_) { |
| service_status_.dwCurrentState = state; |
| ::SetServiceStatus(service_status_handle_, &service_status_); |
| if (state == SERVICE_STOPPED) { |
| // The handle becomes invalid once STOPPED has been sent. |
| service_status_handle_ = nullptr; |
| } |
| } |
| } |
| |
| HRESULT Service::Run(const base::CommandLine& command_line) { |
| if (HRESULT hr = InitializeComSecurity(); FAILED(hr)) { |
| return hr; |
| } |
| |
| // If `PreRun` returned `true`, the delegate's `Run` method is expected to do |
| // all the logic of registering/unregistering classes and running the COM |
| // server. |
| if (delegate_implements_run_) { |
| base::AutoUnlock unlock(lock_); |
| return delegate_->Run(command_line); |
| } |
| |
| // Registering class objects is sufficient for the service to be running. |
| // Unretained is safe here because the callback is cleared in |
| // `UnregisterClassObjects()`, which is run under lock during shutdown. |
| return RegisterClassObjects( |
| *delegate_, |
| base::BindOnce(&Service::OnModuleReleased, base::Unretained(this)), |
| cookies_); |
| } |
| |
| // 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::OnModuleReleased() { |
| base::AutoLock lock(lock_); |
| |
| // It is tempting to send a STOP_PENDING message to the service control |
| // manager to tell it that shutdown has started. Unfortunately, it seems that |
| // doing so does nothing to reduce errors on rapid reuse. It could be that |
| // doing so actually increases the errors, as it delays shutdown. |
| |
| // Revoke the service's class objects. |
| UnregisterClassObjects(cookies_); |
| // Exit the service. |
| (this->*exit_routine_)(); |
| } |
| |
| void Service::OnStopRequested() { |
| // Tell the delegate that a stop has been requested. Do this before running |
| // the exit routine, as that will cause the service control dispatcher to |
| // return on the main thread. |
| delegate_->OnServiceControlStop(); |
| |
| base::AutoLock lock(lock_); |
| |
| // Revoke the service's class objects. |
| UnregisterClassObjects(cookies_); |
| // Exit the service. |
| (this->*exit_routine_)(); |
| } |