From 90ca6945dfaf4dfb051848752c664fbb88453ba1 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 5 May 2026 12:46:41 -0400 Subject: [PATCH 1/5] Emit fixed crash report header and footer to platform log via DOTNET_CRASH tag Adds SignalSafeConsoleWriter, a bounded async-signal-safe sibling to SignalSafeJsonWriter that emits one logical line at a time to a platform-specific console: __android_log_write under tag `DOTNET_CRASH` on Android (so consumers can filter crash reports out of an otherwise noisy logcat with `adb logcat *:S DOTNET_CRASH:F`) and minipal_log_write_error to stderr elsewhere. Promotes the wide-char signal name table out of pal/src/thread/process.cpp's file-static GetSignalName into PROCGetSignalName declared in pal/src/include/pal/process.h, so the crashreport TU can reuse the same mapping that PROCLogManagedCallstackForSignal uses (single source of truth for the ASCII signal-name table). Hooks the new writer into InProcCrashReporter::CreateReport: emits a fixed header (separator, .NET Crash Report version, Build, ABI, Cmdline, pid, signal) before the JSON open and a closing separator after the JSON close. Per-thread frame data lands in commit 2; modules table in commit 3. The compact log version literal is hoisted to a single CRASHREPORT_PROTOCOL_VERSION constant shared with the existing JSON `protocol_version` field. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/debug/crashreport/CMakeLists.txt | 1 + .../debug/crashreport/inproccrashreporter.cpp | 65 ++++++++- .../debug/crashreport/inproccrashreporter.h | 3 + .../crashreport/signalsafeconsolewriter.cpp | 138 ++++++++++++++++++ .../crashreport/signalsafeconsolewriter.h | 58 ++++++++ src/coreclr/pal/src/include/pal/process.h | 12 ++ src/coreclr/pal/src/thread/process.cpp | 4 +- 7 files changed, 278 insertions(+), 3 deletions(-) create mode 100644 src/coreclr/debug/crashreport/signalsafeconsolewriter.cpp create mode 100644 src/coreclr/debug/crashreport/signalsafeconsolewriter.h 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..f2913117a643f1 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" @@ -23,6 +24,12 @@ #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" @@ -250,13 +257,15 @@ InProcCrashReporter::CreateReport( (void)siginfo; + EmitConsoleHeader(signal); + 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.WriteString("protocol_version", CRASHREPORT_PROTOCOL_VERSION); m_jsonWriter.OpenObject("configuration"); #if defined(__x86_64__) @@ -327,6 +336,8 @@ InProcCrashReporter::CreateReport( unlink(reportPath); } } + + EmitConsoleModulesAndFooter(); } InProcCrashReporter& @@ -1099,3 +1110,55 @@ InProcCrashReporter::EmitSynthesizedCrashThread( 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() +{ + s_consoleWriter.WriteSeparator(); +} diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.h b/src/coreclr/debug/crashreport/inproccrashreporter.h index 5018f3b0d10793..1c7e5785e2880c 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.h +++ b/src/coreclr/debug/crashreport/inproccrashreporter.h @@ -88,6 +88,9 @@ class InProcCrashReporter void* context, bool walkStack); + void EmitConsoleHeader(int signal); + void EmitConsoleModulesAndFooter(); + SignalSafeJsonWriter m_jsonWriter; InProcCrashReportIsManagedThreadCallback m_isManagedThreadCallback = nullptr; InProcCrashReportWalkStackCallback m_walkStackCallback = nullptr; 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/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); } } From 4c411e83b1b27bffb24d6b10acf67dcbd9812cf6 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 5 May 2026 12:49:56 -0400 Subject: [PATCH 2/5] Emit per-thread frame stacks to the compact crash report log Adds a unified frame-emission path that fans every walked managed frame out to both crash-report sinks: the JSON file (existing behavior) and the new SignalSafeConsoleWriter compact log. FrameSinks groups the writer + console-writer + per-thread frame counter into a single context that ThreadEnumerationContext (and the synthesized crash thread fallback) hands to the new FrameSinkCallback bridge. The previous JsonFrameCallback now delegates to a freshly extracted WriteFrameToJson so the JSON layout is unchanged. WriteFrameToConsole formats one frame as ` #NN Class.Method + 0xILOFFSET (token=0xTOKEN)` for managed frames and ` #NN 0xIP (module + 0xOFFSET)` for native frames (mirrors AOSP debuggerd/tombstone). ThreadEnumerationContext now also opens each thread's compact-log block (`--- thread 0xTID --- `, with `(crashed)` suffix on the crash thread) and emits the per-thread `managed exception:` line where the EE callback hands us the type/HRESULT. `EnumerateThreads` no longer takes `crashingTid` separately; it lives on the context alongside the per-thread frame counter so FrameSinkCallback can index frames consistently. EmitSynthesizedCrashThread mirrors the same compact-log block when the EE enumerator is unavailable or never observed the crashing thread, and falls back to `(no managed frames)` when no frames land in the block. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../debug/crashreport/inproccrashreporter.cpp | 254 +++++++++++++++++- 1 file changed, 246 insertions(+), 8 deletions(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index f2913117a643f1..8ad0a92e361cd6 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -63,10 +63,15 @@ class ThreadEnumerationContext public: ThreadEnumerationContext( SignalSafeJsonWriter* writer, + SignalSafeConsoleWriter* consoleWriter, + uint64_t crashingTid, void* signalContext) : m_writer(writer), + m_consoleWriter(consoleWriter), m_signalContext(signalContext), m_threadCount(0), + m_crashingTid(crashingTid), + m_currentThreadFrameCount(0), m_sawCrashThread(false) { } @@ -77,8 +82,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, @@ -121,9 +127,14 @@ 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; bool m_sawCrashThread; }; @@ -154,6 +165,13 @@ class CrashReportOutputContext class CrashReportHelpers { public: + struct FrameSinks + { + SignalSafeJsonWriter* writer; + SignalSafeConsoleWriter* consoleWriter; + uint32_t* currentThreadFrameCount; + }; + static void GetVersionString( char* buffer, size_t bufferSize); @@ -209,6 +227,45 @@ 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, + 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, @@ -290,10 +347,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, context); - threadContext.EnumerateThreads(m_enumerateThreadsCallback, crashingTid); + threadContext.EnumerateThreads(m_enumerateThreadsCallback); if (threadContext.ThreadCount() == 0 || !threadContext.SawCrashThread()) { @@ -921,6 +978,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); @@ -963,6 +1044,96 @@ CrashReportHelpers::JsonFrameCallback( writer->CloseObject(); // frame } +void +CrashReportHelpers::WriteFrameToConsole( + SignalSafeConsoleWriter* consoleWriter, + uint32_t frameIndex, + 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; + } + + // Frame index always two digits ("#04 ..."); matches Android/AOSP debuggerd. + consoleWriter->AppendStr(" #"); + if (frameIndex < 10) + { + consoleWriter->AppendChar('0'); + } + consoleWriter->AppendDecimal(static_cast(frameIndex)); + consoleWriter->AppendChar(' '); + + 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; + + WriteFrameToJson(sinks->writer, ip, stackPointer, methodName, className, moduleName, + nativeOffset, token, ilOffset, moduleTimestamp, moduleSize, moduleGuid); + + WriteFrameToConsole(sinks->consoleWriter, frameIndex, ip, methodName, className, moduleName, + nativeOffset, token, ilOffset); + + if (sinks->currentThreadFrameCount != nullptr) + { + ++*sinks->currentThreadFrameCount; + } +} + void ThreadEnumerationContext::OnFrame( uint64_t ip, @@ -977,7 +1148,14 @@ 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, + }; + CrashReportHelpers::FrameSinkCallback(ip, stackPointer, methodName, className, moduleName, + nativeOffset, token, ilOffset, moduleTimestamp, moduleSize, moduleGuid, &sinks); } void @@ -1002,6 +1180,20 @@ 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)"); + } +} + void ThreadEnumerationContext::OnThread( uint64_t osThreadId, @@ -1011,6 +1203,8 @@ ThreadEnumerationContext::OnThread( { if (m_threadCount > 0) { + FinishCurrentThreadCompactBlock(); + m_writer->CloseArray(); // stack_frames m_writer->CloseObject(); // thread @@ -1022,6 +1216,7 @@ ThreadEnumerationContext::OnThread( m_sawCrashThread = true; } m_threadCount++; + m_currentThreadFrameCount = 0; m_writer->OpenObject(); m_writer->WriteString("is_managed", "true"); @@ -1044,6 +1239,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 @@ -1063,21 +1281,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 @@ -1103,10 +1322,29 @@ 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; if (walkStack && m_walkStackCallback != nullptr) { - m_walkStackCallback(&CrashReportHelpers::JsonFrameCallback, &m_jsonWriter); + CrashReportHelpers::FrameSinks sinks = + { + &m_jsonWriter, + &s_consoleWriter, + &synthesizedFrameCount, + }; + m_walkStackCallback(&CrashReportHelpers::FrameSinkCallback, &sinks); + } + if (synthesizedFrameCount == 0) + { + s_consoleWriter.WriteLine(" (no managed frames)"); } + m_jsonWriter.CloseArray(); // stack_frames m_jsonWriter.CloseObject(); // thread } From eed3d3c8a8d142aff24e0a5e83585e7982de6a8a Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 5 May 2026 12:51:33 -0400 Subject: [PATCH 3/5] Intern modules into a fixed-size table and render frames with module-index references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds ModuleTable, a 32-slot fixed-capacity intern table keyed by MVID. Each unique module observed during a single CreateReport gets a small `[N]` index; frames in the compact log are rendered as ` #NN [M] Class.Method + 0xILOFFSET (token=0xTOKEN)` instead of repeating the (often verbose) module filename + GUID on every frame line. A `modules:` block at the end of the report — emitted by EmitConsoleModulesAndFooter just before the closing separator — maps each `[N]` back to `Name {Mvid}` so triagers can correlate a frame to its full module identity exactly once per report. Modules beyond the cap render as `[?]` and are silently dropped from the modules block. Single `s_moduleTable` instance: CreateReport's existing `s_generating` InterlockedCompareExchange one-shot guard hard-prevents re-entry, so the table never accumulates state across reports. JSON layout is unchanged; the index is a compact-log-only artifact. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../debug/crashreport/inproccrashreporter.cpp | 100 +++++++++++++++++- 1 file changed, 98 insertions(+), 2 deletions(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index 8ad0a92e361cd6..577318c2dba0ff 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #ifdef __APPLE__ #include @@ -58,6 +59,71 @@ 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: @@ -244,6 +310,7 @@ class CrashReportHelpers static void WriteFrameToConsole( SignalSafeConsoleWriter* consoleWriter, uint32_t frameIndex, + int moduleIndex, uint64_t ip, const char* methodName, const char* className, @@ -1048,6 +1115,7 @@ void CrashReportHelpers::WriteFrameToConsole( SignalSafeConsoleWriter* consoleWriter, uint32_t frameIndex, + int moduleIndex, uint64_t ip, const char* methodName, const char* className, @@ -1061,7 +1129,6 @@ CrashReportHelpers::WriteFrameToConsole( return; } - // Frame index always two digits ("#04 ..."); matches Android/AOSP debuggerd. consoleWriter->AppendStr(" #"); if (frameIndex < 10) { @@ -1070,6 +1137,17 @@ CrashReportHelpers::WriteFrameToConsole( 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]; @@ -1122,10 +1200,12 @@ CrashReportHelpers::FrameSinkCallback( ? *sinks->currentThreadFrameCount : 0; + int moduleIndex = s_moduleTable.Intern(moduleName, moduleGuid); + WriteFrameToJson(sinks->writer, ip, stackPointer, methodName, className, moduleName, nativeOffset, token, ilOffset, moduleTimestamp, moduleSize, moduleGuid); - WriteFrameToConsole(sinks->consoleWriter, frameIndex, ip, methodName, className, moduleName, + WriteFrameToConsole(sinks->consoleWriter, frameIndex, moduleIndex, ip, methodName, className, moduleName, nativeOffset, token, ilOffset); if (sinks->currentThreadFrameCount != nullptr) @@ -1398,5 +1478,21 @@ InProcCrashReporter::EmitConsoleHeader(int signal) 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(); } From b7a959e81fa099ee6bf285d918baa16c544d6175 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 5 May 2026 12:54:34 -0400 Subject: [PATCH 4/5] Cap frames per thread in the compact crash report log Adds a configurable per-thread frame cap to the compact-log output, gated on the new `DOTNET_CrashReportFrameLimitPerThread` env var (default 32, base-10 parse, plumbed through `InProcCrashReporterSettings.frameLimitPerThread`). Frames past the cap are still recorded in the JSON file (the authoritative post-mortem store); the compact log replaces them with ` ... +N more frames` so a deeply recursive or infinite-loop crash thread doesn't drown the logcat tag. FrameSinks gains `currentThreadDroppedCount` and `frameLimitPerThread`; FrameSinkCallback short-circuits the WriteFrameToConsole call once the cap is reached and bumps the dropped counter. ThreadEnumerationContext::FinishCurrentThreadCompactBlock and EmitSynthesizedCrashThread emit the `+N more frames` line when any frames were dropped, otherwise the existing `(no managed frames)` indicator is used. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../debug/crashreport/inproccrashreporter.cpp | 45 +++++++++++++++++-- .../debug/crashreport/inproccrashreporter.h | 2 + src/coreclr/inc/clrconfigvalues.h | 1 + src/coreclr/vm/crashreportstackwalker.cpp | 1 + 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index 577318c2dba0ff..a36db9812ea58f 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -131,6 +131,7 @@ class ThreadEnumerationContext SignalSafeJsonWriter* writer, SignalSafeConsoleWriter* consoleWriter, uint64_t crashingTid, + uint32_t frameLimitPerThread, void* signalContext) : m_writer(writer), m_consoleWriter(consoleWriter), @@ -138,6 +139,8 @@ class ThreadEnumerationContext m_threadCount(0), m_crashingTid(crashingTid), m_currentThreadFrameCount(0), + m_currentThreadDroppedCount(0), + m_frameLimitPerThread(frameLimitPerThread), m_sawCrashThread(false) { } @@ -201,6 +204,8 @@ class ThreadEnumerationContext size_t m_threadCount; uint64_t m_crashingTid; uint32_t m_currentThreadFrameCount; + uint32_t m_currentThreadDroppedCount; + uint32_t m_frameLimitPerThread; bool m_sawCrashThread; }; @@ -236,6 +241,8 @@ class CrashReportHelpers SignalSafeJsonWriter* writer; SignalSafeConsoleWriter* consoleWriter; uint32_t* currentThreadFrameCount; + uint32_t* currentThreadDroppedCount; + uint32_t frameLimitPerThread; }; static void GetVersionString( @@ -415,7 +422,7 @@ InProcCrashReporter::CreateReport( if (m_enumerateThreadsCallback != nullptr) { uint64_t crashingTid = static_cast(minipal_get_current_thread_id()); - ThreadEnumerationContext threadContext(&m_jsonWriter, &s_consoleWriter, crashingTid, context); + ThreadEnumerationContext threadContext(&m_jsonWriter, &s_consoleWriter, crashingTid, m_frameLimitPerThread, context); threadContext.EnumerateThreads(m_enumerateThreadsCallback); @@ -478,6 +485,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'; @@ -1202,11 +1210,22 @@ CrashReportHelpers::FrameSinkCallback( 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); - WriteFrameToConsole(sinks->consoleWriter, frameIndex, moduleIndex, ip, methodName, className, moduleName, - nativeOffset, token, ilOffset); + 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) { @@ -1233,6 +1252,8 @@ ThreadEnumerationContext::OnFrame( m_writer, m_consoleWriter, &m_currentThreadFrameCount, + &m_currentThreadDroppedCount, + m_frameLimitPerThread, }; CrashReportHelpers::FrameSinkCallback(ip, stackPointer, methodName, className, moduleName, nativeOffset, token, ilOffset, moduleTimestamp, moduleSize, moduleGuid, &sinks); @@ -1272,6 +1293,13 @@ ThreadEnumerationContext::FinishCurrentThreadCompactBlock() { 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 @@ -1297,6 +1325,7 @@ ThreadEnumerationContext::OnThread( } m_threadCount++; m_currentThreadFrameCount = 0; + m_currentThreadDroppedCount = 0; m_writer->OpenObject(); m_writer->WriteString("is_managed", "true"); @@ -1410,6 +1439,7 @@ InProcCrashReporter::EmitSynthesizedCrashThread( s_consoleWriter.EndLine(); uint32_t synthesizedFrameCount = 0; + uint32_t synthesizedDroppedCount = 0; if (walkStack && m_walkStackCallback != nullptr) { CrashReportHelpers::FrameSinks sinks = @@ -1417,6 +1447,8 @@ InProcCrashReporter::EmitSynthesizedCrashThread( &m_jsonWriter, &s_consoleWriter, &synthesizedFrameCount, + &synthesizedDroppedCount, + m_frameLimitPerThread, }; m_walkStackCallback(&CrashReportHelpers::FrameSinkCallback, &sinks); } @@ -1424,6 +1456,13 @@ InProcCrashReporter::EmitSynthesizedCrashThread( { 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 diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.h b/src/coreclr/debug/crashreport/inproccrashreporter.h index 1c7e5785e2880c..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 @@ -102,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/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/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index 1670ec970d91ff..7f98b931963677 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -437,6 +437,7 @@ CrashReportConfigure() 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. From d3151a19309b6800712afd7b23d85f86f0691aa7 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 5 May 2026 12:56:12 -0400 Subject: [PATCH 5/5] Run the in-proc crash reporter without DbgMiniDumpName for compact-log-only mode Enables the in-proc crash reporter to run for callers that only want the compact platform-log output (DOTNET_CRASH on Android, stderr elsewhere) and don't need the JSON file: `DOTNET_EnableCrashReport=1` alone now activates the reporter, with the JSON file written only when `DOTNET_DbgMiniDumpName` is also supplied. CreateReport now treats `m_reportPath` empty as a runtime signal to skip `open()` and route the JSON formatter through CrashReportHelpers::DiscardOutputCallback (a no-op sink). All other emission code paths run unchanged so the formatter's bookkeeping stays consistent in both modes; only the file-cleanup tail (close/unlink + trailing newline write) is gated on `jsonEnabled`. The console header/footer pair is unconditional. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../debug/crashreport/inproccrashreporter.cpp | 51 +++++++++++++++---- src/coreclr/vm/crashreportstackwalker.cpp | 4 -- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index a36db9812ea58f..fff8b5fdc15d12 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -345,6 +345,12 @@ class CrashReportHelpers 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, @@ -375,15 +381,22 @@ 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; @@ -392,7 +405,14 @@ InProcCrashReporter::CreateReport( 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"); @@ -456,7 +476,7 @@ InProcCrashReporter::CreateReport( m_jsonWriter.CloseObject(); // root - if (fd != -1) + if (jsonEnabled) { bool writeSucceeded = m_jsonWriter.Finish() && !outputContext.WriteFailed() && @@ -467,6 +487,10 @@ InProcCrashReporter::CreateReport( unlink(reportPath); } } + else + { + (void)m_jsonWriter.Finish(); + } EmitConsoleModulesAndFooter(); } @@ -587,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, diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index 7f98b931963677..0509cc6e836c85 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -427,10 +427,6 @@ 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;