Merge pull request #3713 from SleglJan/fix/libkernel-flexible-module-charge
fix(libkernel): count guest module segments against the flexible memory budget
This commit is contained in:
commit
32f6414314
|
|
@ -907,12 +907,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="$<TARGET_FILE: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="$<TARGET_FILE:guest_memory_module>")
|
||||
set_tests_properties(guest_memory PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@
|
|||
#include <cstdio>
|
||||
#include <mutex>
|
||||
|
||||
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<size_t>(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<size_t>(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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include <vector>
|
||||
#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
|
||||
|
|
@ -65,6 +66,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<typename TInfo>
|
||||
struct ImageSearch {
|
||||
|
|
@ -181,6 +187,24 @@ std::vector<GuestImage> 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<std::uint64_t*>(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<const char*>(section.Name), name, IMAGE_SIZEOF_SHORT_NAME) == 0;
|
||||
|
|
@ -286,6 +310,37 @@ std::vector<GuestImage> GuestImages() {
|
|||
}
|
||||
return images;
|
||||
}
|
||||
|
||||
std::uint64_t ImageSegmentBytes(std::uintptr_t base) {
|
||||
const auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(base);
|
||||
const auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS64*>(base + static_cast<std::uintptr_t>(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<const char*>(section.Name), ".elf", 4) != 0)) continue;
|
||||
bytes += PageSpan(section.VirtualAddress, section.Misc.VirtualSize);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
std::uint64_t GuestSegmentBytes() {
|
||||
const auto executable = reinterpret_cast<std::uintptr_t>(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<HMODULE>(image.address));
|
||||
if (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)
|
||||
continue;
|
||||
}
|
||||
bytes += ImageSegmentBytes(image.address);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
|
@ -342,4 +397,8 @@ int APS5_VABI sceKernelGetModuleInfo(KernelModule handle, ModuleInfo* info) {
|
|||
return SCE_KERNEL_ERROR_ESRCH;
|
||||
}
|
||||
|
||||
std::uint64_t GuestImageSegmentBytes_nid_no_patch() {
|
||||
return GuestSegmentBytes();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -1695,6 +1710,7 @@ int main() {
|
|||
CheckVirtualQueryPartialMunmap();
|
||||
CheckNamedAndHintedMappings();
|
||||
CheckInternalNamedFlexibleMapping();
|
||||
CheckGuestModuleUsesFlexibleMemory();
|
||||
CheckBatchMapStopsAtInvalidEntry();
|
||||
CheckCheckedReleaseDirectMemory();
|
||||
CheckAudioCoprocessorProtection();
|
||||
|
|
|
|||
|
|
@ -175,6 +175,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
|
||||
|
|
|
|||
Loading…
Reference in New Issue