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 <noreply@anthropic.com>
This commit is contained in:
poonasor 2026-10-12 09:52:00 -04:00
parent d592d54e11
commit 08824f3b90
3 changed files with 87 additions and 23 deletions

View File

@ -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<const link_map*>(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<const char*>(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<std::uintptr_t>(GetModuleHandleW(nullptr))) return false;
constexpr std::wstring_view suffix = L".guest.prx";
const auto path = ModulePath(reinterpret_cast<HMODULE>(image.address));
return path.size() < suffix.size() || CompareStringOrdinal(path.data() + path.size() - suffix.size(), static_cast<int>(suffix.size()), suffix.data(), static_cast<int>(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<ModuleInfo> search{image.address, &result, false};
dl_iterate_phdr(FindImage<ModuleInfo>, &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<ModuleInfo> search{image.address, &result, false};
dl_iterate_phdr(FindImage<ModuleInfo>, &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() {

View File

@ -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<typename TFunction>
@ -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<KernelModule> 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<std::uintptr_t>(add), 2, nullptr) == SCE_KERNEL_ERROR_EFAULT);

View File

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