#include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/Shutdown.hpp" #include "prx/libc/include/Api.hpp" #include "prx/libkernel/include/Api.hpp" #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #include #else #include #endif namespace { bool cleaned = false; void Require(bool condition, const char* message) { if (!condition) { std::fprintf(stderr, "Exec fixture: %s\n", message); std::fflush(stderr); std::_Exit(98); } } std::filesystem::path Root() { #ifdef _WIN32 std::wstring value(32768, L'\0'); const auto length = GetEnvironmentVariableW(L"ANYPS5_LOADEXEC_TEST_ROOT", value.data(), static_cast(value.size())); Require(length > 0 && length < value.size(), "missing fixture directory"); value.resize(length); return value; #else const auto* value = std::getenv("ANYPS5_LOADEXEC_TEST_ROOT"); Require(value != nullptr, "missing fixture directory"); return std::filesystem::path(reinterpret_cast(value)); #endif } void RecordPid() { std::ofstream file(Root() / "pids.txt", std::ios::app); #ifdef _WIN32 file << GetCurrentProcessId() << '\n'; #else file << getpid() << '\n'; #endif file.close(); Require(!file.fail(), "writing the process ID file"); } void Record(const char* value) { std::ofstream file(Root() / "events.txt", std::ios::app); file << value << '\n'; file.close(); Require(!file.fail(), "writing the event file"); } std::string Events() { std::ifstream file(Root() / "events.txt"); return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; } void Shutdown() { cleaned = true; Record("shutdown"); } void APS5_VABI Finalize(void*) { Record("guest-exit"); } void HostFinalize() { Record("host-exit"); } void Register() { LibcRegisterShutdown_nid_postfix(Shutdown); Require(__cxa_atexit_nid_postfix(Finalize, nullptr, nullptr) == 0, "guest exit registration"); Require(std::atexit(HostFinalize) == 0, "host exit registration"); } void Arguments(const std::uintptr_t* block, const std::vector& expected) { Require(block[0] == expected.size(), "entry argument count"); const auto* argv = reinterpret_cast(block + 1); Require(getargc_nid_postfix() == static_cast(expected.size()), "getargc argument count"); const auto* kernel = getargv_nid_postfix(); for (std::size_t index = 0; index < expected.size(); ++index) { Require(argv[index] && expected[index] == argv[index], "entry argument value"); Require(kernel[index] && expected[index] == kernel[index], "getargv argument value"); } Require(argv[expected.size()] == nullptr && kernel[expected.size()] == nullptr, "argument terminator"); } std::string LoaderEnvironment() { const auto* value = std::getenv("LD_LIBRARY_PATH"); return value != nullptr ? std::string("LD_LIBRARY_PATH=") + value : std::string(); } std::vector SecondEnvironment() { std::vector entries{"EXEC_SECOND=stage2"}; auto loader = LoaderEnvironment(); if (!loader.empty()) entries.push_back(loader); const auto* root = std::getenv("ANYPS5_LOADEXEC_TEST_ROOT"); Require(root != nullptr, "missing fixture directory for the environment"); entries.emplace_back(std::string("ANYPS5_LOADEXEC_TEST_ROOT=") + root); return entries; } } extern "C" void APS5_VABI ExecFirst_nid_no_patch(const std::uintptr_t* block) { (void)block; RecordPid(); Record("first"); Register(); char* const present[] = {const_cast("/app0/picker/second.bin"), nullptr}; char* const empty[] = {nullptr}; char* const unpaired[] = {const_cast("EXEC_SECOND"), nullptr}; const char* const absent = "/app0/picker/absent.bin"; Require(execv_nid_postfix(nullptr, present) == -1 && *__error_nid_postfix() == 9, "null path rejected with EBADF"); Require(execv_nid_postfix("/app0/picker/second.bin", nullptr) == -1 && *__error_nid_postfix() == 14, "null arguments rejected with EFAULT"); Require(execv_nid_postfix(absent, empty) == -1 && *__error_nid_postfix() == 2, "an empty argument list is accepted and an absent path rejected with ENOENT"); Require(execve_nid_postfix(absent, present, nullptr) == -1 && *__error_nid_postfix() == 2, "a null environment is accepted and an absent path rejected with ENOENT"); Require(execve_nid_postfix(absent, present, unpaired) == -1 && *__error_nid_postfix() == 2, "an unpaired environment entry is copied and an absent path rejected with ENOENT"); Require(execvp_nid_postfix("absent-bin", present) == -1 && *__error_nid_postfix() == 2, "PATH search ends with ENOENT"); static std::string huge(300000, 'X'); char* const oversized[] = {const_cast("/app0/picker/second.bin"), &huge[0], nullptr}; Require(execv_nid_postfix("/app0/picker/second.bin", oversized) == -1 && *__error_nid_postfix() == 7, "oversized arguments rejected with E2BIG"); static std::string largeVector(262201, 'K'); static std::string largeEntry = "EXEC_LARGE_SECOND=" + largeVector; char* const largeEnvironment[] = {&largeEntry[0], nullptr}; Require(execve_nid_postfix(absent, present, largeEnvironment) == -1 && *__error_nid_postfix() == 7, "an environment vector beyond the limit rejected with E2BIG"); char* const beyond[] = {const_cast("/app0/picker/second.bin/child"), nullptr}; Require(execv_nid_postfix("/app0/picker/second.bin/child", beyond) == -1 && *__error_nid_postfix() == 20, "a path beyond a regular file rejected with ENOTDIR"); #ifndef _WIN32 char* const stripped[] = {const_cast("/app0/picker/noexec.bin"), nullptr}; Require(execv_nid_postfix("/app0/picker/noexec.bin", stripped) == -1 && *__error_nid_postfix() == 13, "a regular file without execute permission rejected with EACCES"); #endif #ifdef _WIN32 const auto second = Root() / L"picker.second.bin.exe"; #else const auto second = Root() / "picker.second.bin.elf"; #endif auto intact = second; intact += ".intact"; std::filesystem::rename(second, intact); { std::ofstream broken(second, std::ios::binary); broken << "no image"; broken.close(); Require(execv_nid_postfix("/app0/picker/second.bin", present) == -1 && *__error_nid_postfix() == 8, "a converted target without an executable image rejected with ENOEXEC"); } std::filesystem::remove(second); std::filesystem::rename(intact, second); auto saved = second; saved += ".saved"; std::filesystem::rename(second, saved); bool rejected = false; try { execv_nid_postfix("/app0/picker/second.bin", present); } catch (const std::runtime_error&) { rejected = true; } std::filesystem::rename(saved, second); Require(rejected && Events() == "first\n", "missing conversion rejected without replacing"); std::filesystem::current_path(Root() / "app0" / "working"); Record("launch"); std::vector values = SecondEnvironment(); std::vector environment; for (auto& value : values) environment.push_back(&value[0]); environment.push_back(nullptr); char* const target[] = {const_cast("/app0/picker/second.bin"), const_cast("argument with spaces"), const_cast("quote\"and\\slash\\ Кириллица 東京"), nullptr}; try { execve_nid_postfix("/app0/picker/second.bin", target, environment.data()); } catch (...) { Record("throw"); std::fflush(nullptr); std::_Exit(96); } Require(false, "execve returned to the first executable"); } extern "C" void APS5_VABI ExecSecond_nid_no_patch(const std::uintptr_t* block) { Arguments(block, {"/app0/picker/second.bin", "argument with spaces", "quote\"and\\slash\\ Кириллица 東京"}); #ifdef _WIN32 Require(std::filesystem::equivalent(std::filesystem::current_path(), Root()), "secondary inherits the image directory"); #else Require(std::filesystem::equivalent(std::filesystem::current_path(), Root() / "app0" / "working"), "exec keeps the working directory"); #endif Require(Events() == "first\nlaunch\n", "exec replaced the image without running exit handlers"); const std::vector entries{SecondEnvironment()}; #ifdef _WIN32 char value[64]{}; Require(GetEnvironmentVariableA("EXEC_SECOND", value, 64) == 6 && value == std::string("stage2"), "execve environment entry reached the replacement"); Require(GetEnvironmentVariableA("PATH", value, 64) != 0, "execve keeps the inherited host variables"); Require(GetEnvironmentVariableA("ANYPS5_LOADEXEC_PARENT", value, 64) == 0, "parent wait variable was retained"); Require(GetEnvironmentVariableA("ANYPS5_LOADEXEC_ARGV_SKIP", value, 64) == 0, "argument marker was retained"); #else extern char** environ; Require(environ[0] != nullptr, "replacement environment present"); for (const auto& entry : entries) { bool found = false; for (std::size_t index = 0; environ[index] != nullptr; ++index) found = found || std::strcmp(environ[index], entry.c_str()) == 0; Require(found, "replacement environment entry value"); } bool hostPath = false; for (std::size_t index = 0; environ[index] != nullptr; ++index) hostPath = hostPath || std::strncmp(environ[index], "PATH=", 5) == 0; Require(hostPath, "exec keeps the inherited host variables"); Require(!cleaned, "secondary inherited old runtime state"); #endif RecordPid(); Record("second"); std::fflush(nullptr); std::_Exit(37); }