From 2d1342cf31dc711538568eca7c1f58b080a4dc8e Mon Sep 17 00:00:00 2001 From: Gruzzberry Date: Sat, 31 Oct 2026 10:21:59 +0000 Subject: [PATCH 1/2] fix(libkernel): fail chflags and fchflags with EPERM when a system flag changes FreeBSD 11.4 ufs_setattr refuses a change of any SF_* flag to a caller without PRIV_VFS_SYSFLAGS, which prison_priv_check denies a jailed root without allow.chflags. Under the credential model agreed on #11216 every SF_* flag now fails with EPERM, after the EOPNOTSUPP check for unknown bits, instead of throwing. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/File/src/Stdio.cpp | 4 ++-- core/libs/tests/GuestFilesystem.cpp | 13 +++++++++++-- core/libs/tests/GuestKernelSys.cpp | 3 +++ docs/dev/debt/libkernel.md | 4 ++-- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index a0d6f80550..08e0a82b9c 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -717,7 +717,7 @@ static constexpr std::pair GUEST_FILE_FLAGS[] = { {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 constexpr std::uint64_t GUEST_SF_SETTABLE = 0xffff0000; static std::string FileFlagNames(std::uint64_t flags) { std::string names; @@ -733,7 +733,7 @@ static int SetFileFlags(const char* function, const std::string& target, std::ui std::uint64_t known = 0; for (const auto& entry : GUEST_FILE_FLAGS) known |= entry.first; if ((flags & ~known) != 0) return GUEST_EOPNOTSUPP; - if ((flags & GUEST_SF_SNAPSHOT) != 0) return GUEST_EPERM; + if ((flags & GUEST_SF_SETTABLE) != 0) return GUEST_EPERM; if (flags != 0) throw std::runtime_error(std::string(function) + ": setting " + FileFlagNames(flags) + " on " + target + " is not supported, the host keeps no BSD file flags"); return 0; diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 4c8ffa4dc0..6bfc4f8f79 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -675,7 +675,12 @@ int main() { 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")); + Require(ChflagsThrows(file, 0x1000, "UF_READONLY")); + for (const std::uint64_t system : {0x10000ULL, 0x20000ULL, 0x40000ULL, 0x100000ULL, 0x200000ULL, 0x370000ULL, 0x1000ULL | 0x40000ULL, 0x2ULL | 0x20000ULL}) + Require(chflags_nid_postfix(file.string().c_str(), system) == -1 && *__error_nid_postfix() == 1); + Require(chflags_nid_postfix(file.string().c_str(), 0x40000 | 0x20) == -1 && *__error_nid_postfix() == 45); + Require(chflags_nid_postfix(root.string().c_str(), 0x20000) == -1 && *__error_nid_postfix() == 1); + Require(chflags_nid_postfix((root / "missing").string().c_str(), 0x20000) == -1 && *__error_nid_postfix() == 2); const auto chflagsDangling = root / "dangling.link"; const auto chflagsPointing = root / "file.link"; #ifdef _WIN32 @@ -704,7 +709,11 @@ int main() { Require(fchflags_nid_postfix(flagged, ~0ULL) == -1 && *__error_nid_postfix() == 45); Require(fchflags_nid_postfix(flagged, 0x200000) == -1 && *__error_nid_postfix() == 1); Require(FchflagsThrows(flagged, 0x2, "UF_IMMUTABLE")); - Require(FchflagsThrows(flagged, 0x1000 | 0x40000, "UF_READONLY|SF_APPEND")); + Require(FchflagsThrows(flagged, 0x1000, "UF_READONLY")); + for (const std::uint64_t system : {0x10000ULL, 0x20000ULL, 0x40000ULL, 0x100000ULL, 0x200000ULL, 0x1000ULL | 0x40000ULL, 0x2ULL | 0x20000ULL}) + Require(fchflags_nid_postfix(flagged, system) == -1 && *__error_nid_postfix() == 1); + Require(fchflags_nid_postfix(flagged, 0x370000) == -1 && *__error_nid_postfix() == 1); + Require(fchflags_nid_postfix(flagged, 0x40000 | 0x20) == -1 && *__error_nid_postfix() == 45); Require(close_nid_postfix(flagged) == 0); Require(fchflags_nid_postfix(flagged, 0) == -1 && *__error_nid_postfix() == 9); Require(fchflags_nid_postfix(flagged, ~0ULL) == -1 && *__error_nid_postfix() == 9); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 5aeefaf21c..7feb9a8c0e 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -1510,6 +1510,9 @@ int main() { Require(lchflags_nid_postfix((file + "/child").c_str(), 0) == -1 && Error() == 20); Require(lchflags_nid_postfix(file.c_str(), 0x20) == -1 && Error() == 45); Require(lchflags_nid_postfix(file.c_str(), 0x200000) == -1 && Error() == 1); + Require(lchflags_nid_postfix(file.c_str(), 0x20000) == -1 && Error() == 1); + Require(lchflags_nid_postfix(file.c_str(), 0x2 | 0x40000) == -1 && Error() == 1); + Require(lchflags_nid_postfix(file.c_str(), 0x40000 | 0x20) == -1 && Error() == 45); 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.md b/docs/dev/debt/libkernel.md index dda1acaf19..a000c40e19 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -334,8 +334,8 @@ - [settimeofday](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel_sys) checks like FreeBSD 11.4's [`kern_settimeofday`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_time.c#L688-L707) for a root caller: `EINVAL` for a negative second count or a microsecond count outside 0 to 999999, before anything is stored. A valid time throws, naming it, since the host clock is never set, and its time zone is not stored, as FreeBSD stores the zone only after setting the clock. A time zone without a time is kept for the process and reported by `gettimeofday`; `sceKernelGettimezone` still reports 0 and 0, since whether the console reads the same value is not known. FreeBSD checks `PRIV_SETTIMEOFDAY` first, which a jailed process fails with `EPERM`; whether a title holds it on the console is not known. - [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 - [Short sleeps ended by GPU and flip completions](../../../core/libs/prx/libkernel/Time/SleepWake.cpp) (libkernel) - a `sceKernelUsleep`, `usleep`, `nanosleep` or `sceKernelNanosleep` of 1 ms or less returns before the requested time, which FreeBSD and the console do not do: it ends once the AGC driver reports a completed GPU batch or batch completion action, or VideoOut completes a flip, so a title that polls a GPU label or the flip status between sleeps does not sleep the rest. A title that paces frames with such a sleep can wake up to 1 ms early. A thread returns early at most 8 times in a run of sleeps; the run restarts when the thread submits AGC work or did not sleep the requested time since its last sleep, so a loop that counts sleeps to time out still sleeps all but 8 in full. `APS5_NO_SLEEP_WAKE=1` keeps every sleep in full -- [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 -- [fchflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) has the gaps of `chflags`: a valid non-zero flag throws, and the console's filesystem types, `EROFS` and the change-time update are not modelled. Character devices, the random device, other host descriptors and files read from app0.zar throw, since their console filesystems and the privilege check for devices are not modelled. +- [chflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) follows FreeBSD 11.4's `kern_chflagsat` and [`ufs_setattr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/ufs/ufs/ufs_vnops.c#L509-L556): path errors are `EFAULT`, `ENOENT` and `ENOTDIR`; a flag outside the 17 UFS stores (`VNOVAL` included) gives `EOPNOTSUPP`. Any `SF_*` flag gives `EPERM`: the jail model agreed on [#11216](https://github.com/boykopovar/AnyPS5/pull/11216) follows [`prison_priv_check`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_jail.c#L3311-L3315), which denies `PRIV_VFS_SYSFLAGS`; not measured on a console. No host keeps BSD file flags, so every file has none: flags 0 succeed, any other user flag throws. `EROFS`, the console's filesystem types and the change time are not modelled +- [fchflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) has the gaps of `chflags`: a valid non-zero user flag throws and a system flag fails with `EPERM`, and the console's filesystem types, `EROFS` and the change-time update are not modelled. Character devices, the random device, other host descriptors and files read from app0.zar throw, since their console filesystems and the privilege check for devices are not modelled. - [lchflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel_sys) has the gaps of `chflags`. As FreeBSD 11.4's [`sys_lchflags`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2357-L2363) passes `AT_SYMLINK_NOFOLLOW`, a symbolic link in the last component is the file whose flags are set instead of its target, so a dangling link is accepted. This no-follow behaviour and the console's filesystem and flag handling have not been measured on a PS5. - [getpriority](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) follows FreeBSD 11.4's [`sys_getpriority`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_resource.c#L93-L165) for the only guest process: `PRIO_PROCESS` and `PRIO_PGRP` with 0 or its own pid (the guest is taken to lead its own process group, see `kill`) and `PRIO_USER` with 0 or its user id 0 return 0, any other id fails with `ESRCH` and any other `which` with `EINVAL`, leaving errno unchanged on success. `setpriority` fails the same way; `PRIO_PROCESS` returns 0 without storing, so `getpriority` stays 0, as PS5 firmware 13.60 did in a payload process for sets of -25, -5, 5 and 25. A valid `PRIO_PGRP` or `PRIO_USER` set throws: unverified. What a title process reads, and the host nice of the process or of the threads `APS5_THREAD_PRIORITY=high` raises, are not reported or unverified - [rtprio_thread, pthread_setconcurrency, pthread_getconcurrency](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - `RTP_SET` throws for a real-time thread (sce attributes allow only `SCHED_FIFO`/`SCHED_RR`) and for another thread: a payload process on a PS5 (firmware 12.70) answered type 2, prio 0/15/31 on the calling thread with `EINVAL`; no accepted value or set by another thread's id measured. A non-real-time thread (POSIX attributes store `SCHED_OTHER`), unmeasured, reports 3 with 0, and `RTP_SET` accepts only that. `RTP_LOOKUP` reports the policy's type (10 or 2) and the scheduling priority, as measured, not FreeBSD's 0-31; another function fails with `ESRCH` (2 measured), a null `rtprio` with `EFAULT`, an id of no live thread with `ESRCH` (unknown ids measured). `pthread_setconcurrency` stores 0 or more, returns `EINVAL` (not errno) below 0, as libthr does; `pthread_getconcurrency` returns it, a hint. Morrowind (`mshivam019/morrowind-ps5`) imports them From 1f236c96f3ae45fdf521a627af4cc8c90187910f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= Date: Sun, 1 Nov 2026 07:51:44 +0100 Subject: [PATCH 2/2] test(libkernel): drop UF_READONLY throw assertions from chflags tests Co-Authored-By: Claude Sonnet 5.5 --- core/libs/tests/GuestFilesystem.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 6bfc4f8f79..29384fcceb 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -675,7 +675,6 @@ int main() { 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, "UF_READONLY")); for (const std::uint64_t system : {0x10000ULL, 0x20000ULL, 0x40000ULL, 0x100000ULL, 0x200000ULL, 0x370000ULL, 0x1000ULL | 0x40000ULL, 0x2ULL | 0x20000ULL}) Require(chflags_nid_postfix(file.string().c_str(), system) == -1 && *__error_nid_postfix() == 1); Require(chflags_nid_postfix(file.string().c_str(), 0x40000 | 0x20) == -1 && *__error_nid_postfix() == 45); @@ -709,7 +708,6 @@ int main() { Require(fchflags_nid_postfix(flagged, ~0ULL) == -1 && *__error_nid_postfix() == 45); Require(fchflags_nid_postfix(flagged, 0x200000) == -1 && *__error_nid_postfix() == 1); Require(FchflagsThrows(flagged, 0x2, "UF_IMMUTABLE")); - Require(FchflagsThrows(flagged, 0x1000, "UF_READONLY")); for (const std::uint64_t system : {0x10000ULL, 0x20000ULL, 0x40000ULL, 0x100000ULL, 0x200000ULL, 0x1000ULL | 0x40000ULL, 0x2ULL | 0x20000ULL}) Require(fchflags_nid_postfix(flagged, system) == -1 && *__error_nid_postfix() == 1); Require(fchflags_nid_postfix(flagged, 0x370000) == -1 && *__error_nid_postfix() == 1);