/*** 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 #if 0 #include #endif 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 void PlatformHandleFatalEx2(bool fatal, CONTEXT &ctx, bool bNoExit); 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; { static bool bRunOnce {}; struct ArrowEx : Grug::Arrow { AuFunction callback; }; ArrowEx empty; empty.callback = [&]() { try { if (AuExchange(bRunOnce, true)) { Telemetry::Mayday(); } ParseStack(ExceptionInfo->ContextRecord, backtrace); } catch (...) { } }; empty.pCallback =[](Grug::Arrow *pBase) -> void { auto pEx = (ArrowEx *)pBase; pEx->callback(); }; Grug::HurlArrow(&empty, empty.pCallback, {}); Grug::ArrowWait(&empty); } CONTEXT ctx {}; ctx.ContextFlags = CONTEXT_ALL; if (!GetThreadContext(GetCurrentThread(), &ctx)) { Debug::Panic(); } AuVoidFunc doReportLocal; { #if defined(AU_CFG_ID_INTERNAL) || defined(AU_CFG_ID_DEBUG) const bool kShouldPrintErrors = true; #else const bool kShouldPrintErrors = false; #endif doReportLocal = [&]() { ReportSEH(handle, exception, pThrowInfo, handleNoCppObject, backtrace, [&](const AuString &str) { // Pre-submit callback -> its showtime if ((isCritical || kShouldPrintErrors) && (minimal == 0)) { if (gRuntimeConfig.debug.bPrintExceptionStackTracesOut) { AuLogWarn("NT Exception: 0x{:x}, {}", ExceptionInfo->ExceptionRecord->ExceptionCode, str); AuLogWarn("{}", StringifyStackTrace(backtrace)); } } }); }; } auto pThread = AuThreads::GetThread(); { static bool bRunOnce {}; struct ArrowEx : Grug::Arrow { AuFunction callback; }; ArrowEx empty; empty.callback = [&]() { doReportLocal(); ReportStackTrace(backtrace, ""); Exit::PostLevel(pThread, Aurora::Exit::ETriggerLevel::eFatalException); try { if (isCritical || gRuntimeConfig.debug.bIsExceptionThrowFatal) // exception = literally anything { PlatformHandleFatalEx2(true, ctx, true); } else { PlatformHandleFatalEx2(false, ctx, true); } } catch (...) { } }; empty.pCallback =[](Grug::Arrow *pBase) -> void { auto pEx = (ArrowEx *)pBase; pEx->callback(); }; Grug::HurlArrow(&empty, empty.pCallback, {}); Grug::ArrowWait(&empty); ReportStackTrace(backtrace, ""); } 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 = false; 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 = Win32Open(path.c_str(), GENERIC_READ | GENERIC_WRITE, 0, false, CREATE_ALWAYS, 0, FILE_ATTRIBUTE_NORMAL); if (hFile == INVALID_HANDLE_VALUE) { AuLogWarn("Couldn't open minidump file. Has a debugger locked the .dmp file?"); goto miniDumpOut; } if (!pMiniDumpWriteDump) { goto miniDumpOut; } ok = pMiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hFile, (MINIDUMP_TYPE)flags, &info, nullptr, nullptr); if (!ok) { AuLogWarn("Couldn't write minidump: {:x}", GetLastError()); goto miniDumpOut; } CloseHandle(hFile); miniDumpOut: try { AuDebug::PrintError(); } catch (...) { } if (isFatal) { try { Grug::GrugFlushFlushs(); Grug::GrugFlushWrites(); } catch (...) { } Win32Terminate(); } } #endif void BlackboxReport(_EXCEPTION_POINTERS *ExceptionInfo, bool fatal) { HANDLE hFile; std::wstring path; AuString utf8Path; bool ok { true }; MINIDUMP_EXCEPTION_INFORMATION info; if (fatal) { AuDebug::AddMemoryCrunch(); } auto dumpName = GetDumpName(); info.ClientPointers = false; info.ThreadId = GetCurrentThreadId(); info.ExceptionPointers = ExceptionInfo; static const DWORD flags = MiniDumpWithDataSegs | MiniDumpWithFullMemoryInfo | MiniDumpWithHandleData | MiniDumpWithUnloadedModules | MiniDumpWithThreadInfo; while (path.empty()) { try { utf8Path = AuIOFS::NormalizePathRet("./Logs/Crashes/" + dumpName); path = Locale::ConvertFromUTF8(utf8Path); } catch (...) { } } AuIOFS::CreateDirectories(utf8Path, true); // potentially unsafe / could throw inside hFile = Win32Open(path.c_str(), GENERIC_READ | GENERIC_WRITE, 0, false, CREATE_ALWAYS, 0, FILE_ATTRIBUTE_NORMAL); if (hFile != INVALID_HANDLE_VALUE) { AuLogWarn("[1] Couldn't open minidump file. Has a debugger locked the .dmp file?"); goto miniMiniDumpOut; } if (!pMiniDumpWriteDump) { goto miniMiniDumpOut; } ok = pMiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hFile, (MINIDUMP_TYPE)flags, &info, nullptr, nullptr); if (!ok) { AuLogWarn("Couldn't write minidump: {:x}", GetLastError()); goto miniMiniDumpOut; } CloseHandle(hFile); miniMiniDumpOut: if (fatal) { Telemetry::NewBlackBoxEntryMinidump report {}; report.includesRx = false; report.resource.path = dumpName; report.resource.type = Telemetry::ENewBlackBoxResourceType::eLocal; Telemetry::ReportDyingBreath(report); Win32Terminate(); } } static void CacheInternalBuildSymbols() { #if defined(AU_CFG_ID_INTERNAL) || defined(AU_CFG_ID_DEBUG) if (pSymInitialize) { pSymInitialize(GetCurrentProcess(), NULL, TRUE); } #endif } static void DisableWindowsErrorReporting() { #if 0 // 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 a choice to forcefully terminate as soon as they spam click a busy app, // or never at all. latterly, its not uncommon for the app to not come back up, bc win32. // 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); #else // This implementation doesn't require a single IAT entry to a pointless dll AuString procName; if (!Process::GetProcName(procName)) { return; } auto procNameWide = AuLocale::ConvertFromUTF8(procName); HKEY hKey; if (pRegOpenKeyExW && pRegOpenKeyExW(HKEY_CURRENT_USER, L"SOFTWARE\\Microsoft\\Windows\\Windows Error Reporting\\ExcludedApplications", 0, KEY_WRITE, &hKey) == ERROR_SUCCESS) { DWORD bioluminescenceReductionFactor { 1 }; if (pRegSetValueExW) { (void)pRegSetValueExW(hKey, procNameWide.c_str(), 0, REG_DWORD, (const BYTE *)&bioluminescenceReductionFactor, sizeof(DWORD)); } if (pRegCloseKey) { pRegCloseKey(hKey); } } #endif } void InitWin32() { // ... if (gRuntimeConfig.debug.bNonshipPrecachesSymbols) { CacheInternalBuildSymbols(); } // ... DisableWindowsErrorReporting(); // .. if (gRuntimeConfig.debug.bEnableWin32RootExceptionHandler) { AddVectoredExceptionHandler(1, HandleVectorException); } } }