From e3c1d01815efbac21b5bc29e8792b7a961b76c3c Mon Sep 17 00:00:00 2001 From: Nixitov Date: Mon, 12 Oct 2026 17:32:16 +0200 Subject: [PATCH 1/2] feat(libkernel_sys): implement fchmodat 2 Ship 2 Harkinian (PPSA99621) imports fchmodat through std::filesystem::permissions and stops at start-up on main. Resolve AT_FDCWD and absolute paths as usual and join other relative paths to the directory descriptor like openat, accept 0 and AT_SYMLINK_NOFOLLOW, and mask the mode to 07777. --- core/libs/prx/libkernel_sys/Export.cpp | 54 ++++++++++++++++++++++++++ core/libs/tests/GuestKernelSys.cpp | 18 +++++++++ docs/dev/debt/libkernel_sys.md | 1 + 3 files changed, 73 insertions(+) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 238aca391..ac62bc0f6 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #ifdef __linux__ #include #include @@ -81,6 +82,24 @@ constexpr int PcAclExtended = 59; constexpr int PcAclPathMax = 60; constexpr int PcMacPresent = 63; constexpr int PcAclNfs4 = 64; +constexpr int GuestAtSymlinkNofollow = 0x0200; + +int GuestFileError(int error) { + switch (error) { + case ENOENT: return GuestEnoent; + case EACCES: return 13; + case ENOTDIR: return 20; + case EINVAL: return GuestEinval; + case EPERM: return GuestEperm; + case EROFS: return 30; + case ENAMETOOLONG: return GuestEnametoolong; + case EIO: return GuestEio; +#ifdef ELOOP + case ELOOP: return GuestEloop; +#endif + default: return GuestEio; + } +} std::int64_t Fail(int error) { *__error_nid_postfix() = error; @@ -470,6 +489,41 @@ int APS5_VABI mkdirat_nid_postfix(int fd, const char* path, std::uint16_t mode) return 0; } +int APS5_VABI fchmodat_nid_postfix(int fd, const char* path, int mode, int flag) { + if (flag != 0 && flag != GuestAtSymlinkNofollow) return static_cast(Fail(GuestEinval)); + if (path == nullptr) return static_cast(Fail(GuestEfault)); + if (*path == '\0') return static_cast(Fail(GuestEnoent)); + std::filesystem::path native; + if (fd == GuestAtFdcwd || *path == '/') { + if (const int error = ResolvePathError_nid_no_patch(path, native)) return static_cast(Fail(error)); + } else { + int error = 0; + const char* base = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); + if (base == nullptr) return static_cast(Fail(error)); + native = std::filesystem::path(reinterpret_cast(base)) / std::filesystem::path(reinterpret_cast(path)); + } + if (flag == GuestAtSymlinkNofollow) { +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); + const bool symbolic = attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0; +#else + std::error_code status; + const bool symbolic = std::filesystem::is_symlink(std::filesystem::symlink_status(native, status)); +#endif + if (symbolic) { + NotImplemented_nid_no_patch("fchmodat: AT_SYMLINK_NOFOLLOW on a symbolic link"); + return -1; + } + } +#ifdef _WIN32 + if (::_wchmod(native.wstring().c_str(), mode & 07777) != 0) return static_cast(Fail(GuestFileError(errno))); +#else + if (::chmod(native.c_str(), static_cast(mode & 07777)) != 0) return static_cast(Fail(GuestFileError(errno))); +#endif + RecordWrittenPath_nid_no_patch(native); + return 0; +} + int APS5_VABI unlink_nid_postfix(const char* path); int APS5_VABI rmdir_nid_postfix(const char* path); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 1bbdff4b7..8c58f9189 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -41,6 +41,7 @@ int APS5_VABI unlinkat_nid_postfix(int, const char*, int); std::int64_t APS5_VABI readlinkat_nid_postfix(int, const char*, char*, std::size_t); int APS5_VABI mkdirat_nid_postfix(int, const char*, std::uint16_t); int APS5_VABI renameat_nid_postfix(int, const char*, int, const char*); +int APS5_VABI fchmodat_nid_postfix(int, const char*, int, int); std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); std::uint16_t APS5_VABI umask_nid_postfix(std::uint16_t); int* APS5_VABI __error_nid_postfix(); @@ -404,6 +405,23 @@ int main() { Require(sceKernelClose(subFd) == 0); Require(renameat_nid_postfix(-100, (root / "made.bin").string().c_str(), -100, (root / "final.bin").string().c_str()) == 0); Require(std::filesystem::is_regular_file(root / "final.bin")); + Require(fchmodat_nid_postfix(base, "file.txt", 0600, 0x0200) == 0); + Require(fchmodat_nid_postfix(-100, (root / "file.txt").string().c_str(), 0644, 0) == 0); +#ifndef _WIN32 + Require((std::filesystem::status(root / "file.txt").permissions() & std::filesystem::perms::all) == + (std::filesystem::perms::owner_read | std::filesystem::perms::owner_write | std::filesystem::perms::group_read | std::filesystem::perms::others_read)); +#endif + Require(fchmodat_nid_postfix(base, "missing.bin", 0600, 0) == -1 && Error() == 2); + Require(fchmodat_nid_postfix(base, nullptr, 0600, 0) == -1 && Error() == 14); + Require(fchmodat_nid_postfix(base, "", 0600, 0) == -1 && Error() == 2); + Require(fchmodat_nid_postfix(base, "file.txt", 0600, 0x4000) == -1 && Error() == 22); + Require(fchmodat_nid_postfix(-1, "file.txt", 0600, 0) == -1 && Error() == 9); +#ifndef _WIN32 + Require(symlink_nid_postfix("loop", (root / "loop").string().c_str()) == 0); + Require(fchmodat_nid_postfix(base, "loop/x", 0600, 0) == -1 && Error() == 62); + const std::string longName(300, 'a'); + Require(fchmodat_nid_postfix(base, longName.c_str(), 0600, 0) == -1 && Error() == 63); +#endif Require(sceKernelClose(base) == 0); std::filesystem::remove_all(root); return 0; diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index ea44a7b58..0502aa7f9 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -4,6 +4,7 @@ - [fchdir](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) changes the guest working directory to the descriptor's directory: on Windows through the path recorded when the descriptor was opened, on Linux through the `/proc/self/fd` link target. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. FreeBSD `change_dir`'s search-permission check (`EACCES`) is not modelled, and a directory renamed after it was opened is still reached through the recorded path. Console behaviour is unverified - [openat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `open`. 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. As with `open`, `O_NOFOLLOW` and `O_NONBLOCK` are ignored. PPSA99003 opens directory entries this way. Console behaviour is unverified +- [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `chmod`; any other relative path is joined to the descriptor's host directory the same way as for `openat`, so guest path aliases and case-insensitive name matching do not apply to it and a `..` component is not kept inside the guest root. A flag other than `0` or `AT_SYMLINK_NOFOLLOW` (0x200) returns `EINVAL`; the mode is masked to `07777`. With `AT_SYMLINK_NOFOLLOW` a symbolic link throws, since the host cannot change a link's own mode, while a regular file is changed either way. The mode maps to the host file mode, which on Windows is only the CRT's read-only bit. PPSA99621 calls it through `std::filesystem::permissions`. Console behaviour is unverified - [pathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) looks the path up as FreeBSD 11's `kern_pathconf` does (links followed, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255) and returns the constants of FreeBSD 11's UFS `ufs_pathconf` for files and directories alike: `_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0. Whether the console's filesystems report the same is not verified. It throws for the names whose UFS value depends on the mount (`_PC_ALLOC_SIZE_MIN`, `_PC_REC_INCR_XFER_SIZE`, `_PC_REC_MIN_XFER_SIZE`, `_PC_MIN_HOLE_SIZE`), the kernel options (`_PC_ACL_EXTENDED`, `_PC_ACL_PATH_MAX`, `_PC_ACL_NFS4`, `_PC_MAC_PRESENT`) or the page size (`_PC_REC_XFER_ALIGN`), and on which UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, `_PC_ASYNC_IO`); other names fail with `EINVAL` - [mkdirat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `mkdir`. 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. On Windows, `mode` is ignored. Console behaviour is unverified - [readlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target and a junction throw, as a host path has no guest form. Any other reparse point throws too, where FreeBSD reports `EINVAL` for a file that is not a link From 603d94a53feddc26f53e3c027cbf0b2380aec97e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 13 Oct 2026 16:14:56 +0200 Subject: [PATCH 2/2] test(libkernel_sys): reuse longName in the fchmodat check --- core/libs/tests/GuestKernelSys.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 8c58f9189..f75d6c1d9 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -419,7 +419,6 @@ int main() { #ifndef _WIN32 Require(symlink_nid_postfix("loop", (root / "loop").string().c_str()) == 0); Require(fchmodat_nid_postfix(base, "loop/x", 0600, 0) == -1 && Error() == 62); - const std::string longName(300, 'a'); Require(fchmodat_nid_postfix(base, longName.c_str(), 0600, 0) == -1 && Error() == 63); #endif Require(sceKernelClose(base) == 0);