From 59b21cb13924c63b21f8ba575733ff7c1f355509 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Mon, 11 May 2026 21:26:13 -0400 Subject: [PATCH 1/5] Extract signal-safe integer formatters into a shared SignalSafeFormat namespace Moves the async-signal-safe integer formatting helpers and buffer-size constants out of SignalSafeJsonWriter into a shared SignalSafeFormat namespace. The helpers are JSON-agnostic and are needed by both the JSON writer and later compact console output without introducing a sibling dependency. This is intended as a behavior-preserving refactor: JSON writer call sites continue to use the same bounded fixed-buffer formatting logic, just through the shared helper namespace. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/debug/crashreport/CMakeLists.txt | 1 + .../debug/crashreport/inproccrashreporter.cpp | 5 +- .../debug/crashreport/signalsafeformat.cpp | 114 ++++++++++++++++ .../debug/crashreport/signalsafeformat.h | 42 ++++++ .../crashreport/signalsafejsonwriter.cpp | 124 +----------------- .../debug/crashreport/signalsafejsonwriter.h | 17 --- 6 files changed, 167 insertions(+), 136 deletions(-) create mode 100644 src/coreclr/debug/crashreport/signalsafeformat.cpp create mode 100644 src/coreclr/debug/crashreport/signalsafeformat.h diff --git a/src/coreclr/debug/crashreport/CMakeLists.txt b/src/coreclr/debug/crashreport/CMakeLists.txt index f88699a4c6a464..9d78e9976cb1c2 100644 --- a/src/coreclr/debug/crashreport/CMakeLists.txt +++ b/src/coreclr/debug/crashreport/CMakeLists.txt @@ -1,6 +1,7 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CRASHREPORT_SOURCES + signalsafeformat.cpp signalsafejsonwriter.cpp inproccrashreporter.cpp ) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index fe771432eee5f4..81fdef4d7b62de 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -7,6 +7,7 @@ #include "inproccrashreporter.h" #include "signalsafejsonwriter.h" +#include "signalsafeformat.h" #include "pal.h" @@ -526,7 +527,7 @@ CrashReportHelpers::ExpandDumpTemplate( case 'p': case 'd': - if (SignalSafeJsonWriter::FormatUnsignedDecimal(numberBuf, sizeof(numberBuf), pid) == 0) + if (SignalSafeFormat::FormatUnsignedDecimal(numberBuf, sizeof(numberBuf), pid) == 0) { return 0; } @@ -542,7 +543,7 @@ CrashReportHelpers::ExpandDumpTemplate( break; case 't': - if (SignalSafeJsonWriter::FormatUnsignedDecimal( + if (SignalSafeFormat::FormatUnsignedDecimal( numberBuf, sizeof(numberBuf), static_cast(time(nullptr))) == 0) { return 0; diff --git a/src/coreclr/debug/crashreport/signalsafeformat.cpp b/src/coreclr/debug/crashreport/signalsafeformat.cpp new file mode 100644 index 00000000000000..9efed80e982a22 --- /dev/null +++ b/src/coreclr/debug/crashreport/signalsafeformat.cpp @@ -0,0 +1,114 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include "signalsafeformat.h" + +namespace SignalSafeFormat +{ + +void +FormatHex( + 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 +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 +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; +} + +} // namespace SignalSafeFormat diff --git a/src/coreclr/debug/crashreport/signalsafeformat.h b/src/coreclr/debug/crashreport/signalsafeformat.h new file mode 100644 index 00000000000000..2bf40cf963dfe1 --- /dev/null +++ b/src/coreclr/debug/crashreport/signalsafeformat.h @@ -0,0 +1,42 @@ +// 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 integer-to-string format primitives shared across the +// signal-safe writer family (SignalSafeJsonWriter, SignalSafeConsoleWriter, +// and any other consumer that needs to render integers without stdio, +// locale, or heap allocation). Bounded buffer-size constants document the +// minimum buffer required for each formatter. + +#pragma once + +#include +#include + +namespace SignalSafeFormat +{ + constexpr size_t MAX_HEX_DIGITS_UINT64 = 16; + constexpr size_t MAX_DECIMAL_DIGITS_UINT64 = 20; + constexpr size_t HEX_PREFIX_LEN = 2; // "0x" + constexpr size_t SIGN_LEN = 1; // '-' for signed decimals + constexpr size_t NULL_TERMINATOR_LEN = 1; + + constexpr size_t MAX_HEX_BUFFER_SIZE = HEX_PREFIX_LEN + MAX_HEX_DIGITS_UINT64 + NULL_TERMINATOR_LEN; + constexpr size_t MAX_UNSIGNED_DECIMAL_BUFFER_SIZE = MAX_DECIMAL_DIGITS_UINT64 + NULL_TERMINATOR_LEN; + constexpr size_t MAX_SIGNED_DECIMAL_BUFFER_SIZE = SIGN_LEN + MAX_DECIMAL_DIGITS_UINT64 + NULL_TERMINATOR_LEN; + + // 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 same buffer-state guarantees as FormatHex. + 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); +} diff --git a/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp b/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp index 2cd858ab544564..44adea0af9b321 100644 --- a/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp +++ b/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. #include "signalsafejsonwriter.h" +#include "signalsafeformat.h" #include #include @@ -257,124 +258,13 @@ 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); + char scratch[SignalSafeFormat::MAX_HEX_BUFFER_SIZE]; + SignalSafeFormat::FormatHex(scratch, sizeof(scratch), value); return WriteString(key, scratch); } @@ -383,8 +273,8 @@ SignalSafeJsonWriter::WriteDecimalAsString( const char* key, uint64_t value) { - char scratch[MAX_UNSIGNED_DECIMAL_BUFFER_SIZE]; - (void)FormatUnsignedDecimal(scratch, sizeof(scratch), value); + char scratch[SignalSafeFormat::MAX_UNSIGNED_DECIMAL_BUFFER_SIZE]; + (void)SignalSafeFormat::FormatUnsignedDecimal(scratch, sizeof(scratch), value); return WriteString(key, scratch); } @@ -393,7 +283,7 @@ SignalSafeJsonWriter::WriteSignedDecimalAsString( const char* key, int64_t value) { - char scratch[MAX_SIGNED_DECIMAL_BUFFER_SIZE]; - (void)FormatSignedDecimal(scratch, sizeof(scratch), value); + char scratch[SignalSafeFormat::MAX_SIGNED_DECIMAL_BUFFER_SIZE]; + (void)SignalSafeFormat::FormatSignedDecimal(scratch, sizeof(scratch), value); return WriteString(key, scratch); } diff --git a/src/coreclr/debug/crashreport/signalsafejsonwriter.h b/src/coreclr/debug/crashreport/signalsafejsonwriter.h index 54eac5dbf6d30d..650e1edcb82802 100644 --- a/src/coreclr/debug/crashreport/signalsafejsonwriter.h +++ b/src/coreclr/debug/crashreport/signalsafejsonwriter.h @@ -19,16 +19,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 +45,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); From 1a348af6fc68418b26418d6fd57bccbfba03da77 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Mon, 11 May 2026 22:07:39 -0400 Subject: [PATCH 2/5] Add DOTNET_CRASH compact log alongside the JSON file crash report Adds SignalSafeConsoleWriter, a bounded line-oriented sink that writes DOTNET_CRASH entries through __android_log_write on Android and newline-terminated stderr lines elsewhere. CreateReport now emits a compact tombstone-style header/footer alongside the existing JSON report path. The JSON header/footer emission is split into helpers, and DbgMiniDumpName becomes optional: when no JSON path is configured, the JSON writer uses a no-op sink while the compact log still runs. PROCGetSignalNameAscii exposes the existing signal-name table in the narrow form used by the compact log. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/debug/crashreport/CMakeLists.txt | 1 + .../debug/crashreport/inproccrashreporter.cpp | 217 ++++++++++++++---- .../debug/crashreport/inproccrashreporter.h | 6 + .../crashreport/signalsafeconsolewriter.cpp | 142 ++++++++++++ .../crashreport/signalsafeconsolewriter.h | 75 ++++++ src/coreclr/pal/src/include/pal/process.h | 12 + src/coreclr/pal/src/thread/process.cpp | 15 ++ src/coreclr/vm/crashreportstackwalker.cpp | 4 - 8 files changed, 420 insertions(+), 52 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 9d78e9976cb1c2..f23dd004846df5 100644 --- a/src/coreclr/debug/crashreport/CMakeLists.txt +++ b/src/coreclr/debug/crashreport/CMakeLists.txt @@ -3,6 +3,7 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CRASHREPORT_SOURCES signalsafeformat.cpp signalsafejsonwriter.cpp + signalsafeconsolewriter.cpp inproccrashreporter.cpp ) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index 81fdef4d7b62de..b80333121371dd 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 "signalsafeformat.h" @@ -24,6 +25,42 @@ #include #endif +extern "C" const char* PROCGetSignalNameAscii(int signal); + +static const char CRASHREPORT_PROTOCOL_VERSION[] = "1.0.0"; + +#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"; +#endif + +// Prescribed compact crash report log format. One logical line == one +// __android_log_write entry under tag "DOTNET_CRASH" on Android, one +// '\n'-terminated stderr write elsewhere. +// +// *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** (EmitConsoleHeader) +// .NET Crash Report v +// Build: (omitted if empty) +// ABI: amd64|arm64|arm +// Cmdline: (omitted if empty) +// pid: +// signal () +// (blank between sections) +// --- thread 0xTID [(crashed)] --- (per thread; OnThread) +// managed exception: (0x) (only if EE provided one) +// #NN [M] Class.Method + 0xILOFFSET (token=0xTOKEN) (managed frame; WriteFrameToConsole) +// #NN [M] 0xIP (module + 0xOFFSET) (native frame; WriteFrameToConsole) +// (no managed frames) | ... +N more frames (FinishCurrentThreadCompactBlock) +// (blank between threads) +// modules: (EmitConsoleModulesAndFooter) +// [N] {} (one per ModuleTable entry) +// *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** (closing separator) + +static SignalSafeConsoleWriter s_consoleWriter; + // Include the .NET version string instead of linking because it is "static". #if __has_include("_version.c") #include "_version.c" @@ -208,6 +245,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, @@ -238,46 +281,39 @@ 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; - 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 + EmitConsoleHeader(signal); - if (m_processName[0] != '\0') + CrashReportOutputContext outputContext(fd); + if (jsonEnabled) { - m_jsonWriter.WriteString("process_name", m_processName); + m_jsonWriter.Init(&CrashReportOutputContext::ChunkCallback, &outputContext); + } + else + { + m_jsonWriter.Init(&CrashReportHelpers::DiscardOutputCallback, nullptr); } - m_jsonWriter.WriteDecimalAsString("pid", static_cast(GetCurrentProcessId())); + EmitJsonHeader(); m_jsonWriter.OpenArray("threads"); if (m_enumerateThreadsCallback != nullptr) @@ -298,26 +334,11 @@ InProcCrashReporter::CreateReport( } m_jsonWriter.CloseArray(); // threads - m_jsonWriter.CloseObject(); // payload + EmitJsonFooter(signal); - m_jsonWriter.OpenObject("parameters"); - m_jsonWriter.WriteSignedDecimalAsString("signal", static_cast(signal)); -#ifdef __APPLE__ - 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 + EmitConsoleFooter(); - if (fd != -1) + if (jsonEnabled) { bool writeSucceeded = m_jsonWriter.Finish() && !outputContext.WriteFailed() && @@ -328,6 +349,10 @@ InProcCrashReporter::CreateReport( unlink(reportPath); } } + else + { + (void)m_jsonWriter.Finish(); + } } InProcCrashReporter& @@ -445,6 +470,15 @@ CrashReportHelpers::WriteToFile( return true; } +bool +CrashReportHelpers::DiscardOutputCallback( + const char* /*buffer*/, + size_t /*len*/, + void* /*ctx*/) +{ + return true; +} + bool CrashReportOutputContext::HandleChunk( const char* buffer, @@ -1100,3 +1134,90 @@ 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); + } + + s_consoleWriter.WriteKeyValueStr("ABI", CRASHREPORT_ARCHITECTURE_NAME); + + 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(" ("); + s_consoleWriter.AppendStr(PROCGetSignalNameAscii(signal)); + s_consoleWriter.AppendChar(')'); + s_consoleWriter.EndLine(); +} + +void +InProcCrashReporter::EmitConsoleFooter() +{ + s_consoleWriter.WriteSeparator(); +} + +// --- InProcCrashReporter: JSON header and footer --------------------------- + +void +InProcCrashReporter::EmitJsonHeader() +{ + 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); + char version[sizeof(sccsid)]; + CrashReportHelpers::GetVersionString(version, sizeof(version)); + m_jsonWriter.WriteString("version", version); + 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::EmitJsonFooter(int signal) +{ + m_jsonWriter.CloseObject(); // payload + + m_jsonWriter.OpenObject("parameters"); + m_jsonWriter.WriteSignedDecimalAsString("signal", static_cast(signal)); +#ifdef __APPLE__ + 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 +} diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.h b/src/coreclr/debug/crashreport/inproccrashreporter.h index 5018f3b0d10793..c9bedf46e8243c 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.h +++ b/src/coreclr/debug/crashreport/inproccrashreporter.h @@ -88,6 +88,12 @@ class InProcCrashReporter void* context, bool walkStack); + void EmitConsoleHeader(int signal); + void EmitConsoleFooter(); + + void EmitJsonHeader(); + void EmitJsonFooter(int signal); + 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..8d952c6b81017a --- /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 "signalsafeformat.h" + +#include +#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 || 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) +{ + char buf[SignalSafeFormat::MAX_HEX_BUFFER_SIZE]; + SignalSafeFormat::FormatHex(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[SignalSafeFormat::MAX_UNSIGNED_DECIMAL_BUFFER_SIZE]; + SignalSafeFormat::FormatUnsignedDecimal(buf, sizeof(buf), v); + AppendStr(buf); +} + +void +SignalSafeConsoleWriter::AppendSignedDecimal(int64_t v) +{ + char buf[SignalSafeFormat::MAX_SIGNED_DECIMAL_BUFFER_SIZE]; + SignalSafeFormat::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..0a66d2b6bf76cb --- /dev/null +++ b/src/coreclr/debug/crashreport/signalsafeconsolewriter.h @@ -0,0 +1,75 @@ +// 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 the "DOTNET_CRASH" +// tag, stderr elsewhere). 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 a single tag (`adb +// logcat *:S DOTNET_CRASH:F`) without cutting fields in half. On +// Apple/Linux each Flush adds an explicit '\n' for the same reason. +// +// * Unique "DOTNET_CRASH" 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 + +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..6d4336a3a05720 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: + PROCGetSignalNameAscii + + Returns the ASCII name for the given POSIX signal (e.g. "SIGABRT"), or + "Unknown signal" if not recognized. Async-signal-safe. + +Parameters: + signal - POSIX signal number +--*/ +const char* PROCGetSignalNameAscii(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 25902fcea08b8c..f150f54644003f 100644 --- a/src/coreclr/pal/src/thread/process.cpp +++ b/src/coreclr/pal/src/thread/process.cpp @@ -1882,6 +1882,21 @@ static LPCWSTR GetSignalName(int signal) } } +const char* PROCGetSignalNameAscii(int signal) +{ + 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"; + } +} + /*++ Function: PROCLogManagedCallstackForSignal diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index 1670ec970d91ff..02c3bd59de2f86 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; From 70e99c8f98009859c8957cfc576d93646f0106a2 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Mon, 11 May 2026 22:08:42 -0400 Subject: [PATCH 3/5] Emit per-thread frame stacks to the compact crash report log Adds shared frame-sink plumbing so each walked frame can feed both the JSON writer and the compact console writer. The compact log now emits per-thread headers, managed exception info, managed frame lines with IL offset/token, native frame lines with module offsets, and a marker when no managed frames were reported. Both normal thread enumeration and the synthesized crash-thread fallback use the same console block helpers, keeping per-thread compact-log structure in one place. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../debug/crashreport/inproccrashreporter.cpp | 287 +++++++++++++++++- 1 file changed, 278 insertions(+), 9 deletions(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index b80333121371dd..43199996ec9dff 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -53,7 +53,7 @@ static const char CRASHREPORT_ARCHITECTURE_NAME[] = "arm"; // managed exception: (0x) (only if EE provided one) // #NN [M] Class.Method + 0xILOFFSET (token=0xTOKEN) (managed frame; WriteFrameToConsole) // #NN [M] 0xIP (module + 0xOFFSET) (native frame; WriteFrameToConsole) -// (no managed frames) | ... +N more frames (FinishCurrentThreadCompactBlock) +// (no managed frames) (FinishCurrentThreadCompactBlock) // (blank between threads) // modules: (EmitConsoleModulesAndFooter) // [N] {} (one per ModuleTable entry) @@ -94,10 +94,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) { } @@ -108,8 +113,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, @@ -152,9 +158,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; }; @@ -185,6 +196,13 @@ class CrashReportOutputContext class CrashReportHelpers { public: + struct FrameSinks + { + SignalSafeJsonWriter* writer; + SignalSafeConsoleWriter* consoleWriter; + uint32_t* currentThreadFrameCount; + }; + static void GetVersionString( char* buffer, size_t bufferSize); @@ -240,6 +258,54 @@ 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 WriteThreadBlockHeaderToConsole( + SignalSafeConsoleWriter* consoleWriter, + uint64_t osThreadId, + bool isCrashThread); + + static void WriteThreadBlockCloserToConsole( + SignalSafeConsoleWriter* consoleWriter, + uint32_t frameCount); + + 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, @@ -318,10 +384,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()) { @@ -945,6 +1011,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); @@ -987,6 +1077,134 @@ 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::WriteThreadBlockHeaderToConsole( + 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::WriteThreadBlockCloserToConsole( + SignalSafeConsoleWriter* consoleWriter, + uint32_t frameCount) +{ + if (consoleWriter == nullptr) + { + return; + } + + if (frameCount == 0) + { + consoleWriter->WriteLine(" (no managed frames)"); + } +} + +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, @@ -1001,7 +1219,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 @@ -1026,6 +1251,18 @@ ThreadEnumerationContext::FrameCallback( reinterpret_cast(ctx)->OnFrame(ip, stackPointer, methodName, className, moduleName, nativeOffset, token, ilOffset, moduleTimestamp, moduleSize, moduleGuid); } +void +ThreadEnumerationContext::FinishCurrentThreadCompactBlock() +{ + if (m_threadCount == 0) + { + return; + } + + CrashReportHelpers::WriteThreadBlockCloserToConsole(m_consoleWriter, + m_currentThreadFrameCount); +} + void ThreadEnumerationContext::OnThread( uint64_t osThreadId, @@ -1035,6 +1272,8 @@ ThreadEnumerationContext::OnThread( { if (m_threadCount > 0) { + FinishCurrentThreadCompactBlock(); + m_writer->CloseArray(); // stack_frames m_writer->CloseObject(); // thread @@ -1046,6 +1285,7 @@ ThreadEnumerationContext::OnThread( m_sawCrashThread = true; } m_threadCount++; + m_currentThreadFrameCount = 0; m_writer->OpenObject(); m_writer->WriteString("is_managed", "true"); @@ -1068,6 +1308,21 @@ ThreadEnumerationContext::OnThread( { CrashReportHelpers::WriteCrashSiteFrameToJson(m_writer, m_signalContext); } + + if (m_consoleWriter != nullptr) + { + CrashReportHelpers::WriteThreadBlockHeaderToConsole(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(); + } + } } void @@ -1087,21 +1342,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 @@ -1127,10 +1383,23 @@ InProcCrashReporter::EmitSynthesizedCrashThread( CrashReportHelpers::WriteRegistersToJson(&m_jsonWriter, context); m_jsonWriter.OpenArray("stack_frames"); CrashReportHelpers::WriteCrashSiteFrameToJson(&m_jsonWriter, context); + + CrashReportHelpers::WriteThreadBlockHeaderToConsole(&s_consoleWriter, crashingTid, /*isCrashThread*/ true); + + 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); } + CrashReportHelpers::WriteThreadBlockCloserToConsole(&s_consoleWriter, + synthesizedFrameCount); + m_jsonWriter.CloseArray(); // stack_frames m_jsonWriter.CloseObject(); // thread } From ea0570077f7e973df15cba2cc5e83113bd1e3353 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 5 May 2026 12:51:33 -0400 Subject: [PATCH 4/5] Deduplicate modules into a fixed-size table and render frames with module-index references Adds ModuleTable, a 64-entry fixed-capacity table keyed by MVID for one crash report. Compact-log frames can refer to modules by short [N] indices, and the footer emits a modules block that maps each index back to the module filename and MVID. If a managed frame's module cannot be stored because the table is full or the GUID is missing, the frame renders the module name inline as (in ) instead of using a lossy placeholder. JSON output is unchanged; module indices are only a compact-log representation detail. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../debug/crashreport/inproccrashreporter.cpp | 110 +++++++++++++++++- .../debug/crashreport/inproccrashreporter.h | 2 +- 2 files changed, 107 insertions(+), 5 deletions(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index 43199996ec9dff..51a330f8651b56 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #ifdef __APPLE__ #include @@ -52,7 +53,9 @@ static const char CRASHREPORT_ARCHITECTURE_NAME[] = "arm"; // --- thread 0xTID [(crashed)] --- (per thread; OnThread) // managed exception: (0x) (only if EE provided one) // #NN [M] Class.Method + 0xILOFFSET (token=0xTOKEN) (managed frame; WriteFrameToConsole) +// #NN (in ) Class.Method + 0xILOFFSET (token=0xTOKEN) (overflow form: module didn't fit the table) // #NN [M] 0xIP (module + 0xOFFSET) (native frame; WriteFrameToConsole) +// #NN 0xIP (module + 0xOFFSET) (native frame not in module table) // (no managed frames) (FinishCurrentThreadCompactBlock) // (blank between threads) // modules: (EmitConsoleModulesAndFooter) @@ -89,6 +92,73 @@ static void CacheSysctlString(const char* sysctlName, char* buffer, size_t buffe } #endif // __APPLE__ +// Bounded module name/GUID 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 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 at MAX_MODULES_IN_TABLE (no heap on the fatal-signal +// path). A managed frame whose module didn't fit (table full, or empty/null GUID) +// 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 size_t MAX_MODULES_IN_TABLE = 64; + +class ModuleTable +{ +public: + int GetOrAddIndex(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 = 0; +}; + +static ModuleTable s_moduleTable; + class ThreadEnumerationContext { public: @@ -275,6 +345,7 @@ class CrashReportHelpers static void WriteFrameToConsole( SignalSafeConsoleWriter* consoleWriter, uint32_t frameIndex, + int moduleIndex, uint64_t ip, const char* methodName, const char* className, @@ -402,7 +473,7 @@ InProcCrashReporter::CreateReport( EmitJsonFooter(signal); - EmitConsoleFooter(); + EmitConsoleModulesAndFooter(); if (jsonEnabled) { @@ -1081,6 +1152,7 @@ void CrashReportHelpers::WriteFrameToConsole( SignalSafeConsoleWriter* consoleWriter, uint32_t frameIndex, + int moduleIndex, uint64_t ip, const char* methodName, const char* className, @@ -1094,7 +1166,6 @@ CrashReportHelpers::WriteFrameToConsole( return; } - // Frame index always two digits ("#04 ..."); matches Android/AOSP debuggerd. consoleWriter->AppendStr(" #"); if (frameIndex < 10) { @@ -1103,6 +1174,19 @@ CrashReportHelpers::WriteFrameToConsole( consoleWriter->AppendDecimal(static_cast(frameIndex)); consoleWriter->AppendChar(' '); + if (moduleIndex >= 0) + { + consoleWriter->AppendChar('['); + consoleWriter->AppendDecimal(static_cast(moduleIndex)); + consoleWriter->AppendStr("] "); + } + else if (methodName != nullptr && moduleName != nullptr && moduleName[0] != '\0') + { + consoleWriter->AppendStr("(in "); + consoleWriter->AppendStr(GetFilename(moduleName)); + consoleWriter->AppendStr(") "); + } + if (methodName != nullptr) { char fullName[CRASHREPORT_STRING_BUFFER_SIZE]; @@ -1193,10 +1277,12 @@ CrashReportHelpers::FrameSinkCallback( ? *sinks->currentThreadFrameCount : 0; + int moduleIndex = s_moduleTable.GetOrAddIndex(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) @@ -1439,8 +1525,24 @@ InProcCrashReporter::EmitConsoleHeader(int signal) } void -InProcCrashReporter::EmitConsoleFooter() +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 c9bedf46e8243c..8b3e0352f581f6 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.h +++ b/src/coreclr/debug/crashreport/inproccrashreporter.h @@ -89,7 +89,7 @@ class InProcCrashReporter bool walkStack); void EmitConsoleHeader(int signal); - void EmitConsoleFooter(); + void EmitConsoleModulesAndFooter(); void EmitJsonHeader(); void EmitJsonFooter(int signal); From 86f46592db8a38de959eab4dc139f24f22b26def Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 5 May 2026 12:54:34 -0400 Subject: [PATCH 5/5] Cap frames per thread in the compact crash report log Adds DOTNET_CrashReportFrameLimitPerThread, parsed as base 10 with default 32, to cap the number of frames written per thread to the compact log. Setting the value to 0 disables the limit. Frames past the cap are still emitted to the JSON report. The compact log skips only the console frame line, tracks how many frames were omitted for the current thread, and emits an "... +N more frames" summary in the thread footer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../debug/crashreport/inproccrashreporter.cpp | 50 ++++++++++++++++--- .../debug/crashreport/inproccrashreporter.h | 2 + src/coreclr/inc/clrconfigvalues.h | 1 + src/coreclr/vm/crashreportstackwalker.cpp | 1 + 4 files changed, 46 insertions(+), 8 deletions(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index 51a330f8651b56..51304b2a86a505 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -56,7 +56,7 @@ static const char CRASHREPORT_ARCHITECTURE_NAME[] = "arm"; // #NN (in ) Class.Method + 0xILOFFSET (token=0xTOKEN) (overflow form: module didn't fit the table) // #NN [M] 0xIP (module + 0xOFFSET) (native frame; WriteFrameToConsole) // #NN 0xIP (module + 0xOFFSET) (native frame not in module table) -// (no managed frames) (FinishCurrentThreadCompactBlock) +// (no managed frames) | ... +N more frames (FinishCurrentThreadCompactBlock) // (blank between threads) // modules: (EmitConsoleModulesAndFooter) // [N] {} (one per ModuleTable entry) @@ -166,6 +166,7 @@ class ThreadEnumerationContext SignalSafeJsonWriter* writer, SignalSafeConsoleWriter* consoleWriter, uint64_t crashingTid, + uint32_t frameLimitPerThread, void* signalContext) : m_writer(writer), m_consoleWriter(consoleWriter), @@ -173,6 +174,8 @@ class ThreadEnumerationContext m_threadCount(0), m_crashingTid(crashingTid), m_currentThreadFrameCount(0), + m_currentThreadDroppedCount(0), + m_frameLimitPerThread(frameLimitPerThread), m_sawCrashThread(false) { } @@ -236,6 +239,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; }; @@ -271,6 +276,8 @@ class CrashReportHelpers SignalSafeJsonWriter* writer; SignalSafeConsoleWriter* consoleWriter; uint32_t* currentThreadFrameCount; + uint32_t* currentThreadDroppedCount; + uint32_t frameLimitPerThread; }; static void GetVersionString( @@ -361,7 +368,8 @@ class CrashReportHelpers static void WriteThreadBlockCloserToConsole( SignalSafeConsoleWriter* consoleWriter, - uint32_t frameCount); + uint32_t frameCount, + uint32_t droppedCount); static void FrameSinkCallback( uint64_t ip, @@ -456,7 +464,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); @@ -506,6 +514,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'; @@ -1239,7 +1248,8 @@ CrashReportHelpers::WriteThreadBlockHeaderToConsole( void CrashReportHelpers::WriteThreadBlockCloserToConsole( SignalSafeConsoleWriter* consoleWriter, - uint32_t frameCount) + uint32_t frameCount, + uint32_t droppedCount) { if (consoleWriter == nullptr) { @@ -1250,6 +1260,13 @@ CrashReportHelpers::WriteThreadBlockCloserToConsole( { consoleWriter->WriteLine(" (no managed frames)"); } + else if (droppedCount != 0) + { + consoleWriter->AppendStr(" ... +"); + consoleWriter->AppendDecimal(static_cast(droppedCount)); + consoleWriter->AppendStr(" more frames"); + consoleWriter->EndLine(); + } } void @@ -1279,11 +1296,22 @@ CrashReportHelpers::FrameSinkCallback( int moduleIndex = s_moduleTable.GetOrAddIndex(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) { @@ -1310,6 +1338,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); @@ -1346,7 +1376,7 @@ ThreadEnumerationContext::FinishCurrentThreadCompactBlock() } CrashReportHelpers::WriteThreadBlockCloserToConsole(m_consoleWriter, - m_currentThreadFrameCount); + m_currentThreadFrameCount, m_currentThreadDroppedCount); } void @@ -1372,6 +1402,7 @@ ThreadEnumerationContext::OnThread( } m_threadCount++; m_currentThreadFrameCount = 0; + m_currentThreadDroppedCount = 0; m_writer->OpenObject(); m_writer->WriteString("is_managed", "true"); @@ -1473,6 +1504,7 @@ InProcCrashReporter::EmitSynthesizedCrashThread( CrashReportHelpers::WriteThreadBlockHeaderToConsole(&s_consoleWriter, crashingTid, /*isCrashThread*/ true); uint32_t synthesizedFrameCount = 0; + uint32_t synthesizedDroppedCount = 0; if (walkStack && m_walkStackCallback != nullptr) { CrashReportHelpers::FrameSinks sinks = @@ -1480,11 +1512,13 @@ InProcCrashReporter::EmitSynthesizedCrashThread( &m_jsonWriter, &s_consoleWriter, &synthesizedFrameCount, + &synthesizedDroppedCount, + m_frameLimitPerThread, }; m_walkStackCallback(&CrashReportHelpers::FrameSinkCallback, &sinks); } CrashReportHelpers::WriteThreadBlockCloserToConsole(&s_consoleWriter, - synthesizedFrameCount); + synthesizedFrameCount, synthesizedDroppedCount); 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 8b3e0352f581f6..e395a581856ecf 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 @@ -105,6 +106,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..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 02c3bd59de2f86..0509cc6e836c85 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -433,6 +433,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.