/* * 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 "pm_process_manager.hpp" #include "pm_resource_manager.hpp" #include "pm_process_info.hpp" #include "../boot2/boot2_api.hpp" namespace sts::pm::impl { namespace { /* Types. */ enum HookType { HookType_TitleId = (1 << 0), HookType_Application = (1 << 1), }; struct LaunchProcessArgs { os::ProcessId *out_process_id; ncm::TitleLocation location; u32 flags; }; enum LaunchFlags { LaunchFlags_None = 0, LaunchFlags_SignalOnExit = (1 << 0), LaunchFlags_SignalOnStart = (1 << 1), LaunchFlags_SignalOnException = (1 << 2), LaunchFlags_SignalOnDebugEvent = (1 << 3), LaunchFlags_StartSuspended = (1 << 4), LaunchFlags_DisableAslr = (1 << 5), }; enum LaunchFlagsDeprecated { LaunchFlagsDeprecated_None = 0, LaunchFlagsDeprecated_SignalOnExit = (1 << 0), LaunchFlagsDeprecated_StartSuspended = (1 << 1), LaunchFlagsDeprecated_SignalOnException = (1 << 2), LaunchFlagsDeprecated_DisableAslr = (1 << 3), LaunchFlagsDeprecated_SignalOnDebugEvent = (1 << 4), LaunchFlagsDeprecated_SignalOnStart = (1 << 5), }; #define GET_FLAG_MASK(flag) (hos_version >= hos::Version_500 ? static_cast(LaunchFlags_##flag) : static_cast(LaunchFlagsDeprecated_##flag)) inline bool ShouldSignalOnExit(u32 launch_flags) { const auto hos_version = hos::GetVersion(); return launch_flags & GET_FLAG_MASK(SignalOnExit); } inline bool ShouldSignalOnStart(u32 launch_flags) { const auto hos_version = hos::GetVersion(); if (hos_version < hos::Version_200) { return false; } return launch_flags & GET_FLAG_MASK(SignalOnStart); } inline bool ShouldSignalOnException(u32 launch_flags) { const auto hos_version = hos::GetVersion(); return launch_flags & GET_FLAG_MASK(SignalOnException); } inline bool ShouldSignalOnDebugEvent(u32 launch_flags) { const auto hos_version = hos::GetVersion(); return launch_flags & GET_FLAG_MASK(SignalOnDebugEvent); } inline bool ShouldStartSuspended(u32 launch_flags) { const auto hos_version = hos::GetVersion(); return launch_flags & GET_FLAG_MASK(StartSuspended); } inline bool ShouldDisableAslr(u32 launch_flags) { const auto hos_version = hos::GetVersion(); return launch_flags & GET_FLAG_MASK(DisableAslr); } #undef GET_FLAG_MASK enum class ProcessEvent { None = 0, Exited = 1, Started = 2, Exception = 3, DebugRunning = 4, DebugSuspended = 5, }; enum class ProcessEventDeprecated { None = 0, Exception = 1, Exited = 2, DebugRunning = 3, DebugSuspended = 4, Started = 5, }; inline u32 GetProcessEventValue(ProcessEvent event) { if (hos::GetVersion() >= hos::Version_500) { return static_cast(event); } switch (event) { case ProcessEvent::None: return static_cast(ProcessEventDeprecated::None); case ProcessEvent::Exited: return static_cast(ProcessEventDeprecated::Exited); case ProcessEvent::Started: return static_cast(ProcessEventDeprecated::Started); case ProcessEvent::Exception: return static_cast(ProcessEventDeprecated::Exception); case ProcessEvent::DebugRunning: return static_cast(ProcessEventDeprecated::DebugRunning); case ProcessEvent::DebugSuspended: return static_cast(ProcessEventDeprecated::DebugSuspended); STS_UNREACHABLE_DEFAULT_CASE(); } } /* Process Tracking globals. */ os::Thread g_process_track_thread; /* Process lists. */ ProcessList g_process_list; ProcessList g_dead_process_list; /* Global events. */ os::SystemEvent g_process_event; os::SystemEvent g_hook_to_create_process_event; os::SystemEvent g_hook_to_create_application_process_event; os::SystemEvent g_boot_finished_event; /* Process Launch synchronization globals. */ os::Event g_process_launch_start_event; os::Event g_process_launch_finish_event; Result g_process_launch_result = ResultSuccess; LaunchProcessArgs g_process_launch_args = {}; /* Hook globals. */ std::atomic g_title_id_hook; std::atomic g_application_hook; /* Forward declarations. */ Result LaunchProcess(os::WaitableManager &waitable_manager, const LaunchProcessArgs &args); Result OnProcessSignaled(ProcessListAccessor &list, ProcessInfo *process_info); /* Helpers. */ void ProcessTrackingMain(void *arg) { /* This is the main loop of the process tracking thread. */ /* Setup waitable manager. */ os::WaitableManager process_waitable_manager; os::WaitableHolder start_event_holder(&g_process_launch_start_event); process_waitable_manager.LinkWaitableHolder(&start_event_holder); while (true) { auto signaled_holder = process_waitable_manager.WaitAny(); if (signaled_holder == &start_event_holder) { /* Launch start event signaled. */ /* TryWait will clear signaled, preventing duplicate notifications. */ if (g_process_launch_start_event.TryWait()) { g_process_launch_result = LaunchProcess(process_waitable_manager, g_process_launch_args); g_process_launch_finish_event.Signal(); } } else { /* Some process was signaled. */ ProcessListAccessor list(g_process_list); OnProcessSignaled(list, reinterpret_cast(signaled_holder->GetUserData())); } } } inline u32 GetLoaderCreateProcessFlags(u32 launch_flags) { u32 ldr_flags = 0; if (ShouldSignalOnException(launch_flags) || (hos::GetVersion() >= hos::Version_200 && !ShouldStartSuspended(launch_flags))) { ldr_flags |= ldr::CreateProcessFlag_EnableDebug; } if (ShouldDisableAslr(launch_flags)) { ldr_flags |= ldr::CreateProcessFlag_DisableAslr; } return ldr_flags; } bool HasApplicationProcess() { ProcessListAccessor list(g_process_list); for (auto &process : *list) { if (process.IsApplication()) { return true; } } return false; } Result StartProcess(ProcessInfo *process_info, const ldr::ProgramInfo *program_info) { R_TRY(svcStartProcess(process_info->GetHandle(), program_info->main_thread_priority, program_info->default_cpu_id, program_info->main_thread_stack_size)); process_info->SetState(ProcessState_Running); return ResultSuccess; } void CleanupProcessInfo(ProcessListAccessor &list, ProcessInfo *process_info) { /* Remove the process from the list. */ list->Remove(process_info); /* Delete the process. */ delete process_info; } Result LaunchProcess(os::WaitableManager &waitable_manager, const LaunchProcessArgs &args) { /* Get Program Info. */ ldr::ProgramInfo program_info; R_TRY(ldr::pm::GetProgramInfo(&program_info, args.location)); const bool is_application = (program_info.flags & ldr::ProgramInfoFlag_ApplicationTypeMask) == ldr::ProgramInfoFlag_Application; const bool allow_debug = (program_info.flags & ldr::ProgramInfoFlag_AllowDebug) || hos::GetVersion() < hos::Version_200; /* Ensure we only try to run one application. */ if (is_application && HasApplicationProcess()) { return ResultPmApplicationRunning; } /* Fix the title location to use the right title id. */ const ncm::TitleLocation location = ncm::TitleLocation::Make(program_info.title_id, static_cast(args.location.storage_id)); /* Pin the program with loader. */ ldr::PinId pin_id; R_TRY(ldr::pm::PinTitle(&pin_id, location)); /* Ensure resources are available. */ resource::WaitResourceAvailable(&program_info); /* Actually create the process. */ Handle process_handle; R_TRY_CLEANUP(ldr::pm::CreateProcess(&process_handle, pin_id, GetLoaderCreateProcessFlags(args.flags), resource::GetResourceLimitHandle(&program_info)), { ldr::pm::UnpinTitle(pin_id); }); /* Get the process id. */ os::ProcessId process_id = os::InvalidProcessId; R_ASSERT(svcGetProcessId(&process_id.value, process_handle)); /* Make new process info. */ ProcessInfo *process_info = new ProcessInfo(process_handle, process_id, pin_id, location); /* Link new process info. */ { ProcessListAccessor list(g_process_list); list->push_back(*process_info); process_info->LinkToWaitableManager(waitable_manager); } /* Prevent resource leakage if register fails. */ auto cleanup_guard = SCOPE_GUARD { ProcessListAccessor list(g_process_list); process_info->Cleanup(); CleanupProcessInfo(list, process_info); }; const u8 *acid_sac = program_info.ac_buffer; const u8 *aci_sac = acid_sac + program_info.acid_sac_size; const u8 *acid_fac = aci_sac + program_info.aci_sac_size; const u8 *aci_fah = acid_fac + program_info.acid_fac_size; /* Register with FS and SM. */ R_TRY(fsprRegisterProgram(static_cast(process_id), static_cast(location.title_id), static_cast(location.storage_id), aci_fah, program_info.aci_fah_size, acid_fac, program_info.acid_fac_size)); R_TRY(sm::manager::RegisterProcess(process_id, location.title_id, acid_sac, program_info.acid_sac_size, aci_sac, program_info.aci_sac_size)); /* Set flags. */ if (is_application) { process_info->SetApplication(); } if (ShouldSignalOnStart(args.flags) && allow_debug) { process_info->SetSignalOnStart(); } if (ShouldSignalOnExit(args.flags)) { process_info->SetSignalOnExit(); } if (ShouldSignalOnDebugEvent(args.flags) && allow_debug) { process_info->SetSignalOnDebugEvent(); } /* Process hooks/signaling. */ if (location.title_id == g_title_id_hook) { g_hook_to_create_process_event.Signal(); g_title_id_hook = ncm::TitleId::Invalid; } else if (is_application && g_application_hook) { g_hook_to_create_application_process_event.Signal(); g_application_hook = false; } else if (!ShouldStartSuspended(args.flags)) { R_TRY(StartProcess(process_info, &program_info)); } /* We succeeded, so we can cancel our cleanup. */ cleanup_guard.Cancel(); *args.out_process_id = process_id; return ResultSuccess; } Result OnProcessSignaled(ProcessListAccessor &list, ProcessInfo *process_info) { /* Resest the process's signal. */ svcResetSignal(process_info->GetHandle()); /* Update the process's state. */ const ProcessState old_state = process_info->GetState(); { u64 tmp = 0; R_ASSERT(svcGetProcessInfo(&tmp, process_info->GetHandle(), ProcessInfoType_ProcessState)); process_info->SetState(static_cast(tmp)); } const ProcessState new_state = process_info->GetState(); /* If we're transitioning away from crashed, clear waiting attached. */ if (old_state == ProcessState_Crashed && new_state != ProcessState_Crashed) { process_info->ClearExceptionWaitingAttach(); } switch (new_state) { case ProcessState_Created: case ProcessState_CreatedAttached: case ProcessState_Exiting: break; case ProcessState_Running: if (process_info->ShouldSignalOnDebugEvent()) { process_info->ClearSuspended(); process_info->SetSuspendedStateChanged(); g_process_event.Signal(); } else if (hos::GetVersion() >= hos::Version_200 && process_info->ShouldSignalOnStart()) { process_info->SetStartedStateChanged(); process_info->ClearSignalOnStart(); g_process_event.Signal(); } break; case ProcessState_Crashed: process_info->SetExceptionOccurred(); g_process_event.Signal(); break; case ProcessState_RunningAttached: if (process_info->ShouldSignalOnDebugEvent()) { process_info->ClearSuspended(); process_info->SetSuspendedStateChanged(); g_process_event.Signal(); } break; case ProcessState_Exited: if (hos::GetVersion() < hos::Version_500 && process_info->ShouldSignalOnExit()) { g_process_event.Signal(); } else { /* Free process resources, unlink from waitable manager. */ process_info->Cleanup(); /* Handle the case where we need to keep the process alive some time longer. */ if (hos::GetVersion() >= hos::Version_500 && process_info->ShouldSignalOnExit()) { /* Remove from the living list. */ list->Remove(process_info); /* Add the process to the list of dead processes. */ { ProcessListAccessor dead_list(g_dead_process_list); dead_list->push_back(*process_info); } /* Signal. */ g_process_event.Signal(); } else { /* Actually delete process. */ CleanupProcessInfo(list, process_info); } } /* Return ConnectionClosed to cause libstratosphere to stop waiting on the process. */ return ResultKernelConnectionClosed; case ProcessState_DebugSuspended: if (process_info->ShouldSignalOnDebugEvent()) { process_info->SetSuspended(); process_info->SetSuspendedStateChanged(); g_process_event.Signal(); } break; } return ResultSuccess; } } /* Initialization. */ Result InitializeProcessManager() { /* Create events. */ R_ASSERT(g_process_event.InitializeAsInterProcessEvent()); R_ASSERT(g_hook_to_create_process_event.InitializeAsInterProcessEvent()); R_ASSERT(g_hook_to_create_application_process_event.InitializeAsInterProcessEvent()); R_ASSERT(g_boot_finished_event.InitializeAsInterProcessEvent()); /* Initialize resource limits. */ R_TRY(resource::InitializeResourceManager()); /* Start thread. */ R_ASSERT(g_process_track_thread.Initialize(&ProcessTrackingMain, nullptr, 0x4000, 0x15)); R_ASSERT(g_process_track_thread.Start()); return ResultSuccess; } /* Process Management. */ Result LaunchTitle(os::ProcessId *out_process_id, const ncm::TitleLocation &loc, u32 flags) { /* Ensure we only try to launch one title at a time. */ static os::Mutex s_lock; std::scoped_lock lk(s_lock); /* Set global arguments, signal, wait. */ g_process_launch_args = { .out_process_id = out_process_id, .location = loc, .flags = flags, }; g_process_launch_start_event.Signal(); g_process_launch_finish_event.Wait(); return g_process_launch_result; } Result StartProcess(os::ProcessId process_id) { ProcessListAccessor list(g_process_list); auto process_info = list->Find(process_id); if (process_info == nullptr) { return ResultPmProcessNotFound; } if (process_info->HasStarted()) { return ResultPmAlreadyStarted; } ldr::ProgramInfo program_info; R_TRY(ldr::pm::GetProgramInfo(&program_info, process_info->GetTitleLocation())); return StartProcess(process_info, &program_info); } Result TerminateProcess(os::ProcessId process_id) { ProcessListAccessor list(g_process_list); auto process_info = list->Find(process_id); if (process_info == nullptr) { return ResultPmProcessNotFound; } return svcTerminateProcess(process_info->GetHandle()); } Result TerminateTitle(ncm::TitleId title_id) { ProcessListAccessor list(g_process_list); auto process_info = list->Find(title_id); if (process_info == nullptr) { return ResultPmProcessNotFound; } return svcTerminateProcess(process_info->GetHandle()); } Result GetProcessEventHandle(Handle *out) { *out = g_process_event.GetReadableHandle(); return ResultSuccess; } Result GetProcessEventInfo(ProcessEventInfo *out) { /* Check for event from current process. */ { ProcessListAccessor list(g_process_list); for (auto &process : *list) { if (process.HasStarted() && process.HasStartedStateChanged()) { process.ClearStartedStateChanged(); out->event = GetProcessEventValue(ProcessEvent::Started); out->process_id = process.GetProcessId(); return ResultSuccess; } if (process.HasSuspendedStateChanged()) { process.ClearSuspendedStateChanged(); if (process.IsSuspended()) { out->event = GetProcessEventValue(ProcessEvent::DebugSuspended); } else { out->event = GetProcessEventValue(ProcessEvent::DebugRunning); } out->process_id = process.GetProcessId(); return ResultSuccess; } if (process.HasExceptionOccurred()) { process.ClearExceptionOccurred(); out->event = GetProcessEventValue(ProcessEvent::Exception); out->process_id = process.GetProcessId(); return ResultSuccess; } if (hos::GetVersion() < hos::Version_500 && process.ShouldSignalOnExit() && process.HasExited()) { out->event = GetProcessEventValue(ProcessEvent::Exited); out->process_id = process.GetProcessId(); return ResultSuccess; } } } /* Check for event from exited process. */ if (hos::GetVersion() >= hos::Version_500) { ProcessListAccessor dead_list(g_dead_process_list); if (!dead_list->empty()) { auto &process_info = dead_list->front(); out->event = GetProcessEventValue(ProcessEvent::Exited); out->process_id = process_info.GetProcessId(); CleanupProcessInfo(dead_list, &process_info); return ResultSuccess; } } out->process_id = os::ProcessId{}; out->event = GetProcessEventValue(ProcessEvent::None); return ResultSuccess; } Result CleanupProcess(os::ProcessId process_id) { ProcessListAccessor list(g_process_list); auto process_info = list->Find(process_id); if (process_info == nullptr) { return ResultPmProcessNotFound; } if (!process_info->HasExited()) { return ResultPmNotExited; } CleanupProcessInfo(list, process_info); return ResultSuccess; } Result ClearExceptionOccurred(os::ProcessId process_id) { ProcessListAccessor list(g_process_list); auto process_info = list->Find(process_id); if (process_info == nullptr) { return ResultPmProcessNotFound; } process_info->ClearExceptionOccurred(); return ResultSuccess; } /* Information Getters. */ Result GetModuleIdList(u32 *out_count, u8 *out_buf, size_t max_out_count, u64 unused) { /* This function was always stubbed... */ *out_count = 0; return ResultSuccess; } Result GetExceptionProcessIdList(u32 *out_count, os::ProcessId *out_process_ids, size_t max_out_count) { ProcessListAccessor list(g_process_list); size_t count = 0; for (auto &process : *list) { if (process.HasExceptionWaitingAttach()) { out_process_ids[count++] = process.GetProcessId(); } } *out_count = static_cast(count); return ResultSuccess; } Result GetProcessId(os::ProcessId *out, ncm::TitleId title_id) { ProcessListAccessor list(g_process_list); auto process_info = list->Find(title_id); if (process_info == nullptr) { return ResultPmProcessNotFound; } *out = process_info->GetProcessId(); return ResultSuccess; } Result GetTitleId(ncm::TitleId *out, os::ProcessId process_id) { ProcessListAccessor list(g_process_list); auto process_info = list->Find(process_id); if (process_info == nullptr) { return ResultPmProcessNotFound; } *out = process_info->GetTitleLocation().title_id; return ResultSuccess; } Result GetApplicationProcessId(os::ProcessId *out_process_id) { ProcessListAccessor list(g_process_list); for (auto &process : *list) { if (process.IsApplication()) { *out_process_id = process.GetProcessId(); return ResultSuccess; } } return ResultPmProcessNotFound; } Result AtmosphereGetProcessInfo(Handle *out_process_handle, ncm::TitleLocation *out_loc, os::ProcessId process_id) { ProcessListAccessor list(g_process_list); auto process_info = list->Find(process_id); if (process_info == nullptr) { return ResultPmProcessNotFound; } *out_process_handle = process_info->GetHandle(); *out_loc = process_info->GetTitleLocation(); return ResultSuccess; } /* Hook API. */ Result HookToCreateProcess(Handle *out_hook, ncm::TitleId title_id) { *out_hook = INVALID_HANDLE; ncm::TitleId old_value = ncm::TitleId::Invalid; if (!g_title_id_hook.compare_exchange_strong(old_value, title_id)) { return ResultPmDebugHookInUse; } *out_hook = g_hook_to_create_process_event.GetReadableHandle(); return ResultSuccess; } Result HookToCreateApplicationProcess(Handle *out_hook) { *out_hook = INVALID_HANDLE; bool old_value = false; if (!g_application_hook.compare_exchange_strong(old_value, true)) { return ResultPmDebugHookInUse; } *out_hook = g_hook_to_create_application_process_event.GetReadableHandle(); return ResultSuccess; } Result ClearHook(u32 which) { if (which & HookType_TitleId) { g_title_id_hook = ncm::TitleId::Invalid; } if (which & HookType_Application) { g_application_hook = false; } return ResultSuccess; } /* Boot API. */ Result NotifyBootFinished() { static bool g_has_boot_finished = false; if (!g_has_boot_finished) { boot2::LaunchBootPrograms(); g_has_boot_finished = true; g_boot_finished_event.Signal(); } return ResultSuccess; } Result GetBootFinishedEventHandle(Handle *out) { /* In 8.0.0, Nintendo added this command, which signals that the boot sysmodule has finished. */ /* Nintendo only signals it in safe mode FIRM, and this function aborts on normal FIRM. */ /* We will signal it always, but only allow this function to succeed on safe mode. */ STS_ASSERT(spl::IsRecoveryBoot()); *out = g_boot_finished_event.GetReadableHandle(); return ResultSuccess; } /* Resource Limit API. */ Result BoostSystemMemoryResourceLimit(u64 boost_size) { return resource::BoostSystemMemoryResourceLimit(boost_size); } Result BoostApplicationThreadResourceLimit() { return resource::BoostApplicationThreadResourceLimit(); } Result AtmosphereGetCurrentLimitInfo(u64 *out_cur_val, u64 *out_lim_val, u32 group, u32 resource) { return resource::GetResourceLimitValues(out_cur_val, out_lim_val, static_cast(group), static_cast(resource)); } }