Files
AnyPS5/core/libs/tests/ApplicationHeap.cpp
T
milescovielloandClaude Opus 5.5 b4e129ff80 fix(libc): give the scripting heap hooks blocks from libc's own heap
scriptingGetMem and scriptingFreeMem went through the title's registered
allocator. A title that ships its own libc.prx has already initialized that
allocator, and the application heap's lazy initialization then ran its
initializer a second time: Metal Gear Solid Master Collection (PPSA18194)
started a second Unity heap and aborted when it freed a block of the first.
The hooks now allocate and free through libc's guest heap, so they never
initialize the registered allocator; sizes that overflow return null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 20:43:32 +00:00

433 lines
21 KiB
C++

#include "prx/libc/include/ApplicationHeap.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <cstdio>
#include <limits>
#include <new>
#include <stdexcept>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#elif defined(__linux__)
#include "prx/libc/include/specifics/linux/ElfTypes.hpp"
#include <sys/mman.h>
#include <unistd.h>
#endif
extern "C" {
void APS5_VABI _init_env_nid_postfix();
int APS5_VABI posix_memalign_nid_postfix(void**, std::size_t, std::size_t);
void* APS5_VABI reallocf_nid_postfix(void*, std::size_t);
void* APS5_VABI _Znwm_nid_postfix(std::size_t);
void* APS5_VABI _ZnwmRKSt9nothrow_t_nid_postfix(std::size_t, const void*) noexcept;
void* APS5_VABI _ZnamRKSt9nothrow_t_nid_postfix(std::size_t, const void*) noexcept;
void APS5_VABI _ZdlPv_nid_postfix(void*);
void APS5_VABI _ZdaPv_nid_postfix(void*);
void APS5_VABI _ZdlPvSt11align_val_t_nid_postfix(void*, std::size_t);
void APS5_VABI _ZdlPvmSt11align_val_t_nid_postfix(void*, std::size_t, std::size_t);
void* ApplicationHeapRealign_nid_no_patch(void*, std::size_t, std::size_t);
char* APS5_VABI strdup_nid_postfix(const char*);
char* APS5_VABI strndup_nid_postfix(const char*, std::size_t);
char* APS5_VABI getcwd_nid_postfix(char*, std::size_t);
int* APS5_VABI __error_nid_postfix();
int APS5_VABI atexit_nid_postfix(void (APS5_VABI*)());
void* APS5_VABI scriptingGetMem_nid_postfix(std::size_t, std::size_t);
void APS5_VABI scriptingFreeMem_nid_postfix(void*);
}
namespace {
alignas(64) std::array<std::byte, 256> storage{};
alignas(16) std::array<std::byte, 0x40> process{};
alignas(16) std::array<std::byte, 0x38> libc{};
alignas(16) std::array<std::byte, 0x78> replacement{};
#ifdef _WIN32
[[gnu::section(".proctst"), gnu::used]] alignas(4) std::uint32_t processParameterMetadata[2]{};
#endif
std::size_t lastSize = 0;
std::size_t lastAlignment = 0;
unsigned posixCalls = 0;
unsigned initializes = 0;
unsigned frees = 0;
bool fail = false;
bool recurse = false;
bool nullPosixResult = false;
void require(bool condition) {
if (!condition) throw std::runtime_error("application heap test failed");
}
void installProcessParameters() {
#ifdef _WIN32
auto* image = reinterpret_cast<std::byte*>(GetModuleHandleW(nullptr));
require(image != nullptr);
auto* dos = reinterpret_cast<IMAGE_DOS_HEADER*>(image);
auto* nt = reinterpret_cast<IMAGE_NT_HEADERS64*>(image + dos->e_lfanew);
auto* sections = IMAGE_FIRST_SECTION(nt);
IMAGE_SECTION_HEADER* target = nullptr;
for (unsigned index = 0; index < nt->FileHeader.NumberOfSections; ++index) {
if (std::memcmp(sections[index].Name, ".proctst", 8) == 0) {
target = &sections[index];
break;
}
}
require(target != nullptr);
processParameterMetadata[0] = static_cast<std::uint32_t>(process.data() - image);
processParameterMetadata[1] = static_cast<std::uint32_t>(process.size());
DWORD oldProtect = 0;
require(VirtualProtect(target, sizeof(*target), PAGE_READWRITE, &oldProtect) != 0);
std::memcpy(target->Name, ".procpar", 8);
target->Misc.VirtualSize = sizeof(processParameterMetadata);
DWORD restored = 0;
VirtualProtect(target, sizeof(*target), oldProtect, &restored);
#elif defined(__linux__)
bool installed = false;
dl_iterate_phdr([](dl_phdr_info* image, std::size_t, void* data) {
if (image->dlpi_name != nullptr && image->dlpi_name[0] != '\0') return 0;
for (std::uint16_t index = 0; index < image->dlpi_phnum; ++index) {
const auto& header = image->dlpi_phdr[index];
if (header.p_type == PT_LOAD || header.p_type == PT_GNU_EH_FRAME || header.p_type < 0x60000000u) continue;
auto* mutableHeader = const_cast<Elf64_Phdr*>(&header);
const auto pageSize = static_cast<std::uintptr_t>(sysconf(_SC_PAGESIZE));
const auto pageStart = reinterpret_cast<std::uintptr_t>(mutableHeader) & ~(pageSize - 1);
const auto pageEnd = (reinterpret_cast<std::uintptr_t>(mutableHeader + 1) + pageSize - 1) & ~(pageSize - 1);
if (mprotect(reinterpret_cast<void*>(pageStart), pageEnd - pageStart, PROT_READ | PROT_WRITE) != 0) return 1;
mutableHeader->p_type = 0x61000001;
mutableHeader->p_vaddr = reinterpret_cast<std::uintptr_t>(process.data()) - image->dlpi_addr;
mutableHeader->p_paddr = mutableHeader->p_vaddr;
mutableHeader->p_filesz = process.size();
mutableHeader->p_memsz = process.size();
*static_cast<bool*>(data) = true;
return 1;
}
return 1;
}, &installed);
require(installed);
#endif
}
template<typename TAction>
void reject(TAction action) {
bool rejected = false;
try { action(); } catch (const std::exception&) { rejected = true; }
require(rejected);
}
void APS5_VABI initialize() { ++initializes; }
void APS5_VABI finalize() { require(initializes == 1); }
void APS5_VABI exitCallbackAllocates() {
if (ApplicationHeapAllocate_nid_no_patch(16) != storage.data()) throw std::runtime_error("application heap test failed");
}
void* APS5_VABI allocate(std::size_t bytes) {
lastSize = bytes;
if (recurse) return ApplicationHeapAllocate_nid_no_patch(bytes);
return fail ? nullptr : storage.data();
}
void APS5_VABI release(void* pointer) {
require(pointer == storage.data());
++frees;
}
void* APS5_VABI reallocate(void* pointer, std::size_t bytes) {
require(pointer == storage.data());
return allocate(bytes);
}
void* APS5_VABI allocateZeroed(std::size_t count, std::size_t bytes) {
return allocate(count * bytes);
}
void* APS5_VABI align(std::size_t alignment, std::size_t bytes) {
lastAlignment = alignment;
return allocate(bytes);
}
void* APS5_VABI realign(void* pointer, std::size_t bytes, std::size_t alignment) {
require(pointer == storage.data());
lastAlignment = alignment;
return allocate(bytes);
}
int APS5_VABI posixAlign(void** pointer, std::size_t alignment, std::size_t bytes) {
++posixCalls;
if (fail) { *__error_nid_postfix() = 12; *pointer = nullptr; return 12; }
if (nullPosixResult) { *pointer = nullptr; return 0; }
*pointer = align(alignment, bytes);
return 0;
}
template<typename TValue, std::size_t TSize>
void write(std::array<std::byte, TSize>& data, std::size_t offset, TValue value) {
require(offset <= data.size() && sizeof(value) <= data.size() - offset);
std::memcpy(data.data() + offset, &value, sizeof(value));
}
}
int main(int argc, char** argv) {
reject([] { ApplicationHeapAllocate_nid_no_patch(64); });
reject([] { ApplicationHeapRegister_nid_no_patch(nullptr); });
write(process, 0, std::uint64_t{0x40});
write(process, 8, std::uint32_t{0x4942524f});
write(process, 0x38, libc.data());
write(libc, 0, std::uint64_t{0x38});
write(libc, 0x30, replacement.data());
write(replacement, 0, std::uint64_t{0x78});
write(replacement, 8, std::uint64_t{2});
write(replacement, 0x10, &initialize);
write(replacement, 0x18, &finalize);
write(replacement, 0x20, &allocate);
write(replacement, 0x28, &release);
write(replacement, 0x30, &allocateZeroed);
write(replacement, 0x38, &reallocate);
write(replacement, 0x40, &align);
write(replacement, 0x48, &realign);
write(replacement, 0x50, &posixAlign);
if (argc > 1 && std::strcmp(argv[1], "lazy") == 0) {
installProcessParameters();
require(initializes == 0);
require(_Znwm_nid_postfix(40) == storage.data() && lastSize == 40 && initializes == 1);
_ZdlPv_nid_postfix(storage.data());
require(frees == 1);
_init_env_nid_postfix();
require(initializes == 1);
return 0;
}
if (argc > 1 && std::strcmp(argv[1], "scripting") == 0) {
installProcessParameters();
constexpr std::size_t blockBytes = 0x40000;
auto* first = static_cast<std::byte*>(scriptingGetMem_nid_postfix(0x1000, blockBytes));
require(initializes == 0 && lastSize == 0);
auto* second = static_cast<std::byte*>(scriptingGetMem_nid_postfix(0x1000, blockBytes));
require(initializes == 0 && lastSize == 0);
require(first != nullptr && second != nullptr && reinterpret_cast<std::uintptr_t>(first) % 0x1000 == 0 && reinterpret_cast<std::uintptr_t>(second) % 0x1000 == 0);
require(first + blockBytes <= second || second + blockBytes <= first);
std::memset(first, 0x5a, blockBytes);
std::memset(second, 0xa5, blockBytes);
require(first[blockBytes - 1] == std::byte{0x5a} && second[0] == std::byte{0xa5});
scriptingFreeMem_nid_postfix(first);
scriptingFreeMem_nid_postfix(second);
require(initializes == 0 && frees == 0);
_init_env_nid_postfix();
require(initializes == 1);
require(_Znwm_nid_postfix(40) == storage.data() && lastSize == 40);
_ZdlPv_nid_postfix(storage.data());
require(frees == 1);
return 0;
}
if (argc > 1 && std::strcmp(argv[1], "exit-order") == 0) {
require(atexit_nid_postfix(exitCallbackAllocates) == 0);
ApplicationHeapInitialize_nid_no_patch(process.data());
require(initializes == 1);
return 0;
}
if (argc > 1 && std::strcmp(argv[1], "default") == 0) {
std::array<void*, 10> partial{};
partial[0] = reinterpret_cast<void*>(&allocate);
reject([&] { ApplicationHeapRegister_nid_no_patch(partial.data()); });
std::memset(replacement.data() + 0x20, 0, sizeof(partial));
ApplicationHeapInitialize_nid_no_patch(process.data());
require(initializes == 1);
auto* pointer = static_cast<unsigned char*>(ApplicationHeapCalloc_nid_no_patch(7, 9));
for (unsigned i = 0; i < 63; ++i) require(pointer[i] == 0);
std::memset(pointer, 0x5a, 63);
pointer = static_cast<unsigned char*>(ApplicationHeapReallocate_nid_no_patch(pointer, 150));
for (unsigned i = 0; i < 63; ++i) require(pointer[i] == 0x5a);
pointer = static_cast<unsigned char*>(ApplicationHeapReallocate_nid_no_patch(pointer, 11));
for (unsigned i = 0; i < 11; ++i) require(pointer[i] == 0x5a);
require(ApplicationHeapReallocate_nid_no_patch(pointer, 0) == nullptr);
pointer = static_cast<unsigned char*>(ApplicationHeapReallocate_nid_no_patch(nullptr, 32));
require(pointer != nullptr);
ApplicationHeapFree_nid_no_patch(pointer);
pointer = static_cast<unsigned char*>(reallocf_nid_postfix(nullptr, 24));
require(pointer != nullptr && ApplicationHeapUsableSize_nid_no_patch(pointer) == 24);
std::memset(pointer, 0x3c, 24);
pointer = static_cast<unsigned char*>(reallocf_nid_postfix(pointer, 200));
require(pointer != nullptr && ApplicationHeapUsableSize_nid_no_patch(pointer) == 200);
for (unsigned i = 0; i < 24; ++i) require(pointer[i] == 0x3c);
pointer = static_cast<unsigned char*>(reallocf_nid_postfix(pointer, 9));
require(pointer != nullptr && ApplicationHeapUsableSize_nid_no_patch(pointer) == 9);
for (unsigned i = 0; i < 9; ++i) require(pointer[i] == 0x3c);
*__error_nid_postfix() = 0;
require(reallocf_nid_postfix(pointer, 0) == nullptr && *__error_nid_postfix() == 0);
reject([pointer] { ApplicationHeapUsableSize_nid_no_patch(pointer); });
pointer = static_cast<unsigned char*>(reallocf_nid_postfix(nullptr, 40));
require(pointer != nullptr);
require(reallocf_nid_postfix(pointer, SIZE_MAX - 4096) == nullptr && *__error_nid_postfix() == 12);
reject([pointer] { ApplicationHeapUsableSize_nid_no_patch(pointer); });
*__error_nid_postfix() = 0;
require(reallocf_nid_postfix(nullptr, SIZE_MAX) == nullptr && *__error_nid_postfix() == 12);
void* empty = ApplicationHeapAllocate_nid_no_patch(0);
require(empty != nullptr);
ApplicationHeapFree_nid_no_patch(empty);
empty = ApplicationHeapReallocate_nid_no_patch(nullptr, 0);
require(empty != nullptr);
ApplicationHeapFree_nid_no_patch(empty);
for (std::size_t alignment : {16, 64, 4096}) {
auto* aligned = ApplicationHeapAlign_nid_no_patch(alignment, 37);
require(reinterpret_cast<std::uintptr_t>(aligned) % alignment == 0);
_ZdlPvSt11align_val_t_nid_postfix(aligned, alignment);
}
void* aligned = nullptr;
require(ApplicationHeapPosixAlign_nid_no_patch(&aligned, 256, 99) == 0);
require(reinterpret_cast<std::uintptr_t>(aligned) % 256 == 0);
ApplicationHeapFree_nid_no_patch(aligned);
void* refused = nullptr;
require(ApplicationHeapPosixAlign_nid_no_patch(&refused, 256, SIZE_MAX) == 12 && refused == nullptr);
require(posix_memalign_nid_postfix(&refused, 4096, SIZE_MAX - 4096) == 12 && refused == nullptr);
ApplicationHeapFree_nid_no_patch(nullptr);
reject([] { ApplicationHeapCalloc_nid_no_patch(SIZE_MAX, 2); });
reject([] { ApplicationHeapAlign_nid_no_patch(3, 16); });
ApplicationHeapInitialize_nid_no_patch(process.data());
require(initializes == 1);
return 0;
}
if (argc > 1) {
write(replacement, 8, std::uint64_t{99});
reject([&] { ApplicationHeapInitialize_nid_no_patch(process.data()); });
write(replacement, 8, std::uint64_t{2});
reject([&] { ApplicationHeapInitialize_nid_no_patch(process.data()); });
reject([] { ApplicationHeapAlign_nid_no_patch(4, 64); });
require(initializes == 0);
return 0;
}
ApplicationHeapInitialize_nid_no_patch(process.data());
ApplicationHeapInitialize_nid_no_patch(process.data());
require(initializes == 1);
void* pointer = ApplicationHeapAlign_nid_no_patch(4, 64);
require(pointer == storage.data() && lastAlignment == 4 && lastSize == 64);
release(pointer);
require(frees == 1);
pointer = ApplicationHeapAllocate_nid_no_patch(32);
require(pointer == storage.data() && lastSize == 32);
require(ApplicationHeapReallocate_nid_no_patch(pointer, 96) == storage.data() && lastSize == 96);
ApplicationHeapFree_nid_no_patch(pointer);
require(frees == 2);
require(ApplicationHeapCalloc_nid_no_patch(3, 16) == storage.data() && lastSize == 48);
require(ApplicationHeapPosixAlign_nid_no_patch(&pointer, 64, 128) == 0 && pointer == storage.data() && lastAlignment == 64);
reject([] { ApplicationHeapAlign_nid_no_patch(3, 64); });
reject([] { ApplicationHeapCalloc_nid_no_patch(2, std::numeric_limits<std::size_t>::max()); });
require(_Znwm_nid_postfix(0) == storage.data() && lastSize == 1);
_ZdlPv_nid_postfix(storage.data());
_ZdlPv_nid_postfix(nullptr);
require(frees == 3);
require(_ZnamRKSt9nothrow_t_nid_postfix(24, nullptr) == storage.data() && lastSize == 24);
_ZdaPv_nid_postfix(storage.data());
require(frees == 4);
require(ApplicationHeapRealign_nid_no_patch(storage.data(), 48, 32) == storage.data() && lastSize == 48 && lastAlignment == 32);
reject([] { ApplicationHeapRealign_nid_no_patch(storage.data(), 16, 4096); });
pointer = ApplicationHeapAlign_nid_no_patch(64, 37);
_ZdlPvSt11align_val_t_nid_postfix(pointer, 64);
require(frees == 5);
_ZdlPvSt11align_val_t_nid_postfix(nullptr, 64);
require(frees == 5);
pointer = ApplicationHeapAlign_nid_no_patch(64, 37);
_ZdlPvmSt11align_val_t_nid_postfix(pointer, 37, 64);
require(frees == 6);
_ZdlPvmSt11align_val_t_nid_postfix(nullptr, 37, 64);
require(frees == 6);
require(reallocf_nid_postfix(storage.data(), 80) == storage.data() && lastSize == 80 && frees == 6);
require(reallocf_nid_postfix(storage.data(), 16) == storage.data() && lastSize == 16 && frees == 6);
require(reallocf_nid_postfix(storage.data(), 0) == nullptr && lastSize == 16 && frees == 7);
fail = true;
*__error_nid_postfix() = 0;
require(reallocf_nid_postfix(storage.data(), 48) == nullptr && lastSize == 48 && frees == 8 && *__error_nid_postfix() == 12);
reject([] { ApplicationHeapReallocate_nid_no_patch(storage.data(), 48); });
require(frees == 8);
require(_ZnwmRKSt9nothrow_t_nid_postfix(8, nullptr) == nullptr);
require(_ZnamRKSt9nothrow_t_nid_postfix(8, nullptr) == nullptr);
reject([] { _Znwm_nid_postfix(8); });
reject([] { ApplicationHeapAlign_nid_no_patch(4, 64); });
reject([] { ApplicationHeapAllocate_nid_no_patch(64); });
void* unchanged = storage.data();
*__error_nid_postfix() = 77;
try {
const int error = posix_memalign_nid_postfix(&unchanged, 64, 64);
if (error != 12) { std::fprintf(stderr, "posix_memalign callback: expected ENOMEM 12, received %d\n", error); return 1; }
} catch (const std::exception&) {
std::fputs("posix_memalign callback: expected ENOMEM 12, received exception\n", stderr);
return 1;
}
require(*__error_nid_postfix() == 77);
require(unchanged == storage.data());
fail = false;
nullPosixResult = true;
bool rejectedNull = false;
try {
const int error = posix_memalign_nid_postfix(&unchanged, 64, 64);
std::fprintf(stderr, "posix_memalign callback: success with null pointer must throw, received %d\n", error);
return 1;
} catch (const std::bad_alloc&) { rejectedNull = true; }
require(rejectedNull && unchanged == storage.data());
nullPosixResult = false;
const auto beforeInvalid = posixCalls;
for (const std::size_t alignment : {std::size_t{0}, std::size_t{1}, std::size_t{4}, std::size_t{24}}) {
require(posix_memalign_nid_postfix(&unchanged, alignment, 32) == 22);
require(unchanged == storage.data() && *__error_nid_postfix() == 77);
}
require(posix_memalign_nid_postfix(nullptr, 64, 32) == 22 && posixCalls == beforeInvalid);
require(posix_memalign_nid_postfix(&unchanged, 64, 31) == 0 && unchanged == storage.data());
require(lastSize == 31 && lastAlignment == 64 && *__error_nid_postfix() == 77);
require(_ZnwmRKSt9nothrow_t_nid_postfix(16, nullptr) == storage.data() && lastSize == 16);
require(_ZnwmRKSt9nothrow_t_nid_postfix(0, nullptr) == storage.data() && lastSize == 1);
require(_ZnamRKSt9nothrow_t_nid_postfix(0, nullptr) == storage.data() && lastSize == 1);
recurse = true;
reject([] { ApplicationHeapAllocate_nid_no_patch(64); });
recurse = false;
require(ApplicationHeapAllocate_nid_no_patch(64) == storage.data());
const char text[] = "guest string";
char* copy = strdup_nid_postfix(text);
require(copy == reinterpret_cast<char*>(storage.data()) && lastSize == sizeof(text));
require(std::strcmp(copy, text) == 0 && copy != text);
ApplicationHeapFree_nid_no_patch(copy);
copy = strdup_nid_postfix("");
require(lastSize == 1 && copy[0] == '\0');
ApplicationHeapFree_nid_no_patch(copy);
copy = strndup_nid_postfix(text, 5);
require(lastSize == 6 && std::strcmp(copy, "guest") == 0);
ApplicationHeapFree_nid_no_patch(copy);
char* directory = getcwd_nid_postfix(nullptr, 0);
require(directory == reinterpret_cast<char*>(storage.data()) && directory[0] == '/' && lastSize == std::strlen(directory) + 1);
ApplicationHeapFree_nid_no_patch(directory);
directory = getcwd_nid_postfix(nullptr, 200);
require(directory == reinterpret_cast<char*>(storage.data()) && lastSize == std::strlen(directory) + 1);
ApplicationHeapFree_nid_no_patch(directory);
fail = true;
*__error_nid_postfix() = 0;
require(strdup_nid_postfix(text) == nullptr && *__error_nid_postfix() == 12);
*__error_nid_postfix() = 0;
require(strdup_nid_postfix("") == nullptr && *__error_nid_postfix() == 12);
*__error_nid_postfix() = 0;
require(strndup_nid_postfix(text, 5) == nullptr && *__error_nid_postfix() == 12);
*__error_nid_postfix() = 0;
require(getcwd_nid_postfix(nullptr, 0) == nullptr && *__error_nid_postfix() == 12);
fail = false;
const auto beforeScripting = frees;
const auto sizeBeforeScripting = lastSize;
scriptingFreeMem_nid_postfix(nullptr);
auto* small = static_cast<std::byte*>(scriptingGetMem_nid_postfix(0, 31));
auto* aligned = static_cast<std::byte*>(scriptingGetMem_nid_postfix(64, 37));
require(small != nullptr && small != storage.data() && reinterpret_cast<std::uintptr_t>(small) % 16 == 0);
require(aligned != nullptr && aligned != storage.data() && reinterpret_cast<std::uintptr_t>(aligned) % 64 == 0);
std::memset(small, 0x5a, 31);
std::memset(aligned, 0xa5, 37);
require(small + 31 <= aligned || aligned + 37 <= small);
scriptingFreeMem_nid_postfix(small);
scriptingFreeMem_nid_postfix(aligned);
require(scriptingGetMem_nid_postfix(24, 80) == nullptr);
require(scriptingGetMem_nid_postfix(16, std::numeric_limits<std::size_t>::max()) == nullptr);
fail = true;
auto* unaffected = scriptingGetMem_nid_postfix(16, 64);
require(unaffected != nullptr);
scriptingFreeMem_nid_postfix(unaffected);
fail = false;
require(frees == beforeScripting && lastSize == sizeBeforeScripting);
}