feat(libkernel_sys): add chroot with FreeBSD lookup errors

Porpoise (PPSA99764) imports chroot (AKknh8Cx-V8) from
libkernel_sys.prx, not libkernel.prx, one reference. Predicted stop
once pathconf, the pthread_attr calls and ftrylockfile ahead of it are
covered (not a run):

  FAIL: unresolved ELF import AKknh8Cx-V8 from libkernel_sys.prx

The NID is computed with tools/nid_names.py and matches aerolib.

Follows FreeBSD 11.4 sys/kern/vfs_syscalls.c sys_chroot and change_dir
(8c639945a44588f6cfdddfb057bb5aad88735da0) for a root caller up to the
lookup: EFAULT for a null path, ENOENT for an empty or missing one,
ENOTDIR for a file or a path through a file. A directory that is found
throws with the path named: the resolver's root is fixed and the mount
aliases (/app0, /savedata0) are host paths that do not move with it,
so it cannot be re-based, and a real chroot is not modelled. Nothing
shows that Porpoise calls it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
screamm 2026-10-11 16:24:45 +02:00 committed by p3nicillin
parent 3faa9cb400
commit dc0d316fda
3 changed files with 33 additions and 0 deletions

View File

@ -369,6 +369,18 @@ int APS5_VABI fchdir_nid_postfix(int fd) {
return 0;
}
int APS5_VABI chroot_nid_postfix(const char* path) {
if (path == nullptr) return static_cast<int>(Fail(GuestEfault));
if (*path == '') return static_cast<int>(Fail(GuestEnoent));
const auto native = ResolvePath_nid_no_patch(path);
std::error_code error;
const auto status = std::filesystem::status(native, error);
if (status.type() == std::filesystem::file_type::not_found) return static_cast<int>(Fail(HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestEnoent));
if (error) return static_cast<int>(Fail(GuestError(error)));
if (!std::filesystem::is_directory(status)) return static_cast<int>(Fail(GuestEnotdir));
throw std::runtime_error(std::string("chroot: changing the root directory to ") + path + " is not supported, the guest path resolver keeps a fixed root");
}
int APS5_VABI symlink_nid_postfix(const char* target, const char* created) {
if (target == nullptr || created == nullptr) return static_cast<int>(Fail(GuestEfault));
if (*target == '\0' || *created == '\0') return static_cast<int>(Fail(GuestEnoent));

View File

@ -30,6 +30,7 @@ std::int64_t APS5_VABI readlink_nid_postfix(const char*, char*, std::size_t);
std::int64_t APS5_VABI pathconf_nid_postfix(const char*, int);
int APS5_VABI fchdir_nid_postfix(int);
int APS5_VABI chdir_nid_postfix(const char*);
int APS5_VABI chroot_nid_postfix(const char*);
char* APS5_VABI getcwd_nid_postfix(char*, std::size_t);
int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t);
int APS5_VABI sceKernelClose(int);
@ -66,6 +67,15 @@ bool PathconfThrows(const char* path, int name) {
return false;
}
bool ChrootThrows(const std::string& path) {
try {
chroot_nid_postfix(path.c_str());
} catch (const std::runtime_error& error) {
return std::strstr(error.what(), path.c_str()) != nullptr;
}
return false;
}
void CheckPathconf(const std::filesystem::path& root, const std::string& file) {
const auto directory = root.generic_string();
for (const auto& path : {file, directory}) {
@ -215,6 +225,16 @@ int main() {
Require(sceKernelClose(directory) == 0);
Require(fchdir_nid_postfix(directory) == -1 && Error() == 9);
Require(chdir_nid_postfix("/") == 0);
Require(chroot_nid_postfix(nullptr) == -1 && Error() == 14);
Require(chroot_nid_postfix("") == -1 && Error() == 2);
Require(chroot_nid_postfix((root / "missing").string().c_str()) == -1 && Error() == 2);
Require(chroot_nid_postfix(file.c_str()) == -1 && Error() == 20);
Require(chroot_nid_postfix((root / "file.txt" / "child").string().c_str()) == -1 && Error() == 20);
Require(chroot_nid_postfix((file + "/").c_str()) == -1 && Error() == 20);
Require(ChrootThrows(root.string()));
Require(ChrootThrows("/"));
Require(getcwd_nid_postfix(directoryPath, sizeof(directoryPath)) == directoryPath && std::strcmp(directoryPath, "/") == 0);
Require(readlink_nid_postfix(file.c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22);
const auto symbolic = (root / "symbolic").string();
const int created = symlink_nid_postfix("original.bin", symbolic.c_str());
if (created == 0) {

View File

@ -11,3 +11,4 @@
- [unlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rmdir` (with `AT_REMOVEDIR`, 0x800) or `unlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Removing a directory without `AT_REMOVEDIR` fails with `EPERM`, removing `.` or `..` with `AT_REMOVEDIR` fails with `EINVAL`, and flags other than `AT_REMOVEDIR` fail with `EINVAL`. Console behaviour is unverified
- [renameat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rename`. Any other relative path is joined to its descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to relative descriptors, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified
- [fork](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, Linux named shared-memory creation applies it, but ordinary files are still created with the host process's mask
- [chroot](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L776-L807) for a root caller up to the lookup: `EFAULT` for a null path, `ENOENT` for an empty or missing one and `ENOTDIR` for a path that is not a directory or passes through a file. A directory that is found throws, `/` included: the guest path resolver has a fixed root and mount aliases (`/app0`, `/savedata0`) that do not move with it, so a new root cannot be applied. Whether a title holds `PRIV_VFS_CHROOT` on the console, and `pwd_chroot`'s `EPERM` for a process with open directory descriptors, are unverified. Porpoise (PPSA99764) imports it; no call site is known