Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions core/libs/prx/libSceSystemService/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
add_library(libSceSystemService SHARED EXCLUDE_FROM_ALL Export.cpp)
add_library(libSceSystemService SHARED EXCLUDE_FROM_ALL Export.cpp LoadExec.cpp)
target_include_directories(libSceSystemService PRIVATE ${LIBS_INCLUDE_DIR})
configure_windows_unwind(libSceSystemService)

Expand All @@ -12,12 +12,15 @@ if(BUILD_TESTING)
add_test(NAME guest_quick_exit COMMAND guest_system_exit_tests --quick-exit)
add_test(NAME guest_immediate_exit COMMAND guest_system_exit_tests --immediate)
add_test(NAME guest_return_from_main COMMAND guest_system_exit_tests --return-from-main)
add_test(NAME guest_load_exec_restart COMMAND guest_system_exit_tests --load-exec)
set_tests_properties(guest_system_exit PROPERTIES
PASS_REGULAR_EXPRESSION "Guest shutdown and atexit completed")
set_tests_properties(guest_quick_exit PROPERTIES
PASS_REGULAR_EXPRESSION "Guest quick_exit handlers completed")
set_tests_properties(guest_load_exec_restart PROPERTIES
PASS_REGULAR_EXPRESSION "Guest shut down before the restart[\r\n]+Guest restarted with its arguments")
if(WIN32)
set_tests_properties(guest_system_exit guest_system_exit_parameters guest_immediate_exit guest_return_from_main guest_quick_exit PROPERTIES ENVIRONMENT_MODIFICATION
set_tests_properties(guest_system_exit guest_system_exit_parameters guest_immediate_exit guest_return_from_main guest_quick_exit guest_load_exec_restart PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(guest_system_service_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemService.cpp)
Expand Down
17 changes: 12 additions & 5 deletions core/libs/prx/libSceSystemService/Export.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,24 @@
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include "prx/libSceSystemService/SystemService.hpp"
#include "prx/libSceSystemService/LoadExec.hpp"
#include <string>
#include <vector>

extern "C" {

int APS5_VABI sceSystemServiceLoadExec(const char* path, const char* const* arguments) {
if (!path || !*path) return SYSTEM_SERVICE_ERROR_PARAMETER;
if (std::strcmp(path, "exit") != 0) {
NotImplemented_nid_no_patch("sceSystemServiceLoadExec: executable replacement");
if (std::strcmp(path, "exit") == 0) {
LibcRunShutdown_nid_postfix();
std::exit(0);
}
(void)arguments;
LibcRunShutdown_nid_postfix();
std::exit(0);
if (std::strcmp(path, "/app0/eboot.bin") != 0) {
NotImplemented_nid_no_patch("sceSystemServiceLoadExec: executable other than the running title");
}
std::vector<std::string> list;
for (auto argument = arguments; argument != nullptr && *argument != nullptr; ++argument) list.emplace_back(*argument);
SystemService::RestartProcess(list);
}

int APS5_VABI sceSystemServiceDisableNoticeScreenSkipFlagAutoSet(void) {
Expand Down
182 changes: 182 additions & 0 deletions core/libs/prx/libSceSystemService/LoadExec.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#else
#include <csignal>
#include <fcntl.h>
#include <filesystem>
#include <unistd.h>
#endif

#include "prx/libSceSystemService/LoadExec.hpp"
#include "prx/libc/include/Shutdown.hpp"
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include <cerrno>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <iterator>
#include <limits>
#include <stdexcept>
#include <string>
#include <system_error>
#include <vector>

#ifdef _WIN32

namespace {

constexpr wchar_t PreviousProcessVariable[] = L"ANYPS5_LOAD_EXEC_PREVIOUS_PROCESS";
constexpr std::size_t CommandLineCapacity = 32767;

std::wstring Widen(const std::string& text) {
if (text.empty()) return {};
if (text.size() > static_cast<std::size_t>(std::numeric_limits<int>::max()))
throw std::length_error("sceSystemServiceLoadExec: argument too long");
const auto size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), static_cast<int>(text.size()), nullptr, 0);
if (size == 0) throw std::invalid_argument("sceSystemServiceLoadExec: argument is not UTF-8");
std::wstring result(static_cast<std::size_t>(size), L'\0');
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), static_cast<int>(text.size()), result.data(), size);
return result;
}

void AppendArgument(std::wstring& commandLine, const std::wstring& argument) {
if (!commandLine.empty()) commandLine += L' ';
if (!argument.empty() && argument.find_first_of(L" \t\n\v\"") == std::wstring::npos) {
commandLine += argument;
return;
}
commandLine += L'"';
std::size_t backslashes = 0;
for (const auto character : argument) {
if (character == L'\\') {
++backslashes;
continue;
}
commandLine.append(character == L'"' ? backslashes * 2 + 1 : backslashes, L'\\');
commandLine += character;
backslashes = 0;
}
commandLine.append(backslashes * 2, L'\\');
commandLine += L'"';
}

std::wstring ExecutablePath() {
std::wstring path(CommandLineCapacity, L'\0');
const auto size = GetModuleFileNameW(nullptr, path.data(), static_cast<DWORD>(path.size()));
if (size == 0) throw std::system_error(static_cast<int>(GetLastError()), std::system_category(), "sceSystemServiceLoadExec: executable path");
if (size >= path.size()) throw std::length_error("sceSystemServiceLoadExec: executable path too long");
path.resize(size);
return path;
}

class PreviousProcessWait final {
public:
PreviousProcessWait() {
wchar_t value[32]{};
const auto length = GetEnvironmentVariableW(PreviousProcessVariable, value, static_cast<DWORD>(std::size(value)));
if (length == 0) return;
if (_wputenv_s(PreviousProcessVariable, L"") != 0)
throw std::runtime_error("sceSystemServiceLoadExec: cannot clear the previous process variable");
if (length >= std::size(value)) throw std::runtime_error("sceSystemServiceLoadExec: invalid previous process handle");
wchar_t* end = nullptr;
const auto handle = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(std::wcstoull(value, &end, 10)));
if (end == value || *end != L'\0') throw std::runtime_error("sceSystemServiceLoadExec: invalid previous process handle");
const auto waited = WaitForSingleObject(handle, INFINITE);
CloseHandle(handle);
if (waited != WAIT_OBJECT_0) throw std::runtime_error("sceSystemServiceLoadExec: waiting for the previous process failed");
}
};

const PreviousProcessWait previousProcessWait;

}

[[noreturn]] void SystemService::RestartProcess(const std::vector<std::string>& arguments) {
const auto executable = ExecutablePath();
std::wstring commandLine;
AppendArgument(commandLine, executable);
for (const auto& argument : arguments) AppendArgument(commandLine, Widen(argument));
if (commandLine.size() >= CommandLineCapacity) throw std::length_error("sceSystemServiceLoadExec: command line too long");

HANDLE self = nullptr;
if (!DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(), GetCurrentProcess(), &self, SYNCHRONIZE, TRUE, 0))
throw std::system_error(static_cast<int>(GetLastError()), std::system_category(), "sceSystemServiceLoadExec: process handle");
const auto value = std::to_wstring(reinterpret_cast<std::uintptr_t>(self));
if (!SetEnvironmentVariableW(PreviousProcessVariable, value.c_str())) {
const auto error = GetLastError();
CloseHandle(self);
throw std::system_error(static_cast<int>(error), std::system_category(), "sceSystemServiceLoadExec: previous process variable");
}
STARTUPINFOW startup{};
startup.cb = sizeof(startup);
PROCESS_INFORMATION process{};
const auto created = CreateProcessW(executable.c_str(), commandLine.data(), nullptr, nullptr, TRUE, 0, nullptr, nullptr, &startup, &process);
const auto error = GetLastError();
SetEnvironmentVariableW(PreviousProcessVariable, nullptr);
if (!created) {
CloseHandle(self);
throw std::system_error(static_cast<int>(error), std::system_category(), "sceSystemServiceLoadExec: starting the title");
}
CloseHandle(process.hThread);
CloseHandle(process.hProcess);

LibcRunShutdown_nid_postfix();
std::fflush(nullptr);
TerminateProcess(GetCurrentProcess(), 0);
std::_Exit(0);
}

#else

extern "C" const char** APS5_VABI getargv_nid_postfix(void);

namespace {

void ResetSignals() {
sigset_t none;
sigemptyset(&none);
if (pthread_sigmask(SIG_SETMASK, &none, nullptr) != 0)
throw std::runtime_error("sceSystemServiceLoadExec: cannot unblock signals");
for (int number = 1; number < NSIG; ++number) {
struct sigaction action {};
if (sigaction(number, nullptr, &action) != 0 || action.sa_handler != SIG_IGN) continue;
action.sa_handler = SIG_DFL;
sigaction(number, &action, nullptr);
}
}

void CloseDescriptorsOnExec() {
std::vector<int> descriptors;
for (const auto& entry : std::filesystem::directory_iterator("/proc/self/fd")) {
const auto descriptor = std::stoi(entry.path().filename().string());
if (descriptor > STDERR_FILENO) descriptors.push_back(descriptor);
}
for (const auto descriptor : descriptors) {
const auto flags = fcntl(descriptor, F_GETFD);
if (flags >= 0) fcntl(descriptor, F_SETFD, flags | FD_CLOEXEC);
}
}

}

[[noreturn]] void SystemService::RestartProcess(const std::vector<std::string>& arguments) {
const auto current = getargv_nid_postfix();
if (current == nullptr || current[0] == nullptr) throw std::runtime_error("sceSystemServiceLoadExec: no program name");
std::vector<char*> argv;
argv.push_back(const_cast<char*>(current[0]));
for (const auto& argument : arguments) argv.push_back(const_cast<char*>(argument.c_str()));
argv.push_back(nullptr);

LibcRunShutdown_nid_postfix();
std::fflush(nullptr);
CloseDescriptorsOnExec();
ResetSignals();
execv("/proc/self/exe", argv.data());
throw std::system_error(errno, std::generic_category(), "sceSystemServiceLoadExec: starting the title");
}

#endif
13 changes: 13 additions & 0 deletions core/libs/prx/libSceSystemService/LoadExec.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#ifndef CORE_LIBS_PRX_LIBSCESYSTEMSERVICE_LOADEXEC_HPP
#define CORE_LIBS_PRX_LIBSCESYSTEMSERVICE_LOADEXEC_HPP

#include <string>
#include <vector>

namespace SystemService {

[[noreturn]] void RestartProcess(const std::vector<std::string>& arguments);

} // namespace SystemService

#endif
112 changes: 112 additions & 0 deletions core/libs/tests/GuestSystemExit.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <shellapi.h>
#else
#include <csignal>
#include <pthread.h>
#include <unistd.h>
#endif
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libc/include/Shutdown.hpp"
#include "prx/libSceSystemService/SystemService.hpp"
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <stdexcept>
#include <cstring>
#include <string>
#include <thread>
#include <vector>
extern "C" int APS5_VABI sceSystemServiceLoadExec(const char*, const char* const*);
extern "C" void APS5_VABI _Exit_nid_postfix(int);
extern "C" void APS5_VABI catchReturnFromMain_nid_postfix(int);
Expand All @@ -27,8 +43,97 @@ void APS5_VABI QuickFirst() {
}
void Require(bool value) { if (!value) std::abort(); }
void UnexpectedCleanup() { std::_Exit(3); }
void ReportShutdown() {
std::this_thread::sleep_for(std::chrono::milliseconds(500));
std::puts("Guest shut down before the restart");
std::fflush(stdout);
}
const std::vector<std::string> restartArguments{"--load-exec-restarted", "two words", "quote\"d", "trailing\\", "back\\\\\"slash", "\xc3\xbc", ""};
std::uint64_t ProcessId() {
#ifdef _WIN32
return GetCurrentProcessId();
#else
return static_cast<std::uint64_t>(getpid());
#endif
}
std::vector<std::string> Arguments(int argc, char** argv) {
#ifdef _WIN32
(void)argc;
(void)argv;
int count = 0;
const auto wide = CommandLineToArgvW(GetCommandLineW(), &count);
Require(wide != nullptr);
std::vector<std::string> result;
for (int index = 0; index < count; ++index) {
const auto size = WideCharToMultiByte(CP_UTF8, 0, wide[index], -1, nullptr, 0, nullptr, nullptr);
Require(size > 0);
std::string text(static_cast<std::size_t>(size), '\0');
WideCharToMultiByte(CP_UTF8, 0, wide[index], -1, text.data(), size, nullptr, nullptr);
text.pop_back();
result.push_back(text);
}
LocalFree(wide);
return result;
#else
return {argv, argv + argc};
#endif
}
void ChangeSignals() {
#ifndef _WIN32
struct sigaction ignore {};
ignore.sa_handler = SIG_IGN;
Require(sigaction(SIGPIPE, &ignore, nullptr) == 0);
sigset_t blocked;
sigemptyset(&blocked);
sigaddset(&blocked, SIGUSR1);
Require(pthread_sigmask(SIG_BLOCK, &blocked, nullptr) == 0);
#endif
}
bool SignalsReset() {
#ifdef _WIN32
return true;
#else
struct sigaction action {};
if (sigaction(SIGPIPE, nullptr, &action) != 0 || action.sa_handler != SIG_DFL) return false;
sigset_t blocked;
if (pthread_sigmask(SIG_BLOCK, nullptr, &blocked) != 0) return false;
return sigismember(&blocked, SIGUSR1) == 0;
#endif
}
bool PreviousProcessEnded(std::uint64_t id) {
#ifdef _WIN32
if (GetEnvironmentVariableW(L"ANYPS5_LOAD_EXEC_PREVIOUS_PROCESS", nullptr, 0) != 0) return false;
const auto process = OpenProcess(SYNCHRONIZE, FALSE, static_cast<DWORD>(id));
if (process == nullptr) return true;
const auto ended = WaitForSingleObject(process, 0) == WAIT_OBJECT_0;
CloseHandle(process);
return ended;
#else
return id == ProcessId();
#endif
}
}
int main(int argc, char** argv) {
if (argc > 1 && std::strcmp(argv[1], "--load-exec") == 0) {
LibcRegisterShutdown_nid_postfix(ReportShutdown);
ChangeSignals();
const auto previous = std::to_string(ProcessId());
std::vector<const char*> arguments;
for (const auto& argument : restartArguments) arguments.push_back(argument.c_str());
arguments.push_back(previous.c_str());
arguments.push_back(nullptr);
sceSystemServiceLoadExec("/app0/eboot.bin", arguments.data());
return 2;
}
if (argc > 1 && std::strcmp(argv[1], "--load-exec-restarted") == 0) {
const auto received = Arguments(argc, argv);
Require(received.size() == restartArguments.size() + 2);
Require(std::vector<std::string>(received.begin() + 1, received.end() - 1) == restartArguments);
Require(PreviousProcessEnded(std::stoull(received.back())));
Require(SignalsReset());
std::puts("Guest restarted with its arguments");
return 0;
}
if (argc > 1 && std::strcmp(argv[1], "--immediate") == 0) {
LibcRegisterShutdown_nid_postfix(UnexpectedCleanup);
Require(std::atexit(UnexpectedCleanup) == 0);
Expand Down Expand Up @@ -57,4 +162,11 @@ int main(int argc, char** argv) {
try { sceSystemServiceLoadExec("/app0/another.bin", nullptr); }
catch (const std::runtime_error&) { rejected = true; }
Require(rejected);
#ifdef _WIN32
const char* const invalid[] = {"\xff", nullptr};
bool invalidRejected = false;
try { sceSystemServiceLoadExec("/app0/eboot.bin", invalid); }
catch (const std::invalid_argument&) { invalidRejected = true; }
Require(invalidRejected);
#endif
}
1 change: 1 addition & 0 deletions docs/dev/TechnicalDebt.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ Throughout the project, every function at every stage either **does exactly what
- [sceSystemServiceDisableMediaPlay](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature
- [sceSystemServiceReenableMediaPlay](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature
- [sceSystemServiceParamGetString](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name; other parameters and buffers of 1 to 64 bytes throw
- [sceSystemServiceLoadExec](../../core/libs/prx/libSceSystemService/LoadExec.cpp) (libSceSystemService) - restarts only the running title (`/app0/eboot.bin`); other executables throw because the relinker converts only the title's executable. The argument array is taken as null-terminated and without the program name, as [shadPS4](https://github.com/shadps4-emu/shadPS4) passes it; the restarted title keeps the host `argv[0]`. Not confirmed on a PS5 title. Linux replaces the process with `execv` after the runtime shutdown, without `atexit` handlers, and marks descriptors above 2 close-on-exec. Windows has no process replacement: the new process waits for the old one to end (an inherited handle named by `ANYPS5_LOAD_EXEC_PREVIOUS_PROCESS`) while libSceSystemService loads, before the title starts, so the two never hold direct memory at once, and a caller waiting for the first process (`--autorun`) sees it end with 0. Arguments that are not UTF-8 throw on Windows.
- [sceNgs2SystemSetSampleRate, sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A sample rate of 0 and a null user data pointer throw, any other rate is accepted, and changing the rate while a sampler filter is enabled throws because the filter coefficients were computed for the old rate. Lock and unlock take the same lock as `sceNgs2RackLock`
- [sceNgs2RackGetInfo](../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - field order from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2RackInfo` with the 64-byte name of the other info structures (216 bytes); `type` is assumed to be the rack id divided by 0x1000, `min_grain_samples` 64 (the system minimum) and `state_flags` 1; `uid` comes from a per-process rack counter, `render_count` is the owning system's render count and `active_voice_count` counts voices that are not empty (paused and stopped voices included). The last process ratio and tick and `active_channel_work_count` are left zero
- [sceNgs2VoiceQueryInfo](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - only info id 0x4001 is known: PPSA22520 passes an 8-byte buffer and branches on the first word being 1 or 2, so it is assumed to be the voice channel count (0 before the voice is set up); the second word is zeroed and other ids throw
Expand Down
Loading