From db3ec02c54b1824a4174f1fcdede980705358d7f Mon Sep 17 00:00:00 2001 From: Nicolas Falesy <112726756+nicolasfalesy@users.noreply.github.com> Date: Tue, 13 Oct 2026 01:55:14 -0400 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01EvDbpwUPTa78oCAJqe7m9K --- .../prx/libc/include/exceptions/Unwind.hpp | 5 ++ core/libs/prx/libc/src/exception/Unwind.cpp | 65 +++++++++++++++++++ core/libs/tests/ExceptionRuntime.cpp | 21 ++++++ core/libs/tests/RegisterContext.cpp | 2 +- docs/dev/debt/libc.md | 1 + 5 files changed, 93 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/include/exceptions/Unwind.hpp b/core/libs/prx/libc/include/exceptions/Unwind.hpp index c9b7fdc147..7937252c15 100644 --- a/core/libs/prx/libc/include/exceptions/Unwind.hpp +++ b/core/libs/prx/libc/include/exceptions/Unwind.hpp @@ -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] {}; diff --git a/core/libs/prx/libc/src/exception/Unwind.cpp b/core/libs/prx/libc/src/exception/Unwind.cpp index 685b1ea60d..7ae48a7980 100644 --- a/core/libs/prx/libc/src/exception/Unwind.cpp +++ b/core/libs/prx/libc/src/exception/Unwind.cpp @@ -21,6 +21,71 @@ #include #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 std::uintptr_t Slot(Function function) { + return reinterpret_cast(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*); diff --git a/core/libs/tests/ExceptionRuntime.cpp b/core/libs/tests/ExceptionRuntime.cpp index a2dd29806f..43dc8405c0 100644 --- a/core/libs/tests/ExceptionRuntime.cpp +++ b/core/libs/tests/ExceptionRuntime.cpp @@ -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(context); + const auto validRegister = reinterpret_cast(slots[2]); + const auto getRegister = reinterpret_cast(slots[3]); + const auto setRegister = reinterpret_cast(slots[4]); + const auto validFloatRegister = reinterpret_cast(slots[5]); + const auto isSignalFrame = reinterpret_cast(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(argument); + return _URC_NO_REASON; + }, &frames); + assert(backtrace == _URC_END_OF_STACK && frames >= 2); std::puts("exception runtime tests passed"); } diff --git a/core/libs/tests/RegisterContext.cpp b/core/libs/tests/RegisterContext.cpp index 28c22057c6..5c45536bd4 100644 --- a/core/libs/tests/RegisterContext.cpp +++ b/core/libs/tests/RegisterContext.cpp @@ -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*); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 35299259da..beed675fdd 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -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` 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.