diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 238aca391..bdccb763c 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -369,6 +369,23 @@ 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 == '\0') return static_cast(Fail(GuestEnoent)); + if (std::strlen(path) >= GuestPathMax || HasLongComponent(path)) return static_cast(Fail(GuestEnametoolong)); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return static_cast(Fail(error)); + 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)); + const bool root = std::filesystem::equivalent(native, ResolvePath_nid_no_patch("/"), error); + if (error) return static_cast(Fail(GuestError(error))); + if (root) return 0; + 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..200ebc027 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,39 @@ 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(chroot_nid_postfix((root / "file.txt" / "..").string().c_str()) == -1 && Error() == 20); + Require(chroot_nid_postfix((root / std::string(256, 'n')).string().c_str()) == -1 && Error() == 63); + Require(chroot_nid_postfix(("/" + std::string(1024, 'p')).c_str()) == -1 && Error() == 63); + Require(ChrootThrows(root.string())); + Require(ChrootThrows("/" + root.filename().string() + "/.")); + const auto guestRoot = "/" + root.filename().string(); + const auto chrootFile = guestRoot + "/file.txt"; + const int top = sceKernelOpen("/", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(top >= 0); + Require(chdir_nid_postfix(guestRoot.c_str()) == 0); + Require(fchdir_nid_postfix(top) == 0 && chroot_nid_postfix(".") == 0); + Require(getcwd_nid_postfix(directoryPath, sizeof(directoryPath)) == directoryPath && std::strcmp(directoryPath, "/") == 0); + Require(sceKernelClose(top) == 0); + Require(chroot_nid_postfix("/") == 0 && chroot_nid_postfix("/..") == 0 && chroot_nid_postfix("//./") == 0); + Require(chdir_nid_postfix(guestRoot.c_str()) == 0); + Require(chroot_nid_postfix("..") == 0 && chroot_nid_postfix("../..") == 0); + Require(getcwd_nid_postfix(directoryPath, sizeof(directoryPath)) == directoryPath && guestRoot == directoryPath); + Require(ChrootThrows(".")); + for (const auto& path : {std::string("file.txt"), chrootFile, "/../../.." + chrootFile, "../.." + chrootFile}) { + const int opened = sceKernelOpen(path.c_str(), SCE_KERNEL_O_RDONLY, 0); + char byte = 0; + Require(opened >= 0 && sceKernelRead(opened, &byte, 1) == 1 && byte == 'x'); + Require(sceKernelClose(opened) == 0); + } + Require(chdir_nid_postfix("/") == 0); + 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..f7705b4b1 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) and [`pwd_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L3059-L3085) for a root caller: `EFAULT` for a null path, `ENOENT` for an empty or missing one, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255, and `ENOTDIR` for a path that is not a directory or passes through a file. A directory that is the guest root (`/`, `/..`, or `.` after `fchdir` to a descriptor of `/`) succeeds and changes nothing, as the root stays the same directory; the guest is taken never to have changed its root, so open directory descriptors do not make it fail with `EPERM` (`kern.chroot_allow_open_directories` 1). Any other directory throws, naming the path: the guest path resolver has a fixed root and mount aliases (`/app0`, `/savedata0`) that do not move with it. On the console a title starts in a sandbox root, where `pwd_chroot` refuses with `EPERM` while a directory is open; whether that holds for titles, and whether a title holds `PRIV_VFS_CHROOT`, are unverified. Porpoise (PPSA99764) weak-imports it and calls it only as `fchdir` to its saved descriptor of `/` then `chroot(".")`, to put its sandbox root back after a jailbreak daemon replaced it ([`put_root_back`](https://github.com/elripalda/Porpoise-Dolphin-Emulator-for-PS5/blob/2ed1ab5a06995aa198f7bccd4da0f90c3fc29d39/src/porpoise_jailbreak.cpp#L303-L331))