From 4f1ae1cc3a090eb08992d408f2bf9b0832cf5cbc Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 15:01:15 +0200 Subject: [PATCH] feat(libkernel): implement execv and execve as throwing exports XPSemu PPSA97358 imports both from libkernel.prx; with the imports ahead of them covered, its predicted next stops (relink registry order, not an observed run) are: FAIL: unresolved ELF import KaK52lYTBIs from libkernel.prx (execv, 7 call sites) FAIL: unresolved ELF import kdguLiAheLI from libkernel.prx (execve, 4 call sites) Today the title stops earlier at the observed FAIL: unresolved ELF import aRo9AhFUXcM from libkernel.prx (sigsetjmp, merged in #3486) As for execvp (#2827), the process image cannot be replaced here, so both throw with the execvp message pattern once FreeBSD 11's execve would start loading the image (freebsd-src 8c639945, sys/kern/kern_exec.c exec_copyin_args, do_execve, exec_check_permissions; lib/libc/gen/exec.c execv passes environ). Before that they fail as FreeBSD does: EFAULT for a null argument vector or path, ENOENT and ENOTDIR from the lookup as stat reports it, EACCES for a path that is not a regular file. Paths of PATH_MAX bytes or more and strings over ARG_MAX throw instead of guessing the console's limits. The execv calls in XPSemu's sources run in a fork child (net/tap.c), and its fork fails with ENOSYS (ZiZc3/XPSemu 8eec07b4, ps5/compat/libc_missing.c, ps5/build-title.sh --defsym). NIDs computed with tools/nid_names.py and confirmed in aerolib. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 57f0fa37132bc6c78bc02282044a00e2db859484) --- .../libs/prx/libkernel/System/src/Process.cpp | 47 +++++++++++- core/libs/tests/GuestExecvp.cpp | 72 ++++++++++++++++++- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 116 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 0d7b1a5bf..5ca87e102 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -40,6 +40,7 @@ #endif extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" int APS5_VABI stat_nid_postfix(const char* path, FileStat* sb); namespace { @@ -154,6 +155,37 @@ std::int64_t toGuestLimit(rlim_t value) { } #endif +constexpr int guestNoEntry = 2; +constexpr int guestAccess = 13; +constexpr int guestNotDirectory = 20; +constexpr std::size_t guestPathMax = 1024; +constexpr std::size_t guestArgumentMax = 262144; +constexpr std::uint16_t guestFileType = 0170000; +constexpr std::uint16_t guestRegularFile = 0100000; + +std::size_t addStringSpace(std::size_t space, const char* const* strings) { + for (; strings != nullptr && *strings != nullptr; ++strings) + space += std::strlen(*strings) + 1; + return space; +} + +int executableError(const char* function, const char* path, const char* const* arguments, const char* const* environment) { + if (arguments == nullptr || path == nullptr) + return guestFault; + if (strnlen(path, guestPathMax) == guestPathMax) + throw std::runtime_error(std::string(function) + ": path of PATH_MAX bytes or more"); + if (addStringSpace(addStringSpace(0, arguments), environment) > guestArgumentMax) + throw std::runtime_error(std::string(function) + ": arguments and environment exceed ARG_MAX"); + FileStat status{}; + if (stat_nid_postfix(path, &status) != 0) { + const int error = *__error_nid_postfix(); + if (error != guestNoEntry && error != guestNotDirectory) + throw std::runtime_error(std::string(function) + ": unsupported lookup error " + std::to_string(error)); + return error; + } + return (status.st_mode & guestFileType) == guestRegularFile ? 0 : guestAccess; +} + } extern "C" int* APS5_VABI __error_nid_postfix(); @@ -340,9 +372,20 @@ int APS5_VABI execvp_nid_postfix(const char* file, char* const* arguments) { return -1; } +int APS5_VABI execve_nid_postfix(const char* path, char* const* arguments, char* const* environment) { + if (const int error = executableError("execve", path, arguments, environment)) { + *__error_nid_postfix() = error; + return -1; + } + NotImplemented_nid_no_patch("execve: executable replacement"); + return -1; +} + int APS5_VABI execv_nid_postfix(const char* path, char* const* arguments) { - (void)path; - (void)arguments; + if (const int error = executableError("execv", path, arguments, environ_nid_postfix)) { + *__error_nid_postfix() = error; + return -1; + } NotImplemented_nid_no_patch("execv: executable replacement"); return -1; } diff --git a/core/libs/tests/GuestExecvp.cpp b/core/libs/tests/GuestExecvp.cpp index ebf28b63a..950c21294 100644 --- a/core/libs/tests/GuestExecvp.cpp +++ b/core/libs/tests/GuestExecvp.cpp @@ -1,8 +1,19 @@ -#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/General.hpp" +#include #include +#include #include #include -extern "C" int APS5_VABI execvp_nid_postfix(const char*, char* const*); +extern "C" { +int APS5_VABI execvp_nid_postfix(const char*, char* const*); +int APS5_VABI execv_nid_postfix(const char*, char* const*); +int APS5_VABI execve_nid_postfix(const char*, char* const*, char* const*); +int* APS5_VABI __error_nid_postfix(); +} +static constexpr int GUEST_ENOENT = 2; +static constexpr int GUEST_EACCES = 13; +static constexpr int GUEST_EFAULT = 14; +static constexpr int GUEST_ENOTDIR = 20; static void Require(bool value) { if (!value) std::abort(); } static bool ReportsNotImplemented(const char* file, char* const* arguments) { try { @@ -12,6 +23,22 @@ static bool ReportsNotImplemented(const char* file, char* const* arguments) { } return false; } +template static bool Throws(TCall call, const std::string& message) { + try { + call(); + } catch (const std::runtime_error& error) { + return error.what() == message; + } + return false; +} +template static bool Fails(TCall call, int error) { + *__error_nid_postfix() = 0; + return call() == -1 && *__error_nid_postfix() == error; +} +static void RequireBoth(const char* path, char* const* arguments, int error) { + Require(Fails([&] { return execv_nid_postfix(path, arguments); }, error)); + Require(Fails([&] { return execve_nid_postfix(path, arguments, nullptr); }, error)); +} int main() { char shell[] = "sh"; char option[] = "-c"; @@ -20,4 +47,45 @@ int main() { Require(ReportsNotImplemented("sh", arguments)); Require(ReportsNotImplemented("/bin/sh", arguments)); Require(ReportsNotImplemented(nullptr, nullptr)); + + const auto name = "anyps5-exec-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + const auto directory = std::filesystem::temp_directory_path() / name; + Require(std::filesystem::create_directory(directory)); + { std::ofstream output(directory / "program"); output << "program"; } + AddPathAlias_nid_no_patch("/exec-alias", directory.string().c_str()); + char variable[] = "HOME=/"; + char* environment[] = {variable, nullptr}; + + RequireBoth(nullptr, arguments, GUEST_EFAULT); + RequireBoth("/exec-alias/program", nullptr, GUEST_EFAULT); + RequireBoth(nullptr, nullptr, GUEST_EFAULT); + RequireBoth("/exec-alias/missing", arguments, GUEST_ENOENT); + RequireBoth("/exec-alias/missing/program", arguments, GUEST_ENOENT); + RequireBoth("", arguments, GUEST_ENOENT); + RequireBoth("/exec-alias/program/", arguments, GUEST_ENOTDIR); + RequireBoth("/exec-alias", arguments, GUEST_EACCES); + RequireBoth("/exec-alias/", arguments, GUEST_EACCES); + + Require(Throws([&] { execv_nid_postfix("/exec-alias/program", arguments); }, "execv: executable replacement not implemented")); + Require(Throws([&] { execve_nid_postfix("/exec-alias/program", arguments, environment); }, "execve: executable replacement not implemented")); + Require(Throws([&] { execve_nid_postfix("/exec-alias/program", arguments, nullptr); }, "execve: executable replacement not implemented")); + + std::string fittingPath = "/exec-alias/"; + while (fittingPath.size() < 1016) fittingPath += "./"; + fittingPath += "missing"; + Require(fittingPath.size() == 1023); + RequireBoth(fittingPath.c_str(), arguments, GUEST_ENOENT); + const std::string longPath = fittingPath + "s"; + Require(Throws([&] { execv_nid_postfix(longPath.c_str(), arguments); }, "execv: path of PATH_MAX bytes or more")); + Require(Throws([&] { execve_nid_postfix(longPath.c_str(), arguments, nullptr); }, "execve: path of PATH_MAX bytes or more")); + + std::string fitting(262143, 'a'); + char* fittingArguments[] = {fitting.data(), nullptr}; + RequireBoth("/exec-alias/missing", fittingArguments, GUEST_ENOENT); + char* overflowing[] = {fitting.data(), variable, nullptr}; + Require(Throws([&] { execv_nid_postfix("/exec-alias/missing", overflowing); }, "execv: arguments and environment exceed ARG_MAX")); + Require(Throws([&] { execve_nid_postfix("/exec-alias/missing", fittingArguments, environment); }, "execve: arguments and environment exceed ARG_MAX")); + + RemovePathAlias_nid_no_patch("/exec-alias"); + std::filesystem::remove_all(directory); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 03abba3e1..cdf489a72 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -130,7 +130,7 @@ - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - only the calling thread or the calling process is handled. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified - [dup and dup2](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throw for the combinations they do not model: `dup2` of a guest socket onto descriptors 0 to 2, whose host streams libc writes without the socket table, and on Windows duplicating a directory descriptor. Guest sockets share one socket object between the duplicates; the numbers come from the host descriptor table like those of files. The errno values for descriptor exhaustion are not verified against the console. - [waitpid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns -1 with `ECHILD` (`EINVAL` for unknown options): nothing here creates a child process; child processes would need a real `waitpid`. -- [execv](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported, the same as `execvp`. PPSA97358 imports it and fails to load without it. +- [execv, execve](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throw like `execvp` once FreeBSD's [`execve`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_exec.c) would start loading the image: replacing the process image is not supported. The checks before that are FreeBSD's: `EFAULT` for a null argument vector or path, `ENOENT` and `ENOTDIR` from the path lookup (as `stat` reports them) and `EACCES` for a path that is not a regular file. Other lookup errors, a path of `PATH_MAX` (1024) bytes or more and argument and environment strings over `ARG_MAX` (262144) throw instead of returning `ENAMETOOLONG` or `E2BIG`, and whether a regular file may be executed (mount `noexec`, execute bits, image format) is not checked. `execv` passes `environ`. PPSA97358 imports both and fails to load without them; the `execv` calls in its sources run in a `fork` child (`net/tap.c`), and its `fork` and `vfork` fail with `ENOSYS` ([libc_missing.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/ps5/compat/libc_missing.c#L202-L203), bound by [build-title.sh](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/ps5/build-title.sh#L106-L115)). The callers of its `execve` imports were not identified. - [kern.proc.pathname](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns `/app0/eboot.bin` for pid -1 and the calling process, following FreeBSD's `sysctl_kern_proc_pathname`; the exact pathname on retail consoles remains unverified. - [machdep.cacheline_size](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns -1 with `ENOENT`, matching FreeBSD amd64 where this sysctl OID is not defined. - [hw.model](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the host CPU's name, built as FreeBSD 11 builds `hw.model`: the CPUID brand string (leaves 0x80000002 to 0x80000004) without leading spaces ([identcpu.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/identcpu.c#L683-L691)). Trailing padding stays, as it does there, so an AMD host's name ends in spaces. The name a PS5 reports is unknown. A write returns -1 with `EPERM`. A host without an x86 CPUID brand string, such as an Apple Silicon Mac, throws. PPSA99203 only prints it (`[console] hw.model: %s`)