From 1bd192371806cbeeb27dfbc5cc55ef0d9e731850 Mon Sep 17 00:00:00 2001 From: clem-gb Date: Thu, 8 Oct 2026 15:31:05 +0200 Subject: [PATCH] fix(libkernel): report guest unwind tables in sceKernelGetModuleInfoForUnwind on Linux A title that ships its own libc.prx unwinds C++ exceptions with that libc's libunwind, which asks sceKernelGetModuleInfoForUnwind where each frame's eh_frame is. On Linux the call always reported no tables, so no C++ exception thrown in guest code could be caught. For the main executable and .guest.prx images, report the PT_GNU_EH_FRAME tables that sceKernelGetModuleInfoFromAddr finds, as Windows does from .ehmeta. Other host images still report none. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HeC9bA3n74TzJRYv9AgBE8 --- core/libs/prx/libkernel/CMakeLists.txt | 11 +++ core/libs/prx/libkernel/Module/src/Module.cpp | 40 ++++++++++ .../libs/tests/GuestUnwindModuleInfoLinux.cpp | 76 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 2 +- 4 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 core/libs/tests/GuestUnwindModuleInfoLinux.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 2ceb0cfa1..1ce38e41c 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -428,6 +428,17 @@ if(BUILD_TESTING) add_test(NAME guest_unwind_module_info COMMAND guest_unwind_module_info_tests) set_tests_properties(guest_unwind_module_info PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$;PATH=path_list_prepend:$") + elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_library(guest_unwind_module_info_fixture SHARED ${LIBS_INCLUDE_DIR}/tests/GuestModuleFixture.cpp) + target_include_directories(guest_unwind_module_info_fixture PRIVATE ${LIBS_INCLUDE_DIR}) + set_target_properties(guest_unwind_module_info_fixture PROPERTIES PREFIX "" OUTPUT_NAME guest_unwind_module_info_fixture.prx SUFFIX ".guest.prx") + add_test_executable(guest_unwind_module_info_tests ${LIBS_INCLUDE_DIR}/tests/GuestUnwindModuleInfoLinux.cpp) + target_include_directories(guest_unwind_module_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_unwind_module_info_tests PRIVATE libkernel libc) + add_dependencies(guest_unwind_module_info_tests guest_unwind_module_info_fixture) + add_test(NAME guest_unwind_module_info COMMAND guest_unwind_module_info_tests guest_unwind_module_info_fixture.prx) + set_tests_properties(guest_unwind_module_info PROPERTIES + WORKING_DIRECTORY "$") endif() add_test_executable(guest_sync_on_address_tests ${LIBS_INCLUDE_DIR}/tests/GuestSyncOnAddress.cpp) target_include_directories(guest_sync_on_address_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libkernel/Module/src/Module.cpp b/core/libs/prx/libkernel/Module/src/Module.cpp index 6636e0dfc..1678c0a05 100644 --- a/core/libs/prx/libkernel/Module/src/Module.cpp +++ b/core/libs/prx/libkernel/Module/src/Module.cpp @@ -18,6 +18,7 @@ #include #else #include +#include #endif #ifdef _WIN32 @@ -36,6 +37,44 @@ void FillGuestUnwindInfo(const std::uint8_t* base, ModuleInfoForUnwind* info) { info->seg0_size = nt->OptionalHeader.SizeOfImage; } } +#elif !defined(__APPLE__) +extern "C" int APS5_VABI sceKernelGetModuleInfoFromAddr(std::uint64_t address, int flags, ModuleInfoEx* info); + +namespace { +struct ImageSearch { + std::uint64_t address; + bool relinked; +}; + +int FindRelinkedImage(dl_phdr_info* image, std::size_t, void* data) { + auto& search = *static_cast(data); + bool contains = false; + for (std::uint16_t index = 0; index < image->dlpi_phnum; ++index) { + const auto& header = image->dlpi_phdr[index]; + const auto start = image->dlpi_addr + header.p_vaddr; + if (header.p_type == PT_LOAD && search.address >= start && search.address - start < header.p_memsz) contains = true; + } + if (!contains) return 0; + const std::string_view name = image->dlpi_name != nullptr ? image->dlpi_name : ""; + search.relinked = name.empty() || name.ends_with(".guest.prx"); + return 1; +} + +bool IsRelinkedImage(std::uint64_t address) { + ImageSearch search{address, false}; + dl_iterate_phdr(FindRelinkedImage, &search); + return search.relinked; +} + +void FillGuestUnwindInfo(std::uint64_t address, ModuleInfoForUnwind* info) { + ModuleInfoEx module{}; + module.st_size = sizeof(ModuleInfoEx); + if (sceKernelGetModuleInfoFromAddr(address, 2, &module) != 0) return; + info->eh_frame_hdr_addr = module.eh_frame_hdr_addr; + info->eh_frame_addr = module.eh_frame_addr; + info->eh_frame_size = module.eh_frame_size; +} +} #endif extern "C" { @@ -147,6 +186,7 @@ int APS5_VABI sceKernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleIn info->eh_frame_size = 0; info->seg0_addr = start; info->seg0_size = end - start; + if (IsRelinkedImage(addr)) FillGuestUnwindInfo(addr, info); return 0; } return SCE_KERNEL_ERROR_ESRCH; diff --git a/core/libs/tests/GuestUnwindModuleInfoLinux.cpp b/core/libs/tests/GuestUnwindModuleInfoLinux.cpp new file mode 100644 index 000000000..04da650ff --- /dev/null +++ b/core/libs/tests/GuestUnwindModuleInfoLinux.cpp @@ -0,0 +1,76 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleInfoForUnwind* info); +void* APS5_VABI dlopen_nid_postfix(const char* path, int flags); +void* APS5_VABI dlsym_nid_postfix(void* handle, const char* name); +int APS5_VABI dlclose_nid_postfix(void* handle); +} + +static void Require(bool value) { if (!value) std::abort(); } + +struct TableSearch { + std::uintptr_t address; + std::uintptr_t header; + bool found; +}; + +static int FindTables(dl_phdr_info* image, std::size_t, void* data) { + auto& search = *static_cast(data); + bool contains = false; + std::uintptr_t header = 0; + for (std::uint16_t i = 0; i < image->dlpi_phnum; ++i) { + const auto& segment = image->dlpi_phdr[i]; + const std::uintptr_t start = image->dlpi_addr + segment.p_vaddr; + if (segment.p_type == PT_LOAD && search.address >= start && search.address - start < segment.p_memsz) contains = true; + if (segment.p_type == PT_GNU_EH_FRAME) header = start; + } + if (!contains) return 0; + search.header = header; + search.found = true; + return 1; +} + +static ModuleInfoForUnwind Query(const void* address) { + ModuleInfoForUnwind info{}; + Require(sceKernelGetModuleInfoForUnwind(reinterpret_cast(address), 1, &info) == 0); + Require(info.st_size == sizeof(ModuleInfoForUnwind)); + Require(reinterpret_cast(address) - info.seg0_addr < info.seg0_size); + return info; +} + +static void RequireTables(const void* address) { + TableSearch search{reinterpret_cast(address), 0, false}; + dl_iterate_phdr(FindTables, &search); + Require(search.found && search.header != 0); + const auto* header = reinterpret_cast(search.header); + Require(header[0] == 1 && header[1] == 0x1b); + std::int32_t offset = 0; + std::memcpy(&offset, header + 4, sizeof(offset)); + const auto info = Query(address); + Require(info.eh_frame_hdr_addr == search.header); + Require(info.eh_frame_addr == search.header + 4 + static_cast(offset)); + Require(info.eh_frame_size != 0); +} + +static void RequireNoTables(const void* address) { + const auto info = Query(address); + Require(info.eh_frame_hdr_addr == 0 && info.eh_frame_addr == 0 && info.eh_frame_size == 0); +} + +int main(int argc, char** argv) { + Require(argc == 2); + RequireTables(reinterpret_cast(&Query)); + void* module = dlopen_nid_postfix(argv[1], 2); + Require(module != nullptr); + const void* add = dlsym_nid_postfix(module, "GuestModuleAdd_nid_postfix"); + Require(add != nullptr); + RequireTables(add); + RequireNoTables(reinterpret_cast(&dl_iterate_phdr)); + RequireNoTables(reinterpret_cast(&sceKernelGetModuleInfoForUnwind)); + Require(dlclose_nid_postfix(module) == 0); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 969f607b6..a2c8fee2e 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -125,7 +125,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAgcDcbPrimeUtcl2](../../core/libs/prx/libSceAgc/DcbState/src/Display.cpp) (libSceAgc) - assumed to write the packet `sceAgcAcbPrimeUtcl2` writes: PRIME_UTCL2 has the same 5 dwords on the graphics and compute rings. Not confirmed on a title. Both write a NOP of that size, as there is no TLB to prime - [sceAgcDcbQueueEndOfShaderActionGetSize](../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - the builder is inline in the SDK and not exported; the size is assumed to be that of the RELEASE_MEM packet `sceAgcCbReleaseMem` writes (32 bytes), the same as `sceAgcAcbQueueEndOfShaderActionGetSize`, as in [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [REmu](https://github.com/xnetcat/REmu/blob/main/crates/remu-runtime/src/agc_dcb.rs) - [sceKernelInternalMemoryGetModuleSegmentInfo](../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - unknown signature -- [sceKernelGetModuleInfoForUnwind](../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - `eh_frame_size` is assumed to cover the records up to, not including, the zero terminator. LLVM libunwind's `CFI_Parser::findFDE` stops at the terminator or the length, whichever comes first, so it finds the same FDEs either way; what the console reports is not known. +- [sceKernelGetModuleInfoForUnwind](../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Linux the tables come from `sceKernelGetModuleInfoFromAddr`, whose `eh_frame_size` includes the zero terminator, as measured on firmware 13.60 (reported in [#2419](https://github.com/boykopovar/AnyPS5/issues/2419), not reproduced here); on Windows it is assumed to stop before the terminator. LLVM libunwind's `CFI_Parser::findFDE` stops at the terminator or the length, whichever comes first, so it finds the same FDEs either way. Only the main executable and `.guest.prx` images report tables; other host images report none, as on Windows - [sceKernelGetModuleInfoFromAddr](../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x1A8-byte info layout follows the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation; a title's `libc.prx` reads only `id` (PPSA14632). The meaning of `flags` is unknown; only 2 is accepted, as is only the full `st_size`. The id is the `sceKernelLoadStartModule` handle of an image opened through the loader and otherwise a stable id from the same range. The name is the host file name without `.guest.prx`. An image with more than 4 load segments (the converted executable has 7) reports its first 4. `tls_offset` is 0 and `ref_count` is 1, as the host loader exposes neither. On Windows the module is the PE image holding the address: a relinked image reports its `.elf` sections as segments and the eh_frame_hdr recorded in `.ehmeta`, any other image its first 4 sections with memory access and the `.ehfram` section without an eh_frame_hdr; the TLS fields and `init_proc_addr`/`fini_proc_addr` are 0 - [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 - [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