From 0f550d1fd008f810573bd2b2057127600c9099ab Mon Sep 17 00:00:00 2001 From: "M. Abdullah" <36810219+m1abdullahh@users.noreply.github.com> Date: Sun, 2 Aug 2026 05:23:12 +0500 Subject: [PATCH] fix: keep hint-less guest mappings at the canonical PS5 base (fixes the #135 macOS regression) (#138) * fix: keep hint-less guest mappings at the canonical PS5 base FindGuestFreeRange searched the low system-managed range first for mappings with no address hint, so the first hint-less direct-memory map could land as low as 0x200000. The PS5 kernel never places hint-less user mappings below 0x200000000 and guest code relies on that: Sony's libc maps 4 MiB of direct memory for its internal heap, fails its mspace setup when the returned address is that low, and the first malloc then dereferences a null mspace (a read at 0x38, the mspace magic check). On macOS this made Raiden III crash on the main guest thread a couple of seconds after boot, 100 percent reproducible with --printf-direction Silent. Search from the canonical base first, fall back to the user range, and keep the low system-managed range only as a last resort. The mmap path already anchored hint-less searches at 0x200000000; this aligns the shared search helper with it. Adds two regression tests: the libc-shaped allocation must come back at or above the canonical base and hold writes, and direct-memory content must survive an unmap and remap of the same physical range. * macos: make the fatal-report memory dumps fault-safe IsReadableRange returned true for any nonzero address on macOS, so the fatal report's guest memory dumps dereferenced whatever the crashed thread had in its registers. A fault inside the reporter re-enters the signal handler and wedges the reporting thread, which hid real guest crashes whenever logging was enabled: the game kept running with a dead thread and the report was never completed. Walk the Mach regions covering the range and require read permission before dumping, the same contract the Linux implementation provides. * do not fallthrough HOST_SYSTEM_MANAGED_MIN --------- Co-authored-by: nmzik --- src/kernel/memory.cpp | 12 ++- src/loader/runtimeLinker.cpp | 28 +++++- tests/VirtualMemoryAllocationTests.cpp | 131 +++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 5 deletions(-) diff --git a/src/kernel/memory.cpp b/src/kernel/memory.cpp index c16b192..bb650ec 100644 --- a/src/kernel/memory.cpp +++ b/src/kernel/memory.cpp @@ -818,6 +818,11 @@ static void MemoryPoolSubtractCommitted(uint64_t l // Keep host mappings, physical blocks, placeholders, and virtual ranges in step. static std::recursive_mutex g_memory_operation_mutex; +// The base address the PS5 kernel hands out for hint-less user mappings. Guest code can +// assume mappings it did not place explicitly are at or above this (Sony's libc rejects a +// heap below it), so hint-less searches must not fall back to the low system-managed range. +static constexpr uint64_t GUEST_DEFAULT_MAP_BASE = 0x200000000ull; + static uint64_t FindGuestFreeRange(uint64_t search_addr, uint64_t size, uint64_t alignment) { EXIT_IF(g_guest_address_space == nullptr || g_virtual_ranges == nullptr); @@ -845,8 +850,11 @@ static uint64_t FindGuestFreeRange(uint64_t search_addr, uint64_t size, uint64_t if (search_addr != 0) { return find_in(search_addr, HOST_USER_MAX + 1u); } - auto addr = find_in(HOST_SYSTEM_MANAGED_MIN, HOST_SYSTEM_MANAGED_MAX + 1u); - return addr != 0 ? addr : find_in(HOST_USER_MIN, HOST_USER_MAX + 1u); + auto addr = find_in(GUEST_DEFAULT_MAP_BASE, HOST_SYSTEM_MANAGED_MAX + 1u); + if (addr == 0) { + addr = find_in(HOST_USER_MIN, HOST_USER_MAX + 1u); + } + return addr; } bool TryWriteBacking(uint64_t vaddr, const void* data, uint64_t size) { diff --git a/src/loader/runtimeLinker.cpp b/src/loader/runtimeLinker.cpp index 460be61..529b89b 100644 --- a/src/loader/runtimeLinker.cpp +++ b/src/loader/runtimeLinker.cpp @@ -38,7 +38,10 @@ #include #else #include -#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX && !defined(__APPLE__) +#if defined(__APPLE__) +#include +#include +#elif KYTY_PLATFORM == KYTY_PLATFORM_LINUX #include #include #endif @@ -722,7 +725,26 @@ static bool IsReadableRange(uint64_t addr, uint64_t size) { } current = std::min(region_end, end); } -#elif KYTY_PLATFORM == KYTY_PLATFORM_LINUX && !defined(__APPLE__) +#elif defined(__APPLE__) + // Walk the Mach regions covering the range and require read permission. The fatal + // report dumps memory behind raw register values, and a fault inside the reporter + // re-enters the signal handler and wedges the reporting thread. + uint64_t current = addr; + while (current < end) { + mach_vm_address_t region_addr = current; + mach_vm_size_t region_size = 0; + vm_region_basic_info_data_64_t info {}; + mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64; + mach_port_t object_name = MACH_PORT_NULL; + if (mach_vm_region(mach_task_self(), ®ion_addr, ®ion_size, VM_REGION_BASIC_INFO_64, + reinterpret_cast(&info), &count, + &object_name) != KERN_SUCCESS || + region_addr > current || (info.protection & VM_PROT_READ) == 0) { + return false; + } + current = region_addr + region_size; + } +#elif KYTY_PLATFORM == KYTY_PLATFORM_LINUX const auto page_size = static_cast(sysconf(_SC_PAGESIZE)); if (page_size == 0) { return false; @@ -752,7 +774,7 @@ static bool IsReadableRange(uint64_t addr, uint64_t size) { } static bool IsDumpableRange(uint64_t addr, uint64_t size) { -#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX && !defined(__APPLE__) +#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX return IsReadableRange(addr, size); #else (void)size; diff --git a/tests/VirtualMemoryAllocationTests.cpp b/tests/VirtualMemoryAllocationTests.cpp index 3060e9a..a2157fd 100644 --- a/tests/VirtualMemoryAllocationTests.cpp +++ b/tests/VirtualMemoryAllocationTests.cpp @@ -1356,6 +1356,135 @@ void TestLargeDirectMapAliasesAcrossChunks() { std::printf("[host] %-48s ok\n", test); } +void TestHintlessDirectMapUsesCanonicalGuestBase() { + // Mirrors the allocation Sony's libc.prx makes for its internal heap: 4 MiB of + // direct memory, 2 MiB aligned, mapped with no address hint. The PS5 kernel never + // places hint-less user mappings below 0x200000000 and guest code relies on that + // (libc fails its mspace setup for a lower heap address, and the first malloc then + // dereferences a null mspace). Writes through the mapping must also stick. + const char* test = "HintlessDirectMapUsesCanonicalGuestBase"; + + constexpr uint64_t Len = 0x400000; + constexpr uint64_t Align = 0x200000; + + int64_t phys_addr = 0; + CheckOk(test, + Libs::LibKernel::Memory::KernelAllocateDirectMemory(0, 0x260000000ull, Len, Align, 12, + &phys_addr), + "KernelAllocateDirectMemory"); + + void* address = nullptr; + CheckOk(test, + Libs::LibKernel::Memory::KernelMapNamedDirectMemory(&address, Len, SceKernelProtCpuRw, + 0, phys_addr, Align, "libc_heap"), + "KernelMapNamedDirectMemory"); + const auto base = reinterpret_cast(address); + { + char message[128] = {}; + std::snprintf(message, sizeof(message), + "hint-less direct map landed below the PS5 base: 0x%016" PRIx64, base); + Check(test, base >= 0x200000000ull, message); + } + + auto* header = reinterpret_cast(base); + header[0] = 0x4d53504143453030ull; // "MSPACE00" + header[7] = 0x58585858ull; // magic at +0x38, like the libc mspace + *reinterpret_cast(base + Len - 8) = 0x454e444d41524bull; + + Check(test, header[0] == 0x4d53504143453030ull, "immediate readback of header[0] failed"); + Check(test, header[7] == 0x58585858ull, "immediate readback of header[7] failed"); + Check(test, *reinterpret_cast(base + Len - 8) == 0x454e444d41524bull, + "immediate readback of tail failed"); + + uint64_t backing = 0; + Check(test, Libs::LibKernel::Memory::TryReadBacking(base + 0x38, &backing, sizeof(backing)), + "TryReadBacking(header+0x38)"); + Check(test, backing == 0x58585858ull, "backing store does not see the guest write at +0x38"); + + CheckOk(test, Libs::LibKernel::Memory::KernelMunmap(base, Len), "KernelMunmap"); + CheckOk(test, Libs::LibKernel::Memory::KernelReleaseDirectMemory(phys_addr, Len), + "KernelReleaseDirectMemory"); + + std::printf("[host] %-48s ok\n", test); +} + +void TestDirectMemoryContentPersistsAcrossRemap() { + const char* test = "DirectMemoryContentPersistsAcrossRemap"; + + constexpr uint64_t MapSize = SceKernelPageSize * 4; + + int64_t phys_addr = 0; + CheckOk(test, + Libs::LibKernel::Memory::KernelAllocateDirectMemory( + SceKernelDirectMemoryStart, Libs::LibKernel::Memory::KernelGetDirectMemorySize(), + MapSize, SceKernelPageSize, SceKernelMtypeC, &phys_addr), + "KernelAllocateDirectMemory"); + + // Direct memory is physical: contents must survive unmapping and remapping, including + // a remap of a sub-range at a nonzero physical offset. + void* address = nullptr; + CheckOk(test, + Libs::LibKernel::Memory::KernelMapNamedDirectMemory(&address, MapSize, + SceKernelProtCpuRw, 0, phys_addr, + SceKernelPageSize, "persist_a"), + "KernelMapNamedDirectMemory(first)"); + const auto base = reinterpret_cast(address); + for (uint64_t offset = 0; offset < MapSize; offset += sizeof(uint64_t)) { + *reinterpret_cast(base + offset) = offset ^ 0x4b5954595045525aull; // "KYTYPERZ" + } + CheckOk(test, Libs::LibKernel::Memory::KernelMunmap(base, MapSize), "KernelMunmap(first)"); + + void* remap = nullptr; + CheckOk(test, + Libs::LibKernel::Memory::KernelMapNamedDirectMemory(&remap, MapSize, + SceKernelProtCpuRw, 0, phys_addr, + SceKernelPageSize, "persist_b"), + "KernelMapNamedDirectMemory(remap)"); + const auto remap_base = reinterpret_cast(remap); + for (uint64_t offset = 0; offset < MapSize; offset += sizeof(uint64_t)) { + const auto expected = offset ^ 0x4b5954595045525aull; + const auto actual = *reinterpret_cast(remap_base + offset); + if (actual != expected) { + char message[160] = {}; + std::snprintf(message, sizeof(message), + "content lost across remap at offset 0x%" PRIx64 ": expected 0x%016" PRIx64 + ", read 0x%016" PRIx64, + offset, expected, actual); + Fail(test, message); + } + } + CheckOk(test, Libs::LibKernel::Memory::KernelMunmap(remap_base, MapSize), "KernelMunmap(remap)"); + + // Sub-range remap at a nonzero physical offset: page 2 of the original allocation. + void* partial = nullptr; + CheckOk(test, + Libs::LibKernel::Memory::KernelMapNamedDirectMemory( + &partial, SceKernelPageSize, SceKernelProtCpuRw, 0, + phys_addr + static_cast(SceKernelPageSize * 2), SceKernelPageSize, + "persist_c"), + "KernelMapNamedDirectMemory(partial)"); + const auto partial_base = reinterpret_cast(partial); + for (uint64_t offset = 0; offset < SceKernelPageSize; offset += sizeof(uint64_t)) { + const auto expected = (SceKernelPageSize * 2 + offset) ^ 0x4b5954595045525aull; + const auto actual = *reinterpret_cast(partial_base + offset); + if (actual != expected) { + char message[160] = {}; + std::snprintf(message, sizeof(message), + "content lost in partial remap at offset 0x%" PRIx64 + ": expected 0x%016" PRIx64 ", read 0x%016" PRIx64, + offset, expected, actual); + Fail(test, message); + } + } + CheckOk(test, Libs::LibKernel::Memory::KernelMunmap(partial_base, SceKernelPageSize), + "KernelMunmap(partial)"); + + CheckOk(test, Libs::LibKernel::Memory::KernelReleaseDirectMemory(phys_addr, MapSize), + "KernelReleaseDirectMemory"); + + std::printf("[host] %-48s ok\n", test); +} + void TestDirectMapUnmapReusesHostAddress() { const char* test = "DirectMapUnmapReusesHostAddress"; @@ -2150,6 +2279,8 @@ int main() { RunTest(TestDirectAlignmentStaysWithinSearchRange); RunTest(TestDefaultDirectMapUsesSystemAddressRange); RunTest(TestLargeDirectMapAliasesAcrossChunks); + RunTest(TestHintlessDirectMapUsesCanonicalGuestBase); + RunTest(TestDirectMemoryContentPersistsAcrossRemap); RunTest(TestDirectMapUnmapReusesHostAddress); RunTest(TestFixedReserveReplacesPartialDirectMapping); RunTest(TestFixedReserveRollbackConsumesRestoredPlaceholder);