diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 238aca391..0bd471cf5 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -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(Fail(GuestEfault)); + if (*path == '') return static_cast(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(Fail(HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestEnoent)); + if (error) return static_cast(Fail(GuestError(error))); + if (!std::filesystem::is_directory(status)) return static_cast(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(Fail(GuestEfault)); if (*target == '\0' || *created == '\0') return static_cast(Fail(GuestEnoent)); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 1bbdff4b7..911957525 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -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) { diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index ea44a7b58..a83d88c94 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -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