diff --git a/core/libs/prx/libkernel/Module/src/Module.cpp b/core/libs/prx/libkernel/Module/src/Module.cpp index 3076dd6358e1..9e71a3183c73 100644 --- a/core/libs/prx/libkernel/Module/src/Module.cpp +++ b/core/libs/prx/libkernel/Module/src/Module.cpp @@ -1,8 +1,10 @@ #include #include #include +#include #include #include +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -153,10 +155,22 @@ extern "C" { void* APS5_VABI dlopen_nid_postfix(const char* path, int flags); void* APS5_VABI dlsym_nid_postfix(void* handle, const char* name); int APS5_VABI dlclose_nid_postfix(void* handle); +const char* APS5_VABI sceKernelGetFsSandboxRandomWord(); } namespace { constexpr int kRtldNow = 2; + +std::optional SystemLibraryPath(std::string_view path) { + const std::string sandbox = std::string("/") + sceKernelGetFsSandboxRandomWord() + "/common/lib/"; + for (const std::string_view prefix : {std::string_view("/system/common/lib/"), std::string_view(sandbox)}) { + if (!path.starts_with(prefix)) continue; + const auto name = path.substr(prefix.size()); + if (name.size() <= 5 || !name.ends_with(".sprx") || name.find('/') != std::string_view::npos) return std::nullopt; + return "libs/" + std::string(name.substr(0, name.size() - 5)) + ".prx"; + } + return std::nullopt; +} } extern "C" { @@ -281,7 +295,8 @@ KernelModule APS5_VABI sceKernelLoadStartModule(const char* module_file_name, si if (!module_file_name) return static_cast(SCE_KERNEL_ERROR_EFAULT); pendingModuleArgs = {args, argp}; pendingModuleInitResult = 0; - void* handle = dlopen_nid_postfix(module_file_name, kRtldNow); + const auto systemLibrary = SystemLibraryPath(module_file_name); + void* handle = dlopen_nid_postfix(systemLibrary ? systemLibrary->c_str() : module_file_name, kRtldNow); const int started = pendingModuleInitResult; pendingModuleArgs = {}; pendingModuleInitResult = 0; diff --git a/core/libs/tests/GuestDynamicLoader.cpp b/core/libs/tests/GuestDynamicLoader.cpp index 3a83d086a953..8ea9fe7c9329 100644 --- a/core/libs/tests/GuestDynamicLoader.cpp +++ b/core/libs/tests/GuestDynamicLoader.cpp @@ -19,6 +19,8 @@ int APS5_VABI dlclose_nid_postfix(void*); char* APS5_VABI dlerror_nid_postfix(); int APS5_VABI _sceKernelRtldThreadAtexitIncrement_nid_postfix(const void*); int APS5_VABI _sceKernelRtldThreadAtexitDecrement_nid_postfix(const void*); +int APS5_VABI sceKernelLoadStartModule(const char*, std::size_t, const void*, unsigned, const void*, int*); +int APS5_VABI sceKernelStopUnloadModule(int, std::size_t, const void*, unsigned, const void*, int*); } static void Require(bool value, const std::source_location& location = std::source_location::current()) { if (value) return; @@ -166,6 +168,25 @@ int main(int argc, char** argv) { std::filesystem::remove(guest); std::filesystem::remove(relinked); + const bool createdLibs = std::filesystem::create_directories("libs"); + std::filesystem::copy_file(argv[1], "libs/anyps5-system-module-for-test.prx", std::filesystem::copy_options::overwrite_existing); + for (const char* path : {"/gkbHtXc3Wq/common/lib/anyps5-system-module-for-test.sprx", "/system/common/lib/anyps5-system-module-for-test.sprx"}) { + int started = -1; + const int system = sceKernelLoadStartModule(path, 0, nullptr, 0, nullptr, &started); + Require(system > 0 && started == 0); + add = reinterpret_cast(dlsym_nid_postfix(reinterpret_cast(static_cast(system)), "GuestModuleAdd")); + Require(add && add(30, 12) == 42); + Require(sceKernelStopUnloadModule(system, 0, nullptr, 0, nullptr, nullptr) == 0); + } + Require(sceKernelLoadStartModule("/gkbHtXc3Wq/common/lib/anyps5-missing-system-module.sprx", 0, nullptr, 0, nullptr, nullptr) == static_cast(0x80020002u)); + Require(sceKernelLoadStartModule("/gkbHtXc3Wq/common/lib/anyps5-system-module-for-test.prx", 0, nullptr, 0, nullptr, nullptr) == static_cast(0x80020002u)); + std::filesystem::copy_file(argv[1], "anyps5-escape-module-for-test.prx", std::filesystem::copy_options::overwrite_existing); + Require(sceKernelLoadStartModule("/gkbHtXc3Wq/common/lib/../anyps5-escape-module-for-test.sprx", 0, nullptr, 0, nullptr, nullptr) == static_cast(0x80020002u)); + std::filesystem::remove("anyps5-escape-module-for-test.prx"); + std::filesystem::remove("libs/anyps5-system-module-for-test.prx"); + std::error_code notEmpty; + if (createdLibs) std::filesystem::remove("libs", notEmpty); + void* pinned = dlopen_nid_postfix(argv[1], 2); add = reinterpret_cast(dlsym_nid_postfix(pinned, "GuestModuleAdd")); Require(add && _sceKernelRtldThreadAtexitIncrement_nid_postfix(reinterpret_cast(add)) == 0); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 6384b00c9f34..5d642253a45d 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -233,6 +233,7 @@ - [rtprio_thread, pthread_setconcurrency, pthread_getconcurrency](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - `rtprio_thread` follows FreeBSD 11.4's `rtprio_thread(2)`. Every guest thread here runs under `SCHED_FIFO` or `SCHED_RR` (priority 256-767), whose rtprio class is real-time; the console's mapping from that priority to the rtprio `prio` range (0-31) is unmeasured, so `RTP_LOOKUP` and `RTP_SET` throw for such a thread rather than report an impossible priority. A thread that is not real-time reports `RTP_PRIO_NORMAL` (3) with 0, and `RTP_SET` accepts only that value. Another function fails with `EINVAL`, a null `rtprio` with `EFAULT`, a thread other than the calling one throws, and an id that is not a live guest thread with `ESRCH`. `pthread_setconcurrency` stores 0 or more and returns `EINVAL` for a negative level (the error itself, as FreeBSD's libthr does, with errno untouched); `pthread_getconcurrency` returns the level, which is a hint and changes nothing about the host's concurrency. Morrowind (`mshivam019/morrowind-ps5`) imports all three at start-up - [sceKernelVirtualQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) returns [VirtualQueryInfo](../../../core/libs/SceTypes.hpp) with a one-byte flag allocation unit so Windows and SysV hosts return the same 72-byte representation, with the name at byte 33 and GPU mask at byte 65. This preserves the existing SysV representation; the console offsets beyond the address, protection and size fields observed in Astro Bot's AGC calls are not hardware-measured. Size and offset assertions plus a raw-byte named-mapping regression guard the host ABI. - [app0.zar files](../../../core/libs/prx/libkernel/File/src/ArchivedFile.cpp) (libkernel) - `sceKernelOpen`, `open`, `sceKernelStat` and `sceKernelCheckReachability` resolve a path missing from `app0/` in `app0.zar`. An archived file is a read-only host null device whose reads, positioned and vectored reads, AIO reads, seeks, `fstat` and `FIONREAD` are served from the archive, with the position shared by `dup`, `dup2` and `F_DUPFD`; writes fail with `EBADF` as on a read-only descriptor, opening one for writing or truncation fails with `EROFS` and `O_CREAT | O_EXCL` with `EEXIST`. `fstat` and `sceKernelStat` report mode `0444` (`0555` for a directory), the node index as inode and the archive file's device and times. Opening an archived directory or a directory present both on disk and in the archive throws, as `getdents` would miss entries. On an archived descriptor `mmap`, `flock`, record locks, `fstatfs`, `ftruncate`, `fsync`, `fdatasync`, `getdirentries`, `fchmod` and `futimes` throw instead of acting on the null device; `mmap` is `noexcept`, so its refusal terminates the process. `lstat` and the other path calls on archived files are not implemented +- [sceKernelLoadStartModule](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) loads a system library named by its console path, `/system/common/lib/.sprx` or the sandboxed `//common/lib/.sprx` that `sceKernelGetFsSandboxRandomWord` names, from the host `libs/.prx`, as libSceSysmodule loads modules by ID; a host library has no `module_start`, so `res` is 0. Other system directories, such as `/system/priv/lib`, are not mapped. The OpenOrbis piglet sample loads `libScePigletv2VSH.sprx` and `libScePrecompiledShaders.sprx` this way - [param.sfo](../../../core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp) (libkernel) is read when `param.json` is absent, for `TITLE`, `TITLE_ID`, `DOWNLOAD_DATA_SIZE` and `USER_DEFINED_PARAM_1` to `_4`, and mounts `/data` and `/download0` like `param.json`; the localized `TITLE_00` to `TITLE_30` are not selected by the console language. - [sysctl MIBs for kern.smp.maxcpus, kern.stackprot, kern.sched.name and kern.timecounter.hardware](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - on a PS5 (firmware 12.70), a payload process resolves these names to {1, 2147482816, 2147482814}, {1, 2147483151}, {1, 2147483182, 2147483181} and {1, 2147482838, 2147482834}; `sysctl` returns -1 with `ENOENT` for reads and size queries by those exact MIBs, leaving the buffer and length unchanged, including when either or both output pointers are null. Writes were not measured and throw. `sysctlnametomib` still throws for these four names; #7541 implements the mappings for nine of the other names. - [sceKernelGetProcessAbiType](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) throws: no measured value, prototype or documented consumer of the process ABI type was found, and PS5_MihawkOS (`mihawk-99/PS5_MihawkOS`, PPSA99170) is the only title known to import it. Returning a guessed constant would let a title branch on a value the console never answered with, so the declaration only resolves the import until the console value is measured