From 08824f3b903d052e052fe16af7d7991a3d6b0b58 Mon Sep 17 00:00:00 2001 From: poonasor Date: Mon, 12 Oct 2026 09:52:00 -0400 Subject: [PATCH] feat(libkernel): implement sceKernelGetModuleList2 and sceKernelGetModuleInfo2 The title-module variants of sceKernelGetModuleList and sceKernelGetModuleInfo, following shadPS4: the list leaves out system libraries and the info call answers EPERM for one. A system library is a loaded .prx image that is not a relinked title module. NHL 27 (PPSA34063) imports both and fails to load without them. Co-Authored-By: Claude Opus 5.5 --- .../prx/libkernel/Module/src/ModuleInfo.cpp | 77 +++++++++++++------ core/libs/tests/GuestModuleInfo.cpp | 32 ++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 87 insertions(+), 23 deletions(-) diff --git a/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp b/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp index 90873a76b..378333b50 100644 --- a/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp +++ b/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp @@ -205,6 +205,12 @@ std::uint64_t GuestSegmentBytes() { dl_iterate_phdr(AddGuestSegmentBytes, &bytes); return bytes; } + +bool SystemImage(const GuestImage& image) { + const auto* name = static_cast(image.native)->l_name; + const std::string_view path = name ? name : ""; + return !path.empty() && !path.ends_with(GuestModuleSuffix); +} #else bool SectionNamed(const IMAGE_SECTION_HEADER& section, const char* name) { return std::strncmp(reinterpret_cast(section.Name), name, IMAGE_SIZEOF_SHORT_NAME) == 0; @@ -341,8 +347,46 @@ std::uint64_t GuestSegmentBytes() { } return bytes; } + +bool SystemImage(const GuestImage& image) { + if (image.address == reinterpret_cast(GetModuleHandleW(nullptr))) return false; + constexpr std::wstring_view suffix = L".guest.prx"; + const auto path = ModulePath(reinterpret_cast(image.address)); + return path.size() < suffix.size() || CompareStringOrdinal(path.data() + path.size() - suffix.size(), static_cast(suffix.size()), suffix.data(), static_cast(suffix.size()), TRUE) != CSTR_EQUAL; +} #endif +int FillModuleInfo(KernelModule handle, ModuleInfo* info, bool titleOnly) { + if (!info) return SCE_KERNEL_ERROR_EFAULT; + if (info->st_size != sizeof(ModuleInfo)) return SCE_KERNEL_ERROR_EINVAL; + for (const auto& image : GuestImages()) { + if (ModuleIdForImage_nid_no_patch(image.native) != handle) continue; + if (titleOnly && SystemImage(image)) return SCE_KERNEL_ERROR_EPERM; + ModuleInfo result{}; + result.st_size = sizeof(ModuleInfo); +#ifdef _WIN32 + if (!Fill(image.address, result)) return SCE_KERNEL_ERROR_ESRCH; +#else + ImageSearch search{image.address, &result, false}; + dl_iterate_phdr(FindImage, &search); + if (!search.found) return SCE_KERNEL_ERROR_ESRCH; +#endif + *info = result; + return 0; + } + return SCE_KERNEL_ERROR_ESRCH; +} + +int FillModuleList(KernelModule* handles, std::size_t count, std::size_t* actual, bool titleOnly) { + if (!handles || !actual) return SCE_KERNEL_ERROR_EFAULT; + auto images = GuestImages(); + if (titleOnly) std::erase_if(images, SystemImage); + for (std::size_t i = 0; i < images.size() && i < count; ++i) handles[i] = ModuleIdForImage_nid_no_patch(images[i].native); + if (images.size() > count) return SCE_KERNEL_ERROR_ENOMEM; + *actual = images.size(); + return 0; +} + } extern "C" { @@ -369,32 +413,19 @@ int APS5_VABI sceKernelGetModuleInfoFromAddr(std::uint64_t address, int flags, M } int APS5_VABI sceKernelGetModuleList(KernelModule* handles, std::size_t count, std::size_t* actual) { - if (!handles || !actual) return SCE_KERNEL_ERROR_EFAULT; - const auto images = GuestImages(); - for (std::size_t i = 0; i < images.size() && i < count; ++i) handles[i] = ModuleIdForImage_nid_no_patch(images[i].native); - if (images.size() > count) return SCE_KERNEL_ERROR_ENOMEM; - *actual = images.size(); - return 0; + return FillModuleList(handles, count, actual, false); +} + +int APS5_VABI sceKernelGetModuleList2(KernelModule* handles, std::size_t count, std::size_t* actual) { + return FillModuleList(handles, count, actual, true); } int APS5_VABI sceKernelGetModuleInfo(KernelModule handle, ModuleInfo* info) { - if (!info) return SCE_KERNEL_ERROR_EFAULT; - if (info->st_size != sizeof(ModuleInfo)) return SCE_KERNEL_ERROR_EINVAL; - for (const auto& image : GuestImages()) { - if (ModuleIdForImage_nid_no_patch(image.native) != handle) continue; - ModuleInfo result{}; - result.st_size = sizeof(ModuleInfo); -#ifdef _WIN32 - if (!Fill(image.address, result)) return SCE_KERNEL_ERROR_ESRCH; -#else - ImageSearch search{image.address, &result, false}; - dl_iterate_phdr(FindImage, &search); - if (!search.found) return SCE_KERNEL_ERROR_ESRCH; -#endif - *info = result; - return 0; - } - return SCE_KERNEL_ERROR_ESRCH; + return FillModuleInfo(handle, info, false); +} + +int APS5_VABI sceKernelGetModuleInfo2(KernelModule handle, ModuleInfo* info) { + return FillModuleInfo(handle, info, true); } std::uint64_t GuestImageSegmentBytes_nid_no_patch() { diff --git a/core/libs/tests/GuestModuleInfo.cpp b/core/libs/tests/GuestModuleInfo.cpp index 40eacdf81..417d79545 100644 --- a/core/libs/tests/GuestModuleInfo.cpp +++ b/core/libs/tests/GuestModuleInfo.cpp @@ -16,6 +16,8 @@ int APS5_VABI dlclose_nid_postfix(void*); int APS5_VABI sceKernelGetModuleInfoFromAddr(std::uint64_t, int, ModuleInfoEx*); int APS5_VABI sceKernelGetModuleList(KernelModule*, std::size_t, std::size_t*); int APS5_VABI sceKernelGetModuleInfo(KernelModule, ModuleInfo*); +int APS5_VABI sceKernelGetModuleList2(KernelModule*, std::size_t, std::size_t*); +int APS5_VABI sceKernelGetModuleInfo2(KernelModule, ModuleInfo*); } static void Require(bool value) { if (!value) std::abort(); } template @@ -105,6 +107,36 @@ int main(int argc, char** argv) { unknown.st_size = sizeof(ModuleInfo); Require(sceKernelGetModuleInfo(info.id, &unknown) == SCE_KERNEL_ERROR_ESRCH); Require(sceKernelGetModuleInfo(-1, &unknown) == SCE_KERNEL_ERROR_ESRCH && unknown.name[0] == '\0'); + std::vector titleHandles(handles.size() + 1, -1); + std::size_t titleCount = 0; + Require(sceKernelGetModuleList2(titleHandles.data(), titleHandles.size(), &titleCount) == 0); + Require(titleCount >= 1 && titleCount < count && titleHandles[titleCount] == -1); + titleHandles.resize(titleCount); + Require(titleHandles.front() == self.id); + for (const auto handle : titleHandles) { + Require(std::count(handles.begin(), handles.end(), handle) == 1); + ModuleInfo title{}, listed{}; + title.st_size = listed.st_size = sizeof(ModuleInfo); + Require(sceKernelGetModuleInfo2(handle, &title) == 0 && sceKernelGetModuleInfo(handle, &listed) == 0); + Require(std::string(title.name) == listed.name && title.segment_count == listed.segment_count); + } + std::size_t titleUntouched = 99; + Require(sceKernelGetModuleList2(titleHandles.data(), titleCount - 1, &titleUntouched) == SCE_KERNEL_ERROR_ENOMEM && titleUntouched == 99); + Require(sceKernelGetModuleList2(nullptr, 0, &titleUntouched) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelGetModuleList2(titleHandles.data(), titleHandles.size(), nullptr) == SCE_KERNEL_ERROR_EFAULT); + for (const auto handle : handles) { + if (std::count(titleHandles.begin(), titleHandles.end(), handle) != 0) continue; + ModuleInfo system{}; + system.st_size = sizeof(ModuleInfo); + system.name[0] = 'x'; + Require(sceKernelGetModuleInfo2(handle, &system) == SCE_KERNEL_ERROR_EPERM && system.name[0] == 'x'); + Require(sceKernelGetModuleInfo2(handle, nullptr) == SCE_KERNEL_ERROR_EFAULT); + system.st_size = 0x158; + Require(sceKernelGetModuleInfo2(handle, &system) == SCE_KERNEL_ERROR_EINVAL); + } + ModuleInfo absent{}; + absent.st_size = sizeof(ModuleInfo); + Require(sceKernelGetModuleInfo2(-1, &absent) == SCE_KERNEL_ERROR_ESRCH && absent.name[0] == 0); int local = 0; Query(&local, SCE_KERNEL_ERROR_ESRCH); Require(sceKernelGetModuleInfoFromAddr(reinterpret_cast(add), 2, nullptr) == SCE_KERNEL_ERROR_EFAULT); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index b2f878ee6..48dad8c22 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -204,6 +204,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceKernelGetModuleInfoFromAddr](../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x1A8-byte info layout follows the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation; a title's `libc.prx` reads only `id` (PPSA14632). The meaning of `flags` is unknown; only 2 is accepted. The input `st_size` is ignored, because PPSA21703's `libc.prx` passes an uninitialized 0x1A8-byte buffer; the full size is written back. The id is the `sceKernelLoadStartModule` handle of an image opened through the loader and otherwise a stable id from the same range. The name is the host file name without `.guest.prx`. An image with more than 4 load segments (the converted executable has 7) reports its first 4. `tls_offset` is 0 and `ref_count` is 1, as the host loader exposes neither. On Windows the module is the PE image holding the address: a relinked image reports its `.elf` sections as segments and the eh_frame_hdr recorded in `.ehmeta`, any other image its first 4 sections with memory access and the `.ehfram` section without an eh_frame_hdr; the TLS fields and `init_proc_addr`/`fini_proc_addr` are 0 - [sceKernelGetModuleList](../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - lists the main program and every loaded `.prx` image in load order; host runtime libraries are left out. The handles are the ids `sceKernelGetModuleInfoFromAddr` reports, not the console's. The `(int32_t* handles, size_t count, size_t* actual)` signature and returning `ENOMEM` without writing `actual` after filling the first `count` handles when more images are loaded follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/kernel/process.cpp#L244-L264); whether the console writes any handles in that case is unknown - [sceKernelGetModuleInfo](../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x160-byte layout (`st_size`, `name[256]`, 4 segments, `segment_count`, `fingerprint[20]`) follows [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_types.pas#L319-L337) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/module.h#L15-L31). Only that `st_size` is accepted, as in shadPS4; other sizes return `EINVAL`, a null pointer `EFAULT` and a handle `sceKernelGetModuleList` does not list `ESRCH`. Name and segments are the ones `sceKernelGetModuleInfoFromAddr` reports, on Windows from the same PE sections, so `prot` uses the read/write/execute bits 1/2/4, while fpPS4 and shadPS4 store the ELF `p_flags`. The fingerprint is left zero, as the host image does not carry it +- [sceKernelGetModuleList2 and sceKernelGetModuleInfo2](../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the title-module variants follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/process.cpp): `sceKernelGetModuleList2` lists what `sceKernelGetModuleList` lists without system libraries, and `sceKernelGetModuleInfo2` answers `EPERM` for a system library after the `EFAULT`, `EINVAL` and `ESRCH` checks of `sceKernelGetModuleInfo`. A system library here is a loaded `.prx` image that is not a relinked title module (`.guest.prx`); the main program and the title's own modules are listed. Not verified on the console. PPSA34063 imports both and fails to load without them - [sceKernelReleaseFlexibleMemory](../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - unmaps the range like sceKernelMunmap, as fpPS4 and prosper do; whether the console keeps the virtual range reserved after releasing its pages is unverified - [sceKernelAvailableFlexibleMemorySize](../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - the segments of the title's executable and of its `.guest.prx` modules count against the 448 MiB flexible budget while they are loaded, for this size and for the flexible maps, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L165) maps every segment of a module outside its system module directory as flexible memory on the PS4. [Counted](../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) are the segments the title's ELF gives permissions, each rounded to 16 KiB pages; not counted are segments without permissions, the relinker's own blocks (on Linux both are the read/write/execute `PT_LOAD`s, plus the header block at file offset 0; on Windows everything outside the `.elf` sections) and the host system libraries. The Linux image carries no marker for the relinker's blocks, so they are told apart by those flags and that offset only: a title segment that is writable and executable, or starts at file offset 0, is not counted. A module whose segments exceed what is left still loads, and the available size then reads 0; shadPS4 fails the segment map instead ([memory.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/memory.cpp#L519-L524) returns `EINVAL`, [module.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L168-L169) asserts). Which segments the PS5 charges, its rounding, whether system modules use the title's budget and when an unload releases the charge are not measured. PPSA15508 subtracts the reported size and a 10 MiB module allowance from its own 448 MiB budget at start-up and stops when the result exceeds its 146 MiB code and data limit ([#2883](https://github.com/boykopovar/AnyPS5/issues/2883)) - [Memory protection bits 0x100 and 0x200](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - taken as audio coprocessor (ACP) read and write, as in the [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [etaHEN](https://github.com/etaHEN/etaHEN) headers; the coprocessor work runs on the host CPU, so they grant host read and read/write access like the GPU bits