From 7b068b976b792c4bae5cd5693b11c1a6bbf1888e Mon Sep 17 00:00:00 2001 From: Stenzek Date: Tue, 5 Sep 2023 20:33:54 +1000 Subject: [PATCH] CrashHandler: Use libbacktrace --- CMakeLists.txt | 4 + CMakeModules/FindLibbacktrace.cmake | 31 +++ src/common/CMakeLists.txt | 5 + src/common/crash_handler.cpp | 361 +++++++++++++++++++++------- src/common/crash_handler.h | 3 +- 5 files changed, 320 insertions(+), 84 deletions(-) create mode 100644 CMakeModules/FindLibbacktrace.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 8d0cc1bcb..39da19a0c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,6 +76,10 @@ if(USE_SDL2) endif() if(NOT WIN32 AND NOT ANDROID) find_package(CURL REQUIRED) + find_package(Libbacktrace) + if(NOT LIBBACKTRACE_FOUND) + message(WARNING "libbacktrace not found, crashes will not produce backtraces.") + endif() endif() if(BUILD_QT_FRONTEND) find_package(Qt6 6.5.1 COMPONENTS Core Gui Widgets Network LinguistTools REQUIRED) diff --git a/CMakeModules/FindLibbacktrace.cmake b/CMakeModules/FindLibbacktrace.cmake new file mode 100644 index 000000000..930504a78 --- /dev/null +++ b/CMakeModules/FindLibbacktrace.cmake @@ -0,0 +1,31 @@ +# - Try to find libbacktrace +# Once done this will define +# LIBBACKTRACE_FOUND - System has libbacktrace +# LIBBACKTRACE_INCLUDE_DIRS - The libbacktrace include directories +# LIBBACKTRACE_LIBRARIES - The libraries needed to use libbacktrace + +FIND_PATH( + LIBBACKTRACE_INCLUDE_DIR backtrace.h + HINTS /usr/include /usr/local/include + ${LIBBACKTRACE_PATH_INCLUDES} +) + +FIND_LIBRARY( + LIBBACKTRACE_LIBRARY + NAMES backtrace + PATHS ${ADDITIONAL_LIBRARY_PATHS} ${LIBBACKTRACE_PATH_LIB} +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Libbacktrace DEFAULT_MSG + LIBBACKTRACE_LIBRARY LIBBACKTRACE_INCLUDE_DIR) + +if(LIBBACKTRACE_FOUND) + add_library(libbacktrace::libbacktrace UNKNOWN IMPORTED) + set_target_properties(libbacktrace::libbacktrace PROPERTIES + IMPORTED_LOCATION ${LIBBACKTRACE_LIBRARY} + INTERFACE_INCLUDE_DIRECTORIES ${LIBBACKTRACE_INCLUDE_DIR} + ) +endif() + +mark_as_advanced(LIBBACKTRACE_INCLUDE_DIR LIBBACKTRACE_LIBRARY) diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index aa591f7e3..7adf8f91f 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -92,6 +92,11 @@ if(NOT WIN32 AND NOT ANDROID) target_link_libraries(common PRIVATE CURL::libcurl ) + + if(LIBBACKTRACE_FOUND) + target_compile_definitions(common PRIVATE "-DUSE_LIBBACKTRACE=1") + target_link_libraries(common PRIVATE libbacktrace::libbacktrace) + endif() endif() if(ANDROID) diff --git a/src/common/crash_handler.cpp b/src/common/crash_handler.cpp index 6de6df07b..0293f43e7 100644 --- a/src/common/crash_handler.cpp +++ b/src/common/crash_handler.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "crash_handler.h" @@ -6,6 +6,7 @@ #include "string_util.h" #include #include +#include #if defined(_WIN32) #include "windows_headers.h" @@ -13,12 +14,10 @@ #include "thirdparty/StackWalker.h" #include -namespace CrashHandler { - class CrashHandlerStackWalker : public StackWalker { public: - CrashHandlerStackWalker(HANDLE out_file); + explicit CrashHandlerStackWalker(HANDLE out_file); ~CrashHandlerStackWalker(); protected: @@ -33,11 +32,7 @@ CrashHandlerStackWalker::CrashHandlerStackWalker(HANDLE out_file) { } -CrashHandlerStackWalker::~CrashHandlerStackWalker() -{ - if (m_out_file) - CloseHandle(m_out_file); -} +CrashHandlerStackWalker::~CrashHandlerStackWalker() = default; void CrashHandlerStackWalker::OnOutput(LPCSTR szText) { @@ -59,7 +54,7 @@ static bool WriteMinidump(HMODULE hDbgHelp, HANDLE hFile, HANDLE hProcess, DWORD PMINIDUMP_CALLBACK_INFORMATION CallbackParam); PFNMINIDUMPWRITEDUMP minidump_write_dump = - reinterpret_cast(GetProcAddress(hDbgHelp, "MiniDumpWriteDump")); + hDbgHelp ? reinterpret_cast(GetProcAddress(hDbgHelp, "MiniDumpWriteDump")) : nullptr; if (!minidump_write_dump) return false; @@ -77,9 +72,67 @@ static bool WriteMinidump(HMODULE hDbgHelp, HANDLE hFile, HANDLE hProcess, DWORD } static std::wstring s_write_directory; +static HMODULE s_dbghelp_module = nullptr; static PVOID s_veh_handle = nullptr; static bool s_in_crash_handler = false; +static void GenerateCrashFilename(wchar_t* buf, size_t len, const wchar_t* prefix, const wchar_t* extension) +{ + SYSTEMTIME st = {}; + GetLocalTime(&st); + + _snwprintf_s(buf, len, _TRUNCATE, L"%s%scrash-%04u-%02u-%02u-%02u-%02u-%02u-%03u.%s", prefix ? prefix : L"", + prefix ? L"\\" : L"", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, + extension); +} + +static void WriteMinidumpAndCallstack(PEXCEPTION_POINTERS exi) +{ + s_in_crash_handler = true; + + wchar_t filename[1024] = {}; + GenerateCrashFilename(filename, std::size(filename), s_write_directory.empty() ? nullptr : s_write_directory.c_str(), + L"txt"); + + // might fail + HANDLE hFile = CreateFileW(filename, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, 0, nullptr); + if (exi && hFile != INVALID_HANDLE_VALUE) + { + char line[1024]; + DWORD written; + std::snprintf(line, std::size(line), "Exception 0x%08X at 0x%p\n", + static_cast(exi->ExceptionRecord->ExceptionCode), exi->ExceptionRecord->ExceptionAddress); + WriteFile(hFile, line, static_cast(std::strlen(line)), &written, nullptr); + } + + GenerateCrashFilename(filename, std::size(filename), s_write_directory.empty() ? nullptr : s_write_directory.c_str(), + L"dmp"); + + const MINIDUMP_TYPE minidump_type = + static_cast(MiniDumpNormal | MiniDumpWithHandleData | MiniDumpWithProcessThreadData | + MiniDumpWithThreadInfo | MiniDumpWithIndirectlyReferencedMemory); + const HANDLE hMinidumpFile = CreateFileW(filename, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, 0, nullptr); + if (hMinidumpFile == INVALID_HANDLE_VALUE || + !WriteMinidump(s_dbghelp_module, hMinidumpFile, GetCurrentProcess(), GetCurrentProcessId(), GetCurrentThreadId(), + exi, minidump_type)) + { + static const char error_message[] = "Failed to write minidump file.\n"; + if (hFile != INVALID_HANDLE_VALUE) + { + DWORD written; + WriteFile(hFile, error_message, sizeof(error_message) - 1, &written, nullptr); + } + } + if (hMinidumpFile != INVALID_HANDLE_VALUE) + CloseHandle(hMinidumpFile); + + CrashHandlerStackWalker sw(hFile); + sw.ShowCallstack(GetCurrentThread(), exi ? exi->ContextRecord : nullptr); + + if (hFile != INVALID_HANDLE_VALUE) + CloseHandle(hFile); +} + static LONG NTAPI ExceptionHandler(PEXCEPTION_POINTERS exi) { if (s_in_crash_handler) @@ -107,79 +160,21 @@ static LONG NTAPI ExceptionHandler(PEXCEPTION_POINTERS exi) if (IsDebuggerPresent()) return EXCEPTION_CONTINUE_SEARCH; - s_in_crash_handler = true; - - // we definitely need dbg helper - maintain an extra reference here - HMODULE hDbgHelp = StackWalker::LoadDbgHelpLibrary(); - - wchar_t filename[1024] = {}; - if (!s_write_directory.empty()) - { - wcsncpy_s(filename, countof(filename), s_write_directory.c_str(), _TRUNCATE); - wcsncat_s(filename, countof(filename), L"\\crash.txt", _TRUNCATE); - } - else - { - wcsncat_s(filename, countof(filename), L"crash.txt", _TRUNCATE); - } - - // might fail - HANDLE hFile = CreateFileW(filename, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, 0, nullptr); - if (hFile) - { - char line[1024]; - DWORD written; - std::snprintf(line, countof(line), "Exception 0x%08X at 0x%p\n", - static_cast(exi->ExceptionRecord->ExceptionCode), exi->ExceptionRecord->ExceptionAddress); - WriteFile(hFile, line, static_cast(std::strlen(line)), &written, nullptr); - } - - if (!s_write_directory.empty()) - { - wcsncpy_s(filename, countof(filename), s_write_directory.c_str(), _TRUNCATE); - wcsncat_s(filename, countof(filename), L"\\crash.dmp", _TRUNCATE); - } - else - { - wcsncat_s(filename, countof(filename), L"crash.dmp", _TRUNCATE); - } - - const MINIDUMP_TYPE minidump_type = - static_cast(MiniDumpNormal | MiniDumpWithHandleData | MiniDumpWithProcessThreadData | - MiniDumpWithThreadInfo | MiniDumpWithIndirectlyReferencedMemory); - HANDLE hMinidumpFile = CreateFileW(filename, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, 0, nullptr); - if (!hMinidumpFile || !WriteMinidump(hDbgHelp, hMinidumpFile, GetCurrentProcess(), GetCurrentProcessId(), - GetCurrentThreadId(), exi, minidump_type)) - { - static const char error_message[] = "Failed to write minidump file.\n"; - if (hFile) - { - DWORD written; - WriteFile(hFile, error_message, sizeof(error_message) - 1, &written, nullptr); - } - } - if (hMinidumpFile) - CloseHandle(hMinidumpFile); - - CrashHandlerStackWalker sw(hFile); - sw.ShowCallstack(GetCurrentThread(), exi->ContextRecord); - - if (hFile) - CloseHandle(hFile); - - if (hDbgHelp) - FreeLibrary(hDbgHelp); - + WriteMinidumpAndCallstack(exi); return EXCEPTION_CONTINUE_SEARCH; } -bool Install() +bool CrashHandler::Install() { + // load dbghelp at install/startup, that way we're not LoadLibrary()'ing after a crash + // .. because that probably wouldn't go down well. + s_dbghelp_module = StackWalker::LoadDbgHelpLibrary(); + s_veh_handle = AddVectoredExceptionHandler(0, ExceptionHandler); return (s_veh_handle != nullptr); } -void SetWriteDirectory(const std::string_view& dump_directory) +void CrashHandler::SetWriteDirectory(const std::string_view& dump_directory) { if (!s_veh_handle) return; @@ -187,34 +182,234 @@ void SetWriteDirectory(const std::string_view& dump_directory) s_write_directory = StringUtil::UTF8StringToWideString(dump_directory); } -void Uninstall() +void CrashHandler::WriteDumpForCaller() +{ + WriteMinidumpAndCallstack(nullptr); +} + +void CrashHandler::Uninstall() { if (s_veh_handle) { RemoveVectoredExceptionHandler(s_veh_handle); s_veh_handle = nullptr; } + + if (s_dbghelp_module) + { + FreeLibrary(s_dbghelp_module); + s_dbghelp_module = nullptr; + } } +#elif defined(USE_LIBBACKTRACE) + +#include +#include +#include +#include +#include +#include +#include + +namespace CrashHandler { +struct BacktraceBuffer +{ + char* buffer; + size_t used; + size_t size; +}; + +static const char* GetSignalName(int signal_no); +static void AllocateBuffer(BacktraceBuffer* buf); +static void FreeBuffer(BacktraceBuffer* buf); +static void AppendToBuffer(BacktraceBuffer* buf, const char* format, ...); +static int BacktraceFullCallback(void* data, uintptr_t pc, const char* filename, int lineno, const char* function); +static void CallExistingSignalHandler(int signal, siginfo_t* siginfo, void* ctx); +static void CrashSignalHandler(int signal, siginfo_t* siginfo, void* ctx); + +static std::recursive_mutex s_crash_mutex; +static bool s_in_signal_handler = false; + +static backtrace_state* s_backtrace_state = nullptr; +static struct sigaction s_old_sigbus_action; +static struct sigaction s_old_sigsegv_action; } // namespace CrashHandler +const char* CrashHandler::GetSignalName(int signal_no) +{ + switch (signal_no) + { + // Don't need to list all of them, there's only a couple we register. + // clang-format off + case SIGSEGV: return "SIGSEGV"; + case SIGBUS: return "SIGBUS"; + default: return "UNKNOWN"; + // clang-format on + } +} + +void CrashHandler::AllocateBuffer(BacktraceBuffer* buf) +{ + buf->used = 0; + buf->size = sysconf(_SC_PAGESIZE); + buf->buffer = + static_cast(mmap(nullptr, buf->size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0)); + if (buf->buffer == static_cast(MAP_FAILED)) + { + buf->buffer = nullptr; + buf->size = 0; + } +} + +void CrashHandler::FreeBuffer(BacktraceBuffer* buf) +{ + if (buf->buffer) + munmap(buf->buffer, buf->size); +} + +void CrashHandler::AppendToBuffer(BacktraceBuffer* buf, const char* format, ...) +{ + std::va_list ap; + va_start(ap, format); + + // Hope this doesn't allocate memory... it *can*, but hopefully unlikely since + // it won't be the first call, and we're providing the buffer. + if (buf->size > 0 && buf->used < (buf->size - 1)) + { + const int written = std::vsnprintf(buf->buffer + buf->used, buf->size - buf->used, format, ap); + if (written > 0) + buf->used += static_cast(written); + } + + va_end(ap); +} + +int CrashHandler::BacktraceFullCallback(void* data, uintptr_t pc, const char* filename, int lineno, + const char* function) +{ + BacktraceBuffer* buf = static_cast(data); + AppendToBuffer(buf, " %016p", pc); + if (function) + AppendToBuffer(buf, " %s", function); + if (filename) + AppendToBuffer(buf, " [%s:%d]", filename, lineno); + + AppendToBuffer(buf, "\n"); + return 0; +} + +void CrashHandler::CallExistingSignalHandler(int signal, siginfo_t* siginfo, void* ctx) +{ + const struct sigaction& sa = (signal == SIGBUS) ? s_old_sigbus_action : s_old_sigsegv_action; + if (sa.sa_flags & SA_SIGINFO) + { + sa.sa_sigaction(signal, siginfo, ctx); + } + else if (sa.sa_handler == SIG_DFL) + { + // Re-raising the signal would just queue it, and since we'd restore the handler back to us, + // we'd end up right back here again. So just abort, because that's probably what it'd do anyway. + abort(); + } + else if (sa.sa_handler != SIG_IGN) + { + sa.sa_handler(signal); + } +} + +void CrashHandler::CrashSignalHandler(int signal, siginfo_t* siginfo, void* ctx) +{ + std::unique_lock lock(s_crash_mutex); + + // If we crash somewhere in libbacktrace, don't bother trying again. + if (!s_in_signal_handler) + { + s_in_signal_handler = true; + +#if defined(__APPLE__) && defined(__x86_64__) + void* const exception_pc = reinterpret_cast(static_cast(ctx)->uc_mcontext->__ss.__rip); +#elif defined(__FreeBSD__) && defined(__x86_64__) + void* const exception_pc = reinterpret_cast(static_cast(ctx)->uc_mcontext.mc_rip); +#elif defined(__x86_64__) + void* const exception_pc = reinterpret_cast(static_cast(ctx)->uc_mcontext.gregs[REG_RIP]); +#else + void* const exception_pc = nullptr; +#endif + + BacktraceBuffer buf; + AllocateBuffer(&buf); + AppendToBuffer(&buf, "*************** Unhandled %s at %p ***************\n", GetSignalName(signal), exception_pc); + + const int rc = backtrace_full(s_backtrace_state, 0, BacktraceFullCallback, nullptr, &buf); + if (rc != 0) + AppendToBuffer(&buf, " backtrace_full() failed: %d\n"); + + AppendToBuffer(&buf, "*******************************************************************\n"); + + if (buf.used > 0) + write(STDERR_FILENO, buf.buffer, buf.used); + + FreeBuffer(&buf); + + s_in_signal_handler = false; + } + + // Chances are we're not going to have anything else to call, but just in case. + lock.unlock(); + CallExistingSignalHandler(signal, siginfo, ctx); +} + +bool CrashHandler::Install() +{ + const std::string progpath = FileSystem::GetProgramPath(); + s_backtrace_state = backtrace_create_state(progpath.empty() ? nullptr : progpath.c_str(), 0, nullptr, nullptr); + if (!s_backtrace_state) + return false; + + struct sigaction sa; + + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_SIGINFO | SA_NODEFER; + sa.sa_sigaction = CrashSignalHandler; + if (sigaction(SIGBUS, &sa, &s_old_sigbus_action) != 0) + return false; + if (sigaction(SIGSEGV, &sa, &s_old_sigsegv_action) != 0) + return false; + + return true; +} + +void CrashHandler::SetWriteDirectory(const std::string_view& dump_directory) +{ +} + +void CrashHandler::WriteDumpForCaller() +{ +} + +void CrashHandler::Uninstall() +{ + // We can't really unchain the signal handlers... so, YOLO. +} + #else -namespace CrashHandler { - -bool Install() +bool CrashHandler::Install() { return false; } -void SetWriteDirectory(const std::string_view& dump_directory) +void CrashHandler::SetWriteDirectory(const std::string_view& dump_directory) { } -void Uninstall() +void CrashHandler::WriteDumpForCaller() { } -} // namespace CrashHandler +void CrashHandler::Uninstall() +{ +} -#endif \ No newline at end of file +#endif diff --git a/src/common/crash_handler.h b/src/common/crash_handler.h index 214011179..faac1b0b0 100644 --- a/src/common/crash_handler.h +++ b/src/common/crash_handler.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "types.h" @@ -7,5 +7,6 @@ namespace CrashHandler { bool Install(); void SetWriteDirectory(const std::string_view& dump_directory); +void WriteDumpForCaller(); void Uninstall(); } // namespace CrashHandler