From 4dd97f7dbe7943278d8106840cb92fab3e25e44e Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 16:24:28 +0200 Subject: [PATCH] feat(libkernel): implement chflags through the guest path resolver PS5MC (PPSA99420, Kodi 22) imports chflags (ixrw0h2tWuI) from libkernel.prx, one reference. Predicted stop once the imports ahead of it (pthread_getthreadid_np, sendfile, wcstok, pause, setrlimit and the termios calls) are covered (not a run): FAIL: unresolved ELF import ixrw0h2tWuI from libkernel.prx The NID is computed with tools/nid_names.py and matches aerolib. Follows FreeBSD 11.4 (8c639945a44588f6cfdddfb057bb5aad88735da0) sys/kern/vfs_syscalls.c kern_chflagsat and setfflags, and sys/ufs/ufs/ufs_vnops.c ufs_setattr, for a root caller: the lookup through the guest resolver gives EFAULT, ENOENT and ENOTDIR (also for a file reached through a trailing slash or used as a directory), a bit outside the 17 UFS flags of sys/sys/stat.h (VNOVAL included) gives EOPNOTSUPP, and SF_SNAPSHOT gives EPERM, since the snapshot flag cannot be toggled. No host keeps BSD file flags and stat reports st_flags 0, so flags 0 succeed without touching the file and any other flag throws with the flags named. UF_READONLY and the other Windows-attribute flags are not mapped to FILE_ATTRIBUTE_READONLY and its siblings: UFS stores them without enforcing them, and on Windows the read-only attribute is already the owner write bit of chmod. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 16e5b7ccd04e6467a5a282b6cfc9271304f42244) --- core/libs/prx/libkernel/File/src/Stdio.cpp | 42 ++++++++++++++++++++++ core/libs/tests/GuestFilesystem.cpp | 23 ++++++++++++ docs/dev/debt/libkernel.md | 5 +++ 3 files changed, 70 insertions(+) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index d9ceb8756..f94bb212c 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include struct KernelIovec { @@ -286,6 +287,7 @@ static const struct iovec* NativeIovecs(const KernelIovec* iov) { } #endif +static constexpr int GUEST_EPERM = 1; static constexpr int GUEST_ENOENT = 2; static constexpr int GUEST_EIO = 5; static constexpr int GUEST_EBADF = 9; @@ -297,6 +299,7 @@ static constexpr int GUEST_ENAMETOOLONG = 63; static constexpr int GUEST_ENOTDIR = 20; static constexpr int GUEST_EISDIR = 21; static constexpr int GUEST_EWOULDBLOCK = 35; +static constexpr int GUEST_EOPNOTSUPP = 45; static constexpr int GUEST_ENOTEMPTY = 66; static constexpr int GUEST_EDEADLK = 11; static constexpr int GUEST_ELOOP = 62; @@ -478,6 +481,33 @@ int File::DuplicateFrom(int fd, int start) { #endif } +static int LookupError(const std::filesystem::path& native) { + std::error_code error; + const auto status = std::filesystem::status(native, error); + if (std::filesystem::exists(status)) return native.has_filename() || std::filesystem::is_directory(status) ? 0 : GUEST_ENOTDIR; + if (status.type() != std::filesystem::file_type::not_found) return SceErrorFromErrno(error.default_error_condition().value()) & 0xffff; + const bool fileWithSeparator = !native.has_filename() && std::filesystem::exists(WithoutTrailingSeparator(native), error); + return fileWithSeparator || HasNonDirectoryPrefix(native) ? GUEST_ENOTDIR : GUEST_ENOENT; +} + +static constexpr std::pair GUEST_FILE_FLAGS[] = { + {0x00000001, "UF_NODUMP"}, {0x00000002, "UF_IMMUTABLE"}, {0x00000004, "UF_APPEND"}, {0x00000008, "UF_OPAQUE"}, + {0x00000010, "UF_NOUNLINK"}, {0x00000080, "UF_SYSTEM"}, {0x00000100, "UF_SPARSE"}, {0x00000200, "UF_OFFLINE"}, + {0x00000400, "UF_REPARSE"}, {0x00000800, "UF_ARCHIVE"}, {0x00001000, "UF_READONLY"}, {0x00008000, "UF_HIDDEN"}, + {0x00010000, "SF_ARCHIVED"}, {0x00020000, "SF_IMMUTABLE"}, {0x00040000, "SF_APPEND"}, {0x00100000, "SF_NOUNLINK"}, + {0x00200000, "SF_SNAPSHOT"}}; +static constexpr std::uint64_t GUEST_SF_SNAPSHOT = 0x00200000; + +static std::string FileFlagNames(std::uint64_t flags) { + std::string names; + for (const auto& [flag, name] : GUEST_FILE_FLAGS) { + if ((flags & flag) == 0) continue; + if (!names.empty()) names += '|'; + names += name; + } + return names; +} + extern "C" { int APS5_VABI chmod_nid_postfix(const char* path, int mode) { @@ -488,6 +518,18 @@ int APS5_VABI chmod_nid_postfix(const char* path, int mode) { return 0; } +int APS5_VABI chflags_nid_postfix(const char* path, std::uint64_t flags) { + if (const int error = PathError(path)) return PosixFailure(error); + if (const int error = LookupError(ResolvePath_nid_no_patch(path))) return PosixFailure(error); + std::uint64_t known = 0; + for (const auto& entry : GUEST_FILE_FLAGS) known |= entry.first; + if ((flags & ~known) != 0) return PosixFailure(GUEST_EOPNOTSUPP); + if ((flags & GUEST_SF_SNAPSHOT) != 0) return PosixFailure(GUEST_EPERM); + if (flags != 0) + throw std::runtime_error("chflags: setting " + FileFlagNames(flags) + " on " + path + " is not supported, the host keeps no BSD file flags"); + return 0; +} + int APS5_VABI close_nid_postfix(int d) { int socketError = 0; if (KernelCloseSocket_nid_no_patch(d, &socketError)) { diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 987e09890..e86761e7b 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -93,6 +93,15 @@ static void Check(bool value, int line) { } #define Require(value) Check((value), __LINE__) +static bool ChflagsThrows(const std::filesystem::path& path, std::uint64_t flags, const char* names) { + try { + chflags_nid_postfix(path.string().c_str(), flags); + } catch (const std::runtime_error& error) { + return std::strstr(error.what(), names) != nullptr; + } + return false; +} + static void TestParentTraversal() { const auto root = std::filesystem::path("anyps5-parent-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); @@ -329,6 +338,20 @@ int main() { Require(mkdir_nid_postfix(nullptr, 0700) == -1 && *__error_nid_postfix() == 14); const auto file = root / "file.txt"; { std::ofstream stream(file); stream << "retained until removal"; } + Require(chflags_nid_postfix(nullptr, 0) == -1 && *__error_nid_postfix() == 14); + Require(chflags_nid_postfix("", 0) == -1 && *__error_nid_postfix() == 2); + Require(chflags_nid_postfix((root / "missing").string().c_str(), 0) == -1 && *__error_nid_postfix() == 2); + Require(chflags_nid_postfix((root / "missing").string().c_str(), ~0ULL) == -1 && *__error_nid_postfix() == 2); + Require(chflags_nid_postfix((file / "child").string().c_str(), 0) == -1 && *__error_nid_postfix() == 20); + Require(chflags_nid_postfix((file.string() + "/").c_str(), 0) == -1 && *__error_nid_postfix() == 20); + Require(chflags_nid_postfix(file.string().c_str(), 0) == 0); + Require(chflags_nid_postfix(root.string().c_str(), 0) == 0); + Require(chflags_nid_postfix(file.string().c_str(), 0x20) == -1 && *__error_nid_postfix() == 45); + Require(chflags_nid_postfix(file.string().c_str(), 1ULL << 32) == -1 && *__error_nid_postfix() == 45); + Require(chflags_nid_postfix(file.string().c_str(), ~0ULL) == -1 && *__error_nid_postfix() == 45); + Require(chflags_nid_postfix(file.string().c_str(), 0x200000) == -1 && *__error_nid_postfix() == 1); + Require(ChflagsThrows(file, 0x2, "UF_IMMUTABLE")); + Require(ChflagsThrows(file, 0x1000 | 0x40000, "UF_READONLY|SF_APPEND")); const auto held = root / "held.txt"; const auto heldName = held.string(); { std::ofstream stream(held); stream << "held"; } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 637ab5eb3..4c09ffbd8 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -169,3 +169,8 @@ - [pthread_atfork](../../../core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp) (libkernel) appends the three handlers to a process-wide list and returns 0, or `ENOMEM` when the entry cannot be allocated, as FreeBSD 11.4's [`_pthread_atfork`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_fork.c#L78-L100) does; null handlers are accepted. The handlers never run: `fork` throws and `vfork` is absent, so no child process is created. Handlers of a module that is unloaded stay in the list, where FreeBSD's `__pthread_cxa_finalize` removes them. PPSA97358 has a source-level call from the constructor `rcu_init` ([rcu.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/rcu.c#L488-L495)), but current-main runtime reachability is unverified; XPSemu currently stops first at the unresolved `mlockall` import (#4386) - [sceKernelGetPageTableStats](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - reports 2048 page-table entries on each side (a 4 GiB pool of 2 MiB entries, the constants of [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/kernel/memory.cpp)) and all of them available: no entries are counted as used, so a title never sees page-table pressure. KytyPS5 counts the 2 MiB entries of the committed CPU- or GPU-accessible ranges as used, which this port does not do. The four `int*` arguments follow OpenOrbis' `libkernel.h`, the Orbital type database and SharpProspero; a null pointer returns `SCE_KERNEL_ERROR_EFAULT` as in KytyPS5. None of the values is measured on a console. PPSA30601 (Resonance) calls it during start-up - [clock_settime](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) validates like FreeBSD 11.4's [`kern_clock_settime`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_time.c#L396-L412) for a root caller: `EFAULT` for a null time, `EINVAL` for any clock but `CLOCK_REALTIME`, a negative second count or a nanosecond count outside 0 to 999999999. A valid `CLOCK_REALTIME` request throws, naming the requested time, since the host clock is never set and no guest clock offset is kept. FreeBSD checks `PRIV_CLOCK_SETTIME` first, which a jailed process fails with `EPERM`; whether a title holds it on the console is unverified. PS5MC (PPSA99420) imports it; no call site is known +<<<<<<< HEAD +- [sceKernelGetPageTableStats](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - reports 2048 page-table entries on each side (a 4 GiB pool of 2 MiB entries, the constants of [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/kernel/memory.cpp)) and all of them available: no entries are counted as used, so a title never sees page-table pressure. KytyPS5 counts the 2 MiB entries of the committed CPU- or GPU-accessible ranges as used, which this port does not do. The four `int*` arguments follow OpenOrbis' `libkernel.h`, the Orbital type database and SharpProspero; a null pointer returns `SCE_KERNEL_ERROR_EFAULT` as in KytyPS5. None of the values is measured on a console. PPSA30601 (Resonance) calls it during start-up +======= +- [chflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) follows FreeBSD 11.4's [`kern_chflagsat`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2366-L2383) and UFS's [`ufs_setattr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/ufs/ufs/ufs_vnops.c#L509-L556) for a root caller: the path lookup reports `EFAULT`, `ENOENT` and `ENOTDIR`, a flag outside the 17 UFS stores (`VNOVAL` included) `EOPNOTSUPP`, and `SF_SNAPSHOT` `EPERM`. Neither host keeps BSD file flags, so every file is taken to have none (`stat` reports `st_flags` 0): flags 0 succeed without touching the file, and any other flag throws, naming the flags. `UF_READONLY`, `UF_HIDDEN`, `UF_SYSTEM` and `UF_ARCHIVE` are FreeBSD's names for the Windows attributes but are not mapped to them: UFS stores them without enforcing them, `FILE_ATTRIBUTE_READONLY` already carries the owner write bit of `chmod` on Windows, and Linux has no equivalent a process can set without `CAP_LINUX_IMMUTABLE`. The console's filesystem types, `EROFS` on read-only mounts and the change-time update are not modelled +>>>>>>> cf419bd17 (feat(libkernel): implement chflags through the guest path resolver)