From a32d9d9d917271d641271be6d334f8fa03e6058b Mon Sep 17 00:00:00 2001 From: WizzardSK <42868978+WizzardSK@users.noreply.github.com> Date: Fri, 28 Aug 2026 19:33:25 +0200 Subject: [PATCH] libretro: report where the core was executing when it dies A crash in the Android core currently leaves nothing behind. The report in pcee2-libretro#30 is a RetroArch log that simply stops mid-sentence, in the middle of microVU1 compiling VU1 programs, because RetroArch's log cannot capture a native crash and the core installs no reporter of its own - the only sigaction in the tree is the fastmem page fault filter. So the one question a crash in a recompiled core turns on goes unanswered: was the program counter in generated code, or in the core's own text? A backtrace cannot say, since JIT pages carry no unwind information and the unwinder stops at the signal frame. /proc/self/maps can, because generated code lives in an anonymous mapping and compiled code lives in the .so. This installs a handler for SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGABRT and SIGTRAP that prints the signal and its code, the faulting thread's name, pc/lr/sp/fp and the fault address, and the /proc/self/maps line each of those falls in. It goes to logcat as well as stderr, since logcat is where an Android crash is actually readable. It installs from retro_init(), before vtlb reaches HostSys::InstallPageFaultHandler(). That ordering is the whole trick: fastmem takes SIGSEGV (and SIGBUS on aarch64) and chains to whatever it displaced when a fault is not one of its own, so going in first puts this on the end of that chain instead of out of it. Fastmem's own handling is untouched, and this reports only faults fastmem has already declined. Having reported, it chains onward to whatever was there before it, so the process still dies the way it would have. Tested by faulting on purpose, both ways round. A store to 0x1234 reports SIGSEGV with pc and lr resolved to the executable and the fault address unmapped; jumping into an anonymous PROT_EXEC page holding an undefined encoding reports SIGILL with "pc in ... rwxp 00000000 00:00 0" and lr pointing back at the caller - which is exactly the shape a codegen bug would take. Both then die with the default action, 139 and 132. --- libretro/CMakeLists.txt | 1 + libretro/CrashHandler.cpp | 221 ++++++++++++++++++++++++++++++++++++++ libretro/CrashHandler.h | 12 +++ libretro/main.cpp | 7 ++ 4 files changed, 241 insertions(+) create mode 100644 libretro/CrashHandler.cpp create mode 100644 libretro/CrashHandler.h diff --git a/libretro/CMakeLists.txt b/libretro/CMakeLists.txt index 5bc542103e..1611b77197 100644 --- a/libretro/CMakeLists.txt +++ b/libretro/CMakeLists.txt @@ -5,6 +5,7 @@ target_sources(pcsx2_libretro PRIVATE ${CMAKE_SOURCE_DIR}/libretro/main.cpp ${CMAKE_SOURCE_DIR}/libretro/patches.cpp ${CMAKE_SOURCE_DIR}/libretro/USB.cpp + ${CMAKE_SOURCE_DIR}/libretro/CrashHandler.cpp ${CMAKE_SOURCE_DIR}/libretro/libretro-common/compat/compat_strl.c ${CMAKE_SOURCE_DIR}/libretro/libretro-common/compat/compat_posix_string.c ${CMAKE_SOURCE_DIR}/libretro/libretro-common/compat/fopen_utf8.c diff --git a/libretro/CrashHandler.cpp b/libretro/CrashHandler.cpp new file mode 100644 index 0000000000..cd5fcf47c5 --- /dev/null +++ b/libretro/CrashHandler.cpp @@ -0,0 +1,221 @@ +#include "CrashHandler.h" + +#if defined(__unix__) || defined(__ANDROID__) || defined(__APPLE__) + +#include +#include +#include +#include +#include +#include +#include + +#if defined(__linux__) || defined(__ANDROID__) +#include +#endif +#if defined(__ANDROID__) +#include +#endif + +namespace +{ + /* + * The signals worth reporting. SIGSEGV and SIGBUS are the interesting ones, + * but they arrive here only after fastmem has declined them - see the + * header. The rest are taken directly. + */ + const int kSignals[] = { SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGABRT, SIGTRAP }; + constexpr size_t kSignalCount = sizeof(kSignals) / sizeof(kSignals[0]); + + struct sigaction s_old_actions[kSignalCount]; + bool s_installed = false; + + void Emit(const char* line) + { + /* Two destinations on purpose: logcat is where an Android crash is + * actually readable, and stderr is what reaches a desktop terminal and + * anything capturing the core's output. */ +#if defined(__ANDROID__) + __android_log_write(ANDROID_LOG_ERROR, "lrps2", line); +#endif + const size_t len = strlen(line); + ssize_t written = write(STDERR_FILENO, line, len); + written = write(STDERR_FILENO, "\n", 1); + (void)written; + } + + /* + * The one question a crash in a recompiled core turns on: was the program + * counter in generated code or in the core's own text? A backtrace cannot + * say - JIT pages carry no unwind information, so the unwinder stops at the + * signal frame - but /proc/self/maps can, because generated code lives in an + * anonymous mapping while compiled code lives in the .so. + */ + void DescribeAddress(const char* what, unsigned long long addr) + { + char line[640]; + if (addr == 0) + { + snprintf(line, sizeof(line), " %s in null", what); + Emit(line); + return; + } + + FILE* maps = fopen("/proc/self/maps", "r"); + if (!maps) + { + snprintf(line, sizeof(line), " %s in unknown (no /proc/self/maps)", what); + Emit(line); + return; + } + + char entry[512]; + bool found = false; + while (fgets(entry, sizeof(entry), maps)) + { + unsigned long long start = 0, end = 0; + if (sscanf(entry, "%llx-%llx", &start, &end) != 2) + continue; + if (addr < start || addr >= end) + continue; + + size_t n = strlen(entry); + while (n > 0 && (entry[n - 1] == '\n' || entry[n - 1] == ' ')) + entry[--n] = '\0'; + snprintf(line, sizeof(line), " %s in %s (+0x%llx)", what, entry, addr - start); + found = true; + break; + } + fclose(maps); + + if (!found) + snprintf(line, sizeof(line), " %s in unmapped", what); + Emit(line); + } + + void CurrentThreadName(char* out, size_t size) + { + out[0] = '\0'; +#if defined(__linux__) || defined(__ANDROID__) + char name[32] = {}; + if (prctl(PR_GET_NAME, name, 0, 0, 0) == 0) + snprintf(out, size, "%s", name); +#endif + if (out[0] == '\0') + snprintf(out, size, "unnamed"); + } + + const char* SignalName(int sig) + { + switch (sig) + { + case SIGSEGV: return "SIGSEGV"; + case SIGBUS: return "SIGBUS"; + case SIGILL: return "SIGILL"; + case SIGFPE: return "SIGFPE"; + case SIGABRT: return "SIGABRT"; + case SIGTRAP: return "SIGTRAP"; + default: return "signal"; + } + } + + void ChainToPrevious(int sig, siginfo_t* info, void* ctx) + { + for (size_t i = 0; i < kSignalCount; i++) + { + if (kSignals[i] != sig) + continue; + + const struct sigaction& sa = s_old_actions[i]; + if (sa.sa_flags & SA_SIGINFO) + sa.sa_sigaction(sig, info, ctx); + else if (sa.sa_handler == SIG_DFL) + { + /* Restoring the default and re-raising would come back here, + * since the handler is reinstalled; abort is what the default + * action would do anyway. */ + signal(sig, SIG_DFL); + raise(sig); + abort(); + } + else if (sa.sa_handler != SIG_IGN) + sa.sa_handler(sig); + return; + } + abort(); + } + + void Handler(int sig, siginfo_t* info, void* ctx) + { + /* A second fault while reporting the first would loop; let the second + * one take the default action instead. */ + static volatile sig_atomic_t s_reporting = 0; + if (s_reporting) + { + signal(sig, SIG_DFL); + raise(sig); + return; + } + s_reporting = 1; + + char line[640]; + char thread[64]; + CurrentThreadName(thread, sizeof(thread)); + + const unsigned long long fault = (unsigned long long)(info ? (uintptr_t)info->si_addr : 0); + + snprintf(line, sizeof(line), "[CrashHandler] %s (code %d) on thread \"%s\"", + SignalName(sig), info ? info->si_code : 0, thread); + Emit(line); + +#if defined(__aarch64__) && (defined(__linux__) || defined(__ANDROID__)) + ucontext_t* uc = (ucontext_t*)ctx; + const unsigned long long pc = uc ? (unsigned long long)uc->uc_mcontext.pc : 0; + const unsigned long long lr = uc ? (unsigned long long)uc->uc_mcontext.regs[30] : 0; + const unsigned long long sp = uc ? (unsigned long long)uc->uc_mcontext.sp : 0; + const unsigned long long fp = uc ? (unsigned long long)uc->uc_mcontext.regs[29] : 0; + + snprintf(line, sizeof(line), " fault=0x%llx pc=0x%llx lr=0x%llx sp=0x%llx fp=0x%llx", + fault, pc, lr, sp, fp); + Emit(line); + + DescribeAddress("pc ", pc); + DescribeAddress("lr ", lr); + if (fault != pc) + DescribeAddress("fault", fault); +#else + snprintf(line, sizeof(line), " fault=0x%llx", fault); + Emit(line); + DescribeAddress("fault", fault); + (void)ctx; +#endif + + Emit(" (pc inside the core's .so is a bug in compiled code; pc inside an " + "anonymous mapping means it was executing recompiled code)"); + + s_reporting = 0; + ChainToPrevious(sig, info, ctx); + } +} // namespace + +void CrashHandler_Install(void) +{ + if (s_installed) + return; + s_installed = true; + + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_SIGINFO | SA_NODEFER; + sa.sa_sigaction = Handler; + + for (size_t i = 0; i < kSignalCount; i++) + sigaction(kSignals[i], &sa, &s_old_actions[i]); +} + +#else + +void CrashHandler_Install(void) {} + +#endif diff --git a/libretro/CrashHandler.h b/libretro/CrashHandler.h new file mode 100644 index 0000000000..9b3be56a90 --- /dev/null +++ b/libretro/CrashHandler.h @@ -0,0 +1,12 @@ +#pragma once + +/* + * Installs a reporter for the signals that kill the core outright. + * + * It has to go in before vtlb installs the fastmem page fault handler: that one + * takes over SIGSEGV (and SIGBUS on aarch64) and chains to whatever was there + * before it when a fault is not one of its own, so installing first is what + * makes a genuine segfault reach this instead of going straight to the default + * action. Signals fastmem does not touch are taken directly. + */ +void CrashHandler_Install(void); diff --git a/libretro/main.cpp b/libretro/main.cpp index 516727808d..505014cdbb 100644 --- a/libretro/main.cpp +++ b/libretro/main.cpp @@ -17,6 +17,7 @@ #include #include "libretro_core_options.h" +#include "CrashHandler.h" #include "../pcsx2/GS.h" #include "../pcsx2/SPU2/Global.h" @@ -2349,6 +2350,12 @@ void retro_init(void) bool option_categories = false; enum retro_pixel_format xrgb888 = RETRO_PIXEL_FORMAT_XRGB8888; + /* Before anything else, and in particular before vtlb installs the fastmem + * page fault handler: that one takes SIGSEGV and chains back to whatever it + * displaced, so getting in first is what puts this on the end of that chain + * rather than out of it. */ + CrashHandler_Install(); + environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &xrgb888); if (environ_cb(RETRO_ENVIRONMENT_GET_LOG_INTERFACE, &log)) log_cb = log.log;