/* * Copyright (c) 2018-2019 Atmosphère-NX * * This program is free software; you can redistribute it and/or modify it * under the terms and conditions of the GNU General Public License, * version 2, as published by the Free Software Foundation. * * This program is distributed in the hope it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include "sm_service_manager.hpp" namespace sts::sm::impl { /* Anonymous namespace for implementation details. */ namespace { /* Constexpr definitions. */ static constexpr size_t ProcessCountMax = 0x40; static constexpr size_t ServiceCountMax = 0x100; static constexpr size_t FutureMitmCountMax = 0x20; static constexpr size_t AccessControlSizeMax = 0x200; /* Types. */ struct ProcessInfo { u64 pid; ncm::TitleId tid; size_t access_control_size; u8 access_control[AccessControlSizeMax]; ProcessInfo() { this->Free(); } void Free() { this->pid = InvalidProcessId; this->tid = ncm::TitleId::Invalid; this->access_control_size = 0; std::memset(this->access_control, 0, sizeof(this->access_control)); } }; /* Forward declaration, for use in ServiceInfo. */ ncm::TitleId GetTitleIdForMitm(u64 pid); struct ServiceInfo { ServiceName name; u64 owner_pid; os::ManagedHandle port_h; /* Debug. */ u64 max_sessions; bool is_light; /* Mitm Extension. */ u64 mitm_pid; os::ManagedHandle mitm_port_h; os::ManagedHandle mitm_query_h; /* Acknowledgement members. */ bool mitm_waiting_ack; u64 mitm_waiting_ack_pid; os::ManagedHandle mitm_fwd_sess_h; ServiceInfo() { this->Free(); } void Free() { /* Close any open handles. */ this->port_h.Clear(); this->mitm_port_h.Clear(); this->mitm_query_h.Clear(); this->mitm_fwd_sess_h.Clear(); /* Reset all other members. */ this->name = InvalidServiceName; this->owner_pid = InvalidProcessId; this->max_sessions = 0; this->is_light = false; this->mitm_pid = InvalidProcessId; this->mitm_waiting_ack = false; this->mitm_waiting_ack_pid = InvalidProcessId; } void FreeMitm() { /* Close mitm handles. */ this->mitm_port_h.Clear(); this->mitm_query_h.Clear(); /* Reset mitm members. */ this->mitm_pid = InvalidProcessId; } void AcknowledgeMitmSession(u64 *out_pid, ncm::TitleId *out_tid, Handle *out_hnd) { /* Copy to output. */ *out_pid = this->mitm_waiting_ack_pid; *out_tid = GetTitleIdForMitm(this->mitm_waiting_ack_pid); *out_hnd = this->mitm_fwd_sess_h.Move(); this->mitm_waiting_ack = false; this->mitm_waiting_ack_pid = InvalidProcessId; } }; class AccessControlEntry { private: const u8 *entry; size_t capacity; public: AccessControlEntry(const void *e, size_t c) : entry(reinterpret_cast(e)), capacity(c) { /* ... */ } AccessControlEntry GetNextEntry() const { return AccessControlEntry(this->entry + this->GetSize(), this->capacity - this->GetSize()); } size_t GetSize() const { return this->GetServiceNameSize() + 1; } size_t GetServiceNameSize() const { return (this->entry[0] & 7) + 1; } ServiceName GetServiceName() const { return ServiceName::Encode(reinterpret_cast(this->entry + 1), this->GetServiceNameSize()); } bool IsHost() const { return (this->entry[0] & 0x80) != 0; } bool IsWildcard() const { return this->entry[this->GetServiceNameSize()] == '*'; } bool IsValid() const { /* Validate that we can access data. */ if (this->entry == nullptr || this->capacity == 0) { return false; } /* Validate that the size is correct. */ return this->GetSize() <= this->capacity; } }; class InitialProcessIdLimits { private: u64 min; u64 max; public: InitialProcessIdLimits() { /* Retrieve process limits. */ cfg::GetInitialProcessRange(&this->min, &this->max); /* Ensure range is sane. */ STS_ASSERT(this->min <= this->max); } bool IsInitialProcess(u64 pid) const { STS_ASSERT(pid != InvalidProcessId); return this->min <= pid && pid <= this->max; } }; /* Static members. */ ProcessInfo g_process_list[ProcessCountMax]; ServiceInfo g_service_list[ServiceCountMax]; ServiceName g_future_mitm_list[FutureMitmCountMax]; InitialProcessIdLimits g_initial_process_id_limits; bool g_ended_initial_defers; /* Helper functions for interacting with processes/services. */ ProcessInfo *GetProcessInfo(u64 pid) { for (size_t i = 0; i < ProcessCountMax; i++) { if (g_process_list[i].pid == pid) { return &g_process_list[i]; } } return nullptr; } ProcessInfo *GetFreeProcessInfo() { return GetProcessInfo(InvalidProcessId); } bool HasProcessInfo(u64 pid) { return GetProcessInfo(pid) != nullptr; } bool IsInitialProcess(u64 pid) { return g_initial_process_id_limits.IsInitialProcess(pid); } bool IsValidProcessId(u64 pid) { return pid != InvalidProcessId; } ServiceInfo *GetServiceInfo(ServiceName service_name) { for (size_t i = 0; i < ServiceCountMax; i++) { if (g_service_list[i].name == service_name) { return &g_service_list[i]; } } return nullptr; } ServiceInfo *GetFreeServiceInfo() { return GetServiceInfo(InvalidServiceName); } bool HasServiceInfo(ServiceName service) { return GetServiceInfo(service) != nullptr; } bool HasMitm(ServiceName service) { const ServiceInfo *service_info = GetServiceInfo(service); return service_info != nullptr && IsValidProcessId(service_info->mitm_pid); } ncm::TitleId GetTitleIdForMitm(u64 pid) { /* Anything that can request a mitm session must have a process info. */ const auto process_info = GetProcessInfo(pid); STS_ASSERT(process_info != nullptr); return process_info->tid; } bool IsMitmDisallowed(ncm::TitleId title_id) { /* Mitm used on certain titles can prevent the boot process from completing. */ /* TODO: Is there a way to do this that's less hardcoded? Needs design thought. */ return title_id == ncm::TitleId::Loader || title_id == ncm::TitleId::Boot || title_id == ncm::TitleId::AtmosphereMitm || title_id == ncm::TitleId::Creport; } Result AddFutureMitmDeclaration(ServiceName service) { for (size_t i = 0; i < FutureMitmCountMax; i++) { if (g_future_mitm_list[i] == InvalidServiceName) { g_future_mitm_list[i] = service; return ResultSuccess; } } return ResultSmInsufficientServices; } bool HasFutureMitmDeclaration(ServiceName service) { for (size_t i = 0; i < FutureMitmCountMax; i++) { if (g_future_mitm_list[i] == service) { return true; } } return false; } void ClearFutureMitmDeclaration(ServiceName service) { for (size_t i = 0; i < FutureMitmCountMax; i++) { if (g_future_mitm_list[i] == service) { g_future_mitm_list[i] = InvalidServiceName; } } } void GetServiceInfoRecord(ServiceRecord *out_record, const ServiceInfo *service_info) { out_record->service = service_info->name; out_record->owner_pid = service_info->owner_pid; out_record->max_sessions = service_info->max_sessions; out_record->mitm_pid = service_info->mitm_pid; out_record->mitm_waiting_ack_pid = service_info->mitm_waiting_ack_pid; out_record->is_light = service_info->is_light; out_record->mitm_waiting_ack = service_info->mitm_waiting_ack; } Result ValidateAccessControl(AccessControlEntry access_control, ServiceName service, bool is_host, bool is_wildcard) { /* Iterate over all entries in the access control, checking to see if we have a match. */ while (access_control.IsValid()) { if (access_control.IsHost() == is_host) { if (access_control.IsWildcard() == is_wildcard) { /* Check for exact match. */ if (access_control.GetServiceName() == service) { return ResultSuccess; } } else if (access_control.IsWildcard()) { /* Also allow fuzzy match for wildcard. */ ServiceName ac_service = access_control.GetServiceName(); if (std::memcmp(&ac_service, &service, access_control.GetServiceNameSize() - 1) == 0) { return ResultSuccess; } } } access_control = access_control.GetNextEntry(); } return ResultSmNotAllowed; } Result ValidateAccessControl(AccessControlEntry restriction, AccessControlEntry access) { /* Ensure that every entry in the access control is allowed by the restriction control. */ while (access.IsValid()) { R_TRY(ValidateAccessControl(restriction, access.GetServiceName(), access.IsHost(), access.IsWildcard())); access = access.GetNextEntry(); } return ResultSuccess; } Result ValidateServiceName(ServiceName service) { /* Service names must be non-empty. */ if (service.name[0] == 0) { return ResultSmInvalidServiceName; } /* Get name length. */ size_t name_len; for (name_len = 1; name_len < sizeof(service); name_len++) { if (service.name[name_len] == 0) { break; } } /* Names must be all-zero after they end. */ while (name_len < sizeof(service)) { if (service.name[name_len++] != 0) { return ResultSmInvalidServiceName; } } return ResultSuccess; } bool ShouldDeferForInit(ServiceName service) { /* Once end has been called, we're done. */ if (g_ended_initial_defers) { return false; } /* This is a mechanism by which certain services will always be deferred until sm:m receives a special command. */ /* This can be extended with more services as needed at a later date. */ return service == ServiceName::Encode("fsp-srv"); } Result GetMitmServiceHandleImpl(Handle *out, ServiceInfo *service_info, u64 pid, ncm::TitleId title_id) { /* Send command to query if we should mitm. */ { IpcCommand c; ipcInitialize(&c); struct { u64 magic; u64 cmd_id; u64 pid; ncm::TitleId tid; } *info = ((decltype(info))ipcPrepareHeader(&c, sizeof(*info))); info->magic = SFCI_MAGIC; info->cmd_id = 65000; info->pid = pid; info->tid = title_id; R_TRY(ipcDispatch(service_info->mitm_query_h.Get())); } /* Parse response to see if we should mitm. */ bool should_mitm; { IpcParsedCommand r; ipcParse(&r); struct { u64 magic; u64 result; bool should_mitm; } *resp = ((decltype(resp))r.Raw); R_TRY(resp->result); should_mitm = resp->should_mitm; } /* If we shouldn't mitm, give normal session. */ if (!should_mitm) { return svcConnectToPort(out, service_info->port_h.Get()); } /* Create both handles. */ { os::ManagedHandle fwd_hnd, hnd; R_TRY(svcConnectToPort(fwd_hnd.GetPointer(), service_info->port_h.Get())); R_TRY(svcConnectToPort(hnd.GetPointer(), service_info->mitm_port_h.Get())); service_info->mitm_fwd_sess_h = std::move(fwd_hnd); *out = hnd.Move(); } service_info->mitm_waiting_ack_pid = pid; service_info->mitm_waiting_ack = true; return ResultSuccess; } Result GetServiceHandleImpl(Handle *out, ServiceInfo *service_info, u64 pid) { /* Clear handle output. */ *out = INVALID_HANDLE; /* Check if we should return a mitm handle. */ if (IsValidProcessId(service_info->mitm_pid) && service_info->mitm_pid != pid) { /* Get title id, ensure that we're allowed to mitm the given title. */ const ncm::TitleId title_id = GetTitleIdForMitm(pid); if (!IsMitmDisallowed(title_id)) { /* We're mitm'd. Assert, because mitm service host dead is an error state. */ R_ASSERT(GetMitmServiceHandleImpl(out, service_info, pid, title_id)); return ResultSuccess; } } /* We're not returning a mitm handle, so just return a normal port handle. */ return svcConnectToPort(out, service_info->port_h.Get()); } Result RegisterServiceImpl(Handle *out, u64 pid, ServiceName service, size_t max_sessions, bool is_light) { /* Validate service name. */ R_TRY(ValidateServiceName(service)); /* Don't try to register something already registered. */ if (HasServiceInfo(service)) { return ResultSmAlreadyRegistered; } /* Adjust session limit, if compile flags tell us to. */ #ifdef SM_MINIMUM_SESSION_LIMIT if (max_sessions < SM_MINIMUM_SESSION_LIMIT) { max_sessions = SM_MINIMUM_SESSION_LIMIT; } #endif /* Get free service. */ ServiceInfo *free_service = GetFreeServiceInfo(); if (free_service == nullptr) { return ResultSmInsufficientServices; } /* Create the new service. */ *out = INVALID_HANDLE; R_TRY(svcCreatePort(out, free_service->port_h.GetPointerAndClear(), max_sessions, is_light, free_service->name.name)); /* Save info. */ free_service->name = service; free_service->owner_pid = pid; free_service->max_sessions = max_sessions; free_service->is_light = is_light; return ResultSuccess; } } /* Process management. */ Result RegisterProcess(u64 pid, ncm::TitleId tid, const void *acid_sac, size_t acid_sac_size, const void *aci_sac, size_t aci_sac_size) { /* Check that access control will fit in the ServiceInfo. */ if (aci_sac_size > AccessControlSizeMax) { return ResultSmTooLargeAccessControl; } /* Get free process. */ ProcessInfo *proc = GetFreeProcessInfo(); if (proc == nullptr) { return ResultSmInsufficientProcesses; } /* Validate restrictions. */ if (!aci_sac_size) { return ResultSmNotAllowed; } R_TRY(ValidateAccessControl(AccessControlEntry(acid_sac, acid_sac_size), AccessControlEntry(aci_sac, aci_sac_size))); /* Save info. */ proc->pid = pid; proc->tid = tid; proc->access_control_size = aci_sac_size; std::memcpy(proc->access_control, aci_sac, proc->access_control_size); return ResultSuccess; } Result UnregisterProcess(u64 pid) { /* Find the process. */ ProcessInfo *proc = GetProcessInfo(pid); if (proc == nullptr) { return ResultSmInvalidClient; } proc->Free(); return ResultSuccess; } /* Service management. */ Result HasService(bool *out, ServiceName service) { /* Validate service name. */ R_TRY(ValidateServiceName(service)); *out = HasServiceInfo(service); return ResultSuccess; } Result WaitService(ServiceName service) { bool has_service = false; R_TRY(impl::HasService(&has_service, service)); /* Wait until we have the service. */ if (!has_service) { return ResultServiceFrameworkRequestDeferredByUser; } return ResultSuccess; } Result GetServiceHandle(Handle *out, u64 pid, ServiceName service) { /* Validate service name. */ R_TRY(ValidateServiceName(service)); /* In 8.0.0, Nintendo removed the service apm:p -- however, all homebrew attempts to get */ /* a handle to this when calling appletInitialize(). Because hbl has access to all services, */ /* This would return true, and homebrew would *wait forever* trying to get a handle to a service */ /* that will never register. Thus, in the interest of not breaking every single piece of homebrew */ /* we will provide a little first class help. */ if (GetRuntimeFirmwareVersion() >= FirmwareVersion_800 && service == ServiceName::Encode("apm:p")) { return ResultSmNotAllowed; } /* Check that the process is registered and allowed to get the service. */ if (!IsInitialProcess(pid)) { ProcessInfo *proc = GetProcessInfo(pid); if (proc == nullptr) { return ResultSmInvalidClient; } R_TRY(ValidateAccessControl(AccessControlEntry(proc->access_control, proc->access_control_size), service, false, false)); } /* Get service info. Check to see if we need to defer this until later. */ ServiceInfo *service_info = GetServiceInfo(service); if (service_info == nullptr || ShouldDeferForInit(service) || HasFutureMitmDeclaration(service) || service_info->mitm_waiting_ack) { return ResultServiceFrameworkRequestDeferredByUser; } /* Get a handle from the service info. */ R_TRY_CATCH(GetServiceHandleImpl(out, service_info, pid)) { /* Convert Kernel result to SM result. */ R_CATCH(ResultKernelOutOfSessions) { return ResultSmInsufficientSessions; } } R_END_TRY_CATCH; return ResultSuccess; } Result RegisterService(Handle *out, u64 pid, ServiceName service, size_t max_sessions, bool is_light) { /* Validate service name. */ R_TRY(ValidateServiceName(service)); /* Check that the process is registered and allowed to register the service. */ if (!IsInitialProcess(pid)) { ProcessInfo *proc = GetProcessInfo(pid); if (proc == nullptr) { return ResultSmInvalidClient; } R_TRY(ValidateAccessControl(AccessControlEntry(proc->access_control, proc->access_control_size), service, true, false)); } if (HasServiceInfo(service)) { return ResultSmAlreadyRegistered; } return RegisterServiceImpl(out, pid, service, max_sessions, is_light); } Result RegisterServiceForSelf(Handle *out, ServiceName service, size_t max_sessions) { u64 self_pid; R_TRY(svcGetProcessId(&self_pid, CUR_PROCESS_HANDLE)); return RegisterServiceImpl(out, self_pid, service, max_sessions, false); } Result UnregisterService(u64 pid, ServiceName service) { /* Validate service name. */ R_TRY(ValidateServiceName(service)); /* Check that the process is registered. */ if (!IsInitialProcess(pid)) { if (!HasProcessInfo(pid)) { return ResultSmInvalidClient; } } /* Ensure that the service is actually registered. */ ServiceInfo *service_info = GetServiceInfo(service); if (service_info == nullptr) { return ResultSmNotRegistered; } /* Check if we have permission to do this. */ if (service_info->owner_pid != pid) { return ResultSmNotAllowed; } /* Unregister the service. */ service_info->Free(); return ResultSuccess; } /* Mitm extensions. */ Result HasMitm(bool *out, ServiceName service) { /* Validate service name. */ R_TRY(ValidateServiceName(service)); const ServiceInfo *service_info = GetServiceInfo(service); *out = service_info != nullptr && IsValidProcessId(service_info->mitm_pid); return ResultSuccess; } Result WaitMitm(ServiceName service) { bool has_mitm = false; R_TRY(impl::HasMitm(&has_mitm, service)); /* Wait until we have the mitm. */ if (!has_mitm) { return ResultServiceFrameworkRequestDeferredByUser; } return ResultSuccess; } Result InstallMitm(Handle *out, Handle *out_query, u64 pid, ServiceName service) { /* Validate service name. */ R_TRY(ValidateServiceName(service)); /* Check that the process is registered and allowed to register the service. */ if (!IsInitialProcess(pid)) { ProcessInfo *proc = GetProcessInfo(pid); if (proc == nullptr) { return ResultSmInvalidClient; } R_TRY(ValidateAccessControl(AccessControlEntry(proc->access_control, proc->access_control_size), service, true, false)); } /* Validate that the service exists. */ ServiceInfo *service_info = GetServiceInfo(service); if (service_info == nullptr) { /* If it doesn't exist, defer until it does. */ return ResultServiceFrameworkRequestDeferredByUser; } /* Validate that the service isn't already being mitm'd. */ if (IsValidProcessId(service_info->mitm_pid)) { return ResultSmAlreadyRegistered; } /* Always clear output. */ *out = INVALID_HANDLE; *out_query = INVALID_HANDLE; /* Create mitm handles. */ { os::ManagedHandle hnd, port_hnd, qry_hnd, mitm_qry_hnd; u64 x = 0; R_TRY(svcCreatePort(hnd.GetPointer(), port_hnd.GetPointer(), service_info->max_sessions, service_info->is_light, reinterpret_cast(&x))); R_TRY(svcCreateSession(qry_hnd.GetPointer(), mitm_qry_hnd.GetPointer(), 0, 0)); /* Copy to output. */ service_info->mitm_pid = pid; service_info->mitm_port_h = std::move(port_hnd); service_info->mitm_query_h = std::move(mitm_qry_hnd); *out = hnd.Move(); *out_query = qry_hnd.Move(); } /* Clear the future declaration, if one exists. */ ClearFutureMitmDeclaration(service); return ResultSuccess; } Result UninstallMitm(u64 pid, ServiceName service) { /* Validate service name. */ R_TRY(ValidateServiceName(service)); /* Check that the process is registered. */ if (!IsInitialProcess(pid)) { ProcessInfo *proc = GetProcessInfo(pid); if (proc == nullptr) { return ResultSmInvalidClient; } } /* Validate that the service exists. */ ServiceInfo *service_info = GetServiceInfo(service); if (service_info == nullptr) { return ResultSmNotRegistered; } /* Validate that the client pid is the mitm process. */ if (service_info->mitm_pid != pid) { return ResultSmNotAllowed; } /* Free Mitm session info. */ service_info->FreeMitm(); return ResultSuccess; } Result DeclareFutureMitm(u64 pid, ServiceName service) { /* Validate service name. */ R_TRY(ValidateServiceName(service)); /* Check that the process is registered and allowed to register the service. */ if (!IsInitialProcess(pid)) { ProcessInfo *proc = GetProcessInfo(pid); if (proc == nullptr) { return ResultSmInvalidClient; } R_TRY(ValidateAccessControl(AccessControlEntry(proc->access_control, proc->access_control_size), service, true, false)); } /* Check that mitm hasn't already been registered or declared. */ if (HasMitm(service) || HasFutureMitmDeclaration(service)) { return ResultSmAlreadyRegistered; } /* Try to forward declare it. */ R_TRY(AddFutureMitmDeclaration(service)); return ResultSuccess; } Result AcknowledgeMitmSession(u64 *out_pid, ncm::TitleId *out_tid, Handle *out_hnd, u64 pid, ServiceName service) { /* Validate service name. */ R_TRY(ValidateServiceName(service)); /* Check that the process is registered. */ if (!IsInitialProcess(pid)) { ProcessInfo *proc = GetProcessInfo(pid); if (proc == nullptr) { return ResultSmInvalidClient; } } /* Validate that the service exists. */ ServiceInfo *service_info = GetServiceInfo(service); if (service_info == nullptr) { return ResultSmNotRegistered; } /* Validate that the client pid is the mitm process, and that an acknowledgement is waiting. */ if (service_info->mitm_pid != pid || !service_info->mitm_waiting_ack) { return ResultSmNotAllowed; } /* Acknowledge. */ service_info->AcknowledgeMitmSession(out_pid, out_tid, out_hnd); return ResultSuccess; } /* Dmnt record extensions. */ Result GetServiceRecord(ServiceRecord *out, ServiceName service) { /* Validate service name. */ R_TRY(ValidateServiceName(service)); /* Validate that the service exists. */ const ServiceInfo *service_info = GetServiceInfo(service); if (service_info == nullptr) { return ResultSmNotRegistered; } GetServiceInfoRecord(out, service_info); return ResultSuccess; } Result ListServiceRecords(ServiceRecord *out, u64 *out_count, u64 offset, u64 max_count) { u64 count = 0; for (size_t i = 0; i < ServiceCountMax && count < max_count; i++) { const ServiceInfo *service_info = &g_service_list[i]; if (service_info->name != InvalidServiceName) { if (offset == 0) { GetServiceInfoRecord(out++, service_info); count++; } else { offset--; } } } *out_count = 0; return ResultSuccess; } /* Deferral extension (works around FS bug). */ Result EndInitialDefers() { g_ended_initial_defers = true; return ResultSuccess; } }