CrashHandler: Use libbacktrace

This commit is contained in:
Stenzek 2023-09-05 20:33:54 +10:00
parent d366697141
commit 7b068b976b
5 changed files with 320 additions and 84 deletions

View File

@ -76,6 +76,10 @@ if(USE_SDL2)
endif() endif()
if(NOT WIN32 AND NOT ANDROID) if(NOT WIN32 AND NOT ANDROID)
find_package(CURL REQUIRED) find_package(CURL REQUIRED)
find_package(Libbacktrace)
if(NOT LIBBACKTRACE_FOUND)
message(WARNING "libbacktrace not found, crashes will not produce backtraces.")
endif()
endif() endif()
if(BUILD_QT_FRONTEND) if(BUILD_QT_FRONTEND)
find_package(Qt6 6.5.1 COMPONENTS Core Gui Widgets Network LinguistTools REQUIRED) find_package(Qt6 6.5.1 COMPONENTS Core Gui Widgets Network LinguistTools REQUIRED)

View File

@ -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)

View File

@ -92,6 +92,11 @@ if(NOT WIN32 AND NOT ANDROID)
target_link_libraries(common PRIVATE target_link_libraries(common PRIVATE
CURL::libcurl CURL::libcurl
) )
if(LIBBACKTRACE_FOUND)
target_compile_definitions(common PRIVATE "-DUSE_LIBBACKTRACE=1")
target_link_libraries(common PRIVATE libbacktrace::libbacktrace)
endif()
endif() endif()
if(ANDROID) if(ANDROID)

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com> // SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#include "crash_handler.h" #include "crash_handler.h"
@ -6,6 +6,7 @@
#include "string_util.h" #include "string_util.h"
#include <cinttypes> #include <cinttypes>
#include <cstdio> #include <cstdio>
#include <ctime>
#if defined(_WIN32) #if defined(_WIN32)
#include "windows_headers.h" #include "windows_headers.h"
@ -13,12 +14,10 @@
#include "thirdparty/StackWalker.h" #include "thirdparty/StackWalker.h"
#include <DbgHelp.h> #include <DbgHelp.h>
namespace CrashHandler {
class CrashHandlerStackWalker : public StackWalker class CrashHandlerStackWalker : public StackWalker
{ {
public: public:
CrashHandlerStackWalker(HANDLE out_file); explicit CrashHandlerStackWalker(HANDLE out_file);
~CrashHandlerStackWalker(); ~CrashHandlerStackWalker();
protected: protected:
@ -33,11 +32,7 @@ CrashHandlerStackWalker::CrashHandlerStackWalker(HANDLE out_file)
{ {
} }
CrashHandlerStackWalker::~CrashHandlerStackWalker() CrashHandlerStackWalker::~CrashHandlerStackWalker() = default;
{
if (m_out_file)
CloseHandle(m_out_file);
}
void CrashHandlerStackWalker::OnOutput(LPCSTR szText) void CrashHandlerStackWalker::OnOutput(LPCSTR szText)
{ {
@ -59,7 +54,7 @@ static bool WriteMinidump(HMODULE hDbgHelp, HANDLE hFile, HANDLE hProcess, DWORD
PMINIDUMP_CALLBACK_INFORMATION CallbackParam); PMINIDUMP_CALLBACK_INFORMATION CallbackParam);
PFNMINIDUMPWRITEDUMP minidump_write_dump = PFNMINIDUMPWRITEDUMP minidump_write_dump =
reinterpret_cast<PFNMINIDUMPWRITEDUMP>(GetProcAddress(hDbgHelp, "MiniDumpWriteDump")); hDbgHelp ? reinterpret_cast<PFNMINIDUMPWRITEDUMP>(GetProcAddress(hDbgHelp, "MiniDumpWriteDump")) : nullptr;
if (!minidump_write_dump) if (!minidump_write_dump)
return false; return false;
@ -77,9 +72,67 @@ static bool WriteMinidump(HMODULE hDbgHelp, HANDLE hFile, HANDLE hProcess, DWORD
} }
static std::wstring s_write_directory; static std::wstring s_write_directory;
static HMODULE s_dbghelp_module = nullptr;
static PVOID s_veh_handle = nullptr; static PVOID s_veh_handle = nullptr;
static bool s_in_crash_handler = false; 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<unsigned>(exi->ExceptionRecord->ExceptionCode), exi->ExceptionRecord->ExceptionAddress);
WriteFile(hFile, line, static_cast<DWORD>(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<MINIDUMP_TYPE>(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) static LONG NTAPI ExceptionHandler(PEXCEPTION_POINTERS exi)
{ {
if (s_in_crash_handler) if (s_in_crash_handler)
@ -107,79 +160,21 @@ static LONG NTAPI ExceptionHandler(PEXCEPTION_POINTERS exi)
if (IsDebuggerPresent()) if (IsDebuggerPresent())
return EXCEPTION_CONTINUE_SEARCH; return EXCEPTION_CONTINUE_SEARCH;
s_in_crash_handler = true; WriteMinidumpAndCallstack(exi);
// 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<unsigned>(exi->ExceptionRecord->ExceptionCode), exi->ExceptionRecord->ExceptionAddress);
WriteFile(hFile, line, static_cast<DWORD>(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<MINIDUMP_TYPE>(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);
return EXCEPTION_CONTINUE_SEARCH; 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); s_veh_handle = AddVectoredExceptionHandler(0, ExceptionHandler);
return (s_veh_handle != nullptr); 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) if (!s_veh_handle)
return; return;
@ -187,34 +182,234 @@ void SetWriteDirectory(const std::string_view& dump_directory)
s_write_directory = StringUtil::UTF8StringToWideString(dump_directory); s_write_directory = StringUtil::UTF8StringToWideString(dump_directory);
} }
void Uninstall() void CrashHandler::WriteDumpForCaller()
{
WriteMinidumpAndCallstack(nullptr);
}
void CrashHandler::Uninstall()
{ {
if (s_veh_handle) if (s_veh_handle)
{ {
RemoveVectoredExceptionHandler(s_veh_handle); RemoveVectoredExceptionHandler(s_veh_handle);
s_veh_handle = nullptr; s_veh_handle = nullptr;
} }
if (s_dbghelp_module)
{
FreeLibrary(s_dbghelp_module);
s_dbghelp_module = nullptr;
}
} }
#elif defined(USE_LIBBACKTRACE)
#include <backtrace.h>
#include <cstdarg>
#include <cstdio>
#include <mutex>
#include <signal.h>
#include <sys/mman.h>
#include <unistd.h>
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 } // 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<char*>(mmap(nullptr, buf->size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0));
if (buf->buffer == static_cast<char*>(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<size_t>(written);
}
va_end(ap);
}
int CrashHandler::BacktraceFullCallback(void* data, uintptr_t pc, const char* filename, int lineno,
const char* function)
{
BacktraceBuffer* buf = static_cast<BacktraceBuffer*>(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<void*>(static_cast<ucontext_t*>(ctx)->uc_mcontext->__ss.__rip);
#elif defined(__FreeBSD__) && defined(__x86_64__)
void* const exception_pc = reinterpret_cast<void*>(static_cast<ucontext_t*>(ctx)->uc_mcontext.mc_rip);
#elif defined(__x86_64__)
void* const exception_pc = reinterpret_cast<void*>(static_cast<ucontext_t*>(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 #else
namespace CrashHandler { bool CrashHandler::Install()
bool Install()
{ {
return false; 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 #endif

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com> // SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#include "types.h" #include "types.h"
@ -7,5 +7,6 @@
namespace CrashHandler { namespace CrashHandler {
bool Install(); bool Install();
void SetWriteDirectory(const std::string_view& dump_directory); void SetWriteDirectory(const std::string_view& dump_directory);
void WriteDumpForCaller();
void Uninstall(); void Uninstall();
} // namespace CrashHandler } // namespace CrashHandler