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