/*** 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; } static LONG CALLBACK HandleVectorException(_EXCEPTION_POINTERS *ExceptionInfo) { if (ExceptionInfo->ExceptionRecord->ExceptionCode == DBG_CONTROL_C) { Exit::PostLevel(AuThreads::GetThread(), Exit::ETriggerLevel::eSigTerminate); return EXCEPTION_CONTINUE_SEARCH; } if (ExceptionInfo->ExceptionRecord->ExceptionCode < STATUS_GUARD_PAGE_VIOLATION) { return EXCEPTION_CONTINUE_SEARCH; } if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_BREAKPOINT) { return EXCEPTION_CONTINUE_SEARCH; } 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)) { AuLogWarn("NT Exception: 0x{:x}, {}", ExceptionInfo->ExceptionRecord->ExceptionCode, str); AuLogWarn("{}", StringifyStackTrace(backtrace)); } }); } try { if (isCritical) { Telemetry::Mayday(); } if (isCritical) { PlatformHandleFatal(true); } Grug::Arrow empty; Grug::HurlRaiseProblematicEvent(&empty); } 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(DEBUG) || defined(INTERNAL) 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; std::wstring wpath; AuString utf8Path; MINIDUMP_EXCEPTION_INFORMATION info; auto ok = AuIOFS::GetProfileDomain(path); if (!ok) { AuLogWarn("Couldn't find minidump directory"); goto miniMiniDumpOut; } info.ClientPointers = true; info.ThreadId = GetCurrentThreadId(); info.ExceptionPointers = ExceptionInfo; static const DWORD flags = MiniDumpWithDataSegs | MiniDumpWithFullMemoryInfo | MiniDumpWithHandleData | MiniDumpWithUnloadedModules | MiniDumpWithThreadInfo; while (wpath.empty()) { try { utf8Path = path + "Crashes/" + GetDumpName(); wpath = Locale::ConvertFromUTF8(utf8Path); } catch (...) { } } AuIOFS::CreateDirectories(utf8Path, true); hFile = CreateFileW(wpath.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 = path; report.resource.type = Telemetry::ENewBlackBoxResourceType::eLocal; Telemetry::ReportDyingBreath(report); if (fatal) { __fastfail('fokd'); } } static void CacheInternalBuildSymbols() { #if defined(DEBUG) || defined(STAGING) SymInitialize(GetCurrentProcess(), NULL, TRUE); #endif } static void DisableWindowsErrorReporting() { // Windows has this annoying watchdog that triggers when your main loop doesnt respond after a while // It's aggressive in its approach, giving the users to forcefully terminate as soon as they spam click a busy app, // - or never, if the user decides its time for a coffee break the time an app goes grey // It's too easy to trigger the watchdog and impossible to stop it from deciding the windowed application must die AuString procName; if (!Process::GetProcName(procName)) { AuLogWarn("Couldn't disable Microsoft Glowware Reporting!"); return; } // Yes, this is prone to module-name conflict issues because it's a dumb registry utility function // This isn't what we want, but it's probably what the user wants // MSFT is such a loving company, they'll do almost nothing to ensure they wont steal all your data WerAddExcludedApplication(AuLocale::ConvertFromUTF8(procName).c_str(), false); } void InitWin32() { // ... CacheInternalBuildSymbols(); // ... DisableWindowsErrorReporting(); // .. AddVectoredExceptionHandler(1, HandleVectorException); } }