Files
AnyPS5/core/libs/tests/GuestExecHost.cpp
T
JJenkxandClaude Opus 5.5 ee5055268b fix(tests): pass guest_system_font, exec_arguments and unwrap_self under a non-ASCII TEMP on Windows
With %TEMP% outside ASCII (a profile like C:\Users\José), three tests
fail on Windows because they mix the two narrow encodings: MinGW's
std::filesystem::path::string() is UTF-8, while fopen, getenv and console
tools use the ANSI or OEM code page.

- guest_system_font opened its stderr capture file with
  fopen(path.string()); it now uses _wfopen on Windows.
- The exec_arguments fixture built the second program's environment from
  getenv, which returns ANSI bytes that execve rejects as invalid UTF-8;
  it now reads LD_LIBRARY_PATH and ANYPS5_LOADEXEC_TEST_ROOT as UTF-8
  through GetEnvironmentVariableW.
- unwrap_self decoded icacls output as UTF-8 (Python 3.15 UTF-8 mode);
  icacls writes the OEM code page, so it is decoded as "oem".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 15:20:20 -04:00

246 lines
10 KiB
C++

#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 <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <stdexcept>
#include <string>
#include <vector>
#ifdef _WIN32
#include <windows.h>
#else
#include <unistd.h>
#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<DWORD>(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<const char8_t*>(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<char>(file), std::istreambuf_iterator<char>()};
}
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<std::string>& expected) {
Require(block[0] == expected.size(), "entry argument count");
const auto* argv = reinterpret_cast<const char* const*>(block + 1);
Require(getargc_nid_postfix() == static_cast<int>(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");
}
bool Utf8Environment(const char* name, std::string& value) {
#ifdef _WIN32
const std::wstring wideName = std::filesystem::path(name).wstring();
std::wstring wide(32768, L'\0');
const auto length = GetEnvironmentVariableW(wideName.c_str(), wide.data(), static_cast<DWORD>(wide.size()));
if (length == 0 || length >= wide.size()) return false;
wide.resize(length);
const auto converted = std::filesystem::path(wide).u8string();
value.assign(reinterpret_cast<const char*>(converted.data()), converted.size());
return true;
#else
const auto* found = std::getenv(name);
if (found == nullptr) return false;
value = found;
return true;
#endif
}
std::string LoaderEnvironment() {
std::string value;
return Utf8Environment("LD_LIBRARY_PATH", value) ? "LD_LIBRARY_PATH=" + value : std::string();
}
std::vector<std::string> SecondEnvironment() {
std::vector<std::string> entries{"EXEC_SECOND=stage2"};
auto loader = LoaderEnvironment();
if (!loader.empty()) entries.push_back(loader);
std::string root;
Require(Utf8Environment("ANYPS5_LOADEXEC_TEST_ROOT", root), "missing fixture directory for the environment");
entries.emplace_back("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<char*>("/app0/picker/second.bin"), nullptr};
char* const empty[] = {nullptr};
char* const unpaired[] = {const_cast<char*>("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<char*>("/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<char*>("/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<char*>("/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<std::string> values = SecondEnvironment();
std::vector<char*> environment;
for (auto& value : values) environment.push_back(&value[0]);
environment.push_back(nullptr);
char* const target[] = {const_cast<char*>("/app0/picker/second.bin"), const_cast<char*>("argument with spaces"),
const_cast<char*>("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<std::string> 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);
}