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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HeC9bA3n74TzJRYv9AgBE8
This commit is contained in:
clem-gb 2026-10-08 15:31:05 +02:00 committed by Adrià Franch
parent a291ae0f88
commit 1bd1923718
4 changed files with 128 additions and 1 deletions

View File

@ -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:$<TARGET_FILE_DIR:libkernel>;PATH=path_list_prepend:$<TARGET_FILE_DIR:guest_unwind_module_info_fixture>")
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 "$<TARGET_FILE_DIR:guest_unwind_module_info_fixture>")
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})

View File

@ -18,6 +18,7 @@
#include <mach-o/loader.h>
#else
#include <fstream>
#include <string_view>
#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<ImageSearch*>(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;

View File

@ -0,0 +1,76 @@
#include "SceTypes.hpp"
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <link.h>
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<TableSearch*>(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<std::uint64_t>(address), 1, &info) == 0);
Require(info.st_size == sizeof(ModuleInfoForUnwind));
Require(reinterpret_cast<std::uint64_t>(address) - info.seg0_addr < info.seg0_size);
return info;
}
static void RequireTables(const void* address) {
TableSearch search{reinterpret_cast<std::uintptr_t>(address), 0, false};
dl_iterate_phdr(FindTables, &search);
Require(search.found && search.header != 0);
const auto* header = reinterpret_cast<const std::uint8_t*>(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<std::intptr_t>(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<const void*>(&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<const void*>(&dl_iterate_phdr));
RequireNoTables(reinterpret_cast<const void*>(&sceKernelGetModuleInfoForUnwind));
Require(dlclose_nid_postfix(module) == 0);
}

View File

@ -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