diff --git a/src/coreclr/debug/crashreport/CMakeLists.txt b/src/coreclr/debug/crashreport/CMakeLists.txt index f88699a4c6a464..48b63662b7ed88 100644 --- a/src/coreclr/debug/crashreport/CMakeLists.txt +++ b/src/coreclr/debug/crashreport/CMakeLists.txt @@ -2,6 +2,7 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CRASHREPORT_SOURCES signalsafejsonwriter.cpp + signalsafeconsolewriter.cpp inproccrashreporter.cpp ) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index fe771432eee5f4..fff8b5fdc15d12 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -6,6 +6,7 @@ // Streams a createdump-shaped JSON skeleton to a crashreport.json file. #include "inproccrashreporter.h" +#include "signalsafeconsolewriter.h" #include "signalsafejsonwriter.h" #include "pal.h" @@ -17,12 +18,19 @@ #include #include #include +#include #include #ifdef __APPLE__ #include #include #endif +extern "C" LPCWSTR PROCGetSignalName(int signal); + +static const char CRASHREPORT_PROTOCOL_VERSION[] = "1.0.0"; + +static SignalSafeConsoleWriter s_consoleWriter; + // Include the .NET version string instead of linking because it is "static". #if __has_include("_version.c") #include "_version.c" @@ -51,15 +59,88 @@ static void CacheSysctlString(const char* sysctlName, char* buffer, size_t buffe } #endif // __APPLE__ +// Bounded module name/GUID table that interns each unique module observed +// during a single crash report. Frames in the compact log refer to modules +// by short ``[N]`` indices instead of repeating the (often verbose) filename +// + GUID on every line; the matching ``modules:`` block at the end of the +// report maps each index back to the full data. +// +// Capacity is fixed (no heap on the fatal-signal path); modules beyond the +// cap render as ``[?]`` in frames and never reach the modules block. +// +// Single-instance because CreateReport is one-shot per process (guarded by +// the ``s_generating`` InterlockedCompareExchange in CreateReport). + +static constexpr size_t MAX_MODULES_IN_TABLE = 32; + +class ModuleTable +{ +public: + int Intern(const char* moduleName, const char* moduleGuid) + { + if (moduleName == nullptr || moduleName[0] == '\0' || + moduleGuid == nullptr || moduleGuid[0] == '\0') + { + return -1; + } + + for (size_t i = 0; i < m_count; ++i) + { + if (strncmp(m_entries[i].guid, moduleGuid, MINIPAL_GUID_BUFFER_LEN) == 0) + { + return static_cast(i); + } + } + + if (m_count >= MAX_MODULES_IN_TABLE) + { + return -1; + } + + Entry& entry = m_entries[m_count]; + size_t nameLen = strnlen(moduleName, sizeof(entry.name) - 1); + memcpy(entry.name, moduleName, nameLen); + entry.name[nameLen] = '\0'; + size_t guidLen = strnlen(moduleGuid, sizeof(entry.guid) - 1); + memcpy(entry.guid, moduleGuid, guidLen); + entry.guid[guidLen] = '\0'; + return static_cast(m_count++); + } + + size_t Count() const { return m_count; } + const char* Name(size_t i) const { return m_entries[i].name; } + const char* Guid(size_t i) const { return m_entries[i].guid; } + +private: + struct Entry + { + char name[CRASHREPORT_STRING_BUFFER_SIZE]; + char guid[MINIPAL_GUID_BUFFER_LEN]; + }; + + Entry m_entries[MAX_MODULES_IN_TABLE]; + size_t m_count; +}; + +static ModuleTable s_moduleTable; + class ThreadEnumerationContext { public: ThreadEnumerationContext( SignalSafeJsonWriter* writer, + SignalSafeConsoleWriter* consoleWriter, + uint64_t crashingTid, + uint32_t frameLimitPerThread, void* signalContext) : m_writer(writer), + m_consoleWriter(consoleWriter), m_signalContext(signalContext), m_threadCount(0), + m_crashingTid(crashingTid), + m_currentThreadFrameCount(0), + m_currentThreadDroppedCount(0), + m_frameLimitPerThread(frameLimitPerThread), m_sawCrashThread(false) { } @@ -70,8 +151,9 @@ class ThreadEnumerationContext size_t ThreadCount() const { return m_threadCount; } bool SawCrashThread() const { return m_sawCrashThread; } SignalSafeJsonWriter* Writer() const { return m_writer; } + SignalSafeConsoleWriter* ConsoleWriter() const { return m_consoleWriter; } - void EnumerateThreads(InProcCrashReportEnumerateThreadsCallback callback, uint64_t crashingTid); + void EnumerateThreads(InProcCrashReportEnumerateThreadsCallback callback); static void ThreadCallback( uint64_t osThreadId, @@ -114,9 +196,16 @@ class ThreadEnumerationContext uint32_t moduleSize, const char* moduleGuid); + void FinishCurrentThreadCompactBlock(); + SignalSafeJsonWriter* m_writer; + SignalSafeConsoleWriter* m_consoleWriter; 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; }; @@ -147,6 +236,15 @@ class CrashReportOutputContext class CrashReportHelpers { public: + struct FrameSinks + { + SignalSafeJsonWriter* writer; + SignalSafeConsoleWriter* consoleWriter; + uint32_t* currentThreadFrameCount; + uint32_t* currentThreadDroppedCount; + uint32_t frameLimitPerThread; + }; + static void GetVersionString( char* buffer, size_t bufferSize); @@ -202,11 +300,57 @@ class CrashReportHelpers const char* moduleGuid, void* ctx); + static void WriteFrameToJson( + SignalSafeJsonWriter* writer, + 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); + + static void WriteFrameToConsole( + SignalSafeConsoleWriter* consoleWriter, + uint32_t frameIndex, + int moduleIndex, + uint64_t ip, + const char* methodName, + const char* className, + const char* moduleName, + uint32_t nativeOffset, + uint32_t token, + uint32_t ilOffset); + + static void FrameSinkCallback( + 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); + static bool WriteToFile( int fd, const char* buffer, size_t len); + // 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 bool BuildReportPath( char* buffer, size_t bufferSize, @@ -237,26 +381,42 @@ InProcCrashReporter::CreateReport( char reportPath[CRASHREPORT_PATH_BUFFER_SIZE]; reportPath[0] = '\0'; - if (m_reportPath[0] == '\0' || !CrashReportHelpers::BuildReportPath(reportPath, sizeof(reportPath), m_reportPath, m_processName, m_hostName)) - { - return; - } + // 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' && + CrashReportHelpers::BuildReportPath(reportPath, sizeof(reportPath), m_reportPath, m_processName, m_hostName); - int fd = open(reportPath, O_WRONLY | O_CREAT | O_TRUNC, 0600); - if (fd == -1) + int fd = -1; + if (jsonEnabled) { - return; + fd = open(reportPath, O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (fd == -1) + { + jsonEnabled = false; + } } (void)siginfo; + EmitConsoleHeader(signal); + CrashReportOutputContext outputContext(fd); - m_jsonWriter.Init(&CrashReportOutputContext::ChunkCallback, &outputContext); + if (jsonEnabled) + { + m_jsonWriter.Init(&CrashReportOutputContext::ChunkCallback, &outputContext); + } + else + { + m_jsonWriter.Init(&CrashReportHelpers::DiscardOutputCallback, nullptr); + } m_jsonWriter.OpenObject(); m_jsonWriter.OpenObject("payload"); - m_jsonWriter.WriteString("protocol_version", "1.0.0"); + m_jsonWriter.WriteString("protocol_version", CRASHREPORT_PROTOCOL_VERSION); m_jsonWriter.OpenObject("configuration"); #if defined(__x86_64__) @@ -281,10 +441,10 @@ InProcCrashReporter::CreateReport( m_jsonWriter.OpenArray("threads"); if (m_enumerateThreadsCallback != nullptr) { - ThreadEnumerationContext threadContext(&m_jsonWriter, context); uint64_t crashingTid = static_cast(minipal_get_current_thread_id()); + ThreadEnumerationContext threadContext(&m_jsonWriter, &s_consoleWriter, crashingTid, m_frameLimitPerThread, context); - threadContext.EnumerateThreads(m_enumerateThreadsCallback, crashingTid); + threadContext.EnumerateThreads(m_enumerateThreadsCallback); if (threadContext.ThreadCount() == 0 || !threadContext.SawCrashThread()) { @@ -316,7 +476,7 @@ InProcCrashReporter::CreateReport( m_jsonWriter.CloseObject(); // root - if (fd != -1) + if (jsonEnabled) { bool writeSucceeded = m_jsonWriter.Finish() && !outputContext.WriteFailed() && @@ -327,6 +487,12 @@ InProcCrashReporter::CreateReport( unlink(reportPath); } } + else + { + (void)m_jsonWriter.Finish(); + } + + EmitConsoleModulesAndFooter(); } InProcCrashReporter& @@ -343,6 +509,7 @@ InProcCrashReporter::Initialize( m_isManagedThreadCallback = settings.isManagedThreadCallback; m_walkStackCallback = settings.walkStackCallback; m_enumerateThreadsCallback = settings.enumerateThreadsCallback; + m_frameLimitPerThread = settings.frameLimitPerThread; CrashReportHelpers::CopyString(m_reportPath, sizeof(m_reportPath), settings.reportPath); m_processName[0] = '\0'; @@ -444,6 +611,15 @@ CrashReportHelpers::WriteToFile( return true; } +bool +CrashReportHelpers::DiscardOutputCallback( + const char* /*buffer*/, + size_t /*len*/, + void* /*ctx*/) +{ + return true; +} + bool CrashReportOutputContext::HandleChunk( const char* buffer, @@ -910,6 +1086,30 @@ CrashReportHelpers::JsonFrameCallback( return; } + WriteFrameToJson(writer, ip, stackPointer, methodName, className, moduleName, + nativeOffset, token, ilOffset, moduleTimestamp, moduleSize, moduleGuid); +} + +void +CrashReportHelpers::WriteFrameToJson( + SignalSafeJsonWriter* writer, + 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) +{ + if (writer == nullptr) + { + return; + } + writer->OpenObject(); writer->WriteHexAsString("stack_pointer", stackPointer); writer->WriteHexAsString("native_address", ip); @@ -952,6 +1152,120 @@ CrashReportHelpers::JsonFrameCallback( writer->CloseObject(); // frame } +void +CrashReportHelpers::WriteFrameToConsole( + SignalSafeConsoleWriter* consoleWriter, + uint32_t frameIndex, + int moduleIndex, + uint64_t ip, + const char* methodName, + const char* className, + const char* moduleName, + 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(' '); + + consoleWriter->AppendChar('['); + if (moduleIndex < 0) + { + consoleWriter->AppendChar('?'); + } + else + { + consoleWriter->AppendDecimal(static_cast(moduleIndex)); + } + consoleWriter->AppendStr("] "); + + if (methodName != nullptr) + { + char fullName[CRASHREPORT_STRING_BUFFER_SIZE]; + BuildMethodName(fullName, sizeof(fullName), className, methodName); + consoleWriter->AppendStr(fullName); + consoleWriter->AppendStr(" + 0x"); + consoleWriter->AppendHex(static_cast(ilOffset)); + consoleWriter->AppendStr(" (token=0x"); + consoleWriter->AppendHex(static_cast(token)); + consoleWriter->AppendChar(')'); + } + else + { + consoleWriter->AppendStr("0x"); + consoleWriter->AppendHex(ip); + if (moduleName != nullptr && moduleName[0] != '\0') + { + consoleWriter->AppendStr(" ("); + consoleWriter->AppendStr(GetFilename(moduleName)); + consoleWriter->AppendStr(" + 0x"); + consoleWriter->AppendHex(static_cast(nativeOffset)); + consoleWriter->AppendChar(')'); + } + } + consoleWriter->EndLine(); +} + +void +CrashReportHelpers::FrameSinkCallback( + 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) +{ + FrameSinks* sinks = reinterpret_cast(ctx); + if (sinks == nullptr) + { + return; + } + + uint32_t frameIndex = sinks->currentThreadFrameCount != nullptr + ? *sinks->currentThreadFrameCount + : 0; + + int moduleIndex = s_moduleTable.Intern(moduleName, moduleGuid); + + // 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(sinks->writer, ip, stackPointer, methodName, className, moduleName, + nativeOffset, token, ilOffset, moduleTimestamp, moduleSize, moduleGuid); + + bool consoleCapped = sinks->frameLimitPerThread != 0 && + frameIndex >= sinks->frameLimitPerThread; + if (!consoleCapped) + { + WriteFrameToConsole(sinks->consoleWriter, frameIndex, moduleIndex, ip, methodName, className, moduleName, + nativeOffset, token, ilOffset); + } + else if (sinks->currentThreadDroppedCount != nullptr) + { + ++*sinks->currentThreadDroppedCount; + } + + if (sinks->currentThreadFrameCount != nullptr) + { + ++*sinks->currentThreadFrameCount; + } +} + void ThreadEnumerationContext::OnFrame( uint64_t ip, @@ -966,7 +1280,16 @@ ThreadEnumerationContext::OnFrame( uint32_t moduleSize, const char* moduleGuid) { - CrashReportHelpers::JsonFrameCallback(ip, stackPointer, methodName, className, moduleName, nativeOffset, token, ilOffset, moduleTimestamp, moduleSize, moduleGuid, m_writer); + CrashReportHelpers::FrameSinks sinks = + { + m_writer, + m_consoleWriter, + &m_currentThreadFrameCount, + &m_currentThreadDroppedCount, + m_frameLimitPerThread, + }; + CrashReportHelpers::FrameSinkCallback(ip, stackPointer, methodName, className, moduleName, + nativeOffset, token, ilOffset, moduleTimestamp, moduleSize, moduleGuid, &sinks); } void @@ -991,6 +1314,27 @@ ThreadEnumerationContext::FrameCallback( reinterpret_cast(ctx)->OnFrame(ip, stackPointer, methodName, className, moduleName, nativeOffset, token, ilOffset, moduleTimestamp, moduleSize, moduleGuid); } +void +ThreadEnumerationContext::FinishCurrentThreadCompactBlock() +{ + if (m_consoleWriter == nullptr || m_threadCount == 0) + { + return; + } + + if (m_currentThreadFrameCount == 0) + { + m_consoleWriter->WriteLine(" (no managed frames)"); + } + else if (m_currentThreadDroppedCount != 0) + { + m_consoleWriter->AppendStr(" ... +"); + m_consoleWriter->AppendDecimal(static_cast(m_currentThreadDroppedCount)); + m_consoleWriter->AppendStr(" more frames"); + m_consoleWriter->EndLine(); + } +} + void ThreadEnumerationContext::OnThread( uint64_t osThreadId, @@ -1000,6 +1344,8 @@ ThreadEnumerationContext::OnThread( { if (m_threadCount > 0) { + FinishCurrentThreadCompactBlock(); + m_writer->CloseArray(); // stack_frames m_writer->CloseObject(); // thread @@ -1011,6 +1357,8 @@ ThreadEnumerationContext::OnThread( m_sawCrashThread = true; } m_threadCount++; + m_currentThreadFrameCount = 0; + m_currentThreadDroppedCount = 0; m_writer->OpenObject(); m_writer->WriteString("is_managed", "true"); @@ -1033,6 +1381,29 @@ ThreadEnumerationContext::OnThread( { CrashReportHelpers::WriteCrashSiteFrameToJson(m_writer, m_signalContext); } + + if (m_consoleWriter != nullptr) + { + m_consoleWriter->WriteBlank(); + m_consoleWriter->AppendStr("--- thread 0x"); + m_consoleWriter->AppendHex(osThreadId); + if (isCrashThread) + { + m_consoleWriter->AppendStr(" (crashed)"); + } + m_consoleWriter->AppendStr(" ---"); + m_consoleWriter->EndLine(); + + 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(); + } + } } void @@ -1052,21 +1423,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; } + FinishCurrentThreadCompactBlock(); + // Close the last thread's stack_frames + thread objects opened by OnThread. m_writer->CloseArray(); // stack_frames m_writer->CloseObject(); // thread @@ -1092,10 +1464,107 @@ InProcCrashReporter::EmitSynthesizedCrashThread( CrashReportHelpers::WriteRegistersToJson(&m_jsonWriter, context); m_jsonWriter.OpenArray("stack_frames"); CrashReportHelpers::WriteCrashSiteFrameToJson(&m_jsonWriter, context); + + s_consoleWriter.WriteBlank(); + s_consoleWriter.AppendStr("--- thread 0x"); + s_consoleWriter.AppendHex(crashingTid); + s_consoleWriter.AppendStr(" (crashed) ---"); + s_consoleWriter.EndLine(); + + uint32_t synthesizedFrameCount = 0; + uint32_t synthesizedDroppedCount = 0; if (walkStack && m_walkStackCallback != nullptr) { - m_walkStackCallback(&CrashReportHelpers::JsonFrameCallback, &m_jsonWriter); + CrashReportHelpers::FrameSinks sinks = + { + &m_jsonWriter, + &s_consoleWriter, + &synthesizedFrameCount, + &synthesizedDroppedCount, + m_frameLimitPerThread, + }; + m_walkStackCallback(&CrashReportHelpers::FrameSinkCallback, &sinks); + } + if (synthesizedFrameCount == 0) + { + s_consoleWriter.WriteLine(" (no managed frames)"); + } + else if (synthesizedDroppedCount != 0) + { + s_consoleWriter.AppendStr(" ... +"); + s_consoleWriter.AppendDecimal(static_cast(synthesizedDroppedCount)); + s_consoleWriter.AppendStr(" more frames"); + s_consoleWriter.EndLine(); } + m_jsonWriter.CloseArray(); // stack_frames m_jsonWriter.CloseObject(); // thread } + +// --- InProcCrashReporter: console header and footer ------------------------ + +void +InProcCrashReporter::EmitConsoleHeader(int signal) +{ + s_consoleWriter.WriteSeparator(); + s_consoleWriter.AppendStr(".NET Crash Report v"); + s_consoleWriter.AppendStr(CRASHREPORT_PROTOCOL_VERSION); + s_consoleWriter.EndLine(); + + char version[sizeof(sccsid)]; + CrashReportHelpers::GetVersionString(version, sizeof(version)); + if (version[0] != '\0') + { + s_consoleWriter.WriteKeyValueStr("Build", version); + } + +#if defined(__x86_64__) + s_consoleWriter.WriteKeyValueStr("ABI", "amd64"); +#elif defined(__aarch64__) + s_consoleWriter.WriteKeyValueStr("ABI", "arm64"); +#elif defined(__arm__) + s_consoleWriter.WriteKeyValueStr("ABI", "arm"); +#endif + + if (m_processName[0] != '\0') + { + s_consoleWriter.WriteKeyValueStr("Cmdline", m_processName); + } + + s_consoleWriter.WriteKeyValueDecimal("pid", static_cast(GetCurrentProcessId())); + + s_consoleWriter.AppendStr("signal "); + s_consoleWriter.AppendSignedDecimal(signal); + s_consoleWriter.AppendStr(" ("); + // PROCGetSignalName returns a wide ASCII literal; narrow inline so the + // compact log shares one source of truth for the signal-name table with + // the wide-char EE policy callback in PROCLogManagedCallstackForSignal. + for (LPCWSTR name = PROCGetSignalName(signal); *name != L'\0'; ++name) + { + s_consoleWriter.AppendChar(static_cast(*name)); + } + s_consoleWriter.AppendChar(')'); + s_consoleWriter.EndLine(); +} + +void +InProcCrashReporter::EmitConsoleModulesAndFooter() +{ + if (s_moduleTable.Count() != 0) + { + s_consoleWriter.WriteBlank(); + s_consoleWriter.WriteLine("modules:"); + for (size_t i = 0; i < s_moduleTable.Count(); ++i) + { + s_consoleWriter.AppendStr(" ["); + s_consoleWriter.AppendDecimal(static_cast(i)); + s_consoleWriter.AppendStr("] "); + s_consoleWriter.AppendStr(CrashReportHelpers::GetFilename(s_moduleTable.Name(i))); + s_consoleWriter.AppendChar(' '); + s_consoleWriter.AppendStr(s_moduleTable.Guid(i)); + s_consoleWriter.EndLine(); + } + } + + s_consoleWriter.WriteSeparator(); +} diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.h b/src/coreclr/debug/crashreport/inproccrashreporter.h index 5018f3b0d10793..9f0a7c17a8c5d9 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.h +++ b/src/coreclr/debug/crashreport/inproccrashreporter.h @@ -63,6 +63,7 @@ struct InProcCrashReporterSettings InProcCrashReportIsManagedThreadCallback isManagedThreadCallback; InProcCrashReportWalkStackCallback walkStackCallback; InProcCrashReportEnumerateThreadsCallback enumerateThreadsCallback; + uint32_t frameLimitPerThread; }; class InProcCrashReporter @@ -88,6 +89,9 @@ class InProcCrashReporter void* context, bool walkStack); + void EmitConsoleHeader(int signal); + void EmitConsoleModulesAndFooter(); + SignalSafeJsonWriter m_jsonWriter; InProcCrashReportIsManagedThreadCallback m_isManagedThreadCallback = nullptr; InProcCrashReportWalkStackCallback m_walkStackCallback = nullptr; @@ -99,6 +103,7 @@ class InProcCrashReporter char m_osVersion[CRASHREPORT_STRING_BUFFER_SIZE] = {}; char m_systemModel[CRASHREPORT_STRING_BUFFER_SIZE] = {}; #endif + uint32_t m_frameLimitPerThread = 0; }; // Free-function entry point used by the runtime to wire the in-proc crash diff --git a/src/coreclr/debug/crashreport/signalsafeconsolewriter.cpp b/src/coreclr/debug/crashreport/signalsafeconsolewriter.cpp new file mode 100644 index 00000000000000..3e6b302b2261f2 --- /dev/null +++ b/src/coreclr/debug/crashreport/signalsafeconsolewriter.cpp @@ -0,0 +1,138 @@ +// 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 "signalsafejsonwriter.h" + +#include + +#if defined(__ANDROID__) +#include +static const char CRASHREPORT_LOG_TAG[] = "DOTNET_CRASH"; +#endif + +static const char CRASHREPORT_LINE_SEPARATOR[] = "*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***"; + +void +SignalSafeConsoleWriter::AppendStr(const char* s) +{ + if (s == nullptr) + { + return; + } + + while (*s != '\0' && m_pos + 1 < sizeof(m_buffer)) + { + m_buffer[m_pos++] = *s++; + } +} + +void +SignalSafeConsoleWriter::AppendChar(char c) +{ + if (m_pos + 1 < sizeof(m_buffer)) + { + m_buffer[m_pos++] = c; + } +} + +void +SignalSafeConsoleWriter::AppendHex(uint64_t v) +{ + char buf[SignalSafeJsonWriter::MAX_HEX_FORMAT_BUFFER_SIZE]; + SignalSafeJsonWriter::FormatHexValue(buf, sizeof(buf), v); + // Skip the leading "0x" so callers control whether the prefix appears + // (the compact format inserts it verbatim around the value). + const char* p = buf; + if (p[0] == '0' && p[1] == 'x') + { + p += 2; + } + AppendStr(p); +} + +void +SignalSafeConsoleWriter::AppendDecimal(uint64_t v) +{ + char buf[SignalSafeJsonWriter::MAX_UNSIGNED_DECIMAL_BUFFER_SIZE]; + SignalSafeJsonWriter::FormatUnsignedDecimal(buf, sizeof(buf), v); + AppendStr(buf); +} + +void +SignalSafeConsoleWriter::AppendSignedDecimal(int64_t v) +{ + char buf[SignalSafeJsonWriter::MAX_SIGNED_DECIMAL_BUFFER_SIZE]; + SignalSafeJsonWriter::FormatSignedDecimal(buf, sizeof(buf), v); + AppendStr(buf); +} + +void +SignalSafeConsoleWriter::EndLine() +{ + 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 + // On Apple/Linux the report goes to stderr; explicitly newline-terminate + // each line so log readers split entries the same way logcat would. + if (m_pos + 1 < sizeof(m_buffer)) + { + m_buffer[m_pos++] = '\n'; + m_buffer[m_pos] = '\0'; + } + 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..3e0f1d20cc7e2e --- /dev/null +++ b/src/coreclr/debug/crashreport/signalsafeconsolewriter.h @@ -0,0 +1,58 @@ +// 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 writes JSON to a file callback; +// SignalSafeConsoleWriter writes one line at a time to the platform console +// (Android logcat under tag "DOTNET_CRASH"; stderr elsewhere). All public +// members are async-signal-safe: no heap allocation, no stdio, no locale +// or variadic formatting. +// +// Each EndLine() / WriteLine() call produces exactly one platform log entry +// on Android (so per-line filtering works) and one '\n'-terminated chunk on +// Apple/Linux stderr. Best-effort: a per-line buffer overflow or short +// platform write is silently dropped and the next line begins fresh, so a +// console hiccup never fails any other crash-report output. + +#pragma once + +#include +#include + +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(); + + char m_buffer[SIGNAL_SAFE_CONSOLE_BUFFER_SIZE]; + size_t m_pos; +}; diff --git a/src/coreclr/inc/clrconfigvalues.h b/src/coreclr/inc/clrconfigvalues.h index c9dd7c485c99c0..7955e9d832f1c5 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; remaining frames are summarized as '... +N more frames'", CLRConfig::LookupOptions::ParseIntegerAsBase10) /// /// R2R diff --git a/src/coreclr/pal/src/include/pal/process.h b/src/coreclr/pal/src/include/pal/process.h index e3f26bde875a03..414ce443c92bf6 100644 --- a/src/coreclr/pal/src/include/pal/process.h +++ b/src/coreclr/pal/src/include/pal/process.h @@ -172,6 +172,18 @@ VOID PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, void* context, --*/ VOID PROCLogManagedCallstackForSignal(int signal); +/*++ +Function: + PROCGetSignalName + + Returns a static, async-signal-safe wide name for the given POSIX signal + number (e.g. "SIGABRT" for 6), or "Unknown signal" for an unrecognized + value. Pointer is stable for the process lifetime; callers must not free + or modify the returned buffer. The returned string is pure ASCII so + callers wanting a narrow form can copy the low byte of each WCHAR. +--*/ +LPCWSTR PROCGetSignalName(int signal); + #ifdef __cplusplus } #endif // __cplusplus diff --git a/src/coreclr/pal/src/thread/process.cpp b/src/coreclr/pal/src/thread/process.cpp index a77427191c17db..a4fd94a05f1738 100644 --- a/src/coreclr/pal/src/thread/process.cpp +++ b/src/coreclr/pal/src/thread/process.cpp @@ -2044,7 +2044,7 @@ static void DoNotOptimize(const void* p) (void)p; } -static LPCWSTR GetSignalName(int signal) +LPCWSTR PROCGetSignalName(int signal) { switch (signal) { @@ -2076,7 +2076,7 @@ PROCLogManagedCallstackForSignal(int signal) { if (g_logManagedCallstackForSignalCallback != nullptr) { - LPCWSTR signalName = GetSignalName(signal); + LPCWSTR signalName = PROCGetSignalName(signal); g_logManagedCallstackForSignalCallback(signalName); } } diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index 1670ec970d91ff..0509cc6e836c85 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -427,16 +427,13 @@ CrashReportConfigure() CLRConfigNoCache dmpNameCfg = CLRConfigNoCache::Get("DbgMiniDumpName", /*noprefix*/ false, &getenv); const char* dumpName = dmpNameCfg.IsSet() ? dmpNameCfg.AsString() : nullptr; - if (dumpName == nullptr || dumpName[0] == '\0') - { - return; - } InProcCrashReporterSettings settings = {}; settings.reportPath = dumpName; settings.isManagedThreadCallback = CrashReportIsCurrentThreadManaged; settings.walkStackCallback = CrashReportWalkStack; settings.enumerateThreadsCallback = CrashReportEnumerateThreads; + settings.frameLimitPerThread = CLRConfig::GetConfigValue(CLRConfig::INTERNAL_CrashReportFrameLimitPerThread); // Initialize the reporter and register the PAL signal-path callback last // so PAL only observes the reporter after all VM callbacks are wired in.