From e7530803917b026a7c0fe495c8fa6ada61edc500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C5=A0l=C3=A9gl?= Date: Sun, 11 Oct 2026 16:52:47 +0200 Subject: [PATCH] fix(libkernel): count guest module segments against the flexible memory budget sceKernelAvailableFlexibleMemorySize and the flexible maps counted only explicit flexible mappings, so the full 448 MiB was reported while the title's executable and modules were loaded. A title that derives its code and data size from the budget minus the available size got an unsigned underflow. The segments of the main executable and of every .guest.prx module that the title's ELF gives permissions now count, rounded to 16 KiB pages, as shadPS4 maps every non-system module segment as flexible memory. The relinker's own blocks and the host system libraries do not count. The charge ends when a module is unloaded. Refs #2883 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fnrk5Dazjb2tLooVSpdRQq --- core/libs/prx/libkernel/CMakeLists.txt | 8 +-- .../prx/libkernel/DirectMemory/Export.cpp | 11 +++- .../prx/libkernel/Module/src/ModuleInfo.cpp | 59 +++++++++++++++++++ core/libs/tests/GuestMemory.cpp | 16 +++++ docs/dev/TechnicalDebt.md | 1 + 5 files changed, 88 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 41cb24584..2280313f8 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -870,12 +870,12 @@ if(BUILD_TESTING) target_include_directories(guest_memory_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_memory_tests PRIVATE libkernel libc) configure_windows_unwind(guest_memory_tests) + add_library(guest_memory_module SHARED ${LIBS_INCLUDE_DIR}/tests/GuestMemoryModule.cpp) + set_target_properties(guest_memory_module PROPERTIES PREFIX "" SUFFIX ".guest.prx") + add_dependencies(guest_memory_tests guest_memory_module) + target_compile_definitions(guest_memory_tests PRIVATE GUEST_MEMORY_MODULE="$") add_test(NAME guest_memory COMMAND guest_memory_tests) if(WIN32) - add_library(guest_memory_module SHARED ${LIBS_INCLUDE_DIR}/tests/GuestMemoryModule.cpp) - set_target_properties(guest_memory_module PROPERTIES PREFIX "" SUFFIX ".guest.prx") - add_dependencies(guest_memory_tests guest_memory_module) - target_compile_definitions(guest_memory_tests PRIVATE GUEST_MEMORY_MODULE="$") set_tests_properties(guest_memory PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 91c0a1f6b..7deed55ab 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -30,6 +30,8 @@ #include #include +extern "C" std::uint64_t GuestImageSegmentBytes_nid_no_patch(); + namespace { constexpr size_t FLEXIBLE_MEMORY_SIZE = 448ULL * 1024 * 1024; @@ -90,9 +92,11 @@ size_t _flexibleUsedLocked() { } int _mapFlexible(void** addr, size_t len, int prot, int flags) { + const size_t images = GuestImageSegmentBytes_nid_no_patch(); std::lock_guard lock(g_flexibleLock); - if (len > FLEXIBLE_MEMORY_SIZE - std::min(FLEXIBLE_MEMORY_SIZE, _flexibleUsedLocked())) { - std::fprintf(stderr, "[memory] flexible memory exhausted: request 0x%zx with 0x%zx of 0x%zx in use\n", len, _flexibleUsedLocked(), static_cast(FLEXIBLE_MEMORY_SIZE)); + const size_t used = images + _flexibleUsedLocked(); + if (len > FLEXIBLE_MEMORY_SIZE - std::min(FLEXIBLE_MEMORY_SIZE, used)) { + std::fprintf(stderr, "[memory] flexible memory exhausted: request 0x%zx with 0x%zx of 0x%zx in use\n", len, used, static_cast(FLEXIBLE_MEMORY_SIZE)); return SCE_KERNEL_ERROR_ENOMEM; } const int result = DoMapAnon(addr, len, prot, flags); @@ -350,8 +354,9 @@ int APS5_VABI sceKernelIsStack(void* addr, void** start, void** end) { int APS5_VABI sceKernelAvailableFlexibleMemorySize(size_t* size) { if (!size) return SCE_KERNEL_ERROR_EINVAL; + const size_t images = GuestImageSegmentBytes_nid_no_patch(); std::lock_guard lock(g_flexibleLock); - *size = FLEXIBLE_MEMORY_SIZE - std::min(FLEXIBLE_MEMORY_SIZE, _flexibleUsedLocked()); + *size = FLEXIBLE_MEMORY_SIZE - std::min(FLEXIBLE_MEMORY_SIZE, images + _flexibleUsedLocked()); return 0; } diff --git a/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp b/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp index 66dc83653..8e5a508d4 100644 --- a/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp +++ b/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp @@ -10,6 +10,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Module/EhFrame.hpp" #ifdef _WIN32 @@ -63,6 +64,11 @@ struct GuestImage { std::uintptr_t address; }; +std::uint64_t PageSpan(std::uint64_t address, std::uint64_t size) { + constexpr std::uint64_t pageMask = PS5_PAGE_SIZE - 1; + return ((address + size + pageMask) & ~pageMask) - (address & ~pageMask); +} + #ifndef _WIN32 template struct ImageSearch { @@ -162,6 +168,24 @@ std::vector GuestImages() { } return images; } + +int AddGuestSegmentBytes(dl_phdr_info* image, std::size_t, void* data) { + const std::string_view name = image->dlpi_name ? image->dlpi_name : ""; + if (!name.empty() && !name.ends_with(GuestModuleSuffix)) return 0; + constexpr auto allAccess = PF_R | PF_W | PF_X; + for (std::uint16_t i = 0; i < image->dlpi_phnum; ++i) { + const auto& header = image->dlpi_phdr[i]; + if (header.p_type != PT_LOAD || header.p_offset == 0 || (header.p_flags & allAccess) == allAccess) continue; + *static_cast(data) += PageSpan(header.p_vaddr, header.p_memsz); + } + return 0; +} + +std::uint64_t GuestSegmentBytes() { + std::uint64_t bytes = 0; + dl_iterate_phdr(AddGuestSegmentBytes, &bytes); + return bytes; +} #else bool SectionNamed(const IMAGE_SECTION_HEADER& section, const char* name) { return std::strncmp(reinterpret_cast(section.Name), name, IMAGE_SIZEOF_SHORT_NAME) == 0; @@ -267,6 +291,37 @@ std::vector GuestImages() { } return images; } + +std::uint64_t ImageSegmentBytes(std::uintptr_t base) { + const auto* dos = reinterpret_cast(base); + const auto* nt = reinterpret_cast(base + static_cast(dos->e_lfanew)); + const auto* sections = IMAGE_FIRST_SECTION(nt); + const auto sectionCount = nt->FileHeader.NumberOfSections; + bool relinked = false; + for (unsigned i = 0; i < sectionCount; ++i) relinked = relinked || SectionNamed(sections[i], ".elf0"); + std::uint64_t bytes = 0; + for (unsigned i = 0; i < sectionCount; ++i) { + const auto& section = sections[i]; + if (SectionProtection(section) == 0 || (relinked && std::strncmp(reinterpret_cast(section.Name), ".elf", 4) != 0)) continue; + bytes += PageSpan(section.VirtualAddress, section.Misc.VirtualSize); + } + return bytes; +} + +std::uint64_t GuestSegmentBytes() { + const auto executable = reinterpret_cast(GetModuleHandleW(nullptr)); + constexpr std::wstring_view suffix = L".guest.prx"; + std::uint64_t bytes = 0; + for (const auto& image : GuestImages()) { + if (image.address != executable) { + const auto path = ModulePath(reinterpret_cast(image.address)); + if (path.size() < suffix.size() || CompareStringOrdinal(path.data() + path.size() - suffix.size(), static_cast(suffix.size()), suffix.data(), static_cast(suffix.size()), TRUE) != CSTR_EQUAL) + continue; + } + bytes += ImageSegmentBytes(image.address); + } + return bytes; +} #endif } @@ -323,4 +378,8 @@ int APS5_VABI sceKernelGetModuleInfo(KernelModule handle, ModuleInfo* info) { return SCE_KERNEL_ERROR_ESRCH; } +std::uint64_t GuestImageSegmentBytes_nid_no_patch() { + return GuestSegmentBytes(); +} + } diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index bfab22098..65c6f4e77 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -178,6 +178,21 @@ static void CheckInternalNamedFlexibleMapping() { Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); } +static void CheckGuestModuleUsesFlexibleMemory() { + constexpr std::size_t moduleData = 0x10000; + const auto path = std::filesystem::relative(GUEST_MEMORY_MODULE).generic_string(); + std::size_t before = 0; + std::size_t available = 0; + Require(sceKernelAvailableFlexibleMemorySize(&before) == 0); + void* module = dlopen_nid_postfix(path.c_str(), 2); + Require(module != nullptr); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available + moduleData <= before); + void* mapped = nullptr; + Require(sceKernelMapFlexibleMemory(&mapped, before, 3, 0) == SCE_KERNEL_ERROR_ENOMEM && mapped == nullptr); + Require(dlclose_nid_postfix(module) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); +} + static void CheckBatchMapStopsAtInvalidEntry() { constexpr std::size_t page = 0x4000; constexpr int mapFlexible = 3; @@ -1683,6 +1698,7 @@ int main() { CheckVirtualQueryPartialMunmap(); CheckNamedAndHintedMappings(); CheckInternalNamedFlexibleMapping(); + CheckGuestModuleUsesFlexibleMemory(); CheckBatchMapStopsAtInvalidEntry(); CheckCheckedReleaseDirectMemory(); CheckAudioCoprocessorProtection(); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..16adb0b6a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -168,6 +168,7 @@ Throughout the project, every function at every stage either **does exactly what - [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 - [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 - [_umtx_op](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - `UMTX_OP_WAIT`, `UMTX_OP_WAKE`, `UMTX_OP_WAIT_UINT`, `UMTX_OP_WAIT_UINT_PRIVATE` and `UMTX_OP_WAKE_PRIVATE` follow FreeBSD 11's [kern_umtx.c](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c): a timeout whose size argument is at most 16 is a relative `timespec`, a larger one an `_umtx_time`, and a wake of 0 or fewer wakes one waiter. Waiters share the queue of `sceKernelSyncOnAddressWait`. Operation 0 fails with `EOPNOTSUPP` and operations above 26 with `EINVAL`, as in FreeBSD 11; `UMTX_OP_WAIT_UINT` with a value that does not fit in 32 bits returns 0 at once, as FreeBSD's zero-extended compare does. Operation 1 throws, since PPSA12544's call_once uses it in a way FreeBSD rejects; the other operations throw, and so does a misaligned object. PPSA12544's `libc.prx` passes `UMTX_OP_WAIT` and `UMTX_OP_WAIT_UINT` a pointer to a 4-byte microsecond timeout with size argument 0, which FreeBSD reads as a `timespec`, and waits for a running call_once with operation 1; its `PS5Util.prx` passes a `timespec`. Whether the console reads a microsecond timeout there, and what operation 1 does on it, is unverified. A libc call cannot be told apart from a `timespec` call by the size argument alone, so a timeout with size argument 0 is read as a `timespec` and only throws when it is not a valid one (negative seconds, nanoseconds outside [0, 1e9)), which is where FreeBSD would return `EINVAL`; a microsecond value that happens to read as a valid `timespec` is waited on as one - [sceKernelSyncOnAddressWait](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the only known caller passes a null timeout and a name string as the fourth argument; the timeout is assumed to point to microseconds like the other kernel waits, and the name is ignored