From 3effed87cbc5135fcc392db3c4a60b856b4d387f Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Mon, 26 Oct 2026 20:13:10 -0300 Subject: [PATCH 1/8] feat(libkernel): maintain per-thread guest GS bases on Windows --- core/libs/prx/libc/CMakeLists.txt | 1 + core/libs/prx/libc/include/GuestGs.hpp | 6 +++ .../libc/src/specifics/windows/GuestGs.cpp | 15 ++++++ .../libs/prx/libkernel/Pthread/src/Thread.cpp | 8 ++++ .../libs/prx/libkernel/System/src/Process.cpp | 31 +++++++++--- core/libs/tests/GuestSysarch.cpp | 48 ++++++++++++++++--- docs/dev/debt/libkernel.md | 4 +- 7 files changed, 100 insertions(+), 13 deletions(-) create mode 100644 core/libs/prx/libc/include/GuestGs.hpp create mode 100644 core/libs/prx/libc/src/specifics/windows/GuestGs.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index aa8fa85c27..ff971f23ad 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -53,6 +53,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/FailureReport.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/gcc/StackGuard.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/windows/CrashReport.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/windows/GuestGs.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/x86_64/CallTrace.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/linux/DlIteratePhdr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/macos/GuestImages.cpp diff --git a/core/libs/prx/libc/include/GuestGs.hpp b/core/libs/prx/libc/include/GuestGs.hpp new file mode 100644 index 0000000000..ad94c2966e --- /dev/null +++ b/core/libs/prx/libc/include/GuestGs.hpp @@ -0,0 +1,6 @@ +#pragma once + +#include + +extern "C" std::uint64_t GuestGsGet_nid_no_patch(); +extern "C" void GuestGsSet_nid_no_patch(std::uint64_t base); diff --git a/core/libs/prx/libc/src/specifics/windows/GuestGs.cpp b/core/libs/prx/libc/src/specifics/windows/GuestGs.cpp new file mode 100644 index 0000000000..85a7d55baf --- /dev/null +++ b/core/libs/prx/libc/src/specifics/windows/GuestGs.cpp @@ -0,0 +1,15 @@ +#ifdef _WIN32 +#include +#include "prx/libc/include/GuestGs.hpp" +#include "prx/libc/include/HostThreadSlot.hpp" + +namespace { + +struct BaseTag; +using BaseSlot = HostThreadSlot; + +} + +extern "C" std::uint64_t GuestGsGet_nid_no_patch() { return BaseSlot::Get(); } +extern "C" void GuestGsSet_nid_no_patch(std::uint64_t base) { BaseSlot::Set(base); } +#endif diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 75a44a3cd2..04c3f6b005 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -1,3 +1,4 @@ +#include "prx/libc/include/GuestGs.hpp" #include "prx/libkernel/Pthread/include/ThreadLifecycle.hpp" #include "prx/libkernel/Pthread/include/Cancel.hpp" #include "prx/libkernel/include/Api.hpp" @@ -68,6 +69,9 @@ struct ThreadArgs { PthreadEntry entry; void* arg; PthreadPrivate* self; +#ifdef _WIN32 + std::uint64_t gsBase = 0; +#endif #ifndef _WIN32 sigset_t signalMask; #endif @@ -794,6 +798,9 @@ static void RunThread(std::unique_ptr args) { RegisterStack(self); #ifndef _WIN32 const sigset_t signalMask = args->signalMask; +#endif +#ifdef _WIN32 + GuestGsSet_nid_no_patch(args->gsBase); #endif args.reset(); self->suspended.wait(true, std::memory_order_acquire); @@ -980,6 +987,7 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread std::promise start; auto args = std::make_unique(ThreadArgs{entry, arg, p.get()}); #ifdef _WIN32 + args->gsBase = GuestGsGet_nid_no_patch(); SYSTEM_INFO system{}; GetSystemInfo(&system); if (p->stackSize < 16384 || p->stackSize > std::numeric_limits::max() - HOST_STACK_MARGIN - system.dwPageSize) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 39cdaac297..e8b6be8b63 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -1,3 +1,4 @@ +#include "prx/libc/include/GuestGs.hpp" #include "prx/libkernel/include/Api.hpp" #include "prx/libc/include/Api.hpp" #ifdef _WIN32 @@ -994,35 +995,53 @@ int APS5_VABI sysarch(int operation, void* parameters) { } const bool i386 = operation == i386GetFsBase || operation == i386GetGsBase; if (!i386 && operation != getFsBase && operation != getGsBase && operation != setGsBase) - NotImplemented_nid_no_patch(("sysarch: operation " + std::to_string(operation)).c_str()); -#if defined(__linux__) && defined(__x86_64__) + NotImplemented_nid_no_patch(__func__); +#if (defined(__linux__) || defined(_WIN32)) && defined(__x86_64__) if (parameters == nullptr) { *__error_nid_postfix() = guestFault; return -1; } std::uint64_t base = 0; if (operation == setGsBase) { - std::memcpy(&base, parameters, sizeof(base)); + if (!ReadGuest(&base, parameters, sizeof(base))) { + *__error_nid_postfix() = guestFault; + return -1; + } if (base >= userAddressEnd) { *__error_nid_postfix() = guestInvalid; return -1; } +#ifdef _WIN32 + GuestGsSet_nid_no_patch(base); +#else if (::syscall(SYS_arch_prctl, ARCH_SET_GS, base) != 0) throw std::system_error(errno, std::generic_category(), "sysarch: setting the GS base"); +#endif return 0; } const bool fs = operation == getFsBase || operation == i386GetFsBase; +#ifdef _WIN32 + if (fs) NotImplemented_nid_no_patch(__func__); + base = GuestGsGet_nid_no_patch(); +#else if (::syscall(SYS_arch_prctl, fs ? ARCH_GET_FS : ARCH_GET_GS, &base) != 0) throw std::system_error(errno, std::generic_category(), "sysarch: reading a segment base"); +#endif if (i386) { const auto low = static_cast(base); - std::memcpy(parameters, &low, sizeof(low)); + if (!WriteGuest(parameters, &low, sizeof(low))) { + *__error_nid_postfix() = guestFault; + return -1; + } return 0; } - std::memcpy(parameters, &base, sizeof(base)); + if (!WriteGuest(parameters, &base, sizeof(base))) { + *__error_nid_postfix() = guestFault; + return -1; + } return 0; #else static_cast(parameters); - NotImplemented_nid_no_patch("sysarch: segment bases on this host"); + NotImplemented_nid_no_patch(__func__); return 0; #endif } diff --git a/core/libs/tests/GuestSysarch.cpp b/core/libs/tests/GuestSysarch.cpp index b6533c2e65..b0f027b954 100644 --- a/core/libs/tests/GuestSysarch.cpp +++ b/core/libs/tests/GuestSysarch.cpp @@ -19,7 +19,7 @@ template static std::string Thrown(TCall call) { return {}; } -#if defined(__linux__) && defined(__x86_64__) +#if (defined(__linux__) || defined(_WIN32)) && defined(__x86_64__) static void* APS5_VABI ReadGsBase(void* arg) { auto* read = static_cast(arg); if (sysarch(130, read) != 0) *read = 0; @@ -35,21 +35,41 @@ static void RequireLowHalf(int operation, std::uint64_t base) { Require(low == static_cast(base)); for (int i = 4; i < 8; ++i) Require(bytes[i] == 0xaa); } + +struct ThreadBases { + std::uint64_t expected; + std::uint64_t replacement; +}; + +static void* APS5_VABI ChangeGsBase(void* arg) { + auto& bases = *static_cast(arg); + std::uint64_t read = 0; + Require(sysarch(130, &read) == 0 && read == bases.expected); + Require(sysarch(131, &bases.replacement) == 0); + Require(sysarch(130, &read) == 0 && read == bases.replacement); + RequireLowHalf(9, read); + Pthread nested = nullptr; + std::uint64_t inherited = 0; + Require(pthread_create_nid_postfix(&nested, nullptr, ReadGsBase, &inherited) == 0); + Require(pthread_join_nid_postfix(nested, nullptr) == 0 && inherited == bases.replacement); + return nullptr; +} #endif int main() { - Require(Thrown([] { sysarch(129, nullptr); }) == "sysarch: operation 129 not implemented"); - Require(Thrown([] { sysarch(132, nullptr); }) == "sysarch: operation 132 not implemented"); + Require(Thrown([] { sysarch(129, nullptr); }) == "sysarch not implemented"); + Require(Thrown([] { sysarch(132, nullptr); }) == "sysarch not implemented"); for (const int operation : {2, 5, 6, 12, 127, 133, 134, 1000, -1}) { *__error_nid_postfix() = 0; Require(sysarch(operation, nullptr) == -1 && *__error_nid_postfix() == 22); } -#if defined(__linux__) && defined(__x86_64__) +#if (defined(__linux__) || defined(_WIN32)) && defined(__x86_64__) static std::uint64_t slots[2] = {0x1234567890abcdefull, 0}; std::uint64_t base = reinterpret_cast(slots); Require(sysarch(131, &base) == 0); std::uint64_t read = 0; Require(sysarch(130, &read) == 0 && read == base); +#ifdef __linux__ std::uint64_t through = 0; asm volatile("movq %%gs:0, %0" : "=r"(through)); Require(through == slots[0]); @@ -62,12 +82,24 @@ int main() { RequireLowHalf(9, base); *__error_nid_postfix() = 0; Require(sysarch(7, nullptr) == -1 && *__error_nid_postfix() == 14); +#else + Require(Thrown([&read] { sysarch(128, &read); }) == "sysarch not implemented"); + Require(Thrown([&read] { sysarch(7, &read); }) == "sysarch not implemented"); +#endif + RequireLowHalf(9, base); *__error_nid_postfix() = 0; Require(sysarch(9, nullptr) == -1 && *__error_nid_postfix() == 14); std::uint64_t inherited = 0; Pthread child = nullptr; Require(scePthreadCreate(&child, nullptr, ReadGsBase, &inherited, "sysarch_gs") == 0); Require(scePthreadJoin(child, nullptr) == 0 && inherited == base); + ThreadBases bases{base, 0x0000123456789abcull}; + Require(scePthreadCreate(&child, nullptr, ChangeGsBase, &bases, "sysarch_change") == 0); + Require(scePthreadJoin(child, nullptr) == 0); + Require(sysarch(130, &read) == 0 && read == base); + Require(pthread_create_nid_postfix(&child, nullptr, ChangeGsBase, &bases) == 0); + Require(pthread_join_nid_postfix(child, nullptr) == 0); + Require(sysarch(130, &read) == 0 && read == base); std::uint64_t outside = 0x0000800000000000ull; *__error_nid_postfix() = 0; Require(sysarch(131, &outside) == -1 && *__error_nid_postfix() == 22); @@ -75,12 +107,16 @@ int main() { *__error_nid_postfix() = 0; Require(sysarch(131, nullptr) == -1 && *__error_nid_postfix() == 14); Require(sysarch(130, nullptr) == -1 && *__error_nid_postfix() == 14); + Require(sysarch(131, reinterpret_cast(1)) == -1 && *__error_nid_postfix() == 14); + Require(sysarch(130, reinterpret_cast(1)) == -1 && *__error_nid_postfix() == 14); + Require(sysarch(9, reinterpret_cast(1)) == -1 && *__error_nid_postfix() == 14); + Require(sysarch(130, &read) == 0 && read == base); std::uint64_t zero = 0; Require(sysarch(131, &zero) == 0 && sysarch(130, &read) == 0 && read == 0); #else std::uint64_t base = 0; - Require(Thrown([&base] { sysarch(130, &base); }) == "sysarch: segment bases on this host not implemented"); - Require(Thrown([&base] { sysarch(7, &base); }) == "sysarch: segment bases on this host not implemented"); + Require(Thrown([&base] { sysarch(130, &base); }) == "sysarch not implemented"); + Require(Thrown([&base] { sysarch(7, &base); }) == "sysarch not implemented"); #endif return 0; } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index e90c121433..dd164ef94d 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -207,7 +207,9 @@ - [dup and dup2](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throw for the combinations they do not model: `dup2` of a guest socket onto descriptors 0 to 2, whose host streams libc writes without the socket table. Guest sockets share one socket object between the duplicates; the numbers come from the host descriptor table like those of files. The errno values for descriptor exhaustion are not verified against the console. On Windows a guest `dup2` onto descriptor 1 or 2 keeps host logging on an `_fdopen` of a duplicate, and the guest `FILE` stays on the descriptor that was replaced; opening that duplicate throws (#7791) - [waitpid and wait](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) reaps [posix_spawn](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) children, whose guest process IDs are the host's. On Linux the host call runs: the guest signal numbering applies in bits 8-15 for a stopped child and bits 0-6 for a killed one (7, 10, 12, 17, 18, 19, 20, 23, 29, 31 to 10, 30, 31, 20, 19, 17, 18, 16, 23, 12; Linux 16 and 30 are unmapped and throw), a continued status becomes 0x13, and guest `WCONTINUED` 4 becomes Linux 8. On Windows an unrecognized exit code keeps the guest `exit` byte layout, so `exit(-1)` reads as 255, and a structured-exception code maps to `SIGSEGV`, `SIGILL`, `SIGTRAP`, `SIGFPE` or `SIGABRT` (access violation/stack overflow, illegal instruction, breakpoint, division, fastfail). Windows has no stop/continue status; unknown `options` bits fail with `EINVAL`. Without an unreaped matching child, even after another thread reaps it, the call gives `ECHILD`. - [execv, execvp and execve](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) replace the running image with the converted guest executable, following FreeBSD 11.4 `exec_copyin_args`: a null path is `EBADF` (measured), a null `argv` `EFAULT`, an empty path `ENOENT`; an empty `argv` and a null or unpaired `envp` are copied, `E2BIG` on their bytes; a lookup misses with `ENOENT`, a path over a regular file with `ENOTDIR`, and a regular file with no execute permission `EACCES` (0644 measured); `execvp` walks `PATH` skipping non-regular and inexecutable candidates, keeping their `EACCES`, or ends with `ENOENT`. Linux replaces through `fexecve`, returning `ENOEXEC` (measured) for a non-x86-64 target and `EACCES` for one without execute permission; Windows probes the PE header for the same `ENOEXEC`. A missing converted artifact throws. Windows execute permission is libc `access`'s ACL. The replacement keeps the host `environ`, `execve`'s `envp` name overrides -- [sysarch](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `AMD64_GET_FSBASE`, `AMD64_GET_GSBASE` and `AMD64_SET_GSBASE` follow FreeBSD 11's amd64 [`sysarch`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c) on Linux x86-64 hosts, through the host's `arch_prctl`. The GS base belongs to the calling thread, a base at or above `0x800000000000` (`VM_MAXUSER_ADDRESS`) returns `EINVAL`, and a null argument returns `EFAULT`. `AMD64_SET_FSBASE` throws, since guest and host code share the thread's FS base for their TLS. In a payload process on a PS5 (firmware 12.70), `I386_GET_FSBASE` (7) and `I386_GET_GSBASE` (9) wrote 4 bytes, the low 32 bits of the FS and GS bases, and failed with `EFAULT` for a null argument; here they do the same on Linux x86-64 hosts. There a GS base set by one thread left the main thread's unchanged, and threads made with `pthread_create` and `scePthreadCreate` started with their creator's GS base; here a guest thread's host thread is created by the creating thread, and Linux's [`copy_thread`](https://github.com/torvalds/linux/blob/v6.6/arch/x86/kernel/process.c#L182-L186) gives a new thread its creator's GS base. The operation numbers FreeBSD 11's amd64 `sysarch` has no case for returned `EINVAL` there for the nine tried, each with a null argument (2, 5, 6, 12, 127, 133, 134, 1000 and -1; 6 is `I386_VM86`, which its header names), as its [default case](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c#L324-L325) does; here every such number returns `EINVAL` on every host; the numbers other than those nine follow that default case and were not measured. `AMD64_GET_XFPUSTATE` (132) returned 0 there for lengths 0, 64, 128, 256 and 512 (512 was also the largest length a search accepted) and failed with `EINVAL` for 1024, 2048, 4096 and 8192 and with `EFAULT` for a null argument or address, and `I386_GET_XFPUSTATE` (11) failed with `EFAULT` for a null argument; both throw here. The other operations FreeBSD 11's amd64 `sysarch` handles, and every operation it handles on other hosts (Windows keeps its TEB in GS), throw. +- [sysarch](../../../core/libs/prx/libkernel/System/src/Process.cpp) throws for SET_FSBASE and Windows GET_FSBASE/I386_GET_FSBASE: guest and host FS/TLS cannot be treated as interchangeable. GS GET/SET follows FreeBSD 11 amd64 [sysarch](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c); bases at or above VM_MAXUSER_ADDRESS (0x800000000000) fail with EINVAL and invalid parameter memory with EFAULT. On Linux x86-64 this uses arch_prctl; Windows x86-64 has a separate thread base. Other hosts throw for segment operations. +- The initial Windows [guest GS base](../../../core/libs/prx/libkernel/System/src/Process.cpp) is zero; the console's initial base is unmeasured. Native host-created threads start with zero and their inheritance is not implemented. In a PS5 firmware 12.70 payload, setting one thread's GS left the main thread unchanged, and pthread_create/scePthreadCreate children inherited their creator's base; guest threads here do the same on Linux and Windows. I386_GET_FSBASE (7) and I386_GET_GSBASE (9) wrote four low-base bytes there and failed with EFAULT for null; Windows supports only operation 9. Windows guest GS memory instructions still trap; only the thread state API is implemented. +- [sysarch](../../../core/libs/prx/libkernel/System/src/Process.cpp) XFPUSTATE operations 11 and 132 throw. On PS5 firmware 12.70, operation 132 accepted lengths 0, 64, 128, 256 and 512 (the largest found), rejected 1024, 2048, 4096 and 8192 with EINVAL, and rejected null arguments/addresses with EFAULT; operation 11 rejected a null argument with EFAULT. Other operations FreeBSD 11 amd64 sysarch handles remain unimplemented. Numbers absent from its switch return EINVAL here; the console confirmed that result for 2, 5, 6, 12, 127, 133, 134, 1000 and -1 with null arguments. Other absent numbers follow FreeBSD's default case and are unmeasured. - [kern.proc.pathname](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns `/app0/eboot.bin` for pid -1 and the calling process, following FreeBSD's `sysctl_kern_proc_pathname`; the exact pathname on retail consoles remains unverified. - [kern.proc.pid](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - what the console fills in a process's `kinfo_proc` is unverified; `sysctl` {`CTL_KERN`, `KERN_PROC`, `KERN_PROC_PID`, pid} for the calling process returns a 1,088-byte FreeBSD 11 amd64 `kinfo_proc` ([`sys/sys/user.h`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/sys/user.h)) holding only `ki_structsize`, `ki_pid`, `ki_ppid` and `ki_comm` (`eboot.bin`, the base name of the `kern.proc.pathname` value), with every other field 0, including `ki_flag`, so `P_TRACED` (0x800) is clear. Source Port (PPSA20040) asks for its own record with a 0x440-byte buffer and tests only bit 0x800 of `ki_flag` at offset 0x170, to see whether a debugger is attached. Any other pid, including -1, fails with `ESRCH`, a write with `EPERM` and a short buffer with `ENOMEM`, as for `kern.proc.vmmap`. - [kern.proc.env](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - which variables, if any, the console starts an application with is unverified; `sysctl` {`CTL_KERN`, `KERN_PROC`, `KERN_PROC_ENV`, pid} returns 0 bytes, the empty start-up environment `environ` and libc's `getenv` also begin with, as FreeBSD 11's [`sysctl_kern_proc_env`](https://github.com/freebsd/freebsd-src/blob/releng/11.0/sys/kern/kern_proc.c) copies a process's start-up strings, not later `setenv` changes. Only the calling process's pid is accepted (`ESRCH` otherwise, -1 included); a write returns `EPERM`. PS5VR (PPSA99177) reads it for its own pid at start-up. From c73ccc763785d2fac06a7b5220529005410bcb98 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 27 Oct 2026 09:17:08 -0300 Subject: [PATCH 2/8] test(libkernel): avoid pinning unsupported FS operations as refusals --- core/libs/tests/GuestSysarch.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/libs/tests/GuestSysarch.cpp b/core/libs/tests/GuestSysarch.cpp index b0f027b954..3706461b9a 100644 --- a/core/libs/tests/GuestSysarch.cpp +++ b/core/libs/tests/GuestSysarch.cpp @@ -82,9 +82,6 @@ int main() { RequireLowHalf(9, base); *__error_nid_postfix() = 0; Require(sysarch(7, nullptr) == -1 && *__error_nid_postfix() == 14); -#else - Require(Thrown([&read] { sysarch(128, &read); }) == "sysarch not implemented"); - Require(Thrown([&read] { sysarch(7, &read); }) == "sysarch not implemented"); #endif RequireLowHalf(9, base); *__error_nid_postfix() = 0; From fa743e6e99f1a3f9ce6ba5d66a0a5638c0d62b5d Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Fri, 30 Oct 2026 00:26:34 +1300 Subject: [PATCH 3/8] fix(libkernel): remove sysarch refusal tests and restore diagnostics --- core/libs/prx/libkernel/System/src/Process.cpp | 6 +++--- core/libs/tests/GuestSysarch.cpp | 15 --------------- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index e8b6be8b63..5d7c11d0bf 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -995,7 +995,7 @@ int APS5_VABI sysarch(int operation, void* parameters) { } const bool i386 = operation == i386GetFsBase || operation == i386GetGsBase; if (!i386 && operation != getFsBase && operation != getGsBase && operation != setGsBase) - NotImplemented_nid_no_patch(__func__); + NotImplemented_nid_no_patch(("sysarch: operation " + std::to_string(operation)).c_str()); #if (defined(__linux__) || defined(_WIN32)) && defined(__x86_64__) if (parameters == nullptr) { *__error_nid_postfix() = guestFault; @@ -1020,7 +1020,7 @@ int APS5_VABI sysarch(int operation, void* parameters) { } const bool fs = operation == getFsBase || operation == i386GetFsBase; #ifdef _WIN32 - if (fs) NotImplemented_nid_no_patch(__func__); + if (fs) NotImplemented_nid_no_patch(("sysarch: operation " + std::to_string(operation)).c_str()); base = GuestGsGet_nid_no_patch(); #else if (::syscall(SYS_arch_prctl, fs ? ARCH_GET_FS : ARCH_GET_GS, &base) != 0) @@ -1041,7 +1041,7 @@ int APS5_VABI sysarch(int operation, void* parameters) { return 0; #else static_cast(parameters); - NotImplemented_nid_no_patch(__func__); + NotImplemented_nid_no_patch(("sysarch: operation " + std::to_string(operation)).c_str()); return 0; #endif } diff --git a/core/libs/tests/GuestSysarch.cpp b/core/libs/tests/GuestSysarch.cpp index 3706461b9a..b60480083b 100644 --- a/core/libs/tests/GuestSysarch.cpp +++ b/core/libs/tests/GuestSysarch.cpp @@ -10,15 +10,6 @@ static void Require(bool value) { if (!value) std::abort(); } -template static std::string Thrown(TCall call) { - try { - call(); - } catch (const std::runtime_error& error) { - return error.what(); - } - return {}; -} - #if (defined(__linux__) || defined(_WIN32)) && defined(__x86_64__) static void* APS5_VABI ReadGsBase(void* arg) { auto* read = static_cast(arg); @@ -57,8 +48,6 @@ static void* APS5_VABI ChangeGsBase(void* arg) { #endif int main() { - Require(Thrown([] { sysarch(129, nullptr); }) == "sysarch not implemented"); - Require(Thrown([] { sysarch(132, nullptr); }) == "sysarch not implemented"); for (const int operation : {2, 5, 6, 12, 127, 133, 134, 1000, -1}) { *__error_nid_postfix() = 0; Require(sysarch(operation, nullptr) == -1 && *__error_nid_postfix() == 22); @@ -110,10 +99,6 @@ int main() { Require(sysarch(130, &read) == 0 && read == base); std::uint64_t zero = 0; Require(sysarch(131, &zero) == 0 && sysarch(130, &read) == 0 && read == 0); -#else - std::uint64_t base = 0; - Require(Thrown([&base] { sysarch(130, &base); }) == "sysarch not implemented"); - Require(Thrown([&base] { sysarch(7, &base); }) == "sysarch not implemented"); #endif return 0; } From b9bcb27fb98b90fc2474a9d66443c506bcf748c2 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Thu, 29 Oct 2026 20:36:05 -0300 Subject: [PATCH 4/8] docs(libkernel): preserve sysarch sources and clarify Windows FS debt --- core/libs/tests/GuestSysarch.cpp | 3 --- docs/dev/debt/libkernel.md | 6 +++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/core/libs/tests/GuestSysarch.cpp b/core/libs/tests/GuestSysarch.cpp index b60480083b..9085bc01b5 100644 --- a/core/libs/tests/GuestSysarch.cpp +++ b/core/libs/tests/GuestSysarch.cpp @@ -5,8 +5,6 @@ #include #include #include -#include -#include static void Require(bool value) { if (!value) std::abort(); } @@ -68,7 +66,6 @@ int main() { asm volatile("movq %%fs:0, %0" : "=r"(self)); Require(self == fs); RequireLowHalf(7, fs); - RequireLowHalf(9, base); *__error_nid_postfix() = 0; Require(sysarch(7, nullptr) == -1 && *__error_nid_postfix() == 14); #endif diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 6453ceaa07..23ea484540 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -218,9 +218,9 @@ - [dup and dup2](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throw for the combinations they do not model: `dup2` of a guest socket onto descriptors 0 to 2, whose host streams libc writes without the socket table. Guest sockets share one socket object between the duplicates; the numbers come from the host descriptor table like those of files. The errno values for descriptor exhaustion are not verified against the console. On Windows a guest `dup2` onto descriptor 1 or 2 keeps host logging on an `_fdopen` of a duplicate, and the guest `FILE` stays on the descriptor that was replaced; opening that duplicate throws (#7791) - [waitpid and wait](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) reaps [posix_spawn](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) children, whose guest process IDs are the host's. On Linux the host call runs: the guest signal numbering applies in bits 8-15 for a stopped child and bits 0-6 for a killed one (7, 10, 12, 17, 18, 19, 20, 23, 29, 31 to 10, 30, 31, 20, 19, 17, 18, 16, 23, 12; Linux 16 and 30 are unmapped and throw), a continued status becomes 0x13, and guest `WCONTINUED` 4 becomes Linux 8. On Windows an unrecognized exit code keeps the guest `exit` byte layout, so `exit(-1)` reads as 255, and a structured-exception code maps to `SIGSEGV`, `SIGILL`, `SIGTRAP`, `SIGFPE` or `SIGABRT` (access violation/stack overflow, illegal instruction, breakpoint, division, fastfail). Windows has no stop/continue status; unknown `options` bits fail with `EINVAL`. Without an unreaped matching child, even after another thread reaps it, the call gives `ECHILD`. - [execv, execvp and execve](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) replace the running image with the converted guest executable, following FreeBSD 11.4 `exec_copyin_args`: a null path is `EBADF` (measured), a null `argv` `EFAULT`, an empty path `ENOENT`; an empty `argv` and a null or unpaired `envp` are copied, `E2BIG` on their bytes; a lookup misses with `ENOENT`, a path over a regular file with `ENOTDIR`, and a regular file with no execute permission `EACCES` (0644 measured); `execvp` walks `PATH` skipping non-regular and inexecutable candidates, keeping their `EACCES`, or ends with `ENOENT`. Linux replaces through `fexecve`, returning `ENOEXEC` (measured) for a non-x86-64 target and `EACCES` for one without execute permission; Windows probes the PE header for the same `ENOEXEC`. A missing converted artifact throws. Windows execute permission is libc `access`'s ACL. The replacement keeps the host `environ`, `execve`'s `envp` name overrides -- [sysarch](../../../core/libs/prx/libkernel/System/src/Process.cpp) throws for SET_FSBASE and Windows GET_FSBASE/I386_GET_FSBASE: guest and host FS/TLS cannot be treated as interchangeable. GS GET/SET follows FreeBSD 11 amd64 [sysarch](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c); bases at or above VM_MAXUSER_ADDRESS (0x800000000000) fail with EINVAL and invalid parameter memory with EFAULT. On Linux x86-64 this uses arch_prctl; Windows x86-64 has a separate thread base. Other hosts throw for segment operations. -- The initial Windows [guest GS base](../../../core/libs/prx/libkernel/System/src/Process.cpp) is zero; the console's initial base is unmeasured. Native host-created threads start with zero and their inheritance is not implemented. In a PS5 firmware 12.70 payload, setting one thread's GS left the main thread unchanged, and pthread_create/scePthreadCreate children inherited their creator's base; guest threads here do the same on Linux and Windows. I386_GET_FSBASE (7) and I386_GET_GSBASE (9) wrote four low-base bytes there and failed with EFAULT for null; Windows supports only operation 9. Windows guest GS memory instructions still trap; only the thread state API is implemented. -- [sysarch](../../../core/libs/prx/libkernel/System/src/Process.cpp) XFPUSTATE operations 11 and 132 throw. On PS5 firmware 12.70, operation 132 accepted lengths 0, 64, 128, 256 and 512 (the largest found), rejected 1024, 2048, 4096 and 8192 with EINVAL, and rejected null arguments/addresses with EFAULT; operation 11 rejected a null argument with EFAULT. Other operations FreeBSD 11 amd64 sysarch handles remain unimplemented. Numbers absent from its switch return EINVAL here; the console confirmed that result for 2, 5, 6, 12, 127, 133, 134, 1000 and -1 with null arguments. Other absent numbers follow FreeBSD's default case and are unmeasured. +- [sysarch](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - SET_FSBASE throws because guest and host FS/TLS are shared. Windows GET_FSBASE/I386_GET_FSBASE return EFAULT for null arguments and throw for non-null arguments. GS GET/SET follows FreeBSD 11 amd64 [sysarch](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c); bases at or above VM_MAXUSER_ADDRESS (0x800000000000) fail with EINVAL and invalid parameter memory with EFAULT. Linux x86-64 uses arch_prctl; Windows x86-64 stores a separate guest base. Other hosts throw for segment operations. +- The initial Windows [guest GS base](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) is zero; the console's initial base is unmeasured. Native host-created Windows threads start with zero and their inheritance is not implemented. In a PS5 firmware 12.70 payload, one thread's GS changes left the main thread unchanged and pthread_create/scePthreadCreate children inherited their creator's base. Both guest thread APIs inherit on Windows; Linux [copy_thread](https://github.com/torvalds/linux/blob/v6.6/arch/x86/kernel/process.c#L182-L186) copies the creator's GS base. I386_GET_FSBASE (7) and I386_GET_GSBASE (9) wrote four low-base bytes on the console and failed with EFAULT for null; Linux implements both and Windows implements 9. Windows guest GS memory instructions still trap; only the thread state API is implemented. +- [sysarch](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) XFPUSTATE operations 11 and 132 throw. On PS5 firmware 12.70, operation 132 accepted lengths 0, 64, 128, 256 and 512 (the largest found), rejected 1024, 2048, 4096 and 8192 with EINVAL, and rejected null arguments/addresses with EFAULT; operation 11 rejected a null argument with EFAULT. Other operations FreeBSD 11 amd64 sysarch handles remain unimplemented. Numbers absent from its switch return EINVAL here; the console confirmed that result for 2, 5, 6, 12, 127, 133, 134, 1000 and -1 with null arguments (6 is I386_VM86). Other absent numbers follow FreeBSD's [default case](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c#L324-L325) and are unmeasured. - [kern.proc.pathname](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns `/app0/eboot.bin` for pid -1 and the calling process, following FreeBSD's `sysctl_kern_proc_pathname`; the exact pathname on retail consoles remains unverified. - [kern.proc.pid](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - what the console fills in a process's `kinfo_proc` is unverified; `sysctl` {`CTL_KERN`, `KERN_PROC`, `KERN_PROC_PID`, pid} for the calling process returns a 1,088-byte FreeBSD 11 amd64 `kinfo_proc` ([`sys/sys/user.h`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/sys/user.h)) holding only `ki_structsize`, `ki_pid`, `ki_ppid` and `ki_comm` (`eboot.bin`, the base name of the `kern.proc.pathname` value), with every other field 0, including `ki_flag`, so `P_TRACED` (0x800) is clear. Source Port (PPSA20040) asks for its own record with a 0x440-byte buffer and tests only bit 0x800 of `ki_flag` at offset 0x170, to see whether a debugger is attached. Any other pid, including -1, fails with `ESRCH`, a write with `EPERM` and a short buffer with `ENOMEM`, as for `kern.proc.vmmap`. - [kern.proc.env](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - which variables, if any, the console starts an application with is unverified; `sysctl` {`CTL_KERN`, `KERN_PROC`, `KERN_PROC_ENV`, pid} returns 0 bytes, the empty start-up environment `environ` and libc's `getenv` also begin with, as FreeBSD 11's [`sysctl_kern_proc_env`](https://github.com/freebsd/freebsd-src/blob/releng/11.0/sys/kern/kern_proc.c) copies a process's start-up strings, not later `setenv` changes. Only the calling process's pid is accepted (`ESRCH` otherwise, -1 included); a write returns `EPERM`. PS5VR (PPSA99177) reads it for its own pid at start-up. From 3a30abf703496320f17011003d0c6988e118f3ec Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Sat, 31 Oct 2026 07:47:56 -0300 Subject: [PATCH 5/8] fix(tests): register write-watch fault cases once --- core/libs/tests/write_watch/CMakeLists.txt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/core/libs/tests/write_watch/CMakeLists.txt b/core/libs/tests/write_watch/CMakeLists.txt index 5686786df8..a912f98cc1 100644 --- a/core/libs/tests/write_watch/CMakeLists.txt +++ b/core/libs/tests/write_watch/CMakeLists.txt @@ -37,8 +37,3 @@ foreach(mode execute-fault read-fault) add_test(NAME write_watch_${mode} COMMAND write_watch_tests ${mode}) set_tests_properties(write_watch_${mode} PROPERTIES TIMEOUT 30) endforeach() - -foreach(mode execute-fault read-fault) - add_test(NAME write_watch_${mode} COMMAND write_watch_tests ${mode}) - set_tests_properties(write_watch_${mode} PROPERTIES TIMEOUT 30) -endforeach() From e32141a3d880cb573c89fd92c9d381942a51ab6e Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sun, 1 Nov 2026 23:46:14 +1300 Subject: [PATCH 6/8] docs(debt): keep the sysarch EFAULT fallback qualification after the merge with main --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 8a4ad394fd..9e5ae8295b 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -238,7 +238,7 @@ - [dup and dup2](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throw for the combinations they do not model: `dup2` of a guest socket onto descriptors 0 to 2, whose host streams libc writes without the socket table. Guest sockets share one socket object between the duplicates; the numbers come from the host descriptor table like those of files. The errno values for descriptor exhaustion are not verified against the console. On Windows a guest `dup2` onto descriptor 1 or 2 keeps host logging on an `_fdopen` of a duplicate, and the guest `FILE` stays on the descriptor that was replaced; opening that duplicate throws (#7791) - [waitpid and wait](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) reaps [posix_spawn](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) children, whose guest process IDs are the host's. On Linux the host call runs: the guest signal numbering applies in bits 8-15 for a stopped child and bits 0-6 for a killed one (7, 10, 12, 17, 18, 19, 20, 23, 29, 31 to 10, 30, 31, 20, 19, 17, 18, 16, 23, 12; Linux 16 and 30 are unmapped and throw), a continued status becomes 0x13, and guest `WCONTINUED` 4 becomes Linux 8. On Windows an unrecognized exit code keeps the guest `exit` byte layout, so `exit(-1)` reads as 255, and a structured-exception code maps to `SIGSEGV`, `SIGILL`, `SIGTRAP`, `SIGFPE` or `SIGABRT` (access violation/stack overflow, illegal instruction, breakpoint, division, fastfail). Windows has no stop/continue status; unknown `options` bits fail with `EINVAL`. Without an unreaped matching child, even after another thread reaps it, the call gives `ECHILD`. - [execv, execvp and execve](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) replace the running image with the converted guest executable, following FreeBSD 11.4 `exec_copyin_args`: a null path is `EBADF` (measured), a null `argv` `EFAULT`, an empty path `ENOENT`; an empty `argv` and a null or unpaired `envp` are copied, `E2BIG` on their bytes; a lookup misses with `ENOENT`, a path over a regular file with `ENOTDIR`, and a regular file with no execute permission `EACCES` (0644 measured); `execvp` walks `PATH` skipping non-regular and inexecutable candidates, keeping their `EACCES`, or ends with `ENOENT`. Linux replaces through `fexecve`, returning `ENOEXEC` (measured) for a non-x86-64 target and `EACCES` for one without execute permission; Windows probes the PE header for the same `ENOEXEC`. A missing converted artifact throws. Windows execute permission is libc `access`'s ACL. The replacement keeps the host `environ`, `execve`'s `envp` name overrides -- [sysarch](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - SET_FSBASE throws because guest and host FS/TLS are shared. Windows GET_FSBASE/I386_GET_FSBASE return EFAULT for null arguments and throw for non-null arguments. GS GET/SET follows FreeBSD 11 amd64 [sysarch](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c); bases at or above VM_MAXUSER_ADDRESS (0x800000000000) fail with EINVAL and invalid parameter memory with EFAULT. Linux x86-64 uses arch_prctl; Windows x86-64 stores a separate guest base. Other hosts throw for segment operations. +- [sysarch](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - SET_FSBASE throws because guest and host FS/TLS are shared. Windows GET_FSBASE/I386_GET_FSBASE return EFAULT for null arguments and throw for non-null arguments. GS GET/SET follows FreeBSD 11 amd64 [sysarch](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c); bases at or above VM_MAXUSER_ADDRESS (0x800000000000) fail with EINVAL and invalid parameter memory with EFAULT. A non-null argument the host cannot read (`AMD64_SET_GSBASE`) or write (the `GET` operations) also returns `EFAULT`; this is a host-safety fallback, and the console's answer for a non-null invalid pointer was not measured. Linux x86-64 uses arch_prctl; Windows x86-64 stores a separate guest base. Other hosts throw for segment operations. - The initial Windows [guest GS base](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) is zero; the console's initial base is unmeasured. Native host-created Windows threads start with zero and their inheritance is not implemented. In a PS5 firmware 12.70 payload, one thread's GS changes left the main thread unchanged and pthread_create/scePthreadCreate children inherited their creator's base. Both guest thread APIs inherit on Windows; Linux [copy_thread](https://github.com/torvalds/linux/blob/v6.6/arch/x86/kernel/process.c#L182-L186) copies the creator's GS base. I386_GET_FSBASE (7) and I386_GET_GSBASE (9) wrote four low-base bytes on the console and failed with EFAULT for null; Linux implements both and Windows implements 9. Windows guest GS memory instructions still trap; only the thread state API is implemented. - [sysarch](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) XFPUSTATE operations 11 and 132 throw. On PS5 firmware 12.70, operation 132 accepted lengths 0, 64, 128, 256 and 512 (the largest found), rejected 1024, 2048, 4096 and 8192 with EINVAL, and rejected null arguments/addresses with EFAULT; operation 11 rejected a null argument with EFAULT. Other operations FreeBSD 11 amd64 sysarch handles remain unimplemented. Numbers absent from its switch return EINVAL here; the console confirmed that result for 2, 5, 6, 12, 127, 133, 134, 1000 and -1 with null arguments (6 is I386_VM86). Other absent numbers follow FreeBSD's [default case](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c#L324-L325) and are unmeasured. - [kern.proc.pathname](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns `/app0/eboot.bin` for pid -1 and the calling process, following FreeBSD's `sysctl_kern_proc_pathname`; the exact pathname on retail consoles remains unverified. From 5b93739daadfcde8ac07d1e4e25bfc9a94af23c7 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Sun, 1 Nov 2026 20:28:40 -0300 Subject: [PATCH 7/8] fix(tests): scope implemented FS pointer checks to Linux --- core/libs/tests/GuestSysarch.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/tests/GuestSysarch.cpp b/core/libs/tests/GuestSysarch.cpp index 83df1df2c8..909daa29a6 100644 --- a/core/libs/tests/GuestSysarch.cpp +++ b/core/libs/tests/GuestSysarch.cpp @@ -73,14 +73,18 @@ int main() { *__error_nid_postfix() = 0; Require(sysarch(9, nullptr) == -1 && *__error_nid_postfix() == 14); void* const bad = reinterpret_cast(static_cast(0x10)); +#ifdef __linux__ *__error_nid_postfix() = 0; Require(sysarch(7, bad) == -1 && *__error_nid_postfix() == 14); +#endif *__error_nid_postfix() = 0; Require(sysarch(9, bad) == -1 && *__error_nid_postfix() == 14); *__error_nid_postfix() = 0; Require(sysarch(128, nullptr) == -1 && *__error_nid_postfix() == 14); +#ifdef __linux__ *__error_nid_postfix() = 0; Require(sysarch(128, bad) == -1 && *__error_nid_postfix() == 14); +#endif *__error_nid_postfix() = 0; Require(sysarch(130, bad) == -1 && *__error_nid_postfix() == 14); std::uint64_t inherited = 0; From a0cea872942f2da087f8f8074ce9721ad408491c Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 3 Nov 2026 14:34:35 -0300 Subject: [PATCH 8/8] fix(libkernel): retain shared guest copy semantics in sysarch --- core/libs/prx/libkernel/System/src/Process.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 5d70c94ff6..3d03126b09 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -1028,7 +1028,7 @@ int APS5_VABI sysarch(int operation, void* parameters) { } std::uint64_t base = 0; if (operation == setGsBase) { - if (!ReadGuest(&base, parameters, sizeof(base))) { + if (!GuestCopyIn_nid_no_patch(&base, parameters, sizeof(base))) { *__error_nid_postfix() = guestFault; return -1; } @@ -1053,13 +1053,13 @@ int APS5_VABI sysarch(int operation, void* parameters) { #endif if (i386) { const auto low = static_cast(base); - if (!WriteGuest(parameters, &low, sizeof(low))) { + if (!GuestCopyOut_nid_no_patch(parameters, &low, sizeof(low))) { *__error_nid_postfix() = guestFault; return -1; } return 0; } - if (!WriteGuest(parameters, &base, sizeof(base))) { + if (!GuestCopyOut_nid_no_patch(parameters, &base, sizeof(base))) { *__error_nid_postfix() = guestFault; return -1; }