diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 844af4b6f7..2c73ac75cc 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -106,6 +106,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/LogFile.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 3d0748f52b..0d9151719f 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" @@ -74,6 +75,9 @@ struct ThreadArgs { PthreadEntry entry; void* arg; PthreadPrivate* self; +#ifdef _WIN32 + std::uint64_t gsBase = 0; +#endif #ifndef _WIN32 sigset_t signalMask; #endif @@ -898,6 +902,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); @@ -1141,6 +1148,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 beba3716fc..d5e02ed07e 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 @@ -1020,7 +1021,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(("sysarch: operation " + std::to_string(operation)).c_str()); -#if defined(__linux__) && defined(__x86_64__) +#if (defined(__linux__) || defined(_WIN32)) && defined(__x86_64__) if (parameters == nullptr) { *__error_nid_postfix() = guestFault; return -1; @@ -1035,12 +1036,21 @@ int APS5_VABI sysarch(int operation, void* parameters) { *__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(("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) throw std::system_error(errno, std::generic_category(), "sysarch: reading a segment base"); +#endif if (i386) { const auto low = static_cast(base); if (!GuestCopyOut_nid_no_patch(parameters, &low, sizeof(low))) { @@ -1056,7 +1066,7 @@ int APS5_VABI sysarch(int operation, void* parameters) { return 0; #else static_cast(parameters); - NotImplemented_nid_no_patch("sysarch: segment bases on this host"); + 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 3ac613d84d..909daa29a6 100644 --- a/core/libs/tests/GuestSysarch.cpp +++ b/core/libs/tests/GuestSysarch.cpp @@ -5,21 +5,10 @@ #include #include #include -#include -#include 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(__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 +24,39 @@ 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"); 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]); @@ -59,26 +66,38 @@ 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 + RequireLowHalf(9, base); *__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; 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); @@ -89,12 +108,12 @@ int main() { Require(sysarch(131, bad) == -1 && *__error_nid_postfix() == 14); *__error_nid_postfix() = 0; 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"); #endif return 0; } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 28f8af334f..2fd4c5a11b 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -253,7 +253,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`. 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. `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) (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. - [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.proc](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` {`CTL_KERN`, `KERN_PROC`, `KERN_PROC_PROC`} lists one process, the calling one, as the same 1,088-byte FreeBSD 11 amd64 `kinfo_proc` that `kern.proc.pid` builds, because the runtime hosts one guest process. A fourth element is ignored, as [`sysctl_kern_proc`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_proc.c) takes no pid for `KERN_PROC_PROC`, and more than four elements return `EINVAL`. A null output reports the one record's size, where FreeBSD 11.4's [`sysctl_kern_proc`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_proc.c#L1452-L1468) returns a five-record estimate; what the console answers is unverified, as are how many processes and records it lists and whether its record is 1,088 or 1,096 bytes.