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 <noreply@anthropic.com>
(cherry picked from commit 16e5b7ccd04e6467a5a282b6cfc9271304f42244)
This commit is contained in:
David 2026-10-11 16:24:28 +02:00 committed by MarcZX100
parent 18c438b109
commit 4dd97f7dbe
3 changed files with 70 additions and 0 deletions

View File

@ -29,6 +29,7 @@
#include <optional>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
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<std::uint64_t, const char*> 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)) {

View File

@ -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"; }

View File

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