mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-01 20:18:15 +01:00
sigaltstack() is per thread. InstallExceptionHandler() runs in CPU_Init() on the loading thread, which exits after boot, while the faults come from the CPU thread and UninstallExceptionHandler() runs on the emu thread. So our stack was never used, and the uninstall "restored" the loading thread's previous alt stack onto the emu thread. On Android that's the one bionic gives each thread and frees when the thread exits, so every later fault on the emu thread ran its handler on freed memory. A Play crash report shows scudo finding a corrupted chunk header when freeing our stack in UninstallExceptionHandler(). The alt stack came with the Dolphin code, which needs it to handle its own stack overflows. Ours only handles guest memory faults, so let the handler run on the faulting thread's stack, or the alt stack that thread already has. This also leaves a libretro frontend's alt stack alone. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
303 lines
9.7 KiB
C++
303 lines
9.7 KiB
C++
// Copyright 2008 Dolphin Emulator Project
|
|
// Licensed under GPLv2+
|
|
// Refer to the license.txt file included.
|
|
|
|
// The corresponding file is called MemTools in the Dolphin project.
|
|
|
|
#include "ppsspp_config.h"
|
|
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <vector>
|
|
|
|
#include "Common/CommonFuncs.h"
|
|
#include "Common/CommonTypes.h"
|
|
#include "Common/Log.h"
|
|
#include "Common/StringUtils.h"
|
|
#include "Common/MachineContext.h"
|
|
#include "Common/ExceptionHandlerSetup.h"
|
|
|
|
#if defined(_MSC_VER)
|
|
#include <crtdbg.h>
|
|
#include "Common/CommonWindows.h"
|
|
#endif
|
|
|
|
static BadAccessHandler g_badAccessHandler;
|
|
|
|
void SetupCRT(bool suppressDialogs) {
|
|
#if defined(_MSC_VER)
|
|
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
|
|
|
|
if (suppressDialogs) {
|
|
// 1. Redirect CRT assertions/errors/warnings to stderr.
|
|
const _HFILE reportTarget = _CRTDBG_FILE_STDERR;
|
|
|
|
_CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE);
|
|
_CrtSetReportFile(_CRT_ASSERT, reportTarget);
|
|
|
|
_CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_FILE);
|
|
_CrtSetReportFile(_CRT_ERROR, reportTarget);
|
|
|
|
_CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);
|
|
_CrtSetReportFile(_CRT_WARN, reportTarget);
|
|
|
|
// 2. Suppress the abort() message box & crash reporting dialogs.
|
|
_set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT);
|
|
|
|
#if !PPSSPP_PLATFORM(UWP)
|
|
// 3. Suppress Windows OS-level "Program has stopped working" modal dialogs.
|
|
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX);
|
|
#endif
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#ifdef MACHINE_CONTEXT_SUPPORTED
|
|
|
|
// We cannot handle exceptions in UWP builds. Bleh.
|
|
#if PPSSPP_PLATFORM(WINDOWS) && !PPSSPP_PLATFORM(UWP)
|
|
|
|
#include <dbghelp.h>
|
|
#pragma comment(lib, "dbghelp.lib")
|
|
|
|
static PVOID g_vectoredExceptionHandle;
|
|
static bool g_symInitialized = false;
|
|
static bool g_logCrashStackTrace = false;
|
|
|
|
// Logs a best-effort stack trace when we're about to let a genuinely unhandled access
|
|
// violation crash the process - e.g. a bad host pointer (not a guest PSP memory access)
|
|
// passed to a CRT function like strlen(). Only meant for diagnostics, so failures here are
|
|
// non-fatal; we just lose the extra info.
|
|
static void LogCrashStackTrace() {
|
|
void *stack[32]{};
|
|
USHORT captured = CaptureStackBackTrace(0, (ULONG)ARRAY_SIZE(stack), stack, nullptr);
|
|
|
|
ERROR_LOG(Log::System, "Unhandled access violation - stack trace (%d frames):", (int)captured);
|
|
|
|
HANDLE process = GetCurrentProcess();
|
|
char symbolBuffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME]{};
|
|
SYMBOL_INFO *symbol = (SYMBOL_INFO *)symbolBuffer;
|
|
symbol->SizeOfStruct = sizeof(SYMBOL_INFO);
|
|
symbol->MaxNameLen = MAX_SYM_NAME;
|
|
|
|
for (USHORT i = 0; i < captured; i++) {
|
|
DWORD64 address = (DWORD64)(uintptr_t)stack[i];
|
|
std::string line = StringFromFormat(" #%d %016llx", (int)i, (unsigned long long)address);
|
|
|
|
DWORD64 displacement = 0;
|
|
if (SymFromAddr(process, address, &displacement, symbol)) {
|
|
line += StringFromFormat(" %s+0x%llx", symbol->Name, (unsigned long long)displacement);
|
|
}
|
|
|
|
DWORD lineDisplacement = 0;
|
|
IMAGEHLP_LINE64 lineInfo{};
|
|
lineInfo.SizeOfStruct = sizeof(lineInfo);
|
|
if (SymGetLineFromAddr64(process, address, &lineDisplacement, &lineInfo)) {
|
|
line += StringFromFormat(" (%s:%d)", lineInfo.FileName, (int)lineInfo.LineNumber);
|
|
}
|
|
|
|
ERROR_LOG(Log::System, "%s", line.c_str());
|
|
}
|
|
}
|
|
|
|
static LONG NTAPI GlobalExceptionHandler(PEXCEPTION_POINTERS pPtrs) {
|
|
switch (pPtrs->ExceptionRecord->ExceptionCode) {
|
|
case EXCEPTION_ACCESS_VIOLATION:
|
|
{
|
|
int accessType = (int)pPtrs->ExceptionRecord->ExceptionInformation[0];
|
|
if (accessType == 8) { // Rule out DEP
|
|
return (DWORD)EXCEPTION_CONTINUE_SEARCH;
|
|
}
|
|
|
|
// virtual address of the inaccessible data
|
|
uintptr_t badAddress = (uintptr_t)pPtrs->ExceptionRecord->ExceptionInformation[1];
|
|
CONTEXT* ctx = pPtrs->ContextRecord;
|
|
|
|
if (g_badAccessHandler(badAddress, ctx)) {
|
|
return (DWORD)EXCEPTION_CONTINUE_EXECUTION;
|
|
} else {
|
|
if (g_logCrashStackTrace) {
|
|
ERROR_LOG(Log::System, "Unhandled access violation (%s) at address %016llx, pc=%016llx",
|
|
accessType == 1 ? "write" : "read", (unsigned long long)badAddress, (unsigned long long)(uintptr_t)pPtrs->ExceptionRecord->ExceptionAddress);
|
|
LogCrashStackTrace();
|
|
}
|
|
// Let's not prevent debugging.
|
|
return (DWORD)EXCEPTION_CONTINUE_SEARCH;
|
|
}
|
|
}
|
|
|
|
case EXCEPTION_STACK_OVERFLOW:
|
|
// Dolphin has some handling of this for the RET optimization emulation.
|
|
return EXCEPTION_CONTINUE_SEARCH;
|
|
|
|
case EXCEPTION_ILLEGAL_INSTRUCTION:
|
|
// No SSE support? Or simply bad codegen?
|
|
return EXCEPTION_CONTINUE_SEARCH;
|
|
|
|
case EXCEPTION_PRIV_INSTRUCTION:
|
|
// okay, dynarec codegen is obviously broken.
|
|
return EXCEPTION_CONTINUE_SEARCH;
|
|
|
|
case EXCEPTION_IN_PAGE_ERROR:
|
|
// okay, something went seriously wrong, out of memory?
|
|
return EXCEPTION_CONTINUE_SEARCH;
|
|
|
|
case EXCEPTION_BREAKPOINT:
|
|
// might want to do something fun with this one day?
|
|
return EXCEPTION_CONTINUE_SEARCH;
|
|
|
|
default:
|
|
return EXCEPTION_CONTINUE_SEARCH;
|
|
}
|
|
}
|
|
|
|
void InstallExceptionHandler(BadAccessHandler badAccessHandler, bool logStackTraceOnCrash) {
|
|
g_logCrashStackTrace = logStackTraceOnCrash;
|
|
if (g_vectoredExceptionHandle) {
|
|
g_badAccessHandler = badAccessHandler;
|
|
return;
|
|
}
|
|
|
|
INFO_LOG(Log::System, "Installing exception handler");
|
|
g_badAccessHandler = badAccessHandler;
|
|
|
|
if (logStackTraceOnCrash && !g_symInitialized) {
|
|
SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_DEFERRED_LOADS | SYMOPT_UNDNAME);
|
|
g_symInitialized = SymInitialize(GetCurrentProcess(), nullptr, TRUE) != FALSE;
|
|
}
|
|
|
|
#ifdef USE_ASAN
|
|
g_vectoredExceptionHandle = AddVectoredExceptionHandler(FALSE, GlobalExceptionHandler);
|
|
#else
|
|
g_vectoredExceptionHandle = AddVectoredExceptionHandler(TRUE, GlobalExceptionHandler);
|
|
#endif
|
|
}
|
|
|
|
void UninstallExceptionHandler() {
|
|
if (g_vectoredExceptionHandle) {
|
|
RemoveVectoredExceptionHandler(g_vectoredExceptionHandle);
|
|
INFO_LOG(Log::System, "Removed exception handler");
|
|
g_vectoredExceptionHandle = nullptr;
|
|
}
|
|
g_badAccessHandler = nullptr;
|
|
}
|
|
|
|
#else
|
|
|
|
#include <signal.h>
|
|
|
|
static struct sigaction old_sa_segv;
|
|
static struct sigaction old_sa_bus;
|
|
|
|
// Hand the signal on to whatever was installed before us. Returning from a fault handler just
|
|
// re-runs the faulting instruction, so for anything we can't deal with this is the only exit
|
|
// that isn't an infinite loop.
|
|
static void ChainToPreviousHandler(int sig, siginfo_t *info, void *raw_context) {
|
|
struct sigaction *old_sa = sig == SIGSEGV ? &old_sa_segv : &old_sa_bus;
|
|
// Per the sigaction man page: with SA_SIGINFO it's sa_sigaction, otherwise sa_handler is
|
|
// SIG_DFL, SIG_IGN, or a handler pointer.
|
|
if (old_sa->sa_flags & SA_SIGINFO) {
|
|
old_sa->sa_sigaction(sig, info, raw_context);
|
|
return;
|
|
}
|
|
if (old_sa->sa_handler == SIG_DFL) {
|
|
signal(sig, SIG_DFL);
|
|
return;
|
|
}
|
|
if (old_sa->sa_handler == SIG_IGN) {
|
|
// Ignore signal
|
|
return;
|
|
}
|
|
old_sa->sa_handler(sig);
|
|
}
|
|
|
|
static void sigsegv_handler(int sig, siginfo_t* info, void* raw_context) {
|
|
if (sig != SIGSEGV && sig != SIGBUS) {
|
|
// We are not interested in other signals - handle it as usual.
|
|
return;
|
|
}
|
|
ucontext_t* context = (ucontext_t*)raw_context;
|
|
int sicode = info->si_code;
|
|
// Darwin reports some faults (e.g. on PROT_NONE pages) as SIGBUS.
|
|
bool addressFault = sig == SIGSEGV ? (sicode == SEGV_MAPERR || sicode == SEGV_ACCERR) : (sicode == BUS_ADRERR || sicode == BUS_ADRALN);
|
|
if (!addressFault) {
|
|
// Not an address fault we can do anything with - an MTE, protection-key or shadow
|
|
// stack fault, or a signal sent with kill(). Returning here would re-run the
|
|
// faulting instruction forever at 100% CPU, and would also swallow the signal from
|
|
// whatever was installed before us (a crash reporter, say).
|
|
ChainToPreviousHandler(sig, info, raw_context);
|
|
return;
|
|
}
|
|
uintptr_t bad_address = (uintptr_t)info->si_addr;
|
|
|
|
// Get all the information we can out of the context.
|
|
#ifdef __OpenBSD__
|
|
ucontext_t* ctx = context;
|
|
#elif defined(__APPLE__)
|
|
// uc_mcontext is a pointer here, and the registers are in its thread state.
|
|
SContext* ctx = &context->uc_mcontext->__ss;
|
|
#else
|
|
mcontext_t* ctx = &context->uc_mcontext;
|
|
#endif
|
|
if (!g_badAccessHandler(bad_address, ctx)) {
|
|
// retry and crash
|
|
// According to the sigaction man page, if sa_flags "SA_SIGINFO" is set to the sigaction
|
|
// function pointer, otherwise sa_handler contains one of:
|
|
// SIG_DEF: The 'default' action is performed
|
|
// SIG_IGN: The signal is ignored
|
|
// Any other value is a function pointer to a signal handler
|
|
|
|
ChainToPreviousHandler(sig, info, raw_context);
|
|
}
|
|
}
|
|
|
|
void InstallExceptionHandler(BadAccessHandler badAccessHandler, bool logStackTraceOnCrash) {
|
|
if (!badAccessHandler) {
|
|
return;
|
|
}
|
|
if (g_badAccessHandler) {
|
|
g_badAccessHandler = badAccessHandler;
|
|
return;
|
|
}
|
|
g_badAccessHandler = badAccessHandler;
|
|
|
|
// No sigaltstack of our own: it's per thread, and this runs on the loading thread, while the faults
|
|
// come from the CPU thread. The handler runs on the faulting thread's stack, or on its alternate
|
|
// stack if it has one (bionic gives every thread one).
|
|
INFO_LOG(Log::System, "Installed exception handler");
|
|
|
|
struct sigaction sa{};
|
|
sa.sa_handler = nullptr;
|
|
sa.sa_sigaction = &sigsegv_handler;
|
|
sa.sa_flags = SA_SIGINFO | SA_ONSTACK;
|
|
sigemptyset(&sa.sa_mask);
|
|
sigaction(SIGSEGV, &sa, &old_sa_segv);
|
|
#ifdef __APPLE__
|
|
sigaction(SIGBUS, &sa, &old_sa_bus);
|
|
#endif
|
|
}
|
|
|
|
void UninstallExceptionHandler() {
|
|
if (!g_badAccessHandler) {
|
|
return;
|
|
}
|
|
sigaction(SIGSEGV, &old_sa_segv, nullptr);
|
|
#ifdef __APPLE__
|
|
sigaction(SIGBUS, &old_sa_bus, nullptr);
|
|
#endif
|
|
INFO_LOG(Log::System, "Uninstalled exception handler");
|
|
g_badAccessHandler = nullptr;
|
|
}
|
|
|
|
#endif
|
|
|
|
#else // !MACHINE_CONTEXT_SUPPORTED
|
|
|
|
void InstallExceptionHandler(BadAccessHandler badAccessHandler, bool logStackTraceOnCrash) {
|
|
ERROR_LOG(Log::System, "Exception handler not implemented on this platform, can't install");
|
|
}
|
|
void UninstallExceptionHandler() { }
|
|
|
|
#endif // MACHINE_CONTEXT_SUPPORTED
|