Merge pull request #4553 from Nixitov/feat/fchmodat

feat(libkernel_sys): implement fchmodat
This commit is contained in:
Adrià 2026-10-13 17:03:44 +02:00 committed by GitHub
commit d67a9e5a45
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 72 additions and 0 deletions

View File

@ -10,6 +10,7 @@
#include <string>
#include <string_view>
#include <system_error>
#include <sys/stat.h>
#ifdef __linux__
#include <cerrno>
#include <fcntl.h>
@ -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;
@ -487,6 +506,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<int>(Fail(GuestEinval));
if (path == nullptr) return static_cast<int>(Fail(GuestEfault));
if (*path == '\0') return static_cast<int>(Fail(GuestEnoent));
std::filesystem::path native;
if (fd == GuestAtFdcwd || *path == '/') {
if (const int error = ResolvePathError_nid_no_patch(path, native)) return static_cast<int>(Fail(error));
} else {
int error = 0;
const char* base = GetDirectoryDescriptorPath_nid_no_patch(fd, &error);
if (base == nullptr) return static_cast<int>(Fail(error));
native = std::filesystem::path(reinterpret_cast<const char8_t*>(base)) / std::filesystem::path(reinterpret_cast<const char8_t*>(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<int>(Fail(GuestFileError(errno)));
#else
if (::chmod(native.c_str(), static_cast<mode_t>(mode & 07777)) != 0) return static_cast<int>(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);

View File

@ -42,6 +42,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();
@ -447,6 +448,22 @@ 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);
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;

View File

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