/* * 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 ams::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 { os::ProcessId pid; ncm::TitleId tid; size_t access_control_size; u8 access_control[AccessControlSizeMax]; ProcessInfo() { this->Free(); } void Free() { this->pid = os::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(os::ProcessId pid); struct ServiceInfo { ServiceName name; os::ProcessId owner_pid; os::ManagedHandle port_h; /* Debug. */ u64 max_sessions; bool is_light; /* Mitm Extension. */ os::ProcessId mitm_pid; os::ManagedHandle mitm_port_h; os::ManagedHandle mitm_query_h; /* Acknowledgement members. */ bool mitm_waiting_ack; os::ProcessId 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 = os::InvalidProcessId; this->max_sessions = 0; this->is_light = false; this->mitm_pid = os::InvalidProcessId; this->mitm_waiting_ack = false; this->mitm_waiting_ack_pid = os::InvalidProcessId; } void FreeMitm() { /* Close mitm handles. */ this->mitm_port_h.Clear(); this->mitm_query_h.Clear(); /* Reset mitm members. */ this->mitm_pid = os::InvalidProcessId; } void AcknowledgeMitmSession(os::ProcessId *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 = os::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: os::ProcessId min; os::ProcessId max; public: InitialProcessIdLimits() { /* Retrieve process limits. */ cfg::GetInitialProcessRange(&this->min, &this->max); /* Ensure range is sane. */ AMS_ASSERT(this->min <= this->max); } bool IsInitialProcess(os::ProcessId pid) const { AMS_ASSERT(pid != os::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(os::ProcessId 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(os::InvalidProcessId); } bool HasProcessInfo(os::ProcessId pid) { return GetProcessInfo(pid) != nullptr; } bool IsInitialProcess(os::ProcessId pid) { return g_initial_process_id_limits.IsInitialProcess(pid); } constexpr inline bool IsValidProcessId(os::ProcessId pid) { return pid != os::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(os::ProcessId pid) { /* Anything that can request a mitm session must have a process info. */ const auto process_info = GetProcessInfo(pid); AMS_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 sm::ResultOutOfServices(); } 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) { bool is_valid = true; if (access_control.IsWildcard() == is_wildcard) { /* Check for exact match. */ is_valid &= access_control.GetServiceName() == service; } else if (access_control.IsWildcard()) { /* Also allow fuzzy match for wildcard. */ ServiceName ac_service = access_control.GetServiceName(); is_valid &= std::memcmp(&ac_service, &service, access_control.GetServiceNameSize() - 1) == 0; } R_UNLESS(!is_valid, ResultSuccess()); } access_control = access_control.GetNextEntry(); } return sm::ResultNotAllowed(); } 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. */ R_UNLESS(service.name[0] != 0, sm::ResultInvalidServiceName()); /* 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)) { R_UNLESS(service.name[name_len++] == 0, sm::ResultInvalidServiceName()); } 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, os::ProcessId process_id, ncm::TitleId title_id) { /* Send command to query if we should mitm. */ bool should_mitm; { Service srv { .session = service_info->mitm_query_h.Get() }; const struct { os::ProcessId process_id; ncm::TitleId title_id; } in = { process_id, title_id }; R_TRY(serviceDispatchInOut(&srv, 65000, in, should_mitm)); } /* If we shouldn't mitm, give normal session. */ R_UNLESS(should_mitm, 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 = process_id; service_info->mitm_waiting_ack = true; return ResultSuccess(); } Result GetServiceHandleImpl(Handle *out, ServiceInfo *service_info, os::ProcessId 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, os::ProcessId 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. */ R_UNLESS(!HasServiceInfo(service), sm::ResultAlreadyRegistered()); /* 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(); R_UNLESS(free_service != nullptr, sm::ResultOutOfServices()); /* 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(os::ProcessId 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. */ R_UNLESS(aci_sac_size <= AccessControlSizeMax, sm::ResultTooLargeAccessControl()); /* Get free process. */ ProcessInfo *proc = GetFreeProcessInfo(); R_UNLESS(proc != nullptr, sm::ResultOutOfProcesses()); /* Validate restrictions. */ R_UNLESS(aci_sac_size != 0, sm::ResultNotAllowed()); 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(os::ProcessId pid) { /* Find the process. */ ProcessInfo *proc = GetProcessInfo(pid); R_UNLESS(proc != nullptr, sm::ResultInvalidClient()); 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. */ R_UNLESS(has_service, sf::ResultRequestDeferredByUser()); return ResultSuccess(); } Result GetServiceHandle(Handle *out, os::ProcessId 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. */ constexpr ServiceName ApmP = ServiceName::Encode("apm:p"); R_UNLESS((hos::GetVersion() < hos::Version_800) || (service != ApmP), sm::ResultNotAllowed()); /* Check that the process is registered and allowed to get the service. */ if (!IsInitialProcess(pid)) { ProcessInfo *proc = GetProcessInfo(pid); R_UNLESS(proc != nullptr, sm::ResultInvalidClient()); 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); R_UNLESS(service_info != nullptr, sf::ResultRequestDeferredByUser()); R_UNLESS(!ShouldDeferForInit(service), sf::ResultRequestDeferredByUser()); R_UNLESS(!HasFutureMitmDeclaration(service), sf::ResultRequestDeferredByUser()); R_UNLESS(!service_info->mitm_waiting_ack, sf::ResultRequestDeferredByUser()); /* Get a handle from the service info. */ R_TRY_CATCH(GetServiceHandleImpl(out, service_info, pid)) { R_CONVERT(svc::ResultOutOfSessions, sm::ResultOutOfSessions()) } R_END_TRY_CATCH; return ResultSuccess(); } Result RegisterService(Handle *out, os::ProcessId 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); R_UNLESS(proc != nullptr, sm::ResultInvalidClient()); R_TRY(ValidateAccessControl(AccessControlEntry(proc->access_control, proc->access_control_size), service, true, false)); } R_UNLESS(!HasServiceInfo(service), sm::ResultAlreadyRegistered()); return RegisterServiceImpl(out, pid, service, max_sessions, is_light); } Result RegisterServiceForSelf(Handle *out, ServiceName service, size_t max_sessions) { return RegisterServiceImpl(out, os::GetCurrentProcessId(), service, max_sessions, false); } Result UnregisterService(os::ProcessId pid, ServiceName service) { /* Validate service name. */ R_TRY(ValidateServiceName(service)); /* Check that the process is registered. */ if (!IsInitialProcess(pid)) { R_UNLESS(HasProcessInfo(pid), sm::ResultInvalidClient()); } /* Ensure that the service is actually registered. */ ServiceInfo *service_info = GetServiceInfo(service); R_UNLESS(service_info != nullptr, sm::ResultNotRegistered()); /* Check if we have permission to do this. */ R_UNLESS(service_info->owner_pid == pid, sm::ResultNotAllowed()); /* 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. */ R_UNLESS(has_mitm, sf::ResultRequestDeferredByUser()); return ResultSuccess(); } Result InstallMitm(Handle *out, Handle *out_query, os::ProcessId 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); R_UNLESS(proc != nullptr, sm::ResultInvalidClient()); 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 it doesn't exist, defer until it does. */ R_UNLESS(service_info != nullptr, sf::ResultRequestDeferredByUser()); /* Validate that the service isn't already being mitm'd. */ R_UNLESS(!IsValidProcessId(service_info->mitm_pid), sm::ResultAlreadyRegistered()); /* 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(os::ProcessId pid, ServiceName service) { /* Validate service name. */ R_TRY(ValidateServiceName(service)); /* Check that the process is registered. */ if (!IsInitialProcess(pid)) { ProcessInfo *proc = GetProcessInfo(pid); R_UNLESS(proc != nullptr, sm::ResultInvalidClient()); } /* Validate that the service exists. */ ServiceInfo *service_info = GetServiceInfo(service); R_UNLESS(service_info != nullptr, sm::ResultNotRegistered()); /* Validate that the client pid is the mitm process. */ R_UNLESS(service_info->mitm_pid == pid, sm::ResultNotAllowed()); /* Free Mitm session info. */ service_info->FreeMitm(); return ResultSuccess(); } Result DeclareFutureMitm(os::ProcessId 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); R_UNLESS(proc != nullptr, sm::ResultInvalidClient()); R_TRY(ValidateAccessControl(AccessControlEntry(proc->access_control, proc->access_control_size), service, true, false)); } /* Check that mitm hasn't already been registered or declared. */ R_UNLESS(!HasMitm(service), sm::ResultAlreadyRegistered()); R_UNLESS(!HasFutureMitmDeclaration(service), sm::ResultAlreadyRegistered()); /* Try to forward declare it. */ R_TRY(AddFutureMitmDeclaration(service)); return ResultSuccess(); } Result AcknowledgeMitmSession(os::ProcessId *out_pid, ncm::TitleId *out_tid, Handle *out_hnd, os::ProcessId pid, ServiceName service) { /* Validate service name. */ R_TRY(ValidateServiceName(service)); /* Check that the process is registered. */ if (!IsInitialProcess(pid)) { ProcessInfo *proc = GetProcessInfo(pid); R_UNLESS(proc != nullptr, sm::ResultInvalidClient()); } /* Validate that the service exists. */ ServiceInfo *service_info = GetServiceInfo(service); R_UNLESS(service_info != nullptr, sm::ResultNotRegistered()); /* Validate that the client pid is the mitm process, and that an acknowledgement is waiting. */ R_UNLESS(service_info->mitm_pid == pid, sm::ResultNotAllowed()); R_UNLESS(service_info->mitm_waiting_ack, sm::ResultNotAllowed()); /* 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); R_UNLESS(service_info != nullptr, sm::ResultNotRegistered()); 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(); } }