From 859f2f3b92a2d761951864237b6b59bd27a9c349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 04:03:40 +0200 Subject: [PATCH] feat(libc): implement malloc_usable_size Co-Authored-By: Claude --- .../libs/prx/libc/include/ApplicationHeap.hpp | 1 + core/libs/prx/libc/include/GuestHeap.hpp | 1 + core/libs/prx/libc/src/ApplicationHeap.cpp | 18 ++++++++++++++ core/libs/prx/libc/src/FileAndHeap.cpp | 4 ++++ core/libs/prx/libc/src/GuestHeap.cpp | 6 +++++ core/libs/tests/ApplicationHeapStats.cpp | 24 ++++++++++++++++++- docs/dev/TechnicalDebt.md | 1 + 7 files changed, 54 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/include/ApplicationHeap.hpp b/core/libs/prx/libc/include/ApplicationHeap.hpp index 22212a31d..ea9e7ad41 100644 --- a/core/libs/prx/libc/include/ApplicationHeap.hpp +++ b/core/libs/prx/libc/include/ApplicationHeap.hpp @@ -16,6 +16,7 @@ void* ApplicationHeapRealign_nid_no_patch(void* pointer, std::size_t bytes, std: void* ApplicationHeapCalloc_nid_no_patch(std::size_t count, std::size_t bytes); int ApplicationHeapPosixAlign_nid_no_patch(void** pointer, std::size_t alignment, std::size_t bytes); int ApplicationHeapStatsFast_nid_no_patch(void* stats); +std::size_t ApplicationHeapUsableSize_nid_no_patch(void* pointer); } diff --git a/core/libs/prx/libc/include/GuestHeap.hpp b/core/libs/prx/libc/include/GuestHeap.hpp index 65f9c0d42..0c7b87a87 100644 --- a/core/libs/prx/libc/include/GuestHeap.hpp +++ b/core/libs/prx/libc/include/GuestHeap.hpp @@ -7,6 +7,7 @@ namespace GuestHeap { extern "C" { +std::size_t GuestHeapUsableSize_nid_postfix(const void* pointer); void* GuestHeapAllocate_nid_postfix(std::size_t bytes); void GuestHeapFree_nid_postfix(void* pointer); void* GuestHeapReallocate_nid_postfix(void* pointer, std::size_t bytes); diff --git a/core/libs/prx/libc/src/ApplicationHeap.cpp b/core/libs/prx/libc/src/ApplicationHeap.cpp index 3affde65e..f05e94132 100644 --- a/core/libs/prx/libc/src/ApplicationHeap.cpp +++ b/core/libs/prx/libc/src/ApplicationHeap.cpp @@ -23,6 +23,7 @@ using Realign = void* (APS5_VABI *)(void*, std::size_t, std::size_t); using PosixAlign = int (APS5_VABI *)(void**, std::size_t, std::size_t); using Initialize = void (APS5_VABI *)(); using Stats = int (APS5_VABI *)(void*); +using UsableSize = std::size_t (APS5_VABI *)(void*); std::mutex heapMutex; std::array heapApi{}; @@ -249,3 +250,20 @@ int ApplicationHeapStatsFast_nid_no_patch(void* stats) { CallbackScope scope; return statsFast(stats); } + +std::size_t ApplicationHeapUsableSize_nid_no_patch(void* pointer) { + UsableSize usableSize = nullptr; + { + std::lock_guard lock(heapMutex); + if (heapFailure) std::rethrow_exception(heapFailure); + if (heapFinalized) throw std::runtime_error("application heap: allocator has been finalized"); + if (heapApi[0] == nullptr) throw std::runtime_error("application heap: allocator API is not registered"); + if (heapApi != defaultApi()) { + if (heapApi[9] == nullptr) throw std::runtime_error("malloc_usable_size: implemented only for an application allocator that provides it"); + std::memcpy(&usableSize, &heapApi[9], sizeof(usableSize)); + } + } + if (usableSize == nullptr) return GuestHeap::GuestHeapUsableSize_nid_postfix(pointer); + CallbackScope scope; + return usableSize(pointer); +} diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 311c4973c..20527e876 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -268,6 +268,10 @@ int APS5_VABI malloc_stats_fast_nid_postfix(void* stats) { return ApplicationHeapStatsFast_nid_no_patch(stats); } +size_t APS5_VABI malloc_usable_size_nid_postfix(void* pointer) { + return ApplicationHeapUsableSize_nid_no_patch(pointer); +} + void* APS5_VABI malloc_nid_postfix(size_t size) { return ApplicationHeapAllocate_nid_no_patch(size); } diff --git a/core/libs/prx/libc/src/GuestHeap.cpp b/core/libs/prx/libc/src/GuestHeap.cpp index 24442855c..e651faa3a 100644 --- a/core/libs/prx/libc/src/GuestHeap.cpp +++ b/core/libs/prx/libc/src/GuestHeap.cpp @@ -263,6 +263,12 @@ void* GuestHeapRealign_nid_postfix(void* pointer, std::size_t bytes, std::size_t return result; } +std::size_t GuestHeapUsableSize_nid_postfix(const void* pointer) { + if (pointer == nullptr) return 0; + GuestAllocations::Mutation mutation; + return mutation.Find(pointer).bytes; +} + void* GuestHeapAlign_nid_postfix(std::size_t alignment, std::size_t bytes) { GuestAllocations::Mutation mutation; return allocate(mutation, alignment, bytes); diff --git a/core/libs/tests/ApplicationHeapStats.cpp b/core/libs/tests/ApplicationHeapStats.cpp index 42d97d01d..d2694001d 100644 --- a/core/libs/tests/ApplicationHeapStats.cpp +++ b/core/libs/tests/ApplicationHeapStats.cpp @@ -8,6 +8,7 @@ extern "C" { int APS5_VABI malloc_stats_fast_nid_postfix(void*); +std::size_t APS5_VABI malloc_usable_size_nid_postfix(void*); } namespace { @@ -46,6 +47,15 @@ int APS5_VABI statsFast(void* stats) { return 0x2a; } +unsigned usableCalls = 0; +void* lastUsable = nullptr; + +std::size_t APS5_VABI usableSize(void* pointer) { + ++usableCalls; + lastUsable = pointer; + return 0x55; +} + template void write(std::array& data, std::size_t offset, TValue value) { require(offset <= data.size() && sizeof(value) <= data.size() - offset); @@ -75,19 +85,29 @@ int main(int argc, char** argv) { write(replacement, 0x48, &realign); write(replacement, 0x50, &posixAlign); write(replacement, 0x60, &statsFast); + write(replacement, 0x68, &usableSize); if (argc > 1 && std::strcmp(argv[1], "default") == 0) { std::memset(replacement.data() + 0x20, 0, 10 * sizeof(void*)); ApplicationHeapInitialize_nid_no_patch(process.data()); require(ApplicationHeapAllocate_nid_no_patch(16) != nullptr); reject([&] { malloc_stats_fast_nid_postfix(stats.data()); }); + auto* small = ApplicationHeapAllocate_nid_no_patch(16); + auto* large = ApplicationHeapAllocate_nid_no_patch(100000); + require(malloc_usable_size_nid_postfix(small) == 16 && malloc_usable_size_nid_postfix(large) == 100000); + require(malloc_usable_size_nid_postfix(nullptr) == 0 && usableCalls == 0); + ApplicationHeapFree_nid_no_patch(small); + ApplicationHeapFree_nid_no_patch(large); return 0; } if (argc > 1 && std::strcmp(argv[1], "missing") == 0) { write(replacement, 0x60, static_cast(nullptr)); + write(replacement, 0x68, static_cast(nullptr)); ApplicationHeapInitialize_nid_no_patch(process.data()); require(ApplicationHeapAllocate_nid_no_patch(16) == storage.data()); reject([&] { malloc_stats_fast_nid_postfix(stats.data()); }); - require(statsCalls == 0); + reject([&] { malloc_usable_size_nid_postfix(storage.data()); }); + reject([&] { malloc_usable_size_nid_postfix(nullptr); }); + require(statsCalls == 0 && usableCalls == 0); return 0; } ApplicationHeapInitialize_nid_no_patch(process.data()); @@ -99,4 +119,6 @@ int main(int argc, char** argv) { require(statsCalls == 3); require(malloc_stats_fast_nid_postfix(stats.data()) == 0x2a && statsCalls == 4); require(ApplicationHeapAllocate_nid_no_patch(16) == storage.data()); + require(malloc_usable_size_nid_postfix(storage.data()) == 0x55 && usableCalls == 1 && lastUsable == storage.data()); + require(malloc_usable_size_nid_postfix(nullptr) == 0x55 && usableCalls == 2 && lastUsable == nullptr); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index ed3724fc9..3d6495951 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -162,6 +162,7 @@ Throughout the project, every function at every stage either **does exactly what - [Guest arena](../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws - [sceLibcInternalBacktraceForGame](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature - [sceLibcInternalHeapErrorReportForGame](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature +- [malloc_usable_size](../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - with the default allocator it returns the size the block was requested with (the range registered for it), not the rounded-up chunk size Sony's allocator reports; both satisfy "at least the requested size", and bytes past the request would not be write-watched. With an application allocator it calls entry 9 of the replacement table (`malloc_usable_size` in KytyPS5's `MallocReplace` order) and throws when the table leaves it null, like `malloc_stats_fast` - [_sceLibcInternalThreadAtexit, _sceLibcInternalThreadDtors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - FreeBSD `__cxa_thread_atexit` semantics assumed; a null or non-image destructor and an allocation failure throw - [_sceKernelRtldThreadAtexitIncrement, _sceKernelRtldThreadAtexitDecrement](../../core/libs/prx/libkernel/System/src/Rtld.cpp) (libkernel) - the argument is the `dso` address a title's `libc.prx` passes to `__cxa_thread_atexit` (PPSA14632), which fails when Increment returns nonzero. As in the [fpPS4](https://github.com/red-prig/fpPS4) reimplementation, Increment takes a reference on the loaded image containing the address and Decrement releases one; the variants without the leading underscore are assumed to match - [sceImeGetPanelPositionAndForm](../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the output is taken to be the `PositionAndForm` that `sceImeDialogGetPanelPositionAndForm` fills; it is never written, since no panel can be open. That a closed panel is reported before a null output is assumed by analogy with `sceImeSetCaret`, not confirmed on a PS5 title