diff --git a/src/coreclr/debug/crashreport/CMakeLists.txt b/src/coreclr/debug/crashreport/CMakeLists.txt index f88699a4c6a464..22c66d611422ba 100644 --- a/src/coreclr/debug/crashreport/CMakeLists.txt +++ b/src/coreclr/debug/crashreport/CMakeLists.txt @@ -1,7 +1,9 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CRASHREPORT_SOURCES + signalsafeformatter.cpp signalsafejsonwriter.cpp + signalsafeconsolewriter.cpp inproccrashreporter.cpp ) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index fe771432eee5f4..f9183a9509cb48 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -6,23 +6,89 @@ // Streams a createdump-shaped JSON skeleton to a crashreport.json file. #include "inproccrashreporter.h" +#include "signalsafeconsolewriter.h" #include "signalsafejsonwriter.h" +#include "signalsafeformatter.h" #include "pal.h" +#include "volatile.h" #include #include +#include #include #include #include #include #include +#include +#include #include -#ifdef __APPLE__ +#if defined(__ANDROID__) +#include +#elif defined(TARGET_IOS) || defined(TARGET_TVOS) || defined(TARGET_MACCATALYST) #include #include #endif +static const char CRASHREPORT_PROTOCOL_VERSION[] = "1.0.0"; +static constexpr uint32_t CRASHREPORT_COR_E_STACKOVERFLOW = 0x800703E9; +static const char CRASHREPORT_STACK_OVERFLOW_EXCEPTION_TYPE[] = "System.StackOverflowException"; +static const char CRASHREPORT_STACK_OVERFLOW_TRACE_UNAVAILABLE_REASON[] = "stack_overflow_trace_unavailable"; +static constexpr uint32_t CRASHREPORT_STACK_OVERFLOW_MAX_TRACE_FRAMES = 128; +#if defined(__x86_64__) +static const char CRASHREPORT_ARCHITECTURE_NAME[] = "amd64"; +#elif defined(__aarch64__) +static const char CRASHREPORT_ARCHITECTURE_NAME[] = "arm64"; +#elif defined(__arm__) +static const char CRASHREPORT_ARCHITECTURE_NAME[] = "arm"; +#elif defined(__i386__) +static const char CRASHREPORT_ARCHITECTURE_NAME[] = "x86"; +#else +static const char CRASHREPORT_ARCHITECTURE_NAME[] = "unknown"; +#endif + +// Prescribed compact crash report log format. One logical line == one +// __android_log_write entry under CRASHREPORT_LOG_TAG on Android, one +// '\n'-terminated stderr write on Apple mobile platforms. +// +// *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** (BeginConsoleReport) +// .NET Crash Report v +// Build: (omitted if empty) +// ABI: +// Cmdline: (omitted if empty) +// pid: +// signal () +// (blank between sections) +// --- thread 0xTID [(crashed)] --- (BeginConsoleThreadBlock) +// managed exception: (0x) (only if EE provided one) +// #NN [] Class.Method + 0xILOFFSET (token=0xTOKEN) (managed frame; WriteFrameToConsole) +// #NN (in ) Class.Method + 0xILOFFSET (token=0xTOKEN) (overflow form: module didn't fit the table) +// #NN [] 0xIP (module + 0xOFFSET) (native frame; WriteFrameToConsole) +// #NN 0xIP (module + 0xOFFSET) (native frame not in module table) +// (no managed frames) | ... +N more frames (EndConsoleThreadBlock) +// (blank between threads) +// modules: (EndConsoleReport) +// [N] {} (one per ModuleTable entry) +// *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** (closing separator) + +struct StackOverflowTraceFrame +{ + char methodName[CRASHREPORT_STRING_BUFFER_SIZE]; + uint32_t repeatCount; + uint32_t repeatSequenceLength; +}; + +struct StackOverflowTraceSnapshot +{ + uint64_t crashingTid; + uint32_t totalFrameCount; + uint32_t frameCount; + uint32_t truncatedFrameCount; + StackOverflowTraceFrame frames[CRASHREPORT_STACK_OVERFLOW_MAX_TRACE_FRAMES]; + volatile LONG available; +}; + // Include the .NET version string instead of linking because it is "static". #if __has_include("_version.c") #include "_version.c" @@ -30,10 +96,32 @@ static char sccsid[] = "@(#)Version N/A"; #endif -#ifdef __APPLE__ +static void CopyStringToBuffer(char* buffer, size_t bufferSize, const char* value) +{ + if (buffer == nullptr || bufferSize == 0) + { + return; + } + + if (value == nullptr) + { + buffer[0] = '\0'; + return; + } + + size_t toCopy = strnlen(value, bufferSize - 1); + if (toCopy != 0) + { + memcpy(buffer, value, toCopy); + } + + buffer[toCopy] = '\0'; +} + +#if defined(TARGET_IOS) || defined(TARGET_TVOS) || defined(TARGET_MACCATALYST) // Query a sysctl by name into a caller-supplied buffer. Called from Initialize, NOT from the // signal handler -- sysctl/sysctlbyname is not on POSIX's async-signal-safe list, so the -// queried values are cached for use during crash reporting (mirrors the m_hostName / +// queried values are cached for use during crash reporting (mirrors the hostName / // gethostname pattern). static void CacheSysctlString(const char* sysctlName, char* buffer, size_t bufferSize) { @@ -49,19 +137,80 @@ static void CacheSysctlString(const char* sysctlName, char* buffer, size_t buffe buffer[0] = '\0'; } } -#endif // __APPLE__ +#endif // defined(TARGET_IOS) || defined(TARGET_TVOS) || defined(TARGET_MACCATALYST) + +// Bounded module table that deduplicates each unique module observed during a +// single crash report. Frames in the compact log refer to modules by short +// ``[N]`` indices instead of repeating module identity on every line; the +// matching ``modules:`` block resolves the module handles back to full data. +// +// Capacity is fixed at MAX_MODULES_IN_TABLE (no heap on the fatal-signal +// path). A managed frame whose module didn't fit (table full, missing handle, +// or unresolved module identity) renders the module identity inline as +// ``(in ) `` so the frame stays self-describing — overflow is lossless, +// just less compact for that frame. +// +// Single-instance because CreateReport is one-shot per process (guarded by +// the ``s_generating`` InterlockedCompareExchange in CreateReport). + +static constexpr int MAX_MODULES_IN_TABLE = 256; + +class ModuleTable +{ +public: + int GetOrAddIndex( + const void* moduleHandle) + { + if (moduleHandle == nullptr) + { + return -1; + } + + for (int i = 0; i < m_count; ++i) + { + if (m_moduleHandles[i] == moduleHandle) + { + return i; + } + } + + if (m_count >= MAX_MODULES_IN_TABLE) + { + return -1; + } + + m_moduleHandles[m_count] = moduleHandle; + return m_count++; + } + + int Count() const { return m_count; } + const void* ModuleHandle(int i) const { return m_moduleHandles[i]; } +private: + const void* m_moduleHandles[MAX_MODULES_IN_TABLE]; + int m_count = 0; +}; class ThreadEnumerationContext { public: + ThreadEnumerationContext() + { + Init(nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, 0, 0, 0, nullptr); + } + ThreadEnumerationContext( SignalSafeJsonWriter* writer, + SignalSafeConsoleWriter* consoleWriter, + InProcCrashReportModuleInfoCallback moduleInfoCallback, + ModuleTable* moduleTable, + SignalSafeFormatter* formatter, + char* methodNameScratch, + size_t methodNameScratchSize, + uint64_t crashingTid, + uint32_t frameLimitPerThread, void* signalContext) - : m_writer(writer), - m_signalContext(signalContext), - m_threadCount(0), - m_sawCrashThread(false) { + Init(writer, consoleWriter, moduleInfoCallback, moduleTable, formatter, methodNameScratch, methodNameScratchSize, crashingTid, frameLimitPerThread, signalContext); } ThreadEnumerationContext(const ThreadEnumerationContext&) = delete; @@ -69,9 +218,42 @@ class ThreadEnumerationContext size_t ThreadCount() const { return m_threadCount; } bool SawCrashThread() const { return m_sawCrashThread; } - SignalSafeJsonWriter* Writer() const { return m_writer; } + SignalSafeJsonWriter* JsonWriter() const { return m_jsonWriter; } + SignalSafeConsoleWriter* ConsoleWriter() const { return m_consoleWriter; } + + void Init( + SignalSafeJsonWriter* writer, + SignalSafeConsoleWriter* consoleWriter, + InProcCrashReportModuleInfoCallback moduleInfoCallback, + ModuleTable* moduleTable, + SignalSafeFormatter* formatter, + char* methodNameScratch, + size_t methodNameScratchSize, + uint64_t crashingTid, + uint32_t frameLimitPerThread, + void* signalContext) + { + m_jsonWriter = writer; + m_consoleWriter = consoleWriter; + m_moduleInfoCallback = moduleInfoCallback; + m_moduleTable = moduleTable; + m_formatter = formatter; + m_methodNameScratch = methodNameScratch; + m_methodNameScratchSize = methodNameScratchSize; + m_signalContext = signalContext; + m_threadCount = 0; + m_crashingTid = crashingTid; + m_currentThreadFrameCount = 0; + m_currentThreadDroppedCount = 0; + m_frameLimitPerThread = frameLimitPerThread; + m_sawCrashThread = false; + if (m_methodNameScratch != nullptr && m_methodNameScratchSize != 0) + { + m_methodNameScratch[0] = '\0'; + } + } - void EnumerateThreads(InProcCrashReportEnumerateThreadsCallback callback, uint64_t crashingTid); + void EnumerateThreads(InProcCrashReportEnumerateThreadsCallback callback); static void ThreadCallback( uint64_t osThreadId, @@ -86,12 +268,13 @@ class ThreadEnumerationContext const char* methodName, const char* className, const char* moduleName, + const void* moduleHandle, + uint32_t moduleTimestamp, + uint32_t moduleSize, + const GUID* moduleGuid, uint32_t nativeOffset, uint32_t token, uint32_t ilOffset, - uint32_t moduleTimestamp, - uint32_t moduleSize, - const char* moduleGuid, void* ctx); private: @@ -107,34 +290,59 @@ class ThreadEnumerationContext const char* methodName, const char* className, const char* moduleName, - uint32_t nativeOffset, - uint32_t token, - uint32_t ilOffset, + const void* moduleHandle, uint32_t moduleTimestamp, uint32_t moduleSize, - const char* moduleGuid); - - SignalSafeJsonWriter* m_writer; + const GUID* moduleGuid, + uint32_t nativeOffset, + uint32_t token, + uint32_t ilOffset); + + void EndCurrentConsoleThreadBlock(); + void EndCurrentJsonThreadBlock(); + + SignalSafeJsonWriter* m_jsonWriter; + SignalSafeConsoleWriter* m_consoleWriter; + InProcCrashReportModuleInfoCallback m_moduleInfoCallback; + ModuleTable* m_moduleTable; + SignalSafeFormatter* m_formatter; + char* m_methodNameScratch; + size_t m_methodNameScratchSize; void* m_signalContext; size_t m_threadCount; + uint64_t m_crashingTid; + uint32_t m_currentThreadFrameCount; + uint32_t m_currentThreadDroppedCount; + uint32_t m_frameLimitPerThread; bool m_sawCrashThread; }; class CrashReportOutputContext { public: - explicit CrashReportOutputContext(int fd) - : m_fd(fd), + CrashReportOutputContext() + : m_fd(-1), m_writeFailed(false) { } + explicit CrashReportOutputContext(int fd) + { + Init(fd); + } + CrashReportOutputContext(const CrashReportOutputContext&) = delete; CrashReportOutputContext& operator=(const CrashReportOutputContext&) = delete; int Fd() const { return m_fd; } bool WriteFailed() const { return m_writeFailed; } + void Init(int fd) + { + m_fd = fd; + m_writeFailed = false; + } + static bool ChunkCallback(const char* buffer, size_t len, void* ctx); private: @@ -144,9 +352,103 @@ class CrashReportOutputContext bool m_writeFailed; }; +class InProcCrashReporter +{ +public: + static InProcCrashReporter* GetInstance(); + static bool InitializeInstance(const InProcCrashReporterSettings& settings); + + // Capture configuration and the crash-report template path. Must run before + // the instance is published to the PAL signal-handler path. + void Initialize(const InProcCrashReporterSettings& settings); + + void CreateReport( + int signal, + void* context); + + void SetCrashKind(InProcCrashReportCrashKind crashKind); + void BeginStackOverflowTrace(uint64_t crashingTid, uint32_t totalFrameCount); + void AddStackOverflowTraceFrame( + const char* methodName, + uint32_t repeatCount, + uint32_t repeatSequenceLength); + void EndStackOverflowTrace(); + +private: + InProcCrashReporter() = default; + InProcCrashReporter(const InProcCrashReporter&) = delete; + InProcCrashReporter& operator=(const InProcCrashReporter&) = delete; + + void EmitSynthesizedCrashThread( + void* context, + bool walkStack); + + void EmitStackOverflowCrashThread(); + + void EmitThreads( + InProcCrashReportCrashKind crashKind, + void* context); + + void BeginConsoleReport(int signal); + void EndConsoleReport(); + + void BeginJsonReport(); + void EndJsonReport( + int signal, + bool jsonEnabled, + int fd); + + bool BuildReportPath(); + size_t ExpandDumpTemplate( + char* buffer, + size_t bufferSize, + const char* pattern); + + static const char* GetSignalNameAscii(int signal); + + SignalSafeJsonWriter m_jsonWriter; + SignalSafeConsoleWriter m_consoleWriter; + StackOverflowTraceSnapshot m_stackOverflowTrace; + ModuleTable m_moduleTable; + ThreadEnumerationContext m_threadContext; + CrashReportOutputContext m_outputContext; + SignalSafeFormatter m_formatter; + InProcCrashReportIsManagedThreadCallback m_isManagedThreadCallback = nullptr; + InProcCrashReportWalkStackCallback m_walkStackCallback = nullptr; + InProcCrashReportEnumerateThreadsCallback m_enumerateThreadsCallback = nullptr; + InProcCrashReportModuleInfoCallback m_moduleInfoCallback = nullptr; + volatile LONG m_crashKind = static_cast(InProcCrashReportCrashKind::Unknown); + uint32_t m_frameLimitPerThread = 0; + char m_reportPath[CRASHREPORT_PATH_BUFFER_SIZE]; + char m_reportFilePath[CRASHREPORT_PATH_BUFFER_SIZE]; + char m_processName[CRASHREPORT_STRING_BUFFER_SIZE]; + char m_hostName[CRASHREPORT_STRING_BUFFER_SIZE]; + char m_stringScratch[CRASHREPORT_STRING_BUFFER_SIZE]; +#if defined(TARGET_IOS) || defined(TARGET_TVOS) || defined(TARGET_MACCATALYST) + char m_osVersion[CRASHREPORT_STRING_BUFFER_SIZE]; + char m_systemModel[CRASHREPORT_STRING_BUFFER_SIZE]; +#endif +}; + +static InProcCrashReporter* s_reporter = nullptr; + class CrashReportHelpers { public: + struct FrameContext + { + SignalSafeJsonWriter* jsonWriter; + SignalSafeConsoleWriter* consoleWriter; + InProcCrashReportModuleInfoCallback moduleInfoCallback; + ModuleTable* moduleTable; + SignalSafeFormatter* formatter; + uint32_t* currentThreadFrameCount; + uint32_t* currentThreadDroppedCount; + uint32_t frameLimitPerThread; + char* methodNameBuffer; + size_t methodNameBufferSize; + }; + static void GetVersionString( char* buffer, size_t bufferSize); @@ -188,44 +490,131 @@ class CrashReportHelpers size_t bufferSize, const char* value); - static void JsonFrameCallback( + static void WriteFrameToJson( + SignalSafeJsonWriter* writer, + SignalSafeFormatter* formatter, + char* methodNameBuffer, + size_t methodNameBufferSize, uint64_t ip, uint64_t stackPointer, const char* methodName, const char* className, const char* moduleName, + uint32_t moduleTimestamp, + uint32_t moduleSize, + const GUID* moduleGuid, uint32_t nativeOffset, uint32_t token, - uint32_t ilOffset, + uint32_t ilOffset); + + static void WriteFrameToConsole( + SignalSafeConsoleWriter* consoleWriter, + char* methodNameBuffer, + size_t methodNameBufferSize, + uint32_t frameIndex, + int moduleIndex, + uint64_t ip, + const char* methodName, + const char* className, + const char* fallbackModuleName, + uint32_t nativeOffset, + uint32_t token, + uint32_t ilOffset); + + static void WriteStackOverflowFrameToJson( + SignalSafeJsonWriter* writer, + const StackOverflowTraceFrame& frame, + bool includeRepeatMetadata); + + static void WriteStackOverflowFrameToConsole( + SignalSafeConsoleWriter* consoleWriter, + uint32_t frameIndex, + const StackOverflowTraceFrame& frame); + + static void BeginJsonThreadBlock( + SignalSafeJsonWriter* jsonWriter, + uint64_t osThreadId, + bool isManagedThread, + bool isCrashThread, + const char* exceptionType, + uint32_t exceptionHResult); + + static void BeginJsonStackFrames( + SignalSafeJsonWriter* jsonWriter, + bool writeCrashSiteFrame, + void* signalContext); + + static void EndJsonStackFrames( + SignalSafeJsonWriter* jsonWriter); + + static void EndJsonThreadBlock( + SignalSafeJsonWriter* jsonWriter); + + static void BeginConsoleThreadBlock( + SignalSafeConsoleWriter* consoleWriter, + uint64_t osThreadId, + bool isCrashThread); + + static void EndConsoleThreadBlock( + SignalSafeConsoleWriter* consoleWriter, + uint32_t frameCount, + uint32_t droppedCount); + + static void WriteFrame( + uint64_t ip, + uint64_t stackPointer, + const char* methodName, + const char* className, + const char* moduleName, + const void* moduleHandle, uint32_t moduleTimestamp, uint32_t moduleSize, - const char* moduleGuid, + const GUID* moduleGuid, + uint32_t nativeOffset, + uint32_t token, + uint32_t ilOffset, void* ctx); + static void WriteFrameToReport( + SignalSafeJsonWriter* jsonWriter, + SignalSafeConsoleWriter* consoleWriter, + InProcCrashReportModuleInfoCallback moduleInfoCallback, + ModuleTable* moduleTable, + SignalSafeFormatter* formatter, + char* methodNameBuffer, + size_t methodNameBufferSize, + uint32_t* currentThreadFrameCount, + uint32_t* currentThreadDroppedCount, + uint32_t frameLimitPerThread, + uint64_t ip, + uint64_t stackPointer, + const char* methodName, + const char* className, + const char* moduleName, + const void* moduleHandle, + uint32_t moduleTimestamp, + uint32_t moduleSize, + const GUID* moduleGuid, + uint32_t nativeOffset, + uint32_t token, + uint32_t ilOffset); + static bool WriteToFile( int fd, const char* buffer, size_t len); - static bool BuildReportPath( - char* buffer, - size_t bufferSize, - const char* dumpPath, - const char* processName, - const char* hostName); + // SignalSafeJsonWriter callback that drops everything: used when the + // crash report is running in compact-log-only mode (no DbgMiniDumpName) + // so the JSON formatter still keeps its bookkeeping consistent without + // emitting bytes anywhere. + static bool DiscardOutputCallback(const char* buffer, size_t len, void* ctx); - static size_t ExpandDumpTemplate( - char* buffer, - size_t bufferSize, - const char* pattern, - const char* processName, - const char* hostName); }; void InProcCrashReporter::CreateReport( int signal, - siginfo_t* siginfo, void* context) { static LONG s_generating = 0; @@ -234,59 +623,72 @@ InProcCrashReporter::CreateReport( return; } - char reportPath[CRASHREPORT_PATH_BUFFER_SIZE]; - reportPath[0] = '\0'; + m_reportFilePath[0] = '\0'; + // The JSON file sink is only enabled when DbgMiniDumpName supplied a + // template AND the template expanded to a valid path. Otherwise the + // crash report runs in compact-log-only mode: the JSON emitter still + // executes (so it can keep its bookkeeping consistent) but writes go + // to a no-op DiscardOutputCallback instead of an open fd. + bool jsonEnabled = m_reportPath[0] != '\0' && BuildReportPath(); - if (m_reportPath[0] == '\0' || !CrashReportHelpers::BuildReportPath(reportPath, sizeof(reportPath), m_reportPath, m_processName, m_hostName)) + int fd = -1; + if (jsonEnabled) { - return; + fd = open(m_reportFilePath, O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (fd == -1) + { + jsonEnabled = false; + } } - int fd = open(reportPath, O_WRONLY | O_CREAT | O_TRUNC, 0600); - if (fd == -1) + InProcCrashReportCrashKind crashKind = static_cast( + InterlockedExchange(&m_crashKind, static_cast(InProcCrashReportCrashKind::Unknown))); + + m_outputContext.Init(fd); + if (jsonEnabled) { - return; + m_jsonWriter.Init(&CrashReportOutputContext::ChunkCallback, &m_outputContext); } - - (void)siginfo; - - CrashReportOutputContext outputContext(fd); - - m_jsonWriter.Init(&CrashReportOutputContext::ChunkCallback, &outputContext); - - m_jsonWriter.OpenObject(); - m_jsonWriter.OpenObject("payload"); - m_jsonWriter.WriteString("protocol_version", "1.0.0"); - - m_jsonWriter.OpenObject("configuration"); -#if defined(__x86_64__) - m_jsonWriter.WriteString("architecture", "amd64"); -#elif defined(__aarch64__) - m_jsonWriter.WriteString("architecture", "arm64"); -#elif defined(__arm__) - m_jsonWriter.WriteString("architecture", "arm"); -#endif - char version[sizeof(sccsid)]; - CrashReportHelpers::GetVersionString(version, sizeof(version)); - m_jsonWriter.WriteString("version", version); - m_jsonWriter.CloseObject(); // configuration - - if (m_processName[0] != '\0') + else { - m_jsonWriter.WriteString("process_name", m_processName); + m_jsonWriter.Init(&CrashReportHelpers::DiscardOutputCallback, nullptr); } - m_jsonWriter.WriteDecimalAsString("pid", static_cast(GetCurrentProcessId())); + BeginConsoleReport(signal); + BeginJsonReport(); + EmitThreads(crashKind, context); + EndJsonReport(signal, jsonEnabled, fd); + EndConsoleReport(); +} +void +InProcCrashReporter::EmitThreads( + InProcCrashReportCrashKind crashKind, + void* context) +{ m_jsonWriter.OpenArray("threads"); - if (m_enumerateThreadsCallback != nullptr) + if (crashKind == InProcCrashReportCrashKind::StackOverflow) + { + EmitStackOverflowCrashThread(); + } + else if (m_enumerateThreadsCallback != nullptr) { - ThreadEnumerationContext threadContext(&m_jsonWriter, context); uint64_t crashingTid = static_cast(minipal_get_current_thread_id()); - - threadContext.EnumerateThreads(m_enumerateThreadsCallback, crashingTid); - - if (threadContext.ThreadCount() == 0 || !threadContext.SawCrashThread()) + m_threadContext.Init( + &m_jsonWriter, + &m_consoleWriter, + m_moduleInfoCallback, + &m_moduleTable, + &m_formatter, + m_stringScratch, + sizeof(m_stringScratch), + crashingTid, + m_frameLimitPerThread, + context); + + m_threadContext.EnumerateThreads(m_enumerateThreadsCallback); + + if (m_threadContext.ThreadCount() == 0 || !m_threadContext.SawCrashThread()) { EmitSynthesizedCrashThread(context, /*walkStack*/ false); } @@ -296,44 +698,53 @@ InProcCrashReporter::CreateReport( EmitSynthesizedCrashThread(context, /*walkStack*/ true); } m_jsonWriter.CloseArray(); // threads +} - m_jsonWriter.CloseObject(); // payload +InProcCrashReporter* +InProcCrashReporter::GetInstance() +{ + return VolatileLoad(&s_reporter); +} - m_jsonWriter.OpenObject("parameters"); - m_jsonWriter.WriteSignedDecimalAsString("signal", static_cast(signal)); -#ifdef __APPLE__ - if (m_osVersion[0] != '\0') +bool +InProcCrashReporter::InitializeInstance( + const InProcCrashReporterSettings& settings) +{ + if (VolatileLoad(&s_reporter) != nullptr) { - m_jsonWriter.WriteString("OSVersion", m_osVersion); + return true; } - if (m_systemModel[0] != '\0') + + InProcCrashReporter* reporter = new (std::nothrow) InProcCrashReporter(); + if (reporter == nullptr) { - m_jsonWriter.WriteString("SystemModel", m_systemModel); + InProcCrashReportLogInitializationFailure(".NET crash report disabled: failed to allocate reporter storage"); + return false; } - m_jsonWriter.WriteString("SystemManufacturer", "apple"); -#endif - m_jsonWriter.CloseObject(); // parameters - - m_jsonWriter.CloseObject(); // root - if (fd != -1) + reporter->Initialize(settings); + if (InterlockedCompareExchangePointer(&s_reporter, reporter, nullptr) != nullptr) { - bool writeSucceeded = m_jsonWriter.Finish() && - !outputContext.WriteFailed() && - CrashReportHelpers::WriteToFile(fd, "\n", 1); - - if (close(fd) != 0 || !writeSucceeded) - { - unlink(reportPath); - } + delete reporter; } + + return true; } -InProcCrashReporter& -InProcCrashReporter::GetInstance() +const char* +InProcCrashReporter::GetSignalNameAscii(int signal) { - static InProcCrashReporter s_instance; - return s_instance; + switch (signal) + { + case SIGSEGV: return "SIGSEGV"; + case SIGBUS: return "SIGBUS"; + case SIGFPE: return "SIGFPE"; + case SIGILL: return "SIGILL"; + case SIGABRT: return "SIGABRT"; + case SIGTRAP: return "SIGTRAP"; + case SIGTERM: return "SIGTERM"; + default: return "Unknown signal"; + } } void @@ -343,6 +754,10 @@ InProcCrashReporter::Initialize( m_isManagedThreadCallback = settings.isManagedThreadCallback; m_walkStackCallback = settings.walkStackCallback; m_enumerateThreadsCallback = settings.enumerateThreadsCallback; + m_moduleInfoCallback = settings.moduleInfoCallback; + m_frameLimitPerThread = settings.frameLimitPerThread; + m_crashKind = static_cast(InProcCrashReportCrashKind::Unknown); + m_stackOverflowTrace.available = 0; CrashReportHelpers::CopyString(m_reportPath, sizeof(m_reportPath), settings.reportPath); m_processName[0] = '\0'; @@ -354,13 +769,12 @@ InProcCrashReporter::Initialize( int cmdlineFd = open("/proc/self/cmdline", O_RDONLY | O_CLOEXEC); if (cmdlineFd >= 0) { - char buf[CRASHREPORT_STRING_BUFFER_SIZE]; - ssize_t n = read(cmdlineFd, buf, sizeof(buf) - 1); + ssize_t n = read(cmdlineFd, m_stringScratch, sizeof(m_stringScratch) - 1); close(cmdlineFd); if (n > 0) { - buf[n] = '\0'; - CrashReportHelpers::CopyString(m_processName, sizeof(m_processName), CrashReportHelpers::GetFilename(buf)); + m_stringScratch[n] = '\0'; + CrashReportHelpers::CopyString(m_processName, sizeof(m_processName), CrashReportHelpers::GetFilename(m_stringScratch)); } } #endif @@ -386,7 +800,7 @@ InProcCrashReporter::Initialize( m_hostName[0] = '\0'; } -#ifdef __APPLE__ +#if defined(TARGET_IOS) || defined(TARGET_TVOS) || defined(TARGET_MACCATALYST) // Cache sysctl values at Initialize because sysctl/sysctlbyname is not on POSIX's // async-signal-safe list; CreateReport reads these from the signal-handler path. CacheSysctlString("kern.osproductversion", m_osVersion, sizeof(m_osVersion)); @@ -394,17 +808,71 @@ InProcCrashReporter::Initialize( #endif } +void +InProcCrashReporter::SetCrashKind(InProcCrashReportCrashKind crashKind) +{ + InterlockedExchange(&m_crashKind, static_cast(crashKind)); +} + +void +InProcCrashReporter::BeginStackOverflowTrace( + uint64_t crashingTid, + uint32_t totalFrameCount) +{ + StackOverflowTraceSnapshot& trace = m_stackOverflowTrace; + InterlockedExchange(&trace.available, 0); + trace.crashingTid = crashingTid; + trace.totalFrameCount = totalFrameCount; + trace.frameCount = 0; + trace.truncatedFrameCount = 0; +} + +void +InProcCrashReporter::AddStackOverflowTraceFrame( + const char* methodName, + uint32_t repeatCount, + uint32_t repeatSequenceLength) +{ + StackOverflowTraceSnapshot& trace = m_stackOverflowTrace; + if (trace.frameCount >= CRASHREPORT_STACK_OVERFLOW_MAX_TRACE_FRAMES) + { + trace.truncatedFrameCount++; + return; + } + + StackOverflowTraceFrame& frame = trace.frames[trace.frameCount++]; + CopyStringToBuffer(frame.methodName, sizeof(frame.methodName), methodName); + frame.repeatCount = repeatCount; + frame.repeatSequenceLength = repeatSequenceLength; +} + +void +InProcCrashReporter::EndStackOverflowTrace() +{ + InterlockedExchange(&m_stackOverflowTrace.available, 1); +} + void InProcCrashReportSignalDispatcher(int signal, void* siginfo, void* context) { - InProcCrashReporter& reporter = InProcCrashReporter::GetInstance(); - reporter.CreateReport(signal, static_cast(siginfo), context); + (void)siginfo; + + InProcCrashReporter* reporter = InProcCrashReporter::GetInstance(); + if (reporter == nullptr) + { + return; + } + + reporter->CreateReport(signal, context); } void InProcCrashReportInitialize(const InProcCrashReporterSettings& settings) { - InProcCrashReporter::GetInstance().Initialize(settings); + if (!InProcCrashReporter::InitializeInstance(settings)) + { + return; + } // Register last so PAL only observes the dispatcher after the reporter // singleton is fully populated (mirrors the publication ordering used by @@ -412,18 +880,87 @@ InProcCrashReportInitialize(const InProcCrashReporterSettings& settings) PAL_SetInProcCrashReportCallback(&InProcCrashReportSignalDispatcher); } -bool -CrashReportHelpers::WriteToFile( - int fd, - const char* buffer, - size_t len) +void +InProcCrashReportLogInitializationFailure(const char* message) { - if (fd < 0 || buffer == nullptr) + if (message == nullptr) { - return false; + return; } - size_t totalWritten = 0; +#if defined(__ANDROID__) + __android_log_write(ANDROID_LOG_ERROR, CRASHREPORT_LOG_TAG, message); +#else + minipal_log_write_error(message); + minipal_log_write_error("\n"); +#endif +} + +void +InProcCrashReportSetCrashKind(InProcCrashReportCrashKind crashKind) +{ + InProcCrashReporter* reporter = InProcCrashReporter::GetInstance(); + if (reporter == nullptr) + { + return; + } + + reporter->SetCrashKind(crashKind); +} + +void +InProcCrashReportBeginStackOverflowTrace( + uint64_t crashingTid, + uint32_t totalFrameCount) +{ + InProcCrashReporter* reporter = InProcCrashReporter::GetInstance(); + if (reporter == nullptr) + { + return; + } + + reporter->BeginStackOverflowTrace(crashingTid, totalFrameCount); +} + +void +InProcCrashReportAddStackOverflowTraceFrame( + const char* methodName, + uint32_t repeatCount, + uint32_t repeatSequenceLength) +{ + InProcCrashReporter* reporter = InProcCrashReporter::GetInstance(); + if (reporter == nullptr) + { + return; + } + + reporter->AddStackOverflowTraceFrame(methodName, repeatCount, repeatSequenceLength); +} + +void +InProcCrashReportEndStackOverflowTrace() +{ + InProcCrashReporter* reporter = InProcCrashReporter::GetInstance(); + if (reporter == nullptr) + { + return; + } + + reporter->EndStackOverflowTrace(); +} + +bool +CrashReportHelpers::WriteToFile( + int fd, + const char* buffer, + size_t len) +{ + if (fd < 0 || buffer == nullptr) + { + return false; + } + + size_t totalWritten = 0; while (totalWritten < len) { ssize_t written = write(fd, buffer + totalWritten, len - totalWritten); @@ -444,6 +981,15 @@ CrashReportHelpers::WriteToFile( return true; } +bool +CrashReportHelpers::DiscardOutputCallback( + const char* /*buffer*/, + size_t /*len*/, + void* /*ctx*/) +{ + return true; +} + bool CrashReportOutputContext::HandleChunk( const char* buffer, @@ -485,14 +1031,13 @@ CrashReportOutputContext::ChunkCallback( // specifiers are rejected (return 0) to match createdump and to avoid // silently producing diverging file names from the same template. size_t -CrashReportHelpers::ExpandDumpTemplate( +InProcCrashReporter::ExpandDumpTemplate( char* buffer, size_t bufferSize, - const char* pattern, - const char* processName, - const char* hostName) + const char* pattern) { - if (buffer == nullptr || bufferSize == 0 || pattern == nullptr) + if (buffer == nullptr || bufferSize == 0 || + pattern == nullptr) { return 0; } @@ -512,7 +1057,6 @@ CrashReportHelpers::ExpandDumpTemplate( char specifier = *pattern; const char* substitution = nullptr; - char numberBuf[CRASHREPORT_NUMBER_BUFFER_SIZE]; switch (specifier) { @@ -526,28 +1070,19 @@ CrashReportHelpers::ExpandDumpTemplate( case 'p': case 'd': - if (SignalSafeJsonWriter::FormatUnsignedDecimal(numberBuf, sizeof(numberBuf), pid) == 0) - { - return 0; - } - substitution = numberBuf; + substitution = m_formatter.FormatUnsignedDecimal(pid); break; case 'e': - substitution = (processName != nullptr && processName[0] != '\0') ? processName : nullptr; + substitution = (m_processName[0] != '\0') ? m_processName : nullptr; break; case 'h': - substitution = (hostName != nullptr && hostName[0] != '\0') ? hostName : nullptr; + substitution = (m_hostName[0] != '\0') ? m_hostName : nullptr; break; case 't': - if (SignalSafeJsonWriter::FormatUnsignedDecimal( - numberBuf, sizeof(numberBuf), static_cast(time(nullptr))) == 0) - { - return 0; - } - substitution = numberBuf; + substitution = m_formatter.FormatUnsignedDecimal(static_cast(time(nullptr))); break; default: @@ -591,31 +1126,23 @@ CrashReportHelpers::ExpandDumpTemplate( } bool -CrashReportHelpers::BuildReportPath( - char* buffer, - size_t bufferSize, - const char* dumpPath, - const char* processName, - const char* hostName) +InProcCrashReporter::BuildReportPath() { - if (buffer == nullptr || bufferSize == 0 || dumpPath == nullptr || dumpPath[0] == '\0') + if (m_reportPath[0] == '\0') { return false; } - char expanded[CRASHREPORT_PATH_BUFFER_SIZE]; - size_t expandedLen = ExpandDumpTemplate(expanded, sizeof(expanded), dumpPath, processName, hostName); - if (expandedLen == 0) + size_t pos = ExpandDumpTemplate( + m_reportFilePath, + sizeof(m_reportFilePath), + m_reportPath); + if (pos == 0) { return false; } - size_t pos = 0; - if (!AppendString(buffer, bufferSize, &pos, expanded)) - { - return false; - } - if (!AppendString(buffer, bufferSize, &pos, ".crashreport.json")) + if (!CrashReportHelpers::AppendString(m_reportFilePath, sizeof(m_reportFilePath), &pos, ".crashreport.json")) { return false; } @@ -710,9 +1237,9 @@ CrashReportHelpers::GetInstructionPointer( } ucontext_t* ucontext = reinterpret_cast(context); -#if defined(__APPLE__) && defined(__x86_64__) +#if (defined(TARGET_IOS) || defined(TARGET_TVOS) || defined(TARGET_MACCATALYST)) && defined(__x86_64__) return static_cast(ucontext->uc_mcontext->__ss.__rip); -#elif defined(__APPLE__) && defined(__aarch64__) +#elif (defined(TARGET_IOS) || defined(TARGET_TVOS) || defined(TARGET_MACCATALYST)) && defined(__aarch64__) return reinterpret_cast(arm_thread_state64_get_pc_fptr(ucontext->uc_mcontext->__ss)); #elif defined(__x86_64__) return static_cast(ucontext->uc_mcontext.gregs[REG_RIP]); @@ -735,9 +1262,9 @@ CrashReportHelpers::GetStackPointer( } ucontext_t* ucontext = reinterpret_cast(context); -#if defined(__APPLE__) && defined(__x86_64__) +#if (defined(TARGET_IOS) || defined(TARGET_TVOS) || defined(TARGET_MACCATALYST)) && defined(__x86_64__) return static_cast(ucontext->uc_mcontext->__ss.__rsp); -#elif defined(__APPLE__) && defined(__aarch64__) +#elif (defined(TARGET_IOS) || defined(TARGET_TVOS) || defined(TARGET_MACCATALYST)) && defined(__aarch64__) return static_cast(arm_thread_state64_get_sp(ucontext->uc_mcontext->__ss)); #elif defined(__x86_64__) return static_cast(ucontext->uc_mcontext.gregs[REG_RSP]); @@ -760,9 +1287,9 @@ CrashReportHelpers::GetFramePointer( } ucontext_t* ucontext = reinterpret_cast(context); -#if defined(__APPLE__) && defined(__x86_64__) +#if (defined(TARGET_IOS) || defined(TARGET_TVOS) || defined(TARGET_MACCATALYST)) && defined(__x86_64__) return static_cast(ucontext->uc_mcontext->__ss.__rbp); -#elif defined(__APPLE__) && defined(__aarch64__) +#elif (defined(TARGET_IOS) || defined(TARGET_TVOS) || defined(TARGET_MACCATALYST)) && defined(__aarch64__) return static_cast(arm_thread_state64_get_fp(ucontext->uc_mcontext->__ss)); #elif defined(__x86_64__) return static_cast(ucontext->uc_mcontext.gregs[REG_RBP]); @@ -863,48 +1390,49 @@ CrashReportHelpers::GetFilename( return path; } +static bool +HasModuleName(const char* moduleName) +{ + return moduleName != nullptr && moduleName[0] != '\0'; +} + +static bool +HasManagedIdentity( + const char* methodName, + const char* moduleName, + uint32_t token) +{ + return methodName != nullptr || + (token != 0 && HasModuleName(moduleName)); +} + void CrashReportHelpers::CopyString( char* buffer, size_t bufferSize, const char* value) { - if (buffer == nullptr || bufferSize == 0) - { - return; - } - - if (value == nullptr) - { - buffer[0] = '\0'; - return; - } - - size_t toCopy = strnlen(value, bufferSize - 1); - if (toCopy != 0) - { - memcpy(buffer, value, toCopy); - } - - buffer[toCopy] = '\0'; + CopyStringToBuffer(buffer, bufferSize, value); } void -CrashReportHelpers::JsonFrameCallback( +CrashReportHelpers::WriteFrameToJson( + SignalSafeJsonWriter* writer, + SignalSafeFormatter* formatter, + char* methodNameBuffer, + size_t methodNameBufferSize, uint64_t ip, uint64_t stackPointer, const char* methodName, const char* className, const char* moduleName, - uint32_t nativeOffset, - uint32_t token, - uint32_t ilOffset, uint32_t moduleTimestamp, uint32_t moduleSize, - const char* moduleGuid, - void* ctx) + const GUID* moduleGuid, + uint32_t nativeOffset, + uint32_t token, + uint32_t ilOffset) { - SignalSafeJsonWriter* writer = reinterpret_cast(ctx); if (writer == nullptr) { return; @@ -915,15 +1443,25 @@ CrashReportHelpers::JsonFrameCallback( writer->WriteHexAsString("native_address", ip); writer->WriteHexAsString("native_offset", nativeOffset); - if (methodName != nullptr) + if (HasManagedIdentity(methodName, moduleName, token)) { - char fullName[CRASHREPORT_STRING_BUFFER_SIZE]; - BuildMethodName(fullName, sizeof(fullName), className, methodName); - writer->WriteString("method_name", fullName); writer->WriteString("is_managed", "true"); - writer->WriteHexAsString("token", token); - writer->WriteHexAsString("il_offset", ilOffset); - if (moduleName != nullptr) + if (methodName != nullptr) + { + const char* fullMethodName = methodName; + if (methodNameBuffer != nullptr && methodNameBufferSize != 0) + { + BuildMethodName(methodNameBuffer, methodNameBufferSize, className, methodName); + fullMethodName = methodNameBuffer; + } + writer->WriteString("method_name", fullMethodName); + } + if (methodName != nullptr || token != 0) + { + writer->WriteHexAsString("token", token); + writer->WriteHexAsString("il_offset", ilOffset); + } + if (HasModuleName(moduleName)) { writer->WriteString("filename", moduleName); } @@ -935,15 +1473,18 @@ CrashReportHelpers::JsonFrameCallback( { writer->WriteHexAsString("sizeofimage", moduleSize); } - if (moduleGuid != nullptr && moduleGuid[0] != '\0') + if (moduleGuid != nullptr) { - writer->WriteString("guid", moduleGuid); + if (formatter != nullptr) + { + writer->WriteString("guid", formatter->FormatGuid(*moduleGuid)); + } } } else { writer->WriteString("is_managed", "false"); - if (moduleName != nullptr) + if (HasModuleName(moduleName)) { writer->WriteString("native_module", moduleName); } @@ -953,42 +1494,458 @@ CrashReportHelpers::JsonFrameCallback( } void -ThreadEnumerationContext::OnFrame( +CrashReportHelpers::WriteFrameToConsole( + SignalSafeConsoleWriter* consoleWriter, + char* methodNameBuffer, + size_t methodNameBufferSize, + uint32_t frameIndex, + int moduleIndex, + uint64_t ip, + const char* methodName, + const char* className, + const char* fallbackModuleName, + uint32_t nativeOffset, + uint32_t token, + uint32_t ilOffset) +{ + if (consoleWriter == nullptr) + { + return; + } + + consoleWriter->AppendStr(" #"); + if (frameIndex < 10) + { + consoleWriter->AppendChar('0'); + } + consoleWriter->AppendDecimal(static_cast(frameIndex)); + consoleWriter->AppendChar(' '); + + if (moduleIndex >= 0) + { + consoleWriter->AppendChar('['); + consoleWriter->AppendDecimal(static_cast(moduleIndex)); + consoleWriter->AppendStr("] "); + } + else if ((methodName != nullptr || (token != 0 && HasModuleName(fallbackModuleName))) && HasModuleName(fallbackModuleName)) + { + consoleWriter->AppendStr("(in "); + consoleWriter->AppendStr(GetFilename(fallbackModuleName)); + consoleWriter->AppendStr(") "); + } + + if (methodName != nullptr) + { + const char* fullMethodName = methodName; + if (methodNameBuffer != nullptr && methodNameBufferSize != 0) + { + BuildMethodName(methodNameBuffer, methodNameBufferSize, className, methodName); + fullMethodName = methodNameBuffer; + } + consoleWriter->AppendStr(fullMethodName); + consoleWriter->AppendStr(" + 0x"); + consoleWriter->AppendHex(static_cast(ilOffset)); + consoleWriter->AppendStr(" (token=0x"); + consoleWriter->AppendHex(static_cast(token)); + consoleWriter->AppendChar(')'); + } + else if (token != 0 && HasModuleName(fallbackModuleName)) + { + consoleWriter->AppendStr("token=0x"); + consoleWriter->AppendHex(static_cast(token)); + consoleWriter->AppendStr(" + 0x"); + consoleWriter->AppendHex(static_cast(ilOffset)); + } + else + { + consoleWriter->AppendStr("0x"); + consoleWriter->AppendHex(ip); + if (HasModuleName(fallbackModuleName)) + { + consoleWriter->AppendStr(" ("); + consoleWriter->AppendStr(GetFilename(fallbackModuleName)); + consoleWriter->AppendStr(" + 0x"); + consoleWriter->AppendHex(static_cast(nativeOffset)); + consoleWriter->AppendChar(')'); + } + } + consoleWriter->EndLine(); +} + +void +CrashReportHelpers::WriteStackOverflowFrameToJson( + SignalSafeJsonWriter* writer, + const StackOverflowTraceFrame& frame, + bool includeRepeatMetadata) +{ + if (writer == nullptr) + { + return; + } + + writer->OpenObject(); + writer->WriteString("method_name", frame.methodName); + writer->WriteString("is_managed", "true"); + if (includeRepeatMetadata) + { + writer->WriteDecimalAsString("stack_overflow_repeat_count", frame.repeatCount); + writer->WriteDecimalAsString("stack_overflow_repeat_sequence_length", frame.repeatSequenceLength); + } + writer->CloseObject(); // frame +} + +void +CrashReportHelpers::WriteStackOverflowFrameToConsole( + SignalSafeConsoleWriter* consoleWriter, + uint32_t frameIndex, + const StackOverflowTraceFrame& frame) +{ + if (consoleWriter == nullptr) + { + return; + } + + consoleWriter->AppendStr(" #"); + if (frameIndex < 10) + { + consoleWriter->AppendChar('0'); + } + consoleWriter->AppendDecimal(static_cast(frameIndex)); + consoleWriter->AppendChar(' '); + consoleWriter->AppendStr(frame.methodName); + consoleWriter->EndLine(); +} + +void +CrashReportHelpers::BeginJsonThreadBlock( + SignalSafeJsonWriter* jsonWriter, + uint64_t osThreadId, + bool isManagedThread, + bool isCrashThread, + const char* exceptionType, + uint32_t exceptionHResult) +{ + if (jsonWriter == nullptr) + { + return; + } + + jsonWriter->OpenObject(); + jsonWriter->WriteString("is_managed", isManagedThread ? "true" : "false"); + jsonWriter->WriteString("crashed", isCrashThread ? "true" : "false"); + jsonWriter->WriteHexAsString("native_thread_id", osThreadId); + + if (exceptionType != nullptr && exceptionType[0] != '\0') + { + jsonWriter->WriteString("managed_exception_type", exceptionType); + jsonWriter->WriteHexAsString("managed_exception_hresult", exceptionHResult); + } +} + +void +CrashReportHelpers::BeginJsonStackFrames( + SignalSafeJsonWriter* jsonWriter, + bool writeCrashSiteFrame, + void* signalContext) +{ + if (jsonWriter == nullptr) + { + return; + } + + jsonWriter->OpenArray("stack_frames"); + if (writeCrashSiteFrame) + { + WriteCrashSiteFrameToJson(jsonWriter, signalContext); + } +} + +void +CrashReportHelpers::EndJsonStackFrames( + SignalSafeJsonWriter* jsonWriter) +{ + if (jsonWriter == nullptr) + { + return; + } + + jsonWriter->CloseArray(); // stack_frames +} + +void +CrashReportHelpers::EndJsonThreadBlock( + SignalSafeJsonWriter* jsonWriter) +{ + if (jsonWriter == nullptr) + { + return; + } + + jsonWriter->CloseObject(); // thread +} + +void +CrashReportHelpers::BeginConsoleThreadBlock( + SignalSafeConsoleWriter* consoleWriter, + uint64_t osThreadId, + bool isCrashThread) +{ + if (consoleWriter == nullptr) + { + return; + } + + consoleWriter->WriteBlank(); + consoleWriter->AppendStr("--- thread 0x"); + consoleWriter->AppendHex(osThreadId); + if (isCrashThread) + { + consoleWriter->AppendStr(" (crashed)"); + } + consoleWriter->AppendStr(" ---"); + consoleWriter->EndLine(); +} + +void +CrashReportHelpers::EndConsoleThreadBlock( + SignalSafeConsoleWriter* consoleWriter, + uint32_t frameCount, + uint32_t droppedCount) +{ + if (consoleWriter == nullptr) + { + return; + } + + if (frameCount == 0) + { + consoleWriter->WriteLine(" (no managed frames)"); + } + else if (droppedCount != 0) + { + consoleWriter->AppendStr(" ... +"); + consoleWriter->AppendDecimal(static_cast(droppedCount)); + consoleWriter->AppendStr(" more frames"); + consoleWriter->EndLine(); + } +} + +void +CrashReportHelpers::WriteFrame( uint64_t ip, uint64_t stackPointer, const char* methodName, const char* className, const char* moduleName, + const void* moduleHandle, + uint32_t moduleTimestamp, + uint32_t moduleSize, + const GUID* moduleGuid, uint32_t nativeOffset, uint32_t token, uint32_t ilOffset, + void* ctx) +{ + FrameContext* frameContext = reinterpret_cast(ctx); + if (frameContext == nullptr) + { + return; + } + + WriteFrameToReport( + frameContext->jsonWriter, + frameContext->consoleWriter, + frameContext->moduleInfoCallback, + frameContext->moduleTable, + frameContext->formatter, + frameContext->methodNameBuffer, + frameContext->methodNameBufferSize, + frameContext->currentThreadFrameCount, + frameContext->currentThreadDroppedCount, + frameContext->frameLimitPerThread, + ip, + stackPointer, + methodName, + className, + moduleName, + moduleHandle, + moduleTimestamp, + moduleSize, + moduleGuid, + nativeOffset, + token, + ilOffset); +} + +void +CrashReportHelpers::WriteFrameToReport( + SignalSafeJsonWriter* jsonWriter, + SignalSafeConsoleWriter* consoleWriter, + InProcCrashReportModuleInfoCallback moduleInfoCallback, + ModuleTable* moduleTable, + SignalSafeFormatter* formatter, + char* methodNameBuffer, + size_t methodNameBufferSize, + uint32_t* currentThreadFrameCount, + uint32_t* currentThreadDroppedCount, + uint32_t frameLimitPerThread, + uint64_t ip, + uint64_t stackPointer, + const char* methodName, + const char* className, + const char* moduleName, + const void* moduleHandle, uint32_t moduleTimestamp, uint32_t moduleSize, - const char* moduleGuid) + const GUID* moduleGuid, + uint32_t nativeOffset, + uint32_t token, + uint32_t ilOffset) { - CrashReportHelpers::JsonFrameCallback(ip, stackPointer, methodName, className, moduleName, nativeOffset, token, ilOffset, moduleTimestamp, moduleSize, moduleGuid, m_writer); + uint32_t frameIndex = currentThreadFrameCount != nullptr + ? *currentThreadFrameCount + : 0; + + // Always feed the JSON sink: the file output is the authoritative, + // post-mortem data store and the cap is a compact-log triage knob. + WriteFrameToJson(jsonWriter, + formatter, + methodNameBuffer, + methodNameBufferSize, + ip, stackPointer, methodName, className, moduleName, + moduleTimestamp, moduleSize, moduleGuid, nativeOffset, token, ilOffset); + + bool consoleCapped = frameLimitPerThread != 0 && + frameIndex >= frameLimitPerThread; + if (!consoleCapped) + { + int moduleIndex = -1; + if (moduleTable != nullptr && moduleInfoCallback != nullptr && moduleHandle != nullptr) + { + const char* resolvedModuleName = nullptr; + GUID resolvedModuleGuid; + if (moduleInfoCallback(moduleHandle, &resolvedModuleName, &resolvedModuleGuid) && + HasModuleName(resolvedModuleName)) + { + moduleIndex = moduleTable->GetOrAddIndex(moduleHandle); + } + } + WriteFrameToConsole(consoleWriter, + methodNameBuffer, + methodNameBufferSize, + frameIndex, moduleIndex, ip, methodName, className, moduleName, + nativeOffset, token, ilOffset); + } + else if (currentThreadDroppedCount != nullptr) + { + (*currentThreadDroppedCount)++; + } + + if (currentThreadFrameCount != nullptr) + { + (*currentThreadFrameCount)++; + } } void -ThreadEnumerationContext::FrameCallback( +ThreadEnumerationContext::OnFrame( uint64_t ip, uint64_t stackPointer, const char* methodName, const char* className, const char* moduleName, + const void* moduleHandle, + uint32_t moduleTimestamp, + uint32_t moduleSize, + const GUID* moduleGuid, uint32_t nativeOffset, uint32_t token, - uint32_t ilOffset, + uint32_t ilOffset) +{ + CrashReportHelpers::WriteFrameToReport( + m_jsonWriter, + m_consoleWriter, + m_moduleInfoCallback, + m_moduleTable, + m_formatter, + m_methodNameScratch, + m_methodNameScratchSize, + &m_currentThreadFrameCount, + &m_currentThreadDroppedCount, + m_frameLimitPerThread, + ip, + stackPointer, + methodName, + className, + moduleName, + moduleHandle, + moduleTimestamp, + moduleSize, + moduleGuid, + nativeOffset, + token, + ilOffset); +} + +void +ThreadEnumerationContext::FrameCallback( + uint64_t ip, + uint64_t stackPointer, + const char* methodName, + const char* className, + const char* moduleName, + const void* moduleHandle, uint32_t moduleTimestamp, uint32_t moduleSize, - const char* moduleGuid, + const GUID* moduleGuid, + uint32_t nativeOffset, + uint32_t token, + uint32_t ilOffset, void* ctx) { if (ctx == nullptr) { return; } - reinterpret_cast(ctx)->OnFrame(ip, stackPointer, methodName, className, moduleName, nativeOffset, token, ilOffset, moduleTimestamp, moduleSize, moduleGuid); + reinterpret_cast(ctx)->OnFrame( + ip, + stackPointer, + methodName, + className, + moduleName, + moduleHandle, + moduleTimestamp, + moduleSize, + moduleGuid, + nativeOffset, + token, + ilOffset); +} + +void +ThreadEnumerationContext::EndCurrentConsoleThreadBlock() +{ + if (m_threadCount == 0) + { + return; + } + + CrashReportHelpers::EndConsoleThreadBlock(m_consoleWriter, + m_currentThreadFrameCount, m_currentThreadDroppedCount); +} + +void +ThreadEnumerationContext::EndCurrentJsonThreadBlock() +{ + if (m_threadCount == 0) + { + return; + } + + CrashReportHelpers::EndJsonStackFrames(m_jsonWriter); + CrashReportHelpers::EndJsonThreadBlock(m_jsonWriter); + + (void)m_jsonWriter->Flush(); } void @@ -1000,10 +1957,8 @@ ThreadEnumerationContext::OnThread( { if (m_threadCount > 0) { - m_writer->CloseArray(); // stack_frames - m_writer->CloseObject(); // thread - - (void)m_writer->Flush(); + EndCurrentConsoleThreadBlock(); + EndCurrentJsonThreadBlock(); } if (isCrashThread) @@ -1011,27 +1966,32 @@ ThreadEnumerationContext::OnThread( m_sawCrashThread = true; } m_threadCount++; + m_currentThreadFrameCount = 0; + m_currentThreadDroppedCount = 0; - m_writer->OpenObject(); - m_writer->WriteString("is_managed", "true"); - m_writer->WriteString("crashed", isCrashThread ? "true" : "false"); - m_writer->WriteHexAsString("native_thread_id", osThreadId); - - if (exceptionType != nullptr && exceptionType[0] != '\0') - { - m_writer->WriteString("managed_exception_type", exceptionType); - m_writer->WriteHexAsString("managed_exception_hresult", exceptionHResult); - } + CrashReportHelpers::BeginJsonThreadBlock(m_jsonWriter, + osThreadId, /*isManagedThread*/ true, isCrashThread, exceptionType, exceptionHResult); if (isCrashThread) { - CrashReportHelpers::WriteRegistersToJson(m_writer, m_signalContext); + CrashReportHelpers::WriteRegistersToJson(m_jsonWriter, m_signalContext); } - m_writer->OpenArray("stack_frames"); - if (isCrashThread) + CrashReportHelpers::BeginJsonStackFrames(m_jsonWriter, isCrashThread, m_signalContext); + + if (m_consoleWriter != nullptr) { - CrashReportHelpers::WriteCrashSiteFrameToJson(m_writer, m_signalContext); + CrashReportHelpers::BeginConsoleThreadBlock(m_consoleWriter, osThreadId, isCrashThread); + + if (exceptionType != nullptr && exceptionType[0] != '\0') + { + m_consoleWriter->AppendStr(" managed exception: "); + m_consoleWriter->AppendStr(exceptionType); + m_consoleWriter->AppendStr(" (0x"); + m_consoleWriter->AppendHex(static_cast(exceptionHResult)); + m_consoleWriter->AppendChar(')'); + m_consoleWriter->EndLine(); + } } } @@ -1052,28 +2012,22 @@ ThreadEnumerationContext::ThreadCallback( void ThreadEnumerationContext::EnumerateThreads( - InProcCrashReportEnumerateThreadsCallback callback, - uint64_t crashingTid) + InProcCrashReportEnumerateThreadsCallback callback) { if (callback == nullptr) { return; } - callback(crashingTid, &ThreadCallback, &FrameCallback, this); + callback(m_crashingTid, &ThreadCallback, &FrameCallback, this); if (m_threadCount == 0) { return; } - // Close the last thread's stack_frames + thread objects opened by OnThread. - m_writer->CloseArray(); // stack_frames - m_writer->CloseObject(); // thread - - // Flush the final thread so it reaches the crash report file even if any - // later work (e.g. synthesizing a crash thread fallback) hangs or faults. - (void)m_writer->Flush(); + EndCurrentConsoleThreadBlock(); + EndCurrentJsonThreadBlock(); } void @@ -1083,19 +2037,309 @@ InProcCrashReporter::EmitSynthesizedCrashThread( { uint64_t crashingTid = static_cast(minipal_get_current_thread_id()); - m_jsonWriter.OpenObject(); - m_jsonWriter.WriteString("is_managed", - m_isManagedThreadCallback != nullptr && m_isManagedThreadCallback() ? "true" : "false"); - m_jsonWriter.WriteString("crashed", "true"); - m_jsonWriter.WriteHexAsString("native_thread_id", crashingTid); + bool isManagedThread = m_isManagedThreadCallback != nullptr && m_isManagedThreadCallback(); + CrashReportHelpers::BeginJsonThreadBlock(&m_jsonWriter, + crashingTid, isManagedThread, /*isCrashThread*/ true, nullptr, 0); CrashReportHelpers::WriteRegistersToJson(&m_jsonWriter, context); - m_jsonWriter.OpenArray("stack_frames"); - CrashReportHelpers::WriteCrashSiteFrameToJson(&m_jsonWriter, context); + CrashReportHelpers::BeginJsonStackFrames(&m_jsonWriter, /*writeCrashSiteFrame*/ true, context); + + CrashReportHelpers::BeginConsoleThreadBlock(&m_consoleWriter, crashingTid, /*isCrashThread*/ true); + + uint32_t synthesizedFrameCount = 0; + uint32_t synthesizedDroppedCount = 0; if (walkStack && m_walkStackCallback != nullptr) { - m_walkStackCallback(&CrashReportHelpers::JsonFrameCallback, &m_jsonWriter); + CrashReportHelpers::FrameContext frameContext = + { + &m_jsonWriter, + &m_consoleWriter, + m_moduleInfoCallback, + &m_moduleTable, + &m_formatter, + &synthesizedFrameCount, + &synthesizedDroppedCount, + m_frameLimitPerThread, + m_stringScratch, + sizeof(m_stringScratch), + }; + m_walkStackCallback(&CrashReportHelpers::WriteFrame, &frameContext); + } + CrashReportHelpers::EndConsoleThreadBlock(&m_consoleWriter, + synthesizedFrameCount, synthesizedDroppedCount); + + CrashReportHelpers::EndJsonStackFrames(&m_jsonWriter); + CrashReportHelpers::EndJsonThreadBlock(&m_jsonWriter); +} + +void +InProcCrashReporter::EmitStackOverflowCrashThread() +{ + StackOverflowTraceSnapshot& trace = m_stackOverflowTrace; + bool stackOverflowTraceAvailable = InterlockedCompareExchange(&trace.available, 0, 0) != 0; + uint64_t crashingTid = stackOverflowTraceAvailable && trace.crashingTid != 0 + ? trace.crashingTid + : static_cast(minipal_get_current_thread_id()); + + CrashReportHelpers::BeginJsonThreadBlock(&m_jsonWriter, + crashingTid, + /*isManagedThread*/ true, + /*isCrashThread*/ true, + CRASHREPORT_STACK_OVERFLOW_EXCEPTION_TYPE, + CRASHREPORT_COR_E_STACKOVERFLOW); + if (stackOverflowTraceAvailable) + { + m_jsonWriter.WriteDecimalAsString("stack_overflow_total_frames", trace.totalFrameCount); + if (trace.truncatedFrameCount != 0) + { + m_jsonWriter.WriteDecimalAsString("stack_overflow_trace_truncated_frames", trace.truncatedFrameCount); + } + } + else + { + m_jsonWriter.WriteString("stack_frames_unavailable_reason", CRASHREPORT_STACK_OVERFLOW_TRACE_UNAVAILABLE_REASON); + } + + CrashReportHelpers::BeginJsonStackFrames(&m_jsonWriter, /*writeCrashSiteFrame*/ false, nullptr); + if (stackOverflowTraceAvailable) + { + for (uint32_t i = 0; i < trace.frameCount;) + { + StackOverflowTraceFrame& frame = trace.frames[i]; + uint32_t repeatSequenceLength = frame.repeatSequenceLength; + bool isRepeatSequence = frame.repeatCount > 1 && repeatSequenceLength != 0; + CrashReportHelpers::WriteStackOverflowFrameToJson( + &m_jsonWriter, frame, isRepeatSequence); + ++i; + + if (!isRepeatSequence) + { + continue; + } + + uint32_t sequenceEnd = i + repeatSequenceLength - 1; + if (sequenceEnd > trace.frameCount) + { + sequenceEnd = trace.frameCount; + } + + for (; i < sequenceEnd; ++i) + { + CrashReportHelpers::WriteStackOverflowFrameToJson( + &m_jsonWriter, trace.frames[i], false); + } + } + } + CrashReportHelpers::EndJsonStackFrames(&m_jsonWriter); + CrashReportHelpers::EndJsonThreadBlock(&m_jsonWriter); + + CrashReportHelpers::BeginConsoleThreadBlock(&m_consoleWriter, crashingTid, /*isCrashThread*/ true); + m_consoleWriter.AppendStr(" managed exception: "); + m_consoleWriter.AppendStr(CRASHREPORT_STACK_OVERFLOW_EXCEPTION_TYPE); + m_consoleWriter.AppendStr(" (0x"); + m_consoleWriter.AppendHex(static_cast(CRASHREPORT_COR_E_STACKOVERFLOW)); + m_consoleWriter.AppendChar(')'); + m_consoleWriter.EndLine(); + + if (!stackOverflowTraceAvailable) + { + m_consoleWriter.WriteLine(" stack overflow trace unavailable"); + CrashReportHelpers::EndConsoleThreadBlock(&m_consoleWriter, 0, 0); + return; + } + + m_consoleWriter.AppendStr(" stack overflow frames: "); + m_consoleWriter.AppendDecimal(static_cast(trace.totalFrameCount)); + m_consoleWriter.EndLine(); + + uint32_t consoleFrameCount = 0; + uint32_t consoleDroppedCount = trace.truncatedFrameCount; + for (uint32_t i = 0; i < trace.frameCount;) + { + StackOverflowTraceFrame& frame = trace.frames[i]; + uint32_t repeatSequenceLength = frame.repeatSequenceLength; + if (frame.repeatCount > 1 && repeatSequenceLength != 0) + { + uint32_t sequenceEnd = i + repeatSequenceLength; + if (sequenceEnd > trace.frameCount) + { + sequenceEnd = trace.frameCount; + } + + if (m_frameLimitPerThread != 0 && consoleFrameCount >= m_frameLimitPerThread) + { + consoleDroppedCount += sequenceEnd - i; + i = sequenceEnd; + continue; + } + + m_consoleWriter.AppendStr(" repeated "); + m_consoleWriter.AppendDecimal(static_cast(frame.repeatCount)); + m_consoleWriter.AppendStr(" times:"); + m_consoleWriter.EndLine(); + + for (; i < sequenceEnd; ++i) + { + if (m_frameLimitPerThread != 0 && consoleFrameCount >= m_frameLimitPerThread) + { + consoleDroppedCount++; + continue; + } + + CrashReportHelpers::WriteStackOverflowFrameToConsole( + &m_consoleWriter, consoleFrameCount, trace.frames[i]); + consoleFrameCount++; + } + + continue; + } + + if (m_frameLimitPerThread != 0 && consoleFrameCount >= m_frameLimitPerThread) + { + consoleDroppedCount++; + } + else + { + CrashReportHelpers::WriteStackOverflowFrameToConsole(&m_consoleWriter, consoleFrameCount, frame); + consoleFrameCount++; + } + ++i; + } + + CrashReportHelpers::EndConsoleThreadBlock(&m_consoleWriter, + consoleFrameCount, consoleDroppedCount); +} + +// --- InProcCrashReporter: console report lifecycle ------------------------- + +void +InProcCrashReporter::BeginConsoleReport(int signal) +{ + m_consoleWriter.WriteSeparator(); + m_consoleWriter.AppendStr(".NET Crash Report v"); + m_consoleWriter.AppendStr(CRASHREPORT_PROTOCOL_VERSION); + m_consoleWriter.EndLine(); + + CrashReportHelpers::GetVersionString(m_stringScratch, sizeof(m_stringScratch)); + if (m_stringScratch[0] != '\0') + { + m_consoleWriter.WriteKeyValueStr("Build", m_stringScratch); + } + + m_consoleWriter.WriteKeyValueStr("ABI", CRASHREPORT_ARCHITECTURE_NAME); + + if (m_processName[0] != '\0') + { + m_consoleWriter.WriteKeyValueStr("Cmdline", m_processName); + } + + m_consoleWriter.WriteKeyValueDecimal("pid", static_cast(GetCurrentProcessId())); + + m_consoleWriter.AppendStr("signal "); + m_consoleWriter.AppendSignedDecimal(signal); + m_consoleWriter.AppendStr(" ("); + m_consoleWriter.AppendStr(GetSignalNameAscii(signal)); + m_consoleWriter.AppendChar(')'); + m_consoleWriter.EndLine(); +} + +void +InProcCrashReporter::EndConsoleReport() +{ + if (m_moduleTable.Count() != 0) + { + m_consoleWriter.WriteBlank(); + m_consoleWriter.WriteLine("modules:"); + for (int i = 0; i < m_moduleTable.Count(); ++i) + { + m_consoleWriter.AppendStr(" ["); + m_consoleWriter.AppendDecimal(static_cast(i)); + m_consoleWriter.AppendStr("] "); + const char* moduleName = nullptr; + GUID moduleGuid; + if (m_moduleInfoCallback != nullptr && + m_moduleInfoCallback(m_moduleTable.ModuleHandle(i), &moduleName, &moduleGuid) && + HasModuleName(moduleName)) + { + m_consoleWriter.AppendStr(CrashReportHelpers::GetFilename(moduleName)); + m_consoleWriter.AppendChar(' '); + m_consoleWriter.AppendStr(m_formatter.FormatGuid(moduleGuid)); + } + else + { + m_consoleWriter.AppendStr(""); + } + m_consoleWriter.EndLine(); + } + } + + m_consoleWriter.WriteSeparator(); +} + +// --- InProcCrashReporter: JSON report lifecycle ---------------------------- + +void +InProcCrashReporter::BeginJsonReport() +{ + m_jsonWriter.OpenObject(); + m_jsonWriter.OpenObject("payload"); + m_jsonWriter.WriteString("protocol_version", CRASHREPORT_PROTOCOL_VERSION); + + m_jsonWriter.OpenObject("configuration"); + m_jsonWriter.WriteString("architecture", CRASHREPORT_ARCHITECTURE_NAME); + CrashReportHelpers::GetVersionString(m_stringScratch, sizeof(m_stringScratch)); + m_jsonWriter.WriteString("version", m_stringScratch); + m_jsonWriter.CloseObject(); // configuration + + if (m_processName[0] != '\0') + { + m_jsonWriter.WriteString("process_name", m_processName); + } + + m_jsonWriter.WriteDecimalAsString("pid", static_cast(GetCurrentProcessId())); +} + +void +InProcCrashReporter::EndJsonReport( + int signal, + bool jsonEnabled, + int fd) +{ + m_jsonWriter.CloseObject(); // payload + + m_jsonWriter.OpenObject("parameters"); + m_jsonWriter.WriteSignedDecimalAsString("signal", static_cast(signal)); +#if defined(TARGET_IOS) || defined(TARGET_TVOS) || defined(TARGET_MACCATALYST) + if (m_osVersion[0] != '\0') + { + m_jsonWriter.WriteString("OSVersion", m_osVersion); + } + if (m_systemModel[0] != '\0') + { + m_jsonWriter.WriteString("SystemModel", m_systemModel); + } + m_jsonWriter.WriteString("SystemManufacturer", "apple"); +#endif + m_jsonWriter.CloseObject(); // parameters + + m_jsonWriter.CloseObject(); // root + + if (jsonEnabled) + { + bool finishSucceeded = m_jsonWriter.Finish(); + bool writeFailed = m_outputContext.WriteFailed(); + if (!CrashReportHelpers::WriteToFile(fd, "\n", 1)) + { + writeFailed = true; + } + + if (close(fd) != 0 || !finishSucceeded || writeFailed) + { + unlink(m_reportFilePath); + } + } + else + { + (void)m_jsonWriter.Finish(); } - m_jsonWriter.CloseArray(); // stack_frames - m_jsonWriter.CloseObject(); // thread } diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.h b/src/coreclr/debug/crashreport/inproccrashreporter.h index 5018f3b0d10793..71e9148bec8a4d 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.h +++ b/src/coreclr/debug/crashreport/inproccrashreporter.h @@ -9,20 +9,28 @@ #pragma once #include +#include #include -#include "signalsafejsonwriter.h" +#include // Scratch-buffer sizes used throughout the in-proc crash reporter: // - 1024 (matching createdump's MAX_LONGPATH) for paths (report paths and // expanded dump templates), so DOTNET_DbgMiniDumpName values that work // with createdump also work here. // - 256 for identifiers (process name, type/class/exception names). -// - 32 for a single hex-or-decimal integer formatted as a C string -// (addresses, thread IDs, hresults). static constexpr size_t CRASHREPORT_PATH_BUFFER_SIZE = 1024; static constexpr size_t CRASHREPORT_STRING_BUFFER_SIZE = 256; -static constexpr size_t CRASHREPORT_NUMBER_BUFFER_SIZE = 32; + +#if defined(__ANDROID__) +static const char CRASHREPORT_LOG_TAG[] = "DOTNET_CRASH"; +#endif + +enum class InProcCrashReportCrashKind : uint32_t +{ + Unknown = 0, + StackOverflow = 1, +}; using InProcCrashReportIsManagedThreadCallback = bool (*)(); @@ -32,12 +40,13 @@ using InProcCrashReportFrameCallback = void (*)( const char* methodName, const char* className, const char* moduleName, + const void* moduleHandle, + uint32_t moduleTimestamp, + uint32_t moduleSize, + const GUID* moduleGuid, uint32_t nativeOffset, uint32_t token, uint32_t ilOffset, - uint32_t moduleTimestamp, - uint32_t moduleSize, - const char* moduleGuid, void* ctx); using InProcCrashReportWalkStackCallback = void (*)( @@ -57,53 +66,41 @@ using InProcCrashReportEnumerateThreadsCallback = void (*)( InProcCrashReportFrameCallback frameCallback, void* ctx); +using InProcCrashReportModuleInfoCallback = bool (*)( + const void* moduleHandle, + const char** moduleName, + GUID* moduleGuid); + struct InProcCrashReporterSettings { const char* reportPath; InProcCrashReportIsManagedThreadCallback isManagedThreadCallback; InProcCrashReportWalkStackCallback walkStackCallback; InProcCrashReportEnumerateThreadsCallback enumerateThreadsCallback; -}; - -class InProcCrashReporter -{ -public: - static InProcCrashReporter& GetInstance(); - - // Capture configuration and the crash-report template path. Must be called - // before the PAL enables signal-handler dispatch to CreateReport. - void Initialize(const InProcCrashReporterSettings& settings); - - void CreateReport( - int signal, - siginfo_t* siginfo, - void* context); - -private: - InProcCrashReporter() = default; - InProcCrashReporter(const InProcCrashReporter&) = delete; - InProcCrashReporter& operator=(const InProcCrashReporter&) = delete; - - void EmitSynthesizedCrashThread( - void* context, - bool walkStack); - - SignalSafeJsonWriter m_jsonWriter; - InProcCrashReportIsManagedThreadCallback m_isManagedThreadCallback = nullptr; - InProcCrashReportWalkStackCallback m_walkStackCallback = nullptr; - InProcCrashReportEnumerateThreadsCallback m_enumerateThreadsCallback = nullptr; - char m_reportPath[CRASHREPORT_PATH_BUFFER_SIZE] = {}; - char m_processName[CRASHREPORT_STRING_BUFFER_SIZE] = {}; - char m_hostName[CRASHREPORT_STRING_BUFFER_SIZE] = {}; -#ifdef __APPLE__ - char m_osVersion[CRASHREPORT_STRING_BUFFER_SIZE] = {}; - char m_systemModel[CRASHREPORT_STRING_BUFFER_SIZE] = {}; -#endif + InProcCrashReportModuleInfoCallback moduleInfoCallback; + uint32_t frameLimitPerThread; }; // Free-function entry point used by the runtime to wire the in-proc crash -// reporter into the PAL signal-handler path. Captures `settings` into the -// singleton and registers a signal-safe dispatcher with PAL via -// PAL_SetInProcCrashReportCallback. PAL has no direct dependency on the +// reporter into the PAL signal-handler path. Captures `settings` into an +// init-time allocated reporter and registers a signal-safe dispatcher with PAL +// via PAL_SetInProcCrashReportCallback. PAL has no direct dependency on the // reporter; the only coupling is through this registered callback. void InProcCrashReportInitialize(const InProcCrashReporterSettings& settings); + +// Emits initialization failures before crash-report storage exists. +void InProcCrashReportLogInitializationFailure(const char* message); + +// Records crash kind hints from VM fatal paths that later terminate through PAL +// as a generic signal (for example stack overflow -> SIGABRT). +void InProcCrashReportSetCrashKind(InProcCrashReportCrashKind crashKind); + +// Captures the compressed stack-overflow trace built by the runtime SO helper +// thread so the later in-proc crash reporter can include the same managed stack +// without trying to walk from the exhausted crashing stack. +void InProcCrashReportBeginStackOverflowTrace(uint64_t crashingTid, uint32_t totalFrameCount); +void InProcCrashReportAddStackOverflowTraceFrame( + const char* methodName, + uint32_t repeatCount, + uint32_t repeatSequenceLength); +void InProcCrashReportEndStackOverflowTrace(); diff --git a/src/coreclr/debug/crashreport/signalsafeconsolewriter.cpp b/src/coreclr/debug/crashreport/signalsafeconsolewriter.cpp new file mode 100644 index 00000000000000..8903fea10661e5 --- /dev/null +++ b/src/coreclr/debug/crashreport/signalsafeconsolewriter.cpp @@ -0,0 +1,142 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include "signalsafeconsolewriter.h" +#include "inproccrashreporter.h" + +#include +#include + +#if defined(__ANDROID__) +#include +#endif + +static const char CRASHREPORT_LINE_SEPARATOR[] = "*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***"; + +void +SignalSafeConsoleWriter::AppendStr(const char* s) +{ + if (s == nullptr || m_pos + 1 >= sizeof(m_buffer)) + { + return; + } + + size_t available = sizeof(m_buffer) - 1 - m_pos; + size_t toCopy = strnlen(s, available); + if (toCopy != 0) + { + memcpy(m_buffer + m_pos, s, toCopy); + m_pos += toCopy; + } +} + +void +SignalSafeConsoleWriter::AppendChar(char c) +{ + if (m_pos + 1 < sizeof(m_buffer)) + { + m_buffer[m_pos++] = c; + } +} + +void +SignalSafeConsoleWriter::AppendHex(uint64_t v) +{ + // Skip the leading "0x" so callers control whether the prefix appears + // (the compact format inserts it verbatim around the value). + const char* p = m_formatter.FormatHex(v); + if (p[0] == '0' && p[1] == 'x') + { + p += 2; + } + AppendStr(p); +} + +void +SignalSafeConsoleWriter::AppendDecimal(uint64_t v) +{ + AppendStr(m_formatter.FormatUnsignedDecimal(v)); +} + +void +SignalSafeConsoleWriter::AppendSignedDecimal(int64_t v) +{ + AppendStr(m_formatter.FormatSignedDecimal(v)); +} + +void +SignalSafeConsoleWriter::EndLine() +{ +#if defined(TARGET_IOS) || defined(TARGET_TVOS) || defined(TARGET_MACCATALYST) + // Apple mobile platforms write the report to stderr; explicitly + // newline-terminate each logical line so log readers split entries the + // same way logcat would. + if (m_pos + 1 < sizeof(m_buffer)) + { + m_buffer[m_pos++] = '\n'; + } + else + { + m_buffer[sizeof(m_buffer) - 2] = '\n'; + m_pos = sizeof(m_buffer) - 1; + } +#endif // TARGET_IOS || TARGET_TVOS || TARGET_MACCATALYST + Flush(); +} + +void +SignalSafeConsoleWriter::WriteLine(const char* s) +{ + AppendStr(s); + EndLine(); +} + +void +SignalSafeConsoleWriter::WriteKeyValueStr(const char* key, const char* value) +{ + AppendStr(key); + AppendStr(": "); + AppendStr(value != nullptr ? value : ""); + EndLine(); +} + +void +SignalSafeConsoleWriter::WriteKeyValueDecimal(const char* key, uint64_t value) +{ + AppendStr(key); + AppendStr(": "); + AppendDecimal(value); + EndLine(); +} + +void +SignalSafeConsoleWriter::WriteSeparator() +{ + WriteLine(CRASHREPORT_LINE_SEPARATOR); +} + +void +SignalSafeConsoleWriter::Flush() +{ + // Always null-terminate so the platform write APIs see a proper C string. + if (m_pos < sizeof(m_buffer)) + { + m_buffer[m_pos] = '\0'; + } + else + { + m_buffer[sizeof(m_buffer) - 1] = '\0'; + } + +#if defined(__ANDROID__) + // __android_log_write expects a tag + null-terminated message; it adds its + // own line discipline so we deliberately do not append '\n'. Each call + // becomes one logcat entry, which is what makes per-line filtering useful. + __android_log_write(ANDROID_LOG_FATAL, CRASHREPORT_LOG_TAG, m_buffer); +#else + minipal_log_write_error(m_buffer); +#endif + + m_pos = 0; + m_buffer[0] = '\0'; +} diff --git a/src/coreclr/debug/crashreport/signalsafeconsolewriter.h b/src/coreclr/debug/crashreport/signalsafeconsolewriter.h new file mode 100644 index 00000000000000..49da36950dcea6 --- /dev/null +++ b/src/coreclr/debug/crashreport/signalsafeconsolewriter.h @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Bounded, signal-safe line-oriented console writer. Paired with +// SignalSafeJsonWriter as the second crash-report output sink: +// SignalSafeJsonWriter streams JSON to a file callback (compact, no +// line concept); SignalSafeConsoleWriter emits one logical line at a +// time to the platform console (Android logcat under CRASHREPORT_LOG_TAG +// tag, stderr on Apple mobile platforms). All public members are async-signal-safe: no +// heap allocation, no stdio, no locale or variadic formatting. +// +// Design choices below are driven by the prescribed compact crash report +// log format (specified at the top of inproccrashreporter.cpp): +// +// * One Flush per logical line (triggered by EndLine() / WriteLine()) +// instead of stream-buffer-fill flushing. Each call becomes exactly one +// __android_log_write entry on Android, so the format's line-oriented +// "header / per-thread block / modules / footer" structure maps 1:1 +// to logcat entries that filter cleanly under the crash-report tag without +// cutting fields in half. On +// iOS, tvOS, and MacCatalyst each EndLine adds an explicit '\n' for the same reason. +// +// * Unique crash-report logcat tag (distinct from the runtime's general +// "DOTNET" tag) so consumers can isolate the crash report from +// an otherwise noisy logcat with a single per-tag filter. +// +// * Best-effort silent truncation on per-line buffer overflow (Append* +// helpers all guard with `m_pos + 1 < sizeof(m_buffer)`). 512 bytes +// leaves comfortable headroom over the longest line the format +// produces (a fully-qualified Class.Method line at roughly +// CRASHREPORT_STRING_BUFFER_SIZE + line decoration), so truncation is +// reserved for unforeseen overrun and never fails any other +// crash-report output. + +#pragma once + +#include +#include + +#include "signalsafeformatter.h" + +static constexpr size_t SIGNAL_SAFE_CONSOLE_BUFFER_SIZE = 512; + +class SignalSafeConsoleWriter +{ +public: + SignalSafeConsoleWriter() + : m_pos(0) + { + m_buffer[0] = '\0'; + } + + SignalSafeConsoleWriter(const SignalSafeConsoleWriter&) = delete; + SignalSafeConsoleWriter& operator=(const SignalSafeConsoleWriter&) = delete; + + void AppendStr(const char* s); + void AppendChar(char c); + void AppendHex(uint64_t v); + void AppendDecimal(uint64_t v); + void AppendSignedDecimal(int64_t v); + void EndLine(); + + // Convenience for the many fixed strings emitted during the report. + void WriteLine(const char* s); + // "key: value" line shortcut (no string-escaping; values are trusted CLR strings). + void WriteKeyValueStr(const char* key, const char* value); + void WriteKeyValueDecimal(const char* key, uint64_t value); + + void WriteSeparator(); + void WriteBlank() { WriteLine(""); } + +private: + void Flush(); + + SignalSafeFormatter m_formatter; + char m_buffer[SIGNAL_SAFE_CONSOLE_BUFFER_SIZE]; + size_t m_pos; +}; diff --git a/src/coreclr/debug/crashreport/signalsafeformatter.cpp b/src/coreclr/debug/crashreport/signalsafeformatter.cpp new file mode 100644 index 00000000000000..679276b252f650 --- /dev/null +++ b/src/coreclr/debug/crashreport/signalsafeformatter.cpp @@ -0,0 +1,193 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include "signalsafeformatter.h" + +const char* +SignalSafeFormatter::FormatHex(uint64_t value) +{ + FormatHex(m_hexBuffer, sizeof(m_hexBuffer), value); + return m_hexBuffer; +} + +const char* +SignalSafeFormatter::FormatUnsignedDecimal(uint64_t value) +{ + (void)FormatUnsignedDecimal(m_unsignedDecimalBuffer, sizeof(m_unsignedDecimalBuffer), value); + return m_unsignedDecimalBuffer; +} + +const char* +SignalSafeFormatter::FormatSignedDecimal(int64_t value) +{ + (void)FormatSignedDecimal(m_signedDecimalBuffer, sizeof(m_signedDecimalBuffer), value); + return m_signedDecimalBuffer; +} + +const char* +SignalSafeFormatter::FormatGuid(const GUID& guid) +{ + FormatGuid(m_guidBuffer, sizeof(m_guidBuffer), guid); + return m_guidBuffer; +} + +void +SignalSafeFormatter::FormatHex( + char* buffer, + size_t bufferSize, + uint64_t value) +{ + if (buffer == nullptr || bufferSize == 0) + { + return; + } + + size_t reverseLength = 0; + do + { + unsigned digit = static_cast(value & 0xf); + m_reverse[reverseLength++] = static_cast(digit < 10 ? ('0' + digit) : ('a' + digit - 10)); + value >>= 4; + } while (value != 0 && reverseLength < MAX_HEX_DIGITS_UINT64); + + if (bufferSize < HEX_PREFIX_LEN + reverseLength + NULL_TERMINATOR_LEN) + { + buffer[0] = '\0'; + return; + } + + buffer[0] = '0'; + buffer[1] = 'x'; + + size_t index = HEX_PREFIX_LEN; + while (reverseLength > 0) + { + buffer[index++] = m_reverse[--reverseLength]; + } + buffer[index] = '\0'; +} + +size_t +SignalSafeFormatter::FormatUnsignedDecimal( + char* buffer, + size_t bufferSize, + uint64_t value) +{ + if (buffer == nullptr || bufferSize == 0) + { + return 0; + } + + size_t reverseLength = 0; + do + { + m_reverse[reverseLength++] = static_cast('0' + (value % 10)); + value /= 10; + } while (value != 0 && reverseLength < sizeof(m_reverse)); + + if (bufferSize < reverseLength + NULL_TERMINATOR_LEN) + { + buffer[0] = '\0'; + return 0; + } + + size_t pos = 0; + while (reverseLength > 0) + { + buffer[pos++] = m_reverse[--reverseLength]; + } + buffer[pos] = '\0'; + return pos; +} + +size_t +SignalSafeFormatter::FormatSignedDecimal( + char* buffer, + size_t bufferSize, + int64_t value) +{ + if (buffer == nullptr || bufferSize == 0) + { + return 0; + } + + if (value >= 0) + { + return FormatUnsignedDecimal(buffer, bufferSize, static_cast(value)); + } + + if (bufferSize < SIGN_LEN + NULL_TERMINATOR_LEN) + { + buffer[0] = '\0'; + return 0; + } + + buffer[0] = '-'; + // Cast to unsigned first to handle INT64_MIN without signed overflow. + uint64_t absValue = static_cast(-(value + 1)) + 1; + size_t written = FormatUnsignedDecimal(buffer + 1, bufferSize - 1, absValue); + if (written == 0) + { + buffer[0] = '\0'; + return 0; + } + return written + 1; +} + +void +SignalSafeFormatter::FormatGuid( + char* buffer, + size_t bufferSize, + const GUID& guid) +{ + if (buffer == nullptr || bufferSize == 0) + { + return; + } + + if (bufferSize < MAX_GUID_BUFFER_SIZE) + { + buffer[0] = '\0'; + return; + } + + size_t pos = 0; + buffer[pos++] = '{'; + AppendFixedHex(buffer, &pos, guid.Data1, 8); + buffer[pos++] = '-'; + AppendFixedHex(buffer, &pos, guid.Data2, 4); + buffer[pos++] = '-'; + AppendFixedHex(buffer, &pos, guid.Data3, 4); + buffer[pos++] = '-'; + AppendFixedHex(buffer, &pos, guid.Data4[0], 2); + AppendFixedHex(buffer, &pos, guid.Data4[1], 2); + buffer[pos++] = '-'; + AppendFixedHex(buffer, &pos, guid.Data4[2], 2); + AppendFixedHex(buffer, &pos, guid.Data4[3], 2); + AppendFixedHex(buffer, &pos, guid.Data4[4], 2); + AppendFixedHex(buffer, &pos, guid.Data4[5], 2); + AppendFixedHex(buffer, &pos, guid.Data4[6], 2); + AppendFixedHex(buffer, &pos, guid.Data4[7], 2); + buffer[pos++] = '}'; + buffer[pos] = '\0'; +} + +char +SignalSafeFormatter::GetHexDigit(uint32_t value) +{ + value &= 0xf; + return static_cast(value < 10 ? ('0' + value) : ('a' + value - 10)); +} + +void +SignalSafeFormatter::AppendFixedHex( + char* buffer, + size_t* pos, + uint32_t value, + uint32_t digits) +{ + for (uint32_t i = digits; i != 0; --i) + { + buffer[(*pos)++] = GetHexDigit(value >> ((i - 1) * 4)); + } +} diff --git a/src/coreclr/debug/crashreport/signalsafeformatter.h b/src/coreclr/debug/crashreport/signalsafeformatter.h new file mode 100644 index 00000000000000..39542634147995 --- /dev/null +++ b/src/coreclr/debug/crashreport/signalsafeformatter.h @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Async-signal-safe format primitives shared across the +// signal-safe writer family (SignalSafeJsonWriter, SignalSafeConsoleWriter, +// and any other consumer that needs to render values without stdio, +// locale, or heap allocation). Bounded buffer-size constants document the +// minimum buffer required for each formatter. + +#pragma once + +#include +#include + +#include + +class SignalSafeFormatter +{ +public: + static constexpr size_t MAX_HEX_DIGITS_UINT64 = 16; + static constexpr size_t MAX_DECIMAL_DIGITS_UINT64 = 20; + static constexpr size_t HEX_PREFIX_LEN = 2; // "0x" + static constexpr size_t SIGN_LEN = 1; // '-' for signed decimals + static constexpr size_t NULL_TERMINATOR_LEN = 1; + + static constexpr size_t MAX_HEX_BUFFER_SIZE = HEX_PREFIX_LEN + MAX_HEX_DIGITS_UINT64 + NULL_TERMINATOR_LEN; + static constexpr size_t MAX_UNSIGNED_DECIMAL_BUFFER_SIZE = MAX_DECIMAL_DIGITS_UINT64 + NULL_TERMINATOR_LEN; + static constexpr size_t MAX_SIGNED_DECIMAL_BUFFER_SIZE = SIGN_LEN + MAX_DECIMAL_DIGITS_UINT64 + NULL_TERMINATOR_LEN; + static constexpr size_t MAX_GUID_BUFFER_SIZE = MINIPAL_GUID_BUFFER_LEN; + + SignalSafeFormatter() = default; + SignalSafeFormatter(const SignalSafeFormatter&) = delete; + SignalSafeFormatter& operator=(const SignalSafeFormatter&) = delete; + + const char* FormatHex(uint64_t value); + const char* FormatUnsignedDecimal(uint64_t value); + const char* FormatSignedDecimal(int64_t value); + const char* FormatGuid(const GUID& guid); + +private: + // Writes "0x"-prefixed hex (lowercase) of `value` into `buffer`. On + // success the buffer is null-terminated. If `buffer` is null, `bufferSize` + // is zero, or the buffer is too small to hold the formatted value, the + // buffer is left empty (or null-terminated at index 0 when possible). + void FormatHex(char* buffer, size_t bufferSize, uint64_t value); + + // Writes the unsigned-decimal representation of `value` into `buffer` and + // returns the number of bytes written (excluding the null terminator). + // Returns 0 on failure with the buffer null-terminated at index 0 when possible. + size_t FormatUnsignedDecimal(char* buffer, size_t bufferSize, uint64_t value); + + // Writes the signed-decimal representation of `value` into `buffer` and + // returns the number of bytes written (excluding the null terminator). + // Returns 0 on failure. Handles INT64_MIN without signed overflow. + size_t FormatSignedDecimal(char* buffer, size_t bufferSize, int64_t value); + + void FormatGuid(char* buffer, size_t bufferSize, const GUID& guid); + static char GetHexDigit(uint32_t value); + static void AppendFixedHex(char* buffer, size_t* pos, uint32_t value, uint32_t digits); + + char m_hexBuffer[MAX_HEX_BUFFER_SIZE]; + char m_unsignedDecimalBuffer[MAX_UNSIGNED_DECIMAL_BUFFER_SIZE]; + char m_signedDecimalBuffer[MAX_SIGNED_DECIMAL_BUFFER_SIZE]; + char m_guidBuffer[MAX_GUID_BUFFER_SIZE]; + char m_reverse[MAX_DECIMAL_DIGITS_UINT64]; +}; diff --git a/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp b/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp index 2cd858ab544564..2c1b9c4cc81174 100644 --- a/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp +++ b/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp @@ -257,125 +257,12 @@ SignalSafeJsonWriter::WriteEscapedString( AppendChar('"'); } -// Bounded, async-signal-safe integer-to-string formatters. They write into the -// caller-supplied buffer and never allocate or call into stdio/locale code. -// If the buffer is too small to hold the maximum-width output (per the -// MAX_*_BUFFER_SIZE constants on SignalSafeJsonWriter), they leave only a null -// terminator and return early. - -void -SignalSafeJsonWriter::FormatHexValue( - char* buffer, - size_t bufferSize, - uint64_t value) -{ - if (buffer == nullptr || bufferSize == 0) - { - return; - } - - char reverse[MAX_HEX_DIGITS_UINT64]; - size_t reverseLength = 0; - do - { - unsigned digit = static_cast(value & 0xf); - reverse[reverseLength++] = static_cast(digit < 10 ? ('0' + digit) : ('a' + digit - 10)); - value >>= 4; - } while (value != 0 && reverseLength < sizeof(reverse)); - - if (bufferSize < HEX_PREFIX_LEN + reverseLength + NULL_TERMINATOR_LEN) - { - buffer[0] = '\0'; - return; - } - - buffer[0] = '0'; - buffer[1] = 'x'; - - size_t index = HEX_PREFIX_LEN; - while (reverseLength > 0) - { - buffer[index++] = reverse[--reverseLength]; - } - buffer[index] = '\0'; -} - -size_t -SignalSafeJsonWriter::FormatUnsignedDecimal( - char* buffer, - size_t bufferSize, - uint64_t value) -{ - if (buffer == nullptr || bufferSize == 0) - { - return 0; - } - - char reverse[MAX_DECIMAL_DIGITS_UINT64]; - size_t reverseLength = 0; - do - { - reverse[reverseLength++] = static_cast('0' + (value % 10)); - value /= 10; - } while (value != 0 && reverseLength < sizeof(reverse)); - - if (bufferSize < reverseLength + NULL_TERMINATOR_LEN) - { - buffer[0] = '\0'; - return 0; - } - - size_t pos = 0; - while (reverseLength > 0) - { - buffer[pos++] = reverse[--reverseLength]; - } - buffer[pos] = '\0'; - return pos; -} - -size_t -SignalSafeJsonWriter::FormatSignedDecimal( - char* buffer, - size_t bufferSize, - int64_t value) -{ - if (buffer == nullptr || bufferSize == 0) - { - return 0; - } - - if (value >= 0) - { - return FormatUnsignedDecimal(buffer, bufferSize, static_cast(value)); - } - - if (bufferSize < SIGN_LEN + NULL_TERMINATOR_LEN) - { - buffer[0] = '\0'; - return 0; - } - - buffer[0] = '-'; - // Cast to unsigned first to handle INT64_MIN without signed overflow. - uint64_t absValue = static_cast(-(value + 1)) + 1; - size_t written = FormatUnsignedDecimal(buffer + 1, bufferSize - 1, absValue); - if (written == 0) - { - buffer[0] = '\0'; - return 0; - } - return written + 1; -} - bool SignalSafeJsonWriter::WriteHexAsString( const char* key, uint64_t value) { - char scratch[MAX_HEX_FORMAT_BUFFER_SIZE]; - FormatHexValue(scratch, sizeof(scratch), value); - return WriteString(key, scratch); + return WriteString(key, m_formatter.FormatHex(value)); } bool @@ -383,9 +270,7 @@ SignalSafeJsonWriter::WriteDecimalAsString( const char* key, uint64_t value) { - char scratch[MAX_UNSIGNED_DECIMAL_BUFFER_SIZE]; - (void)FormatUnsignedDecimal(scratch, sizeof(scratch), value); - return WriteString(key, scratch); + return WriteString(key, m_formatter.FormatUnsignedDecimal(value)); } bool @@ -393,7 +278,5 @@ SignalSafeJsonWriter::WriteSignedDecimalAsString( const char* key, int64_t value) { - char scratch[MAX_SIGNED_DECIMAL_BUFFER_SIZE]; - (void)FormatSignedDecimal(scratch, sizeof(scratch), value); - return WriteString(key, scratch); + return WriteString(key, m_formatter.FormatSignedDecimal(value)); } diff --git a/src/coreclr/debug/crashreport/signalsafejsonwriter.h b/src/coreclr/debug/crashreport/signalsafejsonwriter.h index 54eac5dbf6d30d..f39a7a78bfc01f 100644 --- a/src/coreclr/debug/crashreport/signalsafejsonwriter.h +++ b/src/coreclr/debug/crashreport/signalsafejsonwriter.h @@ -12,6 +12,8 @@ #include #include +#include "signalsafeformatter.h" + using SignalSafeJsonOutputCallback = bool (*)(const char* buffer, size_t len, void* ctx); static constexpr size_t SIGNAL_SAFE_JSON_BUFFER_SIZE = 4 * 1024; @@ -19,16 +21,6 @@ static constexpr size_t SIGNAL_SAFE_JSON_BUFFER_SIZE = 4 * 1024; class SignalSafeJsonWriter { public: - // Maximum digit counts and required buffer sizes for the static format helpers below. - static constexpr size_t MAX_HEX_DIGITS_UINT64 = 16; - static constexpr size_t MAX_DECIMAL_DIGITS_UINT64 = 20; - static constexpr size_t HEX_PREFIX_LEN = 2; // "0x" - static constexpr size_t SIGN_LEN = 1; // '-' for signed decimals - static constexpr size_t NULL_TERMINATOR_LEN = 1; - static constexpr size_t MAX_HEX_FORMAT_BUFFER_SIZE = HEX_PREFIX_LEN + MAX_HEX_DIGITS_UINT64 + NULL_TERMINATOR_LEN; - static constexpr size_t MAX_UNSIGNED_DECIMAL_BUFFER_SIZE = MAX_DECIMAL_DIGITS_UINT64 + NULL_TERMINATOR_LEN; - static constexpr size_t MAX_SIGNED_DECIMAL_BUFFER_SIZE = SIGN_LEN + MAX_DECIMAL_DIGITS_UINT64 + NULL_TERMINATOR_LEN; - SignalSafeJsonWriter() : m_pos(0), m_commaNeeded(false), @@ -55,13 +47,6 @@ class SignalSafeJsonWriter bool Finish(); bool Flush(); - // Async-signal-safe integer-to-string formatters used by the Write* members - // above and by the few non-writer call sites that need the raw text (e.g. - // dump-name pattern expansion). All are bounded and never allocate. - static void FormatHexValue(char* buffer, size_t bufferSize, uint64_t value); - static size_t FormatUnsignedDecimal(char* buffer, size_t bufferSize, uint64_t value); - static size_t FormatSignedDecimal(char* buffer, size_t bufferSize, int64_t value); - private: bool Append(const char* str, size_t len); bool AppendChar(char c); @@ -69,6 +54,7 @@ class SignalSafeJsonWriter void WriteSeparator(); void WriteEscapedString(const char* str); + SignalSafeFormatter m_formatter; char m_buffer[SIGNAL_SAFE_JSON_BUFFER_SIZE]; size_t m_pos; bool m_commaNeeded; diff --git a/src/coreclr/inc/clrconfigvalues.h b/src/coreclr/inc/clrconfigvalues.h index c9dd7c485c99c0..d37be482afd0c1 100644 --- a/src/coreclr/inc/clrconfigvalues.h +++ b/src/coreclr/inc/clrconfigvalues.h @@ -578,6 +578,7 @@ RETAIL_CONFIG_STRING_INFO(INTERNAL_DbgMiniDumpName, W("DbgMiniDumpName"), "Crash RETAIL_CONFIG_DWORD_INFO(INTERNAL_DbgMiniDumpType, W("DbgMiniDumpType"), 0, "Crash dump type: 1 normal, 2 withheap, 3 triage, 4 full") RETAIL_CONFIG_DWORD_INFO(INTERNAL_CreateDumpDiagnostics, W("CreateDumpDiagnostics"), 0, "Enable crash dump generation diagnostic logging") RETAIL_CONFIG_DWORD_INFO(INTERNAL_CrashReportBeforeSignalChaining, W("CrashReportBeforeSignalChaining"), 0, "Enable crash report generation before chaining to previous signal handler") +RETAIL_CONFIG_DWORD_INFO_EX(INTERNAL_CrashReportFrameLimitPerThread, W("CrashReportFrameLimitPerThread"), 32, "Maximum number of managed stack frames per thread to emit in the in-proc crash report's compact log; 0 disables the limit; remaining frames are summarized as '... +N more frames'", CLRConfig::LookupOptions::ParseIntegerAsBase10) /// /// R2R diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index 1670ec970d91ff..3ee9a3a5fbde49 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -10,6 +10,7 @@ #include "peassembly.h" #include #include +#include #ifdef FEATURE_INPROC_CRASHREPORT @@ -23,8 +24,179 @@ struct WalkContext void* userCtx; }; +struct CrashReportStackWalkerScratch +{ + char crashExceptionType[CRASHREPORT_STRING_BUFFER_SIZE]; + char className[CRASHREPORT_STRING_BUFFER_SIZE]; + GUID moduleGuid; + bool hasModuleGuid; +}; + +struct CrashReportStackWalkerState +{ + CrashReportStackWalkerScratch scratch; + WalkContext walkContext; +}; + +static CrashReportStackWalkerState* s_crashReportStackWalkerState = nullptr; + +static +bool +EnsureCrashReportStackWalkerState() +{ + if (VolatileLoad(&s_crashReportStackWalkerState) != nullptr) + { + return true; + } + + CrashReportStackWalkerState* state = new (std::nothrow) CrashReportStackWalkerState(); + if (state == nullptr) + { + return false; + } + + if (InterlockedCompareExchangeT(&s_crashReportStackWalkerState, state, nullptr) != nullptr) + { + delete state; + } + + return true; +} + +static +DWORD +GetCrashReportFrameLimitPerThread() +{ + DWORD frameLimitPerThread = CLRConfig::INTERNAL_CrashReportFrameLimitPerThread.defaultValue; + + CLRConfigNoCache frameLimitCfg = CLRConfigNoCache::Get("CrashReportFrameLimitPerThread", /*noprefix*/ false, &getenv); + if (frameLimitCfg.IsSet()) + { + DWORD configuredFrameLimitPerThread = 0; + if (frameLimitCfg.TryAsInteger(10, configuredFrameLimitPerThread)) + { + frameLimitPerThread = configuredFrameLimitPerThread; + } + } + + return frameLimitPerThread; +} + static void BuildTypeName(LPUTF8 buffer, size_t bufferSize, LPCUTF8 namespaceName, LPCUTF8 className); +static +void +CrashReportGetModuleDetails( + Module* pModule, + LPCUTF8* moduleName, + GUID* moduleGuid, + bool* hasModuleGuid, + uint32_t* moduleTimestamp, + uint32_t* moduleSize) +{ + CONTRACTL + { + NOTHROW; + GC_NOTRIGGER; + CANNOT_TAKE_LOCK; + MODE_ANY; + } + CONTRACTL_END; + + if (moduleName != nullptr) + { + *moduleName = nullptr; + } + if (hasModuleGuid != nullptr) + { + *hasModuleGuid = false; + } + if (moduleTimestamp != nullptr) + { + *moduleTimestamp = 0; + } + if (moduleSize != nullptr) + { + *moduleSize = 0; + } + + if (pModule == nullptr) + { + return; + } + + if (moduleName != nullptr) + { + Assembly* pAssembly = pModule->GetAssembly(); + if (pAssembly != nullptr) + { + *moduleName = pAssembly->GetSimpleName(); + } + } + + if (moduleTimestamp != nullptr || moduleSize != nullptr) + { + PEAssembly* pPEAssembly = pModule->GetPEAssembly(); + if (pPEAssembly != nullptr && pPEAssembly->HasLoadedPEImage()) + { + if (moduleTimestamp != nullptr) + { + *moduleTimestamp = pPEAssembly->GetLoadedLayout()->GetTimeDateStamp(); + } + if (moduleSize != nullptr) + { + *moduleSize = static_cast(pPEAssembly->GetLoadedLayout()->GetSize()); + } + } + } + + if (moduleGuid != nullptr) + { + IMDInternalImport* pImport = pModule->GetMDImport(); + if (pImport != nullptr && SUCCEEDED(pImport->GetScopeProps(nullptr, moduleGuid))) + { + if (hasModuleGuid != nullptr) + { + *hasModuleGuid = true; + } + } + } +} + +static +bool +CrashReportGetModuleInfo( + const void* moduleHandle, + const char** moduleName, + GUID* moduleGuid) +{ + CONTRACTL + { + NOTHROW; + GC_NOTRIGGER; + CANNOT_TAKE_LOCK; + MODE_ANY; + } + CONTRACTL_END; + + if (moduleName == nullptr || moduleGuid == nullptr || moduleHandle == nullptr) + { + return false; + } + + LPCUTF8 resolvedModuleName = nullptr; + bool hasModuleGuid = false; + Module* pModule = reinterpret_cast(const_cast(moduleHandle)); + CrashReportGetModuleDetails(pModule, &resolvedModuleName, moduleGuid, &hasModuleGuid, nullptr, nullptr); + if (resolvedModuleName == nullptr || resolvedModuleName[0] == '\0' || !hasModuleGuid) + { + return false; + } + + *moduleName = resolvedModuleName; + return true; +} + static StackWalkAction FrameCallbackAdapter( @@ -41,7 +213,8 @@ FrameCallbackAdapter( CONTRACTL_END; WalkContext* ctx = static_cast(pData); - if (ctx == nullptr) + CrashReportStackWalkerState* state = VolatileLoad(&s_crashReportStackWalkerState); + if (ctx == nullptr || state == nullptr) { return SWA_CONTINUE; } @@ -68,19 +241,11 @@ FrameCallbackAdapter( } } - char classNameBuf[CRASHREPORT_STRING_BUFFER_SIZE]; - BuildTypeName(classNameBuf, sizeof(classNameBuf), namespaceName, className); + CrashReportStackWalkerScratch& scratch = state->scratch; + scratch.className[0] = '\0'; + BuildTypeName(scratch.className, sizeof(scratch.className), namespaceName, className); - LPCUTF8 moduleName = nullptr; Module* pModule = pMD->GetModule(); - if (pModule != nullptr) - { - Assembly* pAssembly = pModule->GetAssembly(); - if (pAssembly != nullptr) - { - moduleName = pAssembly->GetSimpleName(); - } - } uint32_t nativeOffset = pCF->HasFaulted() ? 0 : pCF->GetRelOffset(); uint32_t ilOffset = 0; @@ -122,33 +287,33 @@ FrameCallbackAdapter( } } + LPCUTF8 moduleName = nullptr; uint32_t moduleTimestamp = 0; uint32_t moduleSize = 0; - char moduleGuid[MINIPAL_GUID_BUFFER_LEN]; - moduleGuid[0] = '\0'; - - if (pModule != nullptr) - { - PEAssembly* pPEAssembly = pModule->GetPEAssembly(); - if (pPEAssembly != nullptr && pPEAssembly->HasLoadedPEImage()) - { - moduleTimestamp = pPEAssembly->GetLoadedLayout()->GetTimeDateStamp(); - moduleSize = static_cast(pPEAssembly->GetLoadedLayout()->GetSize()); - } - - IMDInternalImport* pImport = pModule->GetMDImport(); - if (pImport != nullptr) - { - GUID mvid; - if (SUCCEEDED(pImport->GetScopeProps(nullptr, &mvid))) - { - minipal_guid_as_string(mvid, moduleGuid, MINIPAL_GUID_BUFFER_LEN); - } - } - } - - className = classNameBuf[0] == '\0' ? nullptr : classNameBuf; - ctx->callback(static_cast(ip), static_cast(stackPointer), methodName, className, moduleName, nativeOffset, static_cast(token), ilOffset, moduleTimestamp, moduleSize, moduleGuid, ctx->userCtx); + scratch.hasModuleGuid = false; + CrashReportGetModuleDetails( + pModule, + &moduleName, + &scratch.moduleGuid, + &scratch.hasModuleGuid, + &moduleTimestamp, + &moduleSize); + + className = scratch.className[0] == '\0' ? nullptr : scratch.className; + ctx->callback( + static_cast(ip), + static_cast(stackPointer), + methodName, + className, + moduleName, + pModule, + moduleTimestamp, + moduleSize, + scratch.hasModuleGuid ? &scratch.moduleGuid : nullptr, + nativeOffset, + static_cast(token), + ilOffset, + ctx->userCtx); return SWA_CONTINUE; } @@ -159,13 +324,15 @@ CrashReportWalkThread( InProcCrashReportFrameCallback frameCallback, void* ctx) { - if (pThread == nullptr || frameCallback == nullptr) + CrashReportStackWalkerState* state = VolatileLoad(&s_crashReportStackWalkerState); + if (pThread == nullptr || frameCallback == nullptr || state == nullptr) { return; } - WalkContext walkContext = { frameCallback, ctx }; - pThread->StackWalkFrames(FrameCallbackAdapter, &walkContext, + state->walkContext.callback = frameCallback; + state->walkContext.userCtx = ctx; + pThread->StackWalkFrames(FrameCallbackAdapter, &state->walkContext, QUICKUNWIND | FUNCTIONSONLY | ALLOW_ASYNC_STACK_WALK); } @@ -353,14 +520,20 @@ CrashReportEnumerateThreads( InProcCrashReportFrameCallback frameCallback, void* ctx) { + CrashReportStackWalkerState* state = VolatileLoad(&s_crashReportStackWalkerState); + if (state == nullptr) + { + return; + } + Thread* pCrashThread = GetThreadAsyncSafe(); + CrashReportStackWalkerScratch& scratch = state->scratch; // Capture the crashing thread's exception state BEFORE suspending the EE // so the throwable inspection runs in the thread's natural EE-live context, // outside the suspended window which exists for safe-point operations on // other threads. - char crashExceptionType[CRASHREPORT_STRING_BUFFER_SIZE]; - crashExceptionType[0] = '\0'; + scratch.crashExceptionType[0] = '\0'; uint32_t crashHresult = 0; bool crashHasException = false; bool isCrashingThread = pCrashThread != nullptr @@ -368,7 +541,10 @@ CrashReportEnumerateThreads( if (isCrashingThread) { crashHasException = CrashReportGetExceptionForThread( - pCrashThread, crashExceptionType, sizeof(crashExceptionType), &crashHresult); + pCrashThread, + scratch.crashExceptionType, + sizeof(scratch.crashExceptionType), + &crashHresult); } bool runtimeSuspended = CrashReportSuspendThreads(pCrashThread); @@ -378,7 +554,7 @@ CrashReportEnumerateThreads( if (isCrashingThread) { uint64_t crashOsId = static_cast(pCrashThread->GetOSThreadId()); - threadCallback(crashOsId, true, crashHasException ? crashExceptionType : "", crashHresult, ctx); + threadCallback(crashOsId, true, crashHasException ? scratch.crashExceptionType : "", crashHresult, ctx); CrashReportWalkThread(pCrashThread, frameCallback, ctx); } @@ -425,18 +601,22 @@ CrashReportConfigure() return; } - CLRConfigNoCache dmpNameCfg = CLRConfigNoCache::Get("DbgMiniDumpName", /*noprefix*/ false, &getenv); - const char* dumpName = dmpNameCfg.IsSet() ? dmpNameCfg.AsString() : nullptr; - if (dumpName == nullptr || dumpName[0] == '\0') + if (!EnsureCrashReportStackWalkerState()) { + InProcCrashReportLogInitializationFailure(".NET crash report disabled: failed to allocate stack walker storage"); return; } + CLRConfigNoCache dmpNameCfg = CLRConfigNoCache::Get("DbgMiniDumpName", /*noprefix*/ false, &getenv); + const char* dumpName = dmpNameCfg.IsSet() ? dmpNameCfg.AsString() : nullptr; + InProcCrashReporterSettings settings = {}; settings.reportPath = dumpName; settings.isManagedThreadCallback = CrashReportIsCurrentThreadManaged; settings.walkStackCallback = CrashReportWalkStack; settings.enumerateThreadsCallback = CrashReportEnumerateThreads; + settings.moduleInfoCallback = CrashReportGetModuleInfo; + settings.frameLimitPerThread = GetCrashReportFrameLimitPerThread(); // Initialize the reporter and register the PAL signal-path callback last // so PAL only observes the reporter after all VM callbacks are wired in. diff --git a/src/coreclr/vm/eepolicy.cpp b/src/coreclr/vm/eepolicy.cpp index 1462aeb827f681..4089af8a4a0475 100644 --- a/src/coreclr/vm/eepolicy.cpp +++ b/src/coreclr/vm/eepolicy.cpp @@ -16,6 +16,10 @@ #include "typestring.h" +#ifdef FEATURE_INPROC_CRASHREPORT +#include "inproccrashreporter.h" +#endif + #ifndef TARGET_UNIX #include "dwreport.h" #endif // !TARGET_UNIX @@ -197,14 +201,24 @@ class CallStackLogger return SWA_CONTINUE; } - void PrintFrame(int index, const WCHAR* pWordAt) + void PrintFrame( + int index, + const WCHAR* pWordAt, + uint32_t repeatCount = 0, + uint32_t repeatSequenceLength = 0) { WRAPPER_NO_CONTRACT; - SString str(pWordAt); - + SString frame; MethodDesc* pMD = m_frames[index]; - TypeString::AppendMethodInternal(str, pMD, TypeString::FormatNamespace|TypeString::FormatFullInst|TypeString::FormatSignature); + TypeString::AppendMethodInternal(frame, pMD, TypeString::FormatNamespace|TypeString::FormatFullInst|TypeString::FormatSignature); + +#ifdef FEATURE_INPROC_CRASHREPORT + InProcCrashReportAddStackOverflowTraceFrame(frame.GetUTF8(), repeatCount, repeatSequenceLength); +#endif // FEATURE_INPROC_CRASHREPORT + + SString str(pWordAt); + str.Append(frame); str.Append(W("\n")); PrintToStdErrW(str.GetUnicode()); @@ -228,7 +242,7 @@ class CallStackLogger return logger->LogCallstackForLogCallbackWorker(pCF); } - void PrintStackTrace(const WCHAR* pWordAt) + void PrintStackTrace(const WCHAR* pWordAt, uint64_t crashingTid) { WRAPPER_NO_CONTRACT; @@ -302,6 +316,10 @@ class CallStackLogger largestCommonLength = 0; } +#ifdef FEATURE_INPROC_CRASHREPORT + InProcCrashReportBeginStackOverflowTrace(crashingTid, static_cast(m_frames.Count())); +#endif // FEATURE_INPROC_CRASHREPORT + for (int i = 0; i < largestCommonStartOffset; i++) { PrintFrame(i, pWordAt); @@ -317,7 +335,10 @@ class CallStackLogger PrintToStdErrA("--------------------------------\n"); for (int i = largestCommonStartOffset; i < largestCommonStartOffset + largestCommonLength; i++) { - PrintFrame(i, pWordAt); + PrintFrame(i, + pWordAt, + static_cast(largestCommonRepeat), + static_cast(largestCommonLength)); } PrintToStdErrA("--------------------------------\n"); } @@ -326,6 +347,10 @@ class CallStackLogger { PrintFrame(i, pWordAt); } + +#ifdef FEATURE_INPROC_CRASHREPORT + InProcCrashReportEndStackOverflowTrace(); +#endif // FEATURE_INPROC_CRASHREPORT } }; @@ -365,7 +390,7 @@ inline void LogCallstackForLogWorker(Thread* pThread, PEXCEPTION_POINTERS pExcep pThread->StackWalkFrames(&CallStackLogger::LogCallstackForLogCallback, &logger, QUICKUNWIND | FUNCTIONSONLY | ALLOW_ASYNC_STACK_WALK); - logger.PrintStackTrace(WordAt.GetUnicode()); + logger.PrintStackTrace(WordAt.GetUnicode(), static_cast(pThread->GetOSThreadId())); #ifdef _DEBUG if (g_LogStackOverflowExit) PrintToStdErrA("@Exiting stack trace printing thread.\n"); diff --git a/src/coreclr/vm/excep.cpp b/src/coreclr/vm/excep.cpp index 5e87f606da0be9..fafc3bc5219aad 100644 --- a/src/coreclr/vm/excep.cpp +++ b/src/coreclr/vm/excep.cpp @@ -20,6 +20,10 @@ #include "virtualcallstub.h" #include "typestring.h" +#ifdef FEATURE_INPROC_CRASHREPORT +#include "inproccrashreporter.h" +#endif + #ifndef TARGET_UNIX #include "dwreport.h" #endif // !TARGET_UNIX @@ -3481,6 +3485,13 @@ bool GenerateDump( void CrashDumpAndTerminateProcess(UINT exitCode) { +#ifdef FEATURE_INPROC_CRASHREPORT + if (exitCode == COR_E_STACKOVERFLOW) + { + InProcCrashReportSetCrashKind(InProcCrashReportCrashKind::StackOverflow); + } +#endif + #ifdef HOST_WINDOWS CreateCrashDumpIfEnabled(exitCode == COR_E_STACKOVERFLOW); #endif