fix(libc): let a title's own libunwind read _Unwind_Backtrace frames

Homebrew built with the ps5-payload-dev SDK links LLVM libunwind
statically but imports _Unwind_Backtrace from libSceLibcInternal. Its
trace callback then reads each frame with its own _Unwind_GetIP, which
treats the context as a libunwind cursor and calls through the virtual
table in its first word. AnyPS5's context started with the saved RAX, so
the call jumped to a register value and the title crashed on its error
path (vkQuake, PPSA99010, without its game data). The context now starts
with a table in AbstractUnwindCursor's slot order that answers register
reads and writes, validFloatReg and isSignalFrame; other slots throw.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvDbpwUPTa78oCAJqe7m9K
This commit is contained in:
Nicolas FalesyandClaude Opus 5.5 committed 2026-10-13 01:55:14 -04:00
1 parent 7e181fcc38
commit db3ec02c54
5 files changed
+93 -1

No files matched your search

@@ -9,7 +9,12 @@
#include "prx/libc/include/General.hpp"
#include "prx/libc/include/specifics/itanium/UnwindAbi.hpp"
namespace LibcUnwind {
extern const std::uintptr_t CursorSlots[];
}
struct _Unwind_Context {
const std::uintptr_t* cursor = LibcUnwind::CursorSlots;
std::uintptr_t registers[17] {};
#ifdef _WIN32
std::uintptr_t vectorRegisters[16][2] {};
@@ -21,6 +21,71 @@
#include <mach-o/loader.h>
#endif
namespace {
constexpr int CursorInstructionRegister = -1;
constexpr int CursorStackRegister = -2;
constexpr int CursorRegisterCount = 17;
std::uintptr_t* CursorRegister(_Unwind_Context* context, int number) {
if (number == CursorInstructionRegister) return &context->registers[16];
if (number == CursorStackRegister) return &context->registers[7];
if (number >= 0 && number < CursorRegisterCount) return &context->registers[number];
return nullptr;
}
bool APS5_VABI CursorValidRegister(_Unwind_Context* context, int number) {
return CursorRegister(context, number) != nullptr;
}
std::uintptr_t APS5_VABI CursorGetRegister(_Unwind_Context* context, int number) {
auto* value = CursorRegister(context, number);
if (!value) NotImplemented_nid_no_patch("libunwind cursor: read of an unknown register");
return value ? *value : 0;
}
void APS5_VABI CursorSetRegister(_Unwind_Context* context, int number, std::uintptr_t value) {
auto* slot = CursorRegister(context, number);
if (!slot) NotImplemented_nid_no_patch("libunwind cursor: write of an unknown register");
else *slot = value;
}
bool APS5_VABI CursorValidFloatRegister(_Unwind_Context*, int) {
return false;
}
bool APS5_VABI CursorIsSignalFrame(_Unwind_Context* context) {
return context->signalFrame;
}
void APS5_VABI CursorUnsupported(_Unwind_Context*) {
NotImplemented_nid_no_patch("libunwind cursor: operation other than register access or isSignalFrame");
}
template<class Function> std::uintptr_t Slot(Function function) {
return reinterpret_cast<std::uintptr_t>(function);
}
}
namespace LibcUnwind {
const std::uintptr_t CursorSlots[] = {
Slot(&CursorUnsupported),
Slot(&CursorUnsupported),
Slot(&CursorValidRegister),
Slot(&CursorGetRegister),
Slot(&CursorSetRegister),
Slot(&CursorValidFloatRegister),
Slot(&CursorUnsupported),
Slot(&CursorUnsupported),
Slot(&CursorUnsupported),
Slot(&CursorUnsupported),
Slot(&CursorUnsupported),
Slot(&CursorIsSignalFrame),
Slot(&CursorUnsupported),
Slot(&CursorUnsupported),
Slot(&CursorUnsupported),
};
}
#if defined(__linux__) || defined(_WIN32) || defined(__APPLE__)
extern "C" _Unwind_Reason_Code __gxx_personality_v0(int, _Unwind_Action, std::uint64_t, _Unwind_Exception*, _Unwind_Context*);
+21
View File
@@ -32,6 +32,7 @@ extern "C" void __cxa_vec_delete3_nid_postfix(void*, std::size_t, std::size_t, v
extern "C" _Unwind_Reason_Code _Unwind_Backtrace_nid_postfix(_Unwind_Trace_Fn, void*);
extern "C" _Unwind_Reason_Code _Unwind_ForcedUnwind_nid_postfix(_Unwind_Exception*, _Unwind_Stop_Fn, void*);
extern "C" std::uintptr_t _Unwind_GetIP_nid_postfix(_Unwind_Context*);
extern "C" std::uintptr_t _Unwind_GetGR_nid_postfix(_Unwind_Context*, int);
extern "C" void (*_ZSt13set_terminatePFvvE_nid_postfix(void(*)()))();
thread_local int destroyed = 0;
@@ -263,5 +264,25 @@ int main() {
return _URC_NO_REASON;
}, &frames);
assert(backtrace == _URC_END_OF_STACK && frames >= 2);
frames = 0;
backtrace = _Unwind_Backtrace_nid_postfix([](_Unwind_Context* context, void* argument) {
const auto* slots = *reinterpret_cast<const std::uintptr_t* const*>(context);
const auto validRegister = reinterpret_cast<bool (*)(_Unwind_Context*, int)>(slots[2]);
const auto getRegister = reinterpret_cast<std::uintptr_t (*)(_Unwind_Context*, int)>(slots[3]);
const auto setRegister = reinterpret_cast<void (*)(_Unwind_Context*, int, std::uintptr_t)>(slots[4]);
const auto validFloatRegister = reinterpret_cast<bool (*)(_Unwind_Context*, int)>(slots[5]);
const auto isSignalFrame = reinterpret_cast<bool (*)(_Unwind_Context*)>(slots[11]);
assert(validRegister(context, -1) && validRegister(context, -2) && validRegister(context, 16) && !validRegister(context, 17));
assert(getRegister(context, -1) == _Unwind_GetIP_nid_postfix(context) && getRegister(context, -1) != 0);
assert(getRegister(context, -2) == _Unwind_GetGR_nid_postfix(context, 7));
const auto rbx = getRegister(context, 3);
setRegister(context, 3, rbx + 1);
assert(_Unwind_GetGR_nid_postfix(context, 3) == rbx + 1);
setRegister(context, 3, rbx);
assert(!validFloatRegister(context, 17) && !isSignalFrame(context));
++*static_cast<int*>(argument);
return _URC_NO_REASON;
}, &frames);
assert(backtrace == _URC_END_OF_STACK && frames >= 2);
std::puts("exception runtime tests passed");
}
+1 -1
View File
@@ -12,7 +12,7 @@
#endif
static_assert(sizeof(_Unwind_Context::registers) == 17 * sizeof(std::uintptr_t));
static_assert(offsetof(_Unwind_Context, cfa) == 17 * sizeof(std::uintptr_t));
static_assert(offsetof(_Unwind_Context, cfa) == offsetof(_Unwind_Context, registers) + 17 * sizeof(std::uintptr_t));
extern "C" void CaptureProbe(std::uintptr_t*);
extern "C" void RestoreProbe(std::uintptr_t*, std::uintptr_t*);
+1
View File
@@ -41,6 +41,7 @@
- [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) flushes only the host files and directories recorded through `RecordWrittenPath_nid_no_patch` (the libc, libkernel, save data and app content file functions) and their changed parent directories. A host write in a library that does not record its path is not flushed by `sceKernelSync`
- [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) on Windows skips a recorded file it cannot open for writing (read-only, or held open by a handle that denies write sharing), since `FlushFileBuffers` needs a writable handle. On Linux, it skips a recorded file it cannot open for reading. Such a file's data may not reach the disk before a host crash; the guest reads the same data either way
- [`fopen` streams](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows share delete access, like the descriptors [`sceKernelOpen`](../../../core/libs/prx/libkernel/File/src/Open.cpp) opens, so a file the guest holds open can be removed or renamed. A stream reopened with `freopen` still goes through `_wfreopen`, which does not share delete access
- The [`_Unwind_Context`](../../../core/libs/prx/libc/include/exceptions/Unwind.hpp) that `_Unwind_Backtrace` passes to its callback starts with a table laid out like LLVM libunwind's `AbstractUnwindCursor` virtual table, because homebrew built with the ps5-payload-dev SDK links libunwind statically and reads the frames with its own `_Unwind_GetIP` (`__unw_get_reg`), which calls through that table. Only register reads and writes (`validReg`, `getReg`, `setReg`, with `UNW_REG_IP` and `UNW_REG_SP`), `validFloatReg` (always false) and `isSignalFrame` are answered; the other slots throw. The layout follows libunwind's `UnwindCursor.hpp`, not the console's own `libSceLibcInternal`, which is not checked
- The Windows libc [exception unwinder](../../../core/libs/prx/libc/src/exception/Unwind.cpp) steps through frames of modules without the project's `.ehmeta` and `.ehfram` tables (system DLLs, and WinLibs GCC 15.2's `libstdc++-6.dll` and `libwinpthread-1.dll`, whose `.eh_frame` sections are empty) with their PE unwind data through `RtlVirtualUnwind`, but does not run Windows exception or termination handlers: it stops at a frame whose unwind data has one, such as a C++ cleanup through `__gxx_personality_seh0` in `libstdc++-6.dll` or a system DLL function like `qsort` in `ucrtbase.dll` (Windows 10 22H2), and the exception is then reported as unhandled
- The libc [SSE4a trap emulation](../../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it.
- [getifaddrs](../../../core/libs/prx/libc/src/Interfaces.cpp) (libc) lists the host's IPv4 and IPv6 addresses in FreeBSD's `ifaddrs` and sockaddr layouts. FreeBSD's per-interface AF_LINK entries (MAC, index, MTU) and their `if_data` are not produced, so interfaces without an address are omitted. Names are the host's (Linux `lo`, `eth0`; Windows NDIS names such as `ethernet_32769`, or `if<index>` when one does not fit 15 bytes), not the console's. A Linux point-to-point address without a peer loses `IFF_POINTOPOINT`, and an IPv4 broadcast address the host omits is computed from the netmask. On Windows the flags are only `IFF_UP`, `IFF_RUNNING`, `IFF_LOOPBACK`, `IFF_BROADCAST` and `IFF_MULTICAST`, derived from the adapter: PPP and tunnel adapters get neither `IFF_BROADCAST` nor `IFF_POINTOPOINT` because their peer address is not reported, and an address whose prefix length the host reports as unspecified gets a host mask.