/*** Copyright (C) 2021 J Reece Wilson (a/k/a "Reece"). All rights reserved. File: ExceptionWatcher.Win32.cpp Date: 2021-6-12 Author: Reece ***/ #include #include "Debug.hpp" #include "ExceptionWatcher.NT.hpp" #include "ExceptionWatcher.Win32.hpp" #include "Stack.Win32.hpp" #include #include #include #include #include #include #include #include #include #include #include #include static thread_local int gDebugLocked = 0; namespace Aurora::Debug { #define EXCEPTION_ENTRY(n) {n, #n} static const AuHashMap kExceptionTable { EXCEPTION_ENTRY(STILL_ACTIVE), EXCEPTION_ENTRY(EXCEPTION_ACCESS_VIOLATION), EXCEPTION_ENTRY(EXCEPTION_DATATYPE_MISALIGNMENT), EXCEPTION_ENTRY(EXCEPTION_BREAKPOINT), EXCEPTION_ENTRY(EXCEPTION_SINGLE_STEP), EXCEPTION_ENTRY(EXCEPTION_ARRAY_BOUNDS_EXCEEDED), EXCEPTION_ENTRY(EXCEPTION_FLT_DENORMAL_OPERAND), EXCEPTION_ENTRY(EXCEPTION_FLT_DIVIDE_BY_ZERO), EXCEPTION_ENTRY(EXCEPTION_FLT_INEXACT_RESULT), EXCEPTION_ENTRY(EXCEPTION_FLT_INVALID_OPERATION), EXCEPTION_ENTRY(EXCEPTION_FLT_OVERFLOW), EXCEPTION_ENTRY(EXCEPTION_FLT_STACK_CHECK), EXCEPTION_ENTRY(EXCEPTION_FLT_UNDERFLOW), EXCEPTION_ENTRY(EXCEPTION_INT_DIVIDE_BY_ZERO), EXCEPTION_ENTRY(EXCEPTION_INT_OVERFLOW), EXCEPTION_ENTRY(EXCEPTION_PRIV_INSTRUCTION), EXCEPTION_ENTRY(EXCEPTION_IN_PAGE_ERROR), EXCEPTION_ENTRY(EXCEPTION_ILLEGAL_INSTRUCTION), EXCEPTION_ENTRY(EXCEPTION_NONCONTINUABLE_EXCEPTION), EXCEPTION_ENTRY(EXCEPTION_STACK_OVERFLOW), EXCEPTION_ENTRY(EXCEPTION_INVALID_DISPOSITION), EXCEPTION_ENTRY(EXCEPTION_GUARD_PAGE), EXCEPTION_ENTRY(EXCEPTION_INVALID_HANDLE), //EXCEPTION_ENTRY(EXCEPTION_POSSIBLE_DEADLOCK), EXCEPTION_ENTRY(CONTROL_C_EXIT), EXCEPTION_ENTRY(DBG_CONTROL_C) }; #undef EXCEPTION_ENTRY #define EXCEPTION_ENTRY(n) {n, true} static const AuHashMap kExceptionFatalTable { EXCEPTION_ENTRY(EXCEPTION_ACCESS_VIOLATION), EXCEPTION_ENTRY(EXCEPTION_DATATYPE_MISALIGNMENT), EXCEPTION_ENTRY(EXCEPTION_ARRAY_BOUNDS_EXCEEDED), EXCEPTION_ENTRY(EXCEPTION_FLT_DENORMAL_OPERAND), EXCEPTION_ENTRY(EXCEPTION_FLT_DIVIDE_BY_ZERO), EXCEPTION_ENTRY(EXCEPTION_FLT_INEXACT_RESULT), EXCEPTION_ENTRY(EXCEPTION_FLT_INVALID_OPERATION), EXCEPTION_ENTRY(EXCEPTION_FLT_OVERFLOW), EXCEPTION_ENTRY(EXCEPTION_FLT_STACK_CHECK), EXCEPTION_ENTRY(EXCEPTION_FLT_UNDERFLOW), EXCEPTION_ENTRY(EXCEPTION_INT_DIVIDE_BY_ZERO), EXCEPTION_ENTRY(EXCEPTION_INT_OVERFLOW), EXCEPTION_ENTRY(EXCEPTION_PRIV_INSTRUCTION), EXCEPTION_ENTRY(EXCEPTION_IN_PAGE_ERROR), EXCEPTION_ENTRY(EXCEPTION_ILLEGAL_INSTRUCTION), EXCEPTION_ENTRY(EXCEPTION_NONCONTINUABLE_EXCEPTION), EXCEPTION_ENTRY(EXCEPTION_STACK_OVERFLOW), EXCEPTION_ENTRY(EXCEPTION_INVALID_DISPOSITION), EXCEPTION_ENTRY(EXCEPTION_GUARD_PAGE) }; #undef EXCEPTION_ENTRY static bool IsReadable(const void *address) { MEMORY_BASIC_INFORMATION info; if (!VirtualQuery(address, &info, sizeof(info))) { return false; } if (!info.BaseAddress) { return false; } return (info.Protect & (PAGE_READONLY | PAGE_READWRITE)) != 0; } bool InPanic(); static LONG CALLBACK HandleVectorException(_EXCEPTION_POINTERS *ExceptionInfo) { if (ExceptionInfo->ExceptionRecord->ExceptionCode == DBG_CONTROL_C) { Exit::PostLevel(AuThreads::GetThread(), Exit::ETriggerLevel::eSigTerminate); return AuExchange(Exit::gHasCanceled, false) ? EXCEPTION_CONTINUE_EXECUTION : EXCEPTION_CONTINUE_SEARCH; } // https://www.youtube.com/embed/w6P03sTzSqM?start=2&autoplay=1 if (ExceptionInfo->ExceptionRecord->ExceptionCode < STATUS_GUARD_PAGE_VIOLATION) { return EXCEPTION_CONTINUE_SEARCH; } #if defined(AU_CFG_ID_SHIP) // you what? if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_BREAKPOINT) { SysPanic(""); return EXCEPTION_CONTINUE_EXECUTION; } #else // debugger go brrr if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_BREAKPOINT) { return EXCEPTION_CONTINUE_SEARCH; } #endif // debug builds can go do something stupid // QA builds, staging, rel in any form should just give up trying if we're under a panic #if !defined(AU_CFG_ID_DEBUG) if (InPanic()) { return EXCEPTION_CONTINUE_SEARCH; } #endif // something dumb like this mess w hat i had before. // gave up trying auto minimal = gDebugLocked++; if (minimal >= 5) { SysPanic("Nested Exception"); } bool cxxThrow = ExceptionInfo->ExceptionRecord->ExceptionInformation[0] == EH_MAGIC_NUMBER1; bool cxxThrowPure = ExceptionInfo->ExceptionRecord->ExceptionInformation[0] == EH_PURE_MAGIC_NUMBER1; void *exception {}; HMODULE handle {}; ThrowInfo *pThrowInfo {}; if ((ExceptionInfo->ExceptionRecord->ExceptionCode == EH_EXCEPTION_NUMBER) && (ExceptionInfo->ExceptionRecord->NumberParameters >= 3) && ((cxxThrow) || (cxxThrowPure)) ) { pThrowInfo = reinterpret_cast(ExceptionInfo->ExceptionRecord->ExceptionInformation[2]); if (pThrowInfo) { auto attribs = pThrowInfo->attributes; if (_EH_RELATIVE_TYPEINFO) { handle = reinterpret_cast(ExceptionInfo->ExceptionRecord->ExceptionInformation[3]); } exception = reinterpret_cast(ExceptionInfo->ExceptionRecord->ExceptionInformation[1]); } } bool isCritical = AuExists(kExceptionFatalTable, ExceptionInfo->ExceptionRecord->ExceptionCode); auto handleNoCppObject = [&]() -> AuString { const AuString *msg; if (AuTryFind(kExceptionTable, ExceptionInfo->ExceptionRecord->ExceptionCode, msg)) { return *msg; } else { return AuToString(ExceptionInfo->ExceptionRecord->ExceptionCode); } }; StackTrace backtrace; ParseStack(ExceptionInfo->ContextRecord, backtrace); #if defined(_AU_USE_EXTENDED_FWD_FACING_DEBUGGING) bool isInternal = true; #else bool isInternal = false; #endif if (pThrowInfo) { ReportSEH(handle, exception, pThrowInfo, handleNoCppObject, backtrace, [&](const AuString &str) { // Pre-submit callback -> its showtime if ((isCritical || isInternal) && (minimal == 0)) { if (gRuntimeConfig.debug.bPrintExceptionStackTracesOut) { AuLogWarn("NT Exception: 0x{:x}, {}", ExceptionInfo->ExceptionRecord->ExceptionCode, str); AuLogWarn("{}", StringifyStackTrace(backtrace)); } } }); } try { Grug::Arrow empty; Grug::HurlRaiseProblematicEvent(&empty); if (isCritical) { Telemetry::Mayday(); } if (isCritical || gRuntimeConfig.debug.bIsExceptionThrowFatal) // exception = literally anything { PlatformHandleFatal(true); } } catch (...) { } gDebugLocked = 0; return EXCEPTION_CONTINUE_SEARCH; } static AuString GetDumpName() { AuString exeName; try { Process::GetProcName(exeName); } catch (...) { } try { auto tm = Time::ToCivilTime(Time::CurrentClockMS()); return AuLocale::TimeDateToFileNameISO8601(tm) + ".dmp"; } catch (...) { return "errordate.dmp"; } } #if defined(AU_ENABLE_NATIVE_MINIDUMP) void SaveMinidump(_EXCEPTION_POINTERS *ExceptionInfo, bool isFatal) { bool ok {}; MINIDUMP_EXCEPTION_INFORMATION info; info.ThreadId = GetCurrentThreadId(); info.ClientPointers = ExceptionInfo != nullptr; info.ExceptionPointers = ExceptionInfo; static const DWORD flags = MiniDumpWithFullMemory | MiniDumpWithFullMemoryInfo | MiniDumpWithHandleData | MiniDumpWithUnloadedModules | MiniDumpWithThreadInfo; std::wstring path; AuString utf8Path; while (path.empty()) { try { utf8Path = AuIOFS::NormalizePathRet("./Logs/Crashes/" + GetDumpName()); path = Locale::ConvertFromUTF8(utf8Path); } catch (...) { } } AuIOFS::CreateDirectories(utf8Path, true); auto hFile = CreateFileW(path.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (hFile == INVALID_HANDLE_VALUE) { AuLogWarn("Couldn't open minidump file. Has a debugger locked the .dmp file?"); goto miniDumpOut; } ok = MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hFile, (MINIDUMP_TYPE)flags, &info, nullptr, nullptr); if (!ok) { AuLogWarn("Couldn't write minidump"); goto miniDumpOut; } CloseHandle(hFile); miniDumpOut: try { AuDebug::PrintError(); } catch (...) { } if (isFatal) { __fastfail('fokd'); } } #endif void BlackboxReport(_EXCEPTION_POINTERS *ExceptionInfo, bool fatal) { AuString path; HANDLE hFile; AuString utf8Path; MINIDUMP_EXCEPTION_INFORMATION info; auto ok = AuIOFS::GetProfileDomain(path); // < could throw inside if (!ok) { path = ".\\"; } info.ClientPointers = true; info.ThreadId = GetCurrentThreadId(); info.ExceptionPointers = ExceptionInfo; static const DWORD flags = MiniDumpWithDataSegs | MiniDumpWithFullMemoryInfo | MiniDumpWithHandleData | MiniDumpWithUnloadedModules | MiniDumpWithThreadInfo; static std::wstring pathStorage(8192, L' '); int index {}; index += MultiByteToWideChar(CP_UTF8, 0, path.c_str(), path.length(), pathStorage.data() + index, pathStorage.size() - index); static const std::string crashesSlash = "Crashes\\"; index += MultiByteToWideChar(CP_UTF8, 0, crashesSlash.c_str(), crashesSlash.length(), pathStorage.data() + index, pathStorage.size() - index); auto dumpName = GetDumpName();; index += MultiByteToWideChar(CP_UTF8, 0, dumpName.c_str(), dumpName.length(), pathStorage.data() + index, pathStorage.size() - index); pathStorage.resize(index); AuIOFS::CreateDirectories(utf8Path, true); // potentially unsafe / could throw inside hFile = CreateFileW(pathStorage.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (hFile != INVALID_HANDLE_VALUE) { AuLogWarn("[1] Couldn't open minidump file. Has a debugger locked the .dmp file?"); goto miniMiniDumpOut; } ok = MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hFile, (MINIDUMP_TYPE)flags, &info, nullptr, nullptr); if (!ok) { AuLogWarn("Couldn't write minidump"); goto miniMiniDumpOut; } CloseHandle(hFile); miniMiniDumpOut: Telemetry::NewBlackBoxEntryMinidump report {}; report.includesRx = false; report.resource.path = dumpName; //