crash_handler.cpp (11823B)
1 // SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com> 2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) 3 4 #include "crash_handler.h" 5 #include "dynamic_library.h" 6 #include "file_system.h" 7 #include "string_util.h" 8 #include <cinttypes> 9 #include <cstdio> 10 #include <ctime> 11 12 #if defined(_WIN32) 13 #include "windows_headers.h" 14 15 #include "thirdparty/StackWalker.h" 16 #include <DbgHelp.h> 17 18 namespace { 19 class CrashHandlerStackWalker : public StackWalker 20 { 21 public: 22 explicit CrashHandlerStackWalker(HANDLE out_file); 23 ~CrashHandlerStackWalker(); 24 25 protected: 26 void OnOutput(LPCSTR szText) override; 27 28 private: 29 HANDLE m_out_file; 30 }; 31 } // namespace 32 33 CrashHandlerStackWalker::CrashHandlerStackWalker(HANDLE out_file) 34 : StackWalker(RetrieveVerbose, nullptr, GetCurrentProcessId(), GetCurrentProcess()), m_out_file(out_file) 35 { 36 } 37 38 CrashHandlerStackWalker::~CrashHandlerStackWalker() = default; 39 40 void CrashHandlerStackWalker::OnOutput(LPCSTR szText) 41 { 42 if (m_out_file) 43 { 44 DWORD written; 45 WriteFile(m_out_file, szText, static_cast<DWORD>(std::strlen(szText)), &written, nullptr); 46 } 47 48 OutputDebugStringA(szText); 49 } 50 51 static bool WriteMinidump(HMODULE hDbgHelp, HANDLE hFile, HANDLE hProcess, DWORD process_id, DWORD thread_id, 52 PEXCEPTION_POINTERS exception, MINIDUMP_TYPE type) 53 { 54 using PFNMINIDUMPWRITEDUMP = 55 BOOL(WINAPI*)(HANDLE hProcess, DWORD ProcessId, HANDLE hFile, MINIDUMP_TYPE DumpType, 56 PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, 57 PMINIDUMP_CALLBACK_INFORMATION CallbackParam); 58 59 PFNMINIDUMPWRITEDUMP minidump_write_dump = 60 hDbgHelp ? reinterpret_cast<PFNMINIDUMPWRITEDUMP>(GetProcAddress(hDbgHelp, "MiniDumpWriteDump")) : nullptr; 61 if (!minidump_write_dump) 62 return false; 63 64 MINIDUMP_EXCEPTION_INFORMATION mei = {}; 65 if (exception) 66 { 67 mei.ThreadId = thread_id; 68 mei.ExceptionPointers = exception; 69 mei.ClientPointers = FALSE; 70 return minidump_write_dump(hProcess, process_id, hFile, type, &mei, nullptr, nullptr); 71 } 72 73 __try 74 { 75 RaiseException(EXCEPTION_INVALID_HANDLE, 0, 0, nullptr); 76 } 77 __except (WriteMinidump(hDbgHelp, hFile, GetCurrentProcess(), GetCurrentProcessId(), GetCurrentThreadId(), 78 GetExceptionInformation(), type), 79 EXCEPTION_EXECUTE_HANDLER) 80 { 81 } 82 83 return true; 84 } 85 86 static std::wstring s_write_directory; 87 static DynamicLibrary s_dbghelp_module; 88 static CrashHandler::CleanupHandler s_cleanup_handler; 89 static bool s_in_crash_handler = false; 90 91 static void GenerateCrashFilename(wchar_t* buf, size_t len, const wchar_t* prefix, const wchar_t* extension) 92 { 93 SYSTEMTIME st = {}; 94 GetLocalTime(&st); 95 96 _snwprintf_s(buf, len, _TRUNCATE, L"%s%scrash-%04u-%02u-%02u-%02u-%02u-%02u-%03u.%s", prefix ? prefix : L"", 97 prefix ? L"\\" : L"", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, 98 extension); 99 } 100 101 static void WriteMinidumpAndCallstack(PEXCEPTION_POINTERS exi) 102 { 103 wchar_t filename[1024] = {}; 104 GenerateCrashFilename(filename, std::size(filename), s_write_directory.empty() ? nullptr : s_write_directory.c_str(), 105 L"txt"); 106 107 // might fail 108 HANDLE hFile = CreateFileW(filename, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, 0, nullptr); 109 if (exi && hFile != INVALID_HANDLE_VALUE) 110 { 111 char line[1024]; 112 DWORD written; 113 std::snprintf(line, std::size(line), "Exception 0x%08X at 0x%p\n", 114 static_cast<unsigned>(exi->ExceptionRecord->ExceptionCode), exi->ExceptionRecord->ExceptionAddress); 115 WriteFile(hFile, line, static_cast<DWORD>(std::strlen(line)), &written, nullptr); 116 } 117 118 GenerateCrashFilename(filename, std::size(filename), s_write_directory.empty() ? nullptr : s_write_directory.c_str(), 119 L"dmp"); 120 121 const MINIDUMP_TYPE minidump_type = 122 static_cast<MINIDUMP_TYPE>(MiniDumpNormal | MiniDumpWithHandleData | MiniDumpWithProcessThreadData | 123 MiniDumpWithThreadInfo | MiniDumpWithIndirectlyReferencedMemory); 124 const HANDLE hMinidumpFile = CreateFileW(filename, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, 0, nullptr); 125 if (hMinidumpFile == INVALID_HANDLE_VALUE || 126 !WriteMinidump(static_cast<HMODULE>(s_dbghelp_module.GetHandle()), hMinidumpFile, GetCurrentProcess(), 127 GetCurrentProcessId(), GetCurrentThreadId(), exi, minidump_type)) 128 { 129 static const char error_message[] = "Failed to write minidump file.\n"; 130 if (hFile != INVALID_HANDLE_VALUE) 131 { 132 DWORD written; 133 WriteFile(hFile, error_message, sizeof(error_message) - 1, &written, nullptr); 134 } 135 } 136 if (hMinidumpFile != INVALID_HANDLE_VALUE) 137 CloseHandle(hMinidumpFile); 138 139 CrashHandlerStackWalker sw(hFile); 140 sw.ShowCallstack(GetCurrentThread(), exi ? exi->ContextRecord : nullptr); 141 142 if (hFile != INVALID_HANDLE_VALUE) 143 CloseHandle(hFile); 144 } 145 146 static LONG NTAPI ExceptionHandler(PEXCEPTION_POINTERS exi) 147 { 148 // if the debugger is attached, or we're recursively crashing, let it take care of it. 149 if (!s_in_crash_handler) 150 { 151 s_in_crash_handler = true; 152 if (s_cleanup_handler) 153 s_cleanup_handler(); 154 155 WriteMinidumpAndCallstack(exi); 156 } 157 158 // returning EXCEPTION_CONTINUE_SEARCH makes sense, except for the fact that it seems to leave zombie processes 159 // around. instead, force ourselves to terminate. 160 TerminateProcess(GetCurrentProcess(), 0xFEFEFEFEu); 161 return EXCEPTION_CONTINUE_SEARCH; 162 } 163 164 bool CrashHandler::Install(CleanupHandler cleanup_handler) 165 { 166 // load dbghelp at install/startup, that way we're not LoadLibrary()'ing after a crash 167 // .. because that probably wouldn't go down well. 168 HMODULE mod = StackWalker::LoadDbgHelpLibrary(); 169 if (mod) 170 s_dbghelp_module.Adopt(mod); 171 172 SetUnhandledExceptionFilter(ExceptionHandler); 173 s_cleanup_handler = cleanup_handler; 174 return true; 175 } 176 177 void CrashHandler::SetWriteDirectory(std::string_view dump_directory) 178 { 179 s_write_directory = StringUtil::UTF8StringToWideString(dump_directory); 180 } 181 182 void CrashHandler::WriteDumpForCaller() 183 { 184 WriteMinidumpAndCallstack(nullptr); 185 } 186 187 #elif !defined(__APPLE__) 188 189 #include <backtrace.h> 190 #include <cstdarg> 191 #include <cstdio> 192 #include <mutex> 193 #include <signal.h> 194 #include <sys/mman.h> 195 #include <unistd.h> 196 197 namespace CrashHandler { 198 namespace { 199 struct BacktraceBuffer 200 { 201 char* buffer; 202 size_t used; 203 size_t size; 204 }; 205 } // namespace 206 207 static const char* GetSignalName(int signal_no); 208 static void AllocateBuffer(BacktraceBuffer* buf); 209 static void FreeBuffer(BacktraceBuffer* buf); 210 static void AppendToBuffer(BacktraceBuffer* buf, const char* format, ...); 211 static int BacktraceFullCallback(void* data, uintptr_t pc, const char* filename, int lineno, const char* function); 212 static void LogCallstack(int signal, const void* exception_pc); 213 214 static std::recursive_mutex s_crash_mutex; 215 static bool s_in_signal_handler = false; 216 217 static CleanupHandler s_cleanup_handler; 218 static backtrace_state* s_backtrace_state = nullptr; 219 } // namespace CrashHandler 220 221 const char* CrashHandler::GetSignalName(int signal_no) 222 { 223 switch (signal_no) 224 { 225 // Don't need to list all of them, there's only a couple we register. 226 // clang-format off 227 case SIGSEGV: return "SIGSEGV"; 228 case SIGBUS: return "SIGBUS"; 229 default: return "UNKNOWN"; 230 // clang-format on 231 } 232 } 233 234 void CrashHandler::AllocateBuffer(BacktraceBuffer* buf) 235 { 236 buf->used = 0; 237 buf->size = sysconf(_SC_PAGESIZE); 238 buf->buffer = 239 static_cast<char*>(mmap(nullptr, buf->size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0)); 240 if (buf->buffer == static_cast<char*>(MAP_FAILED)) 241 { 242 buf->buffer = nullptr; 243 buf->size = 0; 244 } 245 } 246 247 void CrashHandler::FreeBuffer(BacktraceBuffer* buf) 248 { 249 if (buf->buffer) 250 munmap(buf->buffer, buf->size); 251 } 252 253 void CrashHandler::AppendToBuffer(BacktraceBuffer* buf, const char* format, ...) 254 { 255 std::va_list ap; 256 va_start(ap, format); 257 258 // Hope this doesn't allocate memory... it *can*, but hopefully unlikely since 259 // it won't be the first call, and we're providing the buffer. 260 if (buf->size > 0 && buf->used < (buf->size - 1)) 261 { 262 const int written = std::vsnprintf(buf->buffer + buf->used, buf->size - buf->used, format, ap); 263 if (written > 0) 264 buf->used += static_cast<size_t>(written); 265 } 266 267 va_end(ap); 268 } 269 270 int CrashHandler::BacktraceFullCallback(void* data, uintptr_t pc, const char* filename, int lineno, 271 const char* function) 272 { 273 BacktraceBuffer* buf = static_cast<BacktraceBuffer*>(data); 274 AppendToBuffer(buf, " %016p", pc); 275 if (function) 276 AppendToBuffer(buf, " %s", function); 277 if (filename) 278 AppendToBuffer(buf, " [%s:%d]", filename, lineno); 279 280 AppendToBuffer(buf, "\n"); 281 return 0; 282 } 283 284 void CrashHandler::LogCallstack(int signal, const void* exception_pc) 285 { 286 BacktraceBuffer buf; 287 AllocateBuffer(&buf); 288 if (signal != 0 || exception_pc) 289 AppendToBuffer(&buf, "*************** Unhandled %s at %p ***************\n", GetSignalName(signal), exception_pc); 290 else 291 AppendToBuffer(&buf, "*******************************************************************\n"); 292 293 const int rc = backtrace_full(s_backtrace_state, 0, BacktraceFullCallback, nullptr, &buf); 294 if (rc != 0) 295 AppendToBuffer(&buf, " backtrace_full() failed: %d\n"); 296 297 AppendToBuffer(&buf, "*******************************************************************\n"); 298 299 if (buf.used > 0) 300 write(STDERR_FILENO, buf.buffer, buf.used); 301 302 FreeBuffer(&buf); 303 } 304 305 void CrashHandler::CrashSignalHandler(int signal, siginfo_t* siginfo, void* ctx) 306 { 307 std::unique_lock lock(s_crash_mutex); 308 309 // If we crash somewhere in libbacktrace, don't bother trying again. 310 if (!s_in_signal_handler) 311 { 312 s_in_signal_handler = true; 313 314 if (s_cleanup_handler) 315 s_cleanup_handler(); 316 317 #if defined(__APPLE__) && defined(__x86_64__) 318 void* const exception_pc = reinterpret_cast<void*>(static_cast<ucontext_t*>(ctx)->uc_mcontext->__ss.__rip); 319 #elif defined(__FreeBSD__) && defined(__x86_64__) 320 void* const exception_pc = reinterpret_cast<void*>(static_cast<ucontext_t*>(ctx)->uc_mcontext.mc_rip); 321 #elif defined(__x86_64__) 322 void* const exception_pc = reinterpret_cast<void*>(static_cast<ucontext_t*>(ctx)->uc_mcontext.gregs[REG_RIP]); 323 #else 324 void* const exception_pc = nullptr; 325 #endif 326 327 LogCallstack(signal, exception_pc); 328 329 s_in_signal_handler = false; 330 } 331 332 lock.unlock(); 333 334 // We can't continue from here. Just bail out and dump core. 335 std::fputs("Aborting application.\n", stderr); 336 std::fflush(stderr); 337 std::abort(); 338 } 339 340 bool CrashHandler::Install(CleanupHandler cleanup_handler) 341 { 342 const std::string progpath = FileSystem::GetProgramPath(); 343 s_backtrace_state = backtrace_create_state(progpath.empty() ? nullptr : progpath.c_str(), 0, nullptr, nullptr); 344 if (!s_backtrace_state) 345 return false; 346 347 struct sigaction sa; 348 349 sigemptyset(&sa.sa_mask); 350 sa.sa_flags = SA_SIGINFO | SA_NODEFER; 351 sa.sa_sigaction = CrashSignalHandler; 352 if (sigaction(SIGBUS, &sa, nullptr) != 0) 353 return false; 354 if (sigaction(SIGSEGV, &sa, nullptr) != 0) 355 return false; 356 357 s_cleanup_handler = cleanup_handler; 358 return true; 359 } 360 361 void CrashHandler::SetWriteDirectory(std::string_view dump_directory) 362 { 363 } 364 365 void CrashHandler::WriteDumpForCaller() 366 { 367 LogCallstack(0, nullptr); 368 } 369 370 #else 371 372 bool CrashHandler::Install(CleanupHandler cleanup_handler) 373 { 374 return false; 375 } 376 377 void CrashHandler::SetWriteDirectory(std::string_view dump_directory) 378 { 379 } 380 381 void CrashHandler::WriteDumpForCaller() 382 { 383 } 384 385 void CrashHandler::CrashSignalHandler(int signal, siginfo_t* siginfo, void* ctx) 386 { 387 // We can't continue from here. Just bail out and dump core. 388 std::fputs("Aborting application.\n", stderr); 389 std::fflush(stderr); 390 std::abort(); 391 } 392 393 #endif