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 <noreply@anthropic.com>
(cherry picked from commit 57f0fa37132bc6c78bc02282044a00e2db859484)
This commit is contained in:
David 2026-10-11 15:01:15 +02:00 committed by Adrià Franch
parent 6ac63bf0cb
commit 4f1ae1cc3a
3 changed files with 116 additions and 5 deletions

View File

@ -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;
}

View File

@ -1,8 +1,19 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libc/include/General.hpp"
#include <chrono>
#include <cstdlib>
#include <fstream>
#include <stdexcept>
#include <string>
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 <typename TCall> static bool Throws(TCall call, const std::string& message) {
try {
call();
} catch (const std::runtime_error& error) {
return error.what() == message;
}
return false;
}
template <typename TCall> 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);
}

View File

@ -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`)