From 22d283c7ea5a107ad93e1c4f180d7da05b7a1a58 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 14 Apr 2026 10:01:59 -0400 Subject: [PATCH 01/52] Add PAL foundation for in-proc crash reporting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/pal/src/CMakeLists.txt | 7 + .../pal/src/crashreport/crashjsonwriter.cpp | 211 ++++++++++++++++++ .../pal/src/crashreport/crashjsonwriter.h | 32 +++ .../src/crashreport/inproccrashreporter.cpp | 152 +++++++++++++ .../pal/src/crashreport/inproccrashreporter.h | 14 ++ src/coreclr/pal/src/exception/signal.cpp | 11 +- src/coreclr/pal/src/include/pal/process.h | 1 + src/coreclr/pal/src/thread/process.cpp | 53 ++++- 8 files changed, 468 insertions(+), 13 deletions(-) create mode 100644 src/coreclr/pal/src/crashreport/crashjsonwriter.cpp create mode 100644 src/coreclr/pal/src/crashreport/crashjsonwriter.h create mode 100644 src/coreclr/pal/src/crashreport/inproccrashreporter.cpp create mode 100644 src/coreclr/pal/src/crashreport/inproccrashreporter.h diff --git a/src/coreclr/pal/src/CMakeLists.txt b/src/coreclr/pal/src/CMakeLists.txt index 970c9cb7dabef5..175a4d2610879a 100644 --- a/src/coreclr/pal/src/CMakeLists.txt +++ b/src/coreclr/pal/src/CMakeLists.txt @@ -206,6 +206,13 @@ set(SOURCES thread/threadsusp.cpp ) +if(CLR_CMAKE_TARGET_ANDROID) + list(APPEND SOURCES + crashreport/crashjsonwriter.cpp + crashreport/inproccrashreporter.cpp + ) +endif() + set_source_files_properties( com/guid.cpp PROPERTIES diff --git a/src/coreclr/pal/src/crashreport/crashjsonwriter.cpp b/src/coreclr/pal/src/crashreport/crashjsonwriter.cpp new file mode 100644 index 00000000000000..f89d11bb2ecdc0 --- /dev/null +++ b/src/coreclr/pal/src/crashreport/crashjsonwriter.cpp @@ -0,0 +1,211 @@ +// 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 JSON writer implementation. +// Every function here uses only stack variables and the pre-allocated buffer. +// No malloc, no stdio, no locks — safe to call from a signal handler. + +#include "crashjsonwriter.h" + +static +int +CrashJsonAppend( + CrashJsonWriter* w, + const char* str, + int len); + +static +int +CrashJsonAppendStr( + CrashJsonWriter* w, + const char* str); + +static +char +ToHexChar( + unsigned value); + +static +void +CrashJsonWriteSeparator( + CrashJsonWriter* w); + +static +void +CrashJsonWriteEscapedString( + CrashJsonWriter* w, + const char* str); + +void +CrashJsonInit( + CrashJsonWriter* w) +{ + w->pos = 0; + w->commaNeeded = false; + w->buffer[0] = '\0'; +} + +void +CrashJsonOpenObject( + CrashJsonWriter* w, + const char* key) +{ + CrashJsonWriteSeparator(w); + if (key != NULL) + { + CrashJsonWriteEscapedString(w, key); + CrashJsonAppendStr(w, ": "); + } + CrashJsonAppendStr(w, "{"); + w->commaNeeded = false; +} + +void +CrashJsonCloseObject( + CrashJsonWriter* w) +{ + CrashJsonAppendStr(w, "}"); + w->commaNeeded = true; +} + +void +CrashJsonOpenArray( + CrashJsonWriter* w, + const char* key) +{ + CrashJsonWriteSeparator(w); + if (key != NULL) + { + CrashJsonWriteEscapedString(w, key); + CrashJsonAppendStr(w, ": "); + } + CrashJsonAppendStr(w, "["); + w->commaNeeded = false; +} + +void +CrashJsonCloseArray( + CrashJsonWriter* w) +{ + CrashJsonAppendStr(w, "]"); + w->commaNeeded = true; +} + +void +CrashJsonWriteString( + CrashJsonWriter* w, + const char* key, + const char* value) +{ + CrashJsonWriteSeparator(w); + CrashJsonWriteEscapedString(w, key); + CrashJsonAppendStr(w, ": "); + CrashJsonWriteEscapedString(w, value); +} + +int +CrashJsonGetLength( + CrashJsonWriter* w) +{ + return w->pos; +} + +const char* +CrashJsonGetBuffer( + CrashJsonWriter* w) +{ + w->buffer[w->pos] = '\0'; + return w->buffer; +} + +// Append raw bytes to buffer. Returns 0 if out of space. +int +CrashJsonAppend( + CrashJsonWriter* w, + const char* str, + int len) +{ + if (w->pos + len >= CRASH_JSON_BUFFER_SIZE - 16) + return 0; + + for (int i = 0; i < len; i++) + { + w->buffer[w->pos + i] = str[i]; + } + + w->pos += len; + return 1; +} + +int +CrashJsonAppendStr( + CrashJsonWriter* w, + const char* str) +{ + int len = 0; + while (str[len]) + len++; + + return CrashJsonAppend(w, str, len); +} + +char +ToHexChar( + unsigned value) +{ + return (value < 10) ? (char)('0' + value) : (char)('a' + (value - 10)); +} + +void +CrashJsonWriteSeparator( + CrashJsonWriter* w) +{ + if (w->commaNeeded) + CrashJsonAppendStr(w, ","); + + w->commaNeeded = true; +} + +// Escape a string value for JSON. Handles \, ", and control characters. +void +CrashJsonWriteEscapedString( + CrashJsonWriter* w, + const char* str) +{ + CrashJsonAppendStr(w, "\""); + if (str != NULL) + { + for (int i = 0; str[i]; i++) + { + char c = str[i]; + if (c == '"') + CrashJsonAppendStr(w, "\\\""); + else if (c == '\\') + CrashJsonAppendStr(w, "\\\\"); + else if (c == '\n') + CrashJsonAppendStr(w, "\\n"); + else if (c == '\r') + CrashJsonAppendStr(w, "\\r"); + else if (c == '\t') + CrashJsonAppendStr(w, "\\t"); + else if ((unsigned char)c < 0x20) + { + char esc[7]; + esc[0] = '\\'; + esc[1] = 'u'; + esc[2] = '0'; + esc[3] = '0'; + esc[4] = ToHexChar(((unsigned char)c >> 4) & 0xF); + esc[5] = ToHexChar((unsigned char)c & 0xF); + esc[6] = '\0'; + CrashJsonAppendStr(w, esc); + } + else + { + CrashJsonAppend(w, &c, 1); + } + } + } + + CrashJsonAppendStr(w, "\""); +} diff --git a/src/coreclr/pal/src/crashreport/crashjsonwriter.h b/src/coreclr/pal/src/crashreport/crashjsonwriter.h new file mode 100644 index 00000000000000..bde4dcf788d132 --- /dev/null +++ b/src/coreclr/pal/src/crashreport/crashjsonwriter.h @@ -0,0 +1,32 @@ +// 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 JSON writer for crash reports. +// Writes to a pre-allocated fixed-size buffer using only signal-safe operations. +// No malloc, no stdio, no locks. + +#pragma once + +#include + +// Fixed buffer size for the JSON crash report. +// 32KB leaves room for multiple thread/frame entries while staying heap-free. +#define CRASH_JSON_BUFFER_SIZE (32 * 1024) // 32KB + +struct CrashJsonWriter +{ + char buffer[CRASH_JSON_BUFFER_SIZE]; + int pos; + bool commaNeeded; + + // All methods below are async-signal-safe (no malloc, no locks) +}; + +void CrashJsonInit(CrashJsonWriter* w); +void CrashJsonOpenObject(CrashJsonWriter* w, const char* key); +void CrashJsonCloseObject(CrashJsonWriter* w); +void CrashJsonOpenArray(CrashJsonWriter* w, const char* key); +void CrashJsonCloseArray(CrashJsonWriter* w); +void CrashJsonWriteString(CrashJsonWriter* w, const char* key, const char* value); +int CrashJsonGetLength(CrashJsonWriter* w); +const char* CrashJsonGetBuffer(CrashJsonWriter* w); diff --git a/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp b/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp new file mode 100644 index 00000000000000..aa3356fc115e04 --- /dev/null +++ b/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp @@ -0,0 +1,152 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// In-proc crash report generator. +// +// Emits a createdump-shaped JSON skeleton to logcat / stderr. + +#include "inproccrashreporter.h" +#include "crashjsonwriter.h" + +#include + +#ifdef __ANDROID__ +#include +#endif + +static CrashJsonWriter s_jsonWriter; + +static +void +WriteToLog( + const char* msg, + int len); + +void +InProcCrashReportGenerate( + int signal, + siginfo_t* siginfo, + void* context) +{ + (void)signal; + (void)siginfo; + (void)context; + + CrashJsonInit(&s_jsonWriter); + + CrashJsonOpenObject(&s_jsonWriter, NULL); + CrashJsonOpenObject(&s_jsonWriter, "payload"); + CrashJsonWriteString(&s_jsonWriter, "protocol_version", "1.0.0"); + + CrashJsonOpenObject(&s_jsonWriter, "configuration"); +#if defined(__x86_64__) + CrashJsonWriteString(&s_jsonWriter, "architecture", "amd64"); +#elif defined(__aarch64__) + CrashJsonWriteString(&s_jsonWriter, "architecture", "arm64"); +#elif defined(__arm__) + CrashJsonWriteString(&s_jsonWriter, "architecture", "arm"); +#endif + CrashJsonWriteString(&s_jsonWriter, "version", ""); + CrashJsonCloseObject(&s_jsonWriter); + + CrashJsonWriteString(&s_jsonWriter, "process_name", ""); + + CrashJsonOpenArray(&s_jsonWriter, "threads"); + // TODO: Replace with actual thread enumeration. + int threadCount = 0; + for (int threadIndex = 0; threadIndex < 0; threadIndex++) + { + CrashJsonOpenObject(&s_jsonWriter, NULL); + CrashJsonWriteString(&s_jsonWriter, "is_managed", "false"); + CrashJsonWriteString(&s_jsonWriter, "crashed", "false"); + CrashJsonWriteString(&s_jsonWriter, "native_thread_id", "0x0"); + + CrashJsonOpenObject(&s_jsonWriter, "ctx"); + CrashJsonWriteString(&s_jsonWriter, "IP", "0x0"); + CrashJsonWriteString(&s_jsonWriter, "SP", "0x0"); + CrashJsonWriteString(&s_jsonWriter, "BP", "0x0"); + CrashJsonCloseObject(&s_jsonWriter); + + CrashJsonOpenArray(&s_jsonWriter, "stack_frames"); + // TODO: Replace with actual frame enumeration. + int stackFrameCount = 0; + for (int stackFrameIndex = 0; stackFrameIndex < stackFrameCount; stackFrameIndex++) + { + CrashJsonOpenObject(&s_jsonWriter, NULL); + CrashJsonWriteString(&s_jsonWriter, "is_managed", "false"); + CrashJsonWriteString(&s_jsonWriter, "module_address", "0x0"); + CrashJsonWriteString(&s_jsonWriter, "stack_pointer", "0x0"); + CrashJsonWriteString(&s_jsonWriter, "native_address", "0x0"); + CrashJsonCloseObject(&s_jsonWriter); + } + CrashJsonCloseArray(&s_jsonWriter); + + CrashJsonCloseObject(&s_jsonWriter); + } + CrashJsonCloseArray(&s_jsonWriter); + + CrashJsonCloseObject(&s_jsonWriter); + + CrashJsonOpenObject(&s_jsonWriter, "parameters"); + CrashJsonWriteString(&s_jsonWriter, "ExceptionType", "0x00000000"); +#ifdef __APPLE__ + CrashJsonWriteString(&s_jsonWriter, "OSVersion", ""); + CrashJsonWriteString(&s_jsonWriter, "SystemModel", ""); + CrashJsonWriteString(&s_jsonWriter, "SystemManufacturer", "apple"); +#endif + CrashJsonCloseObject(&s_jsonWriter); + + CrashJsonCloseObject(&s_jsonWriter); + + WriteToLog(CrashJsonGetBuffer(&s_jsonWriter), CrashJsonGetLength(&s_jsonWriter)); +} + +void +WriteToLog( + const char* msg, + int len) +{ +#ifdef __ANDROID__ + if (msg == NULL) + { + return; + } + + if (len < 0) + { + len = 0; + while (msg[len] != '\0') + { + len++; + } + } + + // Emit long payloads in chunks so the JSON is not truncated by Android's + // per-entry log size limit. + int offset = 0; + while (offset < len) + { + int chunk = len - offset; + if (chunk > 3000) + { + chunk = 3000; + } + + char buffer[3001]; + for (int i = 0; i < chunk; i++) + { + buffer[i] = msg[offset + i]; + } + + buffer[chunk] = '\0'; + // TODO-Async: Prefer Android's async_safe/log.h entrypoints here if they + // become available through the supported NDK surface. __android_log_write + // keeps the crash report visible in logcat, but it doesn't document an + // async-signal-safe contract. + __android_log_write(ANDROID_LOG_ERROR, "DOTNET", buffer); + offset += chunk; + } +#else + write(STDERR_FILENO, msg, len); +#endif +} diff --git a/src/coreclr/pal/src/crashreport/inproccrashreporter.h b/src/coreclr/pal/src/crashreport/inproccrashreporter.h new file mode 100644 index 00000000000000..a0e407cb6f95f3 --- /dev/null +++ b/src/coreclr/pal/src/crashreport/inproccrashreporter.h @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// In-proc crash report generation. +// +// Emits a minimal createdump-shaped JSON payload to logcat / stderr. + +#pragma once + +#include + +// Generate an in-proc crash report. Called from PROCCreateCrashDumpIfEnabled. +// All arguments come from the signal handler and are signal-safe to read. +void InProcCrashReportGenerate(int signal, siginfo_t* siginfo, void* context); diff --git a/src/coreclr/pal/src/exception/signal.cpp b/src/coreclr/pal/src/exception/signal.cpp index c27b6e6f92006b..2f2620b430aba7 100644 --- a/src/coreclr/pal/src/exception/signal.cpp +++ b/src/coreclr/pal/src/exception/signal.cpp @@ -471,8 +471,10 @@ static void invoke_previous_action(struct sigaction* action, int code, siginfo_t if (g_crash_report_before_signal_chaining) { PROCNotifyProcessShutdown(IsRunningOnAlternateStack(context)); - - PROCLogManagedCallstackForSignal(code); + if (!PROCIsCrashReportEnabled()) + { + PROCLogManagedCallstackForSignal(code); + } PROCCreateCrashDumpIfEnabled(code, siginfo, context, true); } @@ -493,7 +495,10 @@ static void invoke_previous_action(struct sigaction* action, int code, siginfo_t { PROCNotifyProcessShutdown(IsRunningOnAlternateStack(context)); - PROCLogManagedCallstackForSignal(code); + if (!PROCIsCrashReportEnabled()) + { + PROCLogManagedCallstackForSignal(code); + } PROCCreateCrashDumpIfEnabled(code, siginfo, context, true); } } diff --git a/src/coreclr/pal/src/include/pal/process.h b/src/coreclr/pal/src/include/pal/process.h index e3f26bde875a03..0b7f84e002552d 100644 --- a/src/coreclr/pal/src/include/pal/process.h +++ b/src/coreclr/pal/src/include/pal/process.h @@ -157,6 +157,7 @@ VOID PROCNotifyProcessShutdown(bool isExecutingOnAltStack = false); (no return value) --*/ VOID PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, void* context, bool serialize); +BOOL PROCIsCrashReportEnabled(); /*++ Function: diff --git a/src/coreclr/pal/src/thread/process.cpp b/src/coreclr/pal/src/thread/process.cpp index 31996bb4c3e78c..63350be39420f4 100644 --- a/src/coreclr/pal/src/thread/process.cpp +++ b/src/coreclr/pal/src/thread/process.cpp @@ -35,6 +35,10 @@ SET_DEFAULT_DEBUG_CHANNEL(PROCESS); // some headers have code with asserts, so d #include #include +#ifdef HOST_ANDROID +#include "crashreport/inproccrashreporter.h" +#endif + #include #if HAVE_POLL #include @@ -190,6 +194,10 @@ Volatile g_logManagedCallstackForSignalC #define MAX_ARGV_ENTRIES 32 const char* g_argvCreateDump[MAX_ARGV_ENTRIES] = { nullptr }; +#ifdef HOST_ANDROID +static bool g_inProcCrashReportEnabled = false; +#endif + // // Key used for associating CPalThread's with the underlying pthread // (through pthread_setspecific) @@ -2618,11 +2626,25 @@ PROCAbortInitialize() CLRConfigNoCache enabledCfg = CLRConfigNoCache::Get("DbgEnableMiniDump", /*noprefix*/ false, &getenv); DWORD enabled = 0; - if (enabledCfg.IsSet() && enabledCfg.TryAsInteger(10, enabled) && enabled) - { - CLRConfigNoCache dmpNameCfg = CLRConfigNoCache::Get("DbgMiniDumpName", /*noprefix*/ false, &getenv); - const char* dumpName = dmpNameCfg.IsSet() ? dmpNameCfg.AsString() : nullptr; + bool enableMiniDump = enabledCfg.IsSet() && enabledCfg.TryAsInteger(10, enabled) && enabled != 0; + + CLRConfigNoCache dmpNameCfg = CLRConfigNoCache::Get("DbgMiniDumpName", /*noprefix*/ false, &getenv); + const char* dumpName = dmpNameCfg.IsSet() ? dmpNameCfg.AsString() : nullptr; + + CLRConfigNoCache enabledReportCfg = CLRConfigNoCache::Get("EnableCrashReport", /*noprefix*/ false, &getenv); + DWORD reportEnabled = 0; + bool enableCrashReport = enabledReportCfg.IsSet() && enabledReportCfg.TryAsInteger(10, reportEnabled) && reportEnabled == 1; + + CLRConfigNoCache enabledReportOnlyCfg = CLRConfigNoCache::Get("EnableCrashReportOnly", /*noprefix*/ false, &getenv); + DWORD reportOnlyEnabled = 0; + bool enableCrashReportOnly = enabledReportOnlyCfg.IsSet() && enabledReportOnlyCfg.TryAsInteger(10, reportOnlyEnabled) && reportOnlyEnabled == 1; + +#ifdef HOST_ANDROID + g_inProcCrashReportEnabled = enableMiniDump || enableCrashReport || enableCrashReportOnly; +#endif + if (enableMiniDump) + { CLRConfigNoCache dmpLogToFileCfg = CLRConfigNoCache::Get("CreateDumpLogToFile", /*noprefix*/ false, &getenv); const char* logFilePath = dmpLogToFileCfg.IsSet() ? dmpLogToFileCfg.AsString() : nullptr; @@ -2650,15 +2672,11 @@ PROCAbortInitialize() { flags |= GenerateDumpFlagsVerboseLoggingEnabled; } - CLRConfigNoCache enabledReportCfg = CLRConfigNoCache::Get("EnableCrashReport", /*noprefix*/ false, &getenv); - val = 0; - if (enabledReportCfg.IsSet() && enabledReportCfg.TryAsInteger(10, val) && val == 1) + if (enableCrashReport) { flags |= GenerateDumpFlagsCrashReportEnabled; } - CLRConfigNoCache enabledReportOnlyCfg = CLRConfigNoCache::Get("EnableCrashReportOnly", /*noprefix*/ false, &getenv); - val = 0; - if (enabledReportOnlyCfg.IsSet() && enabledReportOnlyCfg.TryAsInteger(10, val) && val == 1) + if (enableCrashReportOnly) { flags |= GenerateDumpFlagsCrashReportOnlyEnabled; } @@ -2771,6 +2789,16 @@ PROCLogManagedCallstackForSignal(int signal) } } +BOOL +PROCIsCrashReportEnabled() +{ +#ifdef HOST_ANDROID + return g_inProcCrashReportEnabled ? TRUE : FALSE; +#else + return FALSE; +#endif +} + /*++ Function: PROCCreateCrashDumpIfEnabled @@ -2788,6 +2816,7 @@ PROCLogManagedCallstackForSignal(int signal) --*/ #ifdef HOST_ANDROID #include +#include "crashreport/inproccrashreporter.h" VOID PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, void* context, bool serialize) { @@ -2795,6 +2824,10 @@ PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, void* context, bool DoNotOptimize(&context); // TODO: Dump stress log into logcat and/or file when enabled? + if (g_inProcCrashReportEnabled) + { + InProcCrashReportGenerate(signal, siginfo, context); + } minipal_log_write_fatal("Aborting process.\n"); } #else From b866409cb37a7396efb3b31e3250e634fdb30f42 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 14 Apr 2026 11:02:29 -0400 Subject: [PATCH 02/52] Resolve crash report version field Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/crashreport/inproccrashreporter.cpp | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp b/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp index aa3356fc115e04..0ccd6d49333d3e 100644 --- a/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp @@ -9,13 +9,23 @@ #include "crashjsonwriter.h" #include +#include #ifdef __ANDROID__ #include #endif +// Include the .NET version string instead of linking because it is "static". +#include "_version.c" + static CrashJsonWriter s_jsonWriter; +static +void +GetVersionString( + char* buffer, + int bufferSize); + static void WriteToLog( @@ -46,7 +56,9 @@ InProcCrashReportGenerate( #elif defined(__arm__) CrashJsonWriteString(&s_jsonWriter, "architecture", "arm"); #endif - CrashJsonWriteString(&s_jsonWriter, "version", ""); + char version[sizeof(sccsid) + 1]; + GetVersionString(version, sizeof(version)); + CrashJsonWriteString(&s_jsonWriter, "version", version); CrashJsonCloseObject(&s_jsonWriter); CrashJsonWriteString(&s_jsonWriter, "process_name", ""); @@ -150,3 +162,40 @@ WriteToLog( write(STDERR_FILENO, msg, len); #endif } + +void +GetVersionString( + char* buffer, + int bufferSize) +{ + if (buffer == NULL || bufferSize <= 0) + { + return; + } + + if (bufferSize == 1) + { + buffer[0] = '\0'; + return; + } + + buffer[0] = '\0'; + + const char* version = sccsid; + const char versionPrefix[] = "@(#)Version "; + if (strncmp(version, versionPrefix, sizeof(versionPrefix) - 1) != 0) + { + return; + } + version += sizeof(versionPrefix) - 1; + + int index = 0; + while (version[index] != '\0' && index < bufferSize - 2) + { + buffer[index] = version[index]; + index++; + } + + buffer[index++] = ' '; + buffer[index] = '\0'; +} From ef2c1289f14d12922caf540530f5b9b988d7f90e Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 14 Apr 2026 11:15:06 -0400 Subject: [PATCH 03/52] Resolve crash report process name Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/pal/src/CMakeLists.txt | 1 + .../src/crashreport/inproccrashreporter.cpp | 7 +- .../pal/src/crashreport/moduleenumerator.cpp | 250 ++++++++++++++++++ .../pal/src/crashreport/moduleenumerator.h | 12 + 4 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 src/coreclr/pal/src/crashreport/moduleenumerator.cpp create mode 100644 src/coreclr/pal/src/crashreport/moduleenumerator.h diff --git a/src/coreclr/pal/src/CMakeLists.txt b/src/coreclr/pal/src/CMakeLists.txt index 175a4d2610879a..14799c9f1a945b 100644 --- a/src/coreclr/pal/src/CMakeLists.txt +++ b/src/coreclr/pal/src/CMakeLists.txt @@ -210,6 +210,7 @@ if(CLR_CMAKE_TARGET_ANDROID) list(APPEND SOURCES crashreport/crashjsonwriter.cpp crashreport/inproccrashreporter.cpp + crashreport/moduleenumerator.cpp ) endif() diff --git a/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp b/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp index 0ccd6d49333d3e..5507c7f7f81dee 100644 --- a/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp @@ -7,6 +7,7 @@ #include "inproccrashreporter.h" #include "crashjsonwriter.h" +#include "moduleenumerator.h" #include #include @@ -61,7 +62,11 @@ InProcCrashReportGenerate( CrashJsonWriteString(&s_jsonWriter, "version", version); CrashJsonCloseObject(&s_jsonWriter); - CrashJsonWriteString(&s_jsonWriter, "process_name", ""); + char processName[256]; + if (CrashModulesTryGetProcessName(processName, sizeof(processName))) + { + CrashJsonWriteString(&s_jsonWriter, "process_name", processName); + } CrashJsonOpenArray(&s_jsonWriter, "threads"); // TODO: Replace with actual thread enumeration. diff --git a/src/coreclr/pal/src/crashreport/moduleenumerator.cpp b/src/coreclr/pal/src/crashreport/moduleenumerator.cpp new file mode 100644 index 00000000000000..eceee702bdef2e --- /dev/null +++ b/src/coreclr/pal/src/crashreport/moduleenumerator.cpp @@ -0,0 +1,250 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Crash report process-name lookup helper. +// Parses /proc/self/cmdline and /proc/self/maps using only open/read/close. +// No stdio, no sscanf, no heap allocation. + +#include "moduleenumerator.h" +#include +#include +#include + +typedef void (*ModuleCallback)(const char* filename, void* ctx); + +struct ProcessNameCtx +{ + char* filename; + int filenameLen; + int found; +}; + +static +const char* +GetFilename( + const char* path); + +void +CopyFilename( + char* filename, + int filenameLen, + const char* source); + +static +void +ParseMapsLine( + const char* line, + ModuleCallback callback, + void* ctx, + char* lastModule, + int lastModuleSize); + +static +void +EnumerateModules( + ModuleCallback callback, + void* ctx); + +static +void +ProcessNameCallback( + const char* filename, + void* ctx); + +const char* +GetFilename( + const char* path) +{ + const char* last = path; + for (const char* p = path; *p; p++) + { + if (*p == '/') + last = p + 1; + } + return last; +} + +void +CopyFilename( + char* filename, + int filenameLen, + const char* source) +{ + if (filename == NULL || filenameLen <= 0 || source == NULL) + { + return; + } + + int len = 0; + while (source[len] && len < filenameLen - 1) + { + filename[len] = source[len]; + len++; + } + filename[len] = '\0'; +} + +void +ParseMapsLine( + const char* line, + ModuleCallback callback, + void* ctx, + char* lastModule, + int lastModuleSize) +{ + const char* p = line; + while (*p != '\0' && *p != ' ') + { + p++; + } + + if (*p != ' ') + { + return; + } + p++; + + const char* permissions = p; + while (*p != '\0' && *p != ' ') + { + p++; + } + + // We only care about named executable image mappings because they can + // identify the current process image when /proc/self/cmdline is unavailable. + if (*p != ' ' || (p - permissions <= 2) || permissions[2] != 'x') + { + return; + } + p++; + + int spacesToSkip = 3; + while (*p != '\0' && spacesToSkip > 0) + { + if (*p == ' ') + { + while (*p == ' ') + { + p++; + } + spacesToSkip--; + } + else + { + p++; + } + } + + if (spacesToSkip != 0 || *p == '\0' || *p == '\n' || *p == '[') + { + return; + } + + char pathname[256]; + int pathnameLen = 0; + while (p[pathnameLen] != '\0' && p[pathnameLen] != '\n' && pathnameLen < sizeof(pathname) - 1) + { + pathname[pathnameLen] = p[pathnameLen]; + pathnameLen++; + } + pathname[pathnameLen] = '\0'; + + if (strcmp(pathname, lastModule) == 0) + { + return; + } + + CopyFilename(lastModule, lastModuleSize, pathname); + callback(GetFilename(pathname), ctx); +} + +void +EnumerateModules( + ModuleCallback callback, + void* ctx) +{ + // Walk the live executable mappings as a crash-time fallback for process + // name resolution without depending on loader APIs or heap allocation. + int fd = open("/proc/self/maps", O_RDONLY); + if (fd == -1) + { + return; + } + + char readBuf[4096]; + char lineBuf[512]; + int linePos = 0; + char lastModule[256] = { 0 }; + ssize_t bytesRead = 0; + + while ((bytesRead = read(fd, readBuf, sizeof(readBuf))) > 0) + { + for (int i = 0; i < bytesRead; i++) + { + if (readBuf[i] == '\n') + { + lineBuf[linePos] = '\0'; + ParseMapsLine(lineBuf, callback, ctx, lastModule, sizeof(lastModule)); + linePos = 0; + } + else if (linePos < sizeof(lineBuf) - 1) + { + lineBuf[linePos++] = readBuf[i]; + } + } + } + + close(fd); +} + +void +ProcessNameCallback( + const char* filename, + void* ctx) +{ + ProcessNameCtx* processName = (ProcessNameCtx*)ctx; + if (processName->found || processName->filename == NULL || processName->filenameLen <= 0) + { + return; + } + + CopyFilename(processName->filename, processName->filenameLen, filename); + processName->found = processName->filename[0] != '\0'; +} + +int +CrashModulesTryGetProcessName( + char* filename, + int filenameLen) +{ + if (filename == NULL || filenameLen <= 0) + { + return 0; + } + + filename[0] = '\0'; + + int fd = open("/proc/self/cmdline", O_RDONLY); + if (fd != -1) + { + char cmdline[256]; + ssize_t bytesRead = read(fd, cmdline, sizeof(cmdline) - 1); + close(fd); + + if (bytesRead > 0) + { + cmdline[bytesRead] = '\0'; + CopyFilename(filename, filenameLen, GetFilename(cmdline)); + if (filename[0] != '\0') + { + return 1; + } + } + } + + // Fall back to the first executable mapping if the kernel cmdline view is + // unavailable or empty. + ProcessNameCtx ctx = { filename, filenameLen, 0 }; + EnumerateModules(ProcessNameCallback, &ctx); + return ctx.found; +} diff --git a/src/coreclr/pal/src/crashreport/moduleenumerator.h b/src/coreclr/pal/src/crashreport/moduleenumerator.h new file mode 100644 index 00000000000000..5360628a259519 --- /dev/null +++ b/src/coreclr/pal/src/crashreport/moduleenumerator.h @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Crash report process-name lookup helper. +// Uses only open/read/close and manual parsing so it remains usable from +// crash-time code without stdio or heap allocation. + +#pragma once + +// Returns the basename of the current process image. Prefers /proc/self/cmdline +// and falls back to the first executable mapping in /proc/self/maps. +int CrashModulesTryGetProcessName(char* filename, int filenameLen); From 4c6220f7e50674a5acc4b6ff4505ba6e3726bd51 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 14 Apr 2026 13:28:03 -0400 Subject: [PATCH 04/52] Enumerate crash report threads and stack frames Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/crashreport/inproccrashreporter.cpp | 457 +++++++++++++++++- .../pal/src/crashreport/inproccrashreporter.h | 34 ++ src/coreclr/vm/CMakeLists.txt | 1 + src/coreclr/vm/ceemain.cpp | 2 + src/coreclr/vm/crashreportstackwalker.cpp | 199 ++++++++ src/coreclr/vm/crashreportstackwalker.h | 13 + 6 files changed, 683 insertions(+), 23 deletions(-) create mode 100644 src/coreclr/vm/crashreportstackwalker.cpp create mode 100644 src/coreclr/vm/crashreportstackwalker.h diff --git a/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp b/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp index 5507c7f7f81dee..c0a79a03a1f1ed 100644 --- a/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp @@ -8,9 +8,11 @@ #include "inproccrashreporter.h" #include "crashjsonwriter.h" #include "moduleenumerator.h" +#include "pal/thread.hpp" #include #include +#include #ifdef __ANDROID__ #include @@ -20,6 +22,20 @@ #include "_version.c" static CrashJsonWriter s_jsonWriter; +// These callbacks are published during runtime startup and then only read from +// the crash path; this minimal branch intentionally reuses the existing VM +// inspection hooks before the later strict-safety hardening slices. +static volatile InProcCrashReportIsManagedThreadCallback g_isManagedThreadCallback = NULL; +static volatile InProcCrashReportWalkStackCallback g_walkStackCallback = NULL; +static volatile InProcCrashReportEnumerateThreadsCallback g_enumerateThreadsCallback = NULL; + +struct MultiThreadJsonContext +{ + CrashJsonWriter* writer; + void* signalContext; + int threadCount; + int sawCrashThread; +}; static void @@ -27,6 +43,74 @@ GetVersionString( char* buffer, int bufferSize); +static +void +FormatHexValue( + char* buffer, + int bufferSize, + uint64_t value); + +static +void +WriteRegistersToJson( + CrashJsonWriter* writer, + void* context); + +static +uint64_t +GetInstructionPointer( + void* context); + +static +uint64_t +GetStackPointer( + void* context); + +static +void +WriteCrashSiteFrameToJson( + CrashJsonWriter* writer, + void* context); + +static +void +BuildMethodName( + char* buffer, + int bufferSize, + const char* className, + const char* methodName); + +static +void +JsonFrameCallback( + uint64_t ip, + uint64_t stackPointer, + const char* methodName, + const char* className, + const char* moduleName, + uint32_t nativeOffset, + uint32_t token, + void* ctx); + +static +void +JsonThreadFrameCallback( + uint64_t ip, + uint64_t stackPointer, + const char* methodName, + const char* className, + const char* moduleName, + uint32_t nativeOffset, + uint32_t token, + void* ctx); + +static +void +JsonThreadCallback( + uint64_t osThreadId, + int isCrashThread, + void* ctx); + static void WriteToLog( @@ -41,7 +125,6 @@ InProcCrashReportGenerate( { (void)signal; (void)siginfo; - (void)context; CrashJsonInit(&s_jsonWriter); @@ -69,35 +152,60 @@ InProcCrashReportGenerate( } CrashJsonOpenArray(&s_jsonWriter, "threads"); - // TODO: Replace with actual thread enumeration. - int threadCount = 0; - for (int threadIndex = 0; threadIndex < 0; threadIndex++) + if (g_enumerateThreadsCallback != NULL) { - CrashJsonOpenObject(&s_jsonWriter, NULL); - CrashJsonWriteString(&s_jsonWriter, "is_managed", "false"); - CrashJsonWriteString(&s_jsonWriter, "crashed", "false"); - CrashJsonWriteString(&s_jsonWriter, "native_thread_id", "0x0"); - - CrashJsonOpenObject(&s_jsonWriter, "ctx"); - CrashJsonWriteString(&s_jsonWriter, "IP", "0x0"); - CrashJsonWriteString(&s_jsonWriter, "SP", "0x0"); - CrashJsonWriteString(&s_jsonWriter, "BP", "0x0"); - CrashJsonCloseObject(&s_jsonWriter); + MultiThreadJsonContext threadContext = { &s_jsonWriter, context, 0, 0 }; + uint64_t crashingTid = static_cast(THREADSilentGetCurrentThreadId()); - CrashJsonOpenArray(&s_jsonWriter, "stack_frames"); - // TODO: Replace with actual frame enumeration. - int stackFrameCount = 0; - for (int stackFrameIndex = 0; stackFrameIndex < stackFrameCount; stackFrameIndex++) + g_enumerateThreadsCallback(crashingTid, JsonThreadCallback, JsonThreadFrameCallback, &threadContext); + + if (threadContext.threadCount > 0) + { + // Close the last thread's stack_frames + object opened by the + // enumeration callback. + CrashJsonCloseArray(&s_jsonWriter); + CrashJsonCloseObject(&s_jsonWriter); + } + + if (threadContext.threadCount == 0 || !threadContext.sawCrashThread) { CrashJsonOpenObject(&s_jsonWriter, NULL); - CrashJsonWriteString(&s_jsonWriter, "is_managed", "false"); - CrashJsonWriteString(&s_jsonWriter, "module_address", "0x0"); - CrashJsonWriteString(&s_jsonWriter, "stack_pointer", "0x0"); - CrashJsonWriteString(&s_jsonWriter, "native_address", "0x0"); + CrashJsonWriteString(&s_jsonWriter, "is_managed", + g_isManagedThreadCallback != NULL && g_isManagedThreadCallback() ? "true" : "false"); + CrashJsonWriteString(&s_jsonWriter, "crashed", "true"); + + char nativeThreadId[32]; + FormatHexValue(nativeThreadId, sizeof(nativeThreadId), crashingTid); + CrashJsonWriteString(&s_jsonWriter, "native_thread_id", nativeThreadId); + + WriteRegistersToJson(&s_jsonWriter, context); + CrashJsonOpenArray(&s_jsonWriter, "stack_frames"); + WriteCrashSiteFrameToJson(&s_jsonWriter, context); + CrashJsonCloseArray(&s_jsonWriter); CrashJsonCloseObject(&s_jsonWriter); } - CrashJsonCloseArray(&s_jsonWriter); + } + else + { + uint64_t crashingTid = static_cast(THREADSilentGetCurrentThreadId()); + + CrashJsonOpenObject(&s_jsonWriter, NULL); + CrashJsonWriteString(&s_jsonWriter, "is_managed", + g_isManagedThreadCallback != NULL && g_isManagedThreadCallback() ? "true" : "false"); + CrashJsonWriteString(&s_jsonWriter, "crashed", "true"); + + char nativeThreadId[32]; + FormatHexValue(nativeThreadId, sizeof(nativeThreadId), crashingTid); + CrashJsonWriteString(&s_jsonWriter, "native_thread_id", nativeThreadId); + WriteRegistersToJson(&s_jsonWriter, context); + CrashJsonOpenArray(&s_jsonWriter, "stack_frames"); + WriteCrashSiteFrameToJson(&s_jsonWriter, context); + if (g_walkStackCallback != NULL) + { + g_walkStackCallback(JsonFrameCallback, &s_jsonWriter); + } + CrashJsonCloseArray(&s_jsonWriter); CrashJsonCloseObject(&s_jsonWriter); } CrashJsonCloseArray(&s_jsonWriter); @@ -118,6 +226,27 @@ InProcCrashReportGenerate( WriteToLog(CrashJsonGetBuffer(&s_jsonWriter), CrashJsonGetLength(&s_jsonWriter)); } +void +InProcCrashReportSetCurrentThreadManagedResolver( + InProcCrashReportIsManagedThreadCallback callback) +{ + g_isManagedThreadCallback = callback; +} + +void +InProcCrashReportSetStackWalker( + InProcCrashReportWalkStackCallback callback) +{ + g_walkStackCallback = callback; +} + +void +InProcCrashReportSetThreadEnumerator( + InProcCrashReportEnumerateThreadsCallback callback) +{ + g_enumerateThreadsCallback = callback; +} + void WriteToLog( const char* msg, @@ -204,3 +333,285 @@ GetVersionString( buffer[index++] = ' '; buffer[index] = '\0'; } + +void +FormatHexValue( + char* buffer, + int bufferSize, + uint64_t value) +{ + if (buffer == NULL || bufferSize <= 0) + { + return; + } + + if (bufferSize == 1) + { + buffer[0] = '\0'; + return; + } + + buffer[0] = '0'; + if (bufferSize == 2) + { + buffer[1] = '\0'; + return; + } + + buffer[1] = 'x'; + + char reverse[16]; + int reverseLength = 0; + do + { + int digit = static_cast(value & 0xf); + reverse[reverseLength++] = static_cast(digit < 10 ? ('0' + digit) : ('a' + digit - 10)); + value >>= 4; + } while (value != 0 && reverseLength < static_cast(sizeof(reverse))); + + int index = 2; + while (reverseLength > 0 && index < bufferSize - 1) + { + buffer[index++] = reverse[--reverseLength]; + } + buffer[index] = '\0'; +} + +void +WriteRegistersToJson( + CrashJsonWriter* writer, + void* context) +{ + // Only the crashing thread has a reliable signal context in this slice. + uint64_t ipValue = GetInstructionPointer(context); + uint64_t spValue = GetStackPointer(context); + char ip[32] = "0x0"; + char sp[32] = "0x0"; + char bp[32] = "0x0"; + + FormatHexValue(ip, sizeof(ip), ipValue); + FormatHexValue(sp, sizeof(sp), spValue); + + if (context != NULL) + { + ucontext_t* ucontext = reinterpret_cast(context); +#if defined(__x86_64__) + FormatHexValue(bp, sizeof(bp), static_cast(ucontext->uc_mcontext.gregs[REG_RBP])); +#elif defined(__aarch64__) + FormatHexValue(bp, sizeof(bp), static_cast(ucontext->uc_mcontext.regs[29])); +#elif defined(__arm__) + FormatHexValue(bp, sizeof(bp), static_cast(ucontext->uc_mcontext.arm_fp)); +#endif + } + + CrashJsonOpenObject(writer, "ctx"); + CrashJsonWriteString(writer, "IP", ip); + CrashJsonWriteString(writer, "SP", sp); + CrashJsonWriteString(writer, "BP", bp); + CrashJsonCloseObject(writer); +} + +uint64_t +GetInstructionPointer( + void* context) +{ + if (context == NULL) + { + return 0; + } + + ucontext_t* ucontext = reinterpret_cast(context); +#if defined(__x86_64__) + return static_cast(ucontext->uc_mcontext.gregs[REG_RIP]); +#elif defined(__aarch64__) + return static_cast(ucontext->uc_mcontext.pc); +#elif defined(__arm__) + return static_cast(ucontext->uc_mcontext.arm_pc); +#else + return 0; +#endif +} + +uint64_t +GetStackPointer( + void* context) +{ + if (context == NULL) + { + return 0; + } + + ucontext_t* ucontext = reinterpret_cast(context); +#if defined(__x86_64__) + return static_cast(ucontext->uc_mcontext.gregs[REG_RSP]); +#elif defined(__aarch64__) + return static_cast(ucontext->uc_mcontext.sp); +#elif defined(__arm__) + return static_cast(ucontext->uc_mcontext.arm_sp); +#else + return 0; +#endif +} + +void +WriteCrashSiteFrameToJson( + CrashJsonWriter* writer, + void* context) +{ + uint64_t ipValue = GetInstructionPointer(context); + uint64_t spValue = GetStackPointer(context); + char ip[32] = "0x0"; + char sp[32] = "0x0"; + + FormatHexValue(ip, sizeof(ip), ipValue); + FormatHexValue(sp, sizeof(sp), spValue); + + CrashJsonOpenObject(writer, NULL); + CrashJsonWriteString(writer, "is_managed", "false"); + CrashJsonWriteString(writer, "stack_pointer", sp); + CrashJsonWriteString(writer, "native_address", ip); + CrashJsonCloseObject(writer); +} + +void +BuildMethodName( + char* buffer, + int bufferSize, + const char* className, + const char* methodName) +{ + if (buffer == NULL || bufferSize <= 0) + { + return; + } + + int index = 0; + if (className != NULL) + { + while (*className != '\0' && index < bufferSize - 1) + { + buffer[index++] = *className++; + } + } + + if (methodName != NULL) + { + if (index > 0 && index < bufferSize - 1) + { + buffer[index++] = '.'; + } + + while (*methodName != '\0' && index < bufferSize - 1) + { + buffer[index++] = *methodName++; + } + } + + buffer[index] = '\0'; +} + +void +JsonFrameCallback( + uint64_t ip, + uint64_t stackPointer, + const char* methodName, + const char* className, + const char* moduleName, + uint32_t nativeOffset, + uint32_t token, + void* ctx) +{ + CrashJsonWriter* writer = reinterpret_cast(ctx); + + char ipBuffer[32]; + char stackPointerBuffer[32]; + char nativeOffsetBuffer[32]; + char tokenBuffer[32]; + + FormatHexValue(ipBuffer, sizeof(ipBuffer), ip); + FormatHexValue(stackPointerBuffer, sizeof(stackPointerBuffer), stackPointer); + FormatHexValue(nativeOffsetBuffer, sizeof(nativeOffsetBuffer), nativeOffset); + FormatHexValue(tokenBuffer, sizeof(tokenBuffer), token); + + CrashJsonOpenObject(writer, NULL); + CrashJsonWriteString(writer, "stack_pointer", stackPointerBuffer); + CrashJsonWriteString(writer, "native_address", ipBuffer); + CrashJsonWriteString(writer, "native_offset", nativeOffsetBuffer); + + if (methodName != NULL) + { + char fullName[256]; + BuildMethodName(fullName, sizeof(fullName), className, methodName); + CrashJsonWriteString(writer, "method_name", fullName); + CrashJsonWriteString(writer, "is_managed", "true"); + CrashJsonWriteString(writer, "token", tokenBuffer); + if (moduleName != NULL) + { + CrashJsonWriteString(writer, "filename", moduleName); + } + } + else + { + CrashJsonWriteString(writer, "is_managed", "false"); + if (moduleName != NULL) + { + CrashJsonWriteString(writer, "native_module", moduleName); + } + } + + CrashJsonCloseObject(writer); +} + +void +JsonThreadFrameCallback( + uint64_t ip, + uint64_t stackPointer, + const char* methodName, + const char* className, + const char* moduleName, + uint32_t nativeOffset, + uint32_t token, + void* ctx) +{ + MultiThreadJsonContext* threadContext = reinterpret_cast(ctx); + JsonFrameCallback(ip, stackPointer, methodName, className, moduleName, nativeOffset, token, threadContext->writer); +} + +void +JsonThreadCallback( + uint64_t osThreadId, + int isCrashThread, + void* ctx) +{ + MultiThreadJsonContext* threadContext = reinterpret_cast(ctx); + if (threadContext->threadCount > 0) + { + CrashJsonCloseArray(threadContext->writer); + CrashJsonCloseObject(threadContext->writer); + } + + if (isCrashThread) + { + threadContext->sawCrashThread = 1; + } + threadContext->threadCount++; + + CrashJsonOpenObject(threadContext->writer, NULL); + CrashJsonWriteString(threadContext->writer, "is_managed", "true"); + CrashJsonWriteString(threadContext->writer, "crashed", isCrashThread ? "true" : "false"); + + char nativeThreadId[32]; + FormatHexValue(nativeThreadId, sizeof(nativeThreadId), osThreadId); + CrashJsonWriteString(threadContext->writer, "native_thread_id", nativeThreadId); + + if (isCrashThread) + { + WriteRegistersToJson(threadContext->writer, threadContext->signalContext); + } + + CrashJsonOpenArray(threadContext->writer, "stack_frames"); + if (isCrashThread) + { + WriteCrashSiteFrameToJson(threadContext->writer, threadContext->signalContext); + } +} diff --git a/src/coreclr/pal/src/crashreport/inproccrashreporter.h b/src/coreclr/pal/src/crashreport/inproccrashreporter.h index a0e407cb6f95f3..1964cf1b41b6de 100644 --- a/src/coreclr/pal/src/crashreport/inproccrashreporter.h +++ b/src/coreclr/pal/src/crashreport/inproccrashreporter.h @@ -8,7 +8,41 @@ #pragma once #include +#include // Generate an in-proc crash report. Called from PROCCreateCrashDumpIfEnabled. // All arguments come from the signal handler and are signal-safe to read. void InProcCrashReportGenerate(int signal, siginfo_t* siginfo, void* context); + +typedef int (*InProcCrashReportIsManagedThreadCallback)(); + +void InProcCrashReportSetCurrentThreadManagedResolver(InProcCrashReportIsManagedThreadCallback callback); + +typedef void (*InProcCrashReportFrameCallback)( + uint64_t ip, + uint64_t stackPointer, + const char* methodName, + const char* className, + const char* moduleName, + uint32_t nativeOffset, + uint32_t token, + void* ctx); + +typedef void (*InProcCrashReportWalkStackCallback)( + InProcCrashReportFrameCallback frameCallback, + void* ctx); + +void InProcCrashReportSetStackWalker(InProcCrashReportWalkStackCallback callback); + +typedef void (*InProcCrashReportThreadCallback)( + uint64_t osThreadId, + int isCrashThread, + void* ctx); + +typedef void (*InProcCrashReportEnumerateThreadsCallback)( + uint64_t crashingTid, + InProcCrashReportThreadCallback threadCallback, + InProcCrashReportFrameCallback frameCallback, + void* ctx); + +void InProcCrashReportSetThreadEnumerator(InProcCrashReportEnumerateThreadsCallback callback); diff --git a/src/coreclr/vm/CMakeLists.txt b/src/coreclr/vm/CMakeLists.txt index 982ea2ba74831b..f6facfa7fe9ac5 100644 --- a/src/coreclr/vm/CMakeLists.txt +++ b/src/coreclr/vm/CMakeLists.txt @@ -310,6 +310,7 @@ set(VM_SOURCES_WKS comutilnative.cpp coreassemblyspec.cpp corelib.cpp # true + crashreportstackwalker.cpp customattribute.cpp custommarshalerinfo.cpp autotrace.cpp diff --git a/src/coreclr/vm/ceemain.cpp b/src/coreclr/vm/ceemain.cpp index 79b2bf3f33166b..6b36a675474f91 100644 --- a/src/coreclr/vm/ceemain.cpp +++ b/src/coreclr/vm/ceemain.cpp @@ -116,6 +116,7 @@ // boxing this describes this feature. #include "common.h" +#include "crashreportstackwalker.h" #include "vars.hpp" #include "log.h" @@ -696,6 +697,7 @@ void EEStartupHelper() #ifdef HOST_ANDROID PAL_SetLogManagedCallstackForSignalCallback(EEPolicy::LogManagedCallstackForSignal); + CrashReportRegisterStackWalker(); #endif // HOST_ANDROID #ifdef STRESS_LOG diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp new file mode 100644 index 00000000000000..afd72cdd95cd5b --- /dev/null +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -0,0 +1,199 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// VM-side implementation of the in-proc crash report thread callbacks. + +#include "common.h" +#include "codeman.h" +#include "method.hpp" + +#ifdef HOST_ANDROID + +#include "../pal/src/crashreport/inproccrashreporter.h" + +struct WalkContext +{ + InProcCrashReportFrameCallback callback; + void* userCtx; +}; + +static +StackWalkAction +FrameCallbackAdapter( + CrawlFrame* pCF, + VOID* pData) +{ + WalkContext* ctx = static_cast(pData); + MethodDesc* pMD = pCF->GetFunction(); + if (pMD == NULL) + { + return SWA_CONTINUE; + } + + LPCUTF8 methodName = pMD->GetName(); + mdMethodDef token = pMD->GetMemberDef(); + + LPCUTF8 className = NULL; + LPCUTF8 namespaceName = NULL; + MethodTable* pMT = pMD->GetMethodTable(); + if (pMT != NULL) + { + mdTypeDef cl = pMT->GetCl(); + IMDInternalImport* pImport = pMD->GetMDImport(); + if (pImport != NULL && cl != mdTypeDefNil) + { + pImport->GetNameOfTypeDef(cl, &className, &namespaceName); + } + } + + char classNameBuf[256] = { 0 }; + int index = 0; + if (namespaceName != NULL) + { + while (*namespaceName != '\0' && index < static_cast(sizeof(classNameBuf)) - 1) + { + classNameBuf[index++] = *namespaceName++; + } + } + + if (className != NULL) + { + if (index > 0 && index < static_cast(sizeof(classNameBuf)) - 1) + { + classNameBuf[index++] = '.'; + } + + while (*className != '\0' && index < static_cast(sizeof(classNameBuf)) - 1) + { + classNameBuf[index++] = *className++; + } + } + classNameBuf[index] = '\0'; + + const char* moduleName = NULL; + Module* pModule = pMD->GetModule(); + if (pModule != NULL) + { + Assembly* pAssembly = pModule->GetAssembly(); + if (pAssembly != NULL) + { + moduleName = pAssembly->GetSimpleName(); + } + } + + uint32_t nativeOffset = pCF->HasFaulted() ? 0 : pCF->GetRelOffset(); + uint64_t ip = 0; + uint64_t stackPointer = 0; + PREGDISPLAY pRD = pCF->GetRegisterSet(); + if (pRD != NULL) + { + ip = static_cast(GetControlPC(pRD)); + stackPointer = static_cast(GetRegdisplaySP(pRD)); + } + + ctx->callback(ip, stackPointer, methodName, classNameBuf, moduleName, nativeOffset, static_cast(token), ctx->userCtx); + return SWA_CONTINUE; +} + +static +void +CrashReportWalkStack( + InProcCrashReportFrameCallback frameCallback, + void* ctx) +{ + Thread* pThread = GetThreadAsyncSafe(); + if (pThread == NULL) + { + return; + } + + WalkContext walkContext = { frameCallback, ctx }; + pThread->StackWalkFrames(FrameCallbackAdapter, &walkContext, + QUICKUNWIND | FUNCTIONSONLY | ALLOW_ASYNC_STACK_WALK); +} + +static +int +CrashReportIsCurrentThreadManaged() +{ + return GetThreadAsyncSafe() != NULL; +} + +static +void +CrashReportEnumerateThreads( + uint64_t crashingTid, + InProcCrashReportThreadCallback threadCallback, + InProcCrashReportFrameCallback frameCallback, + void* ctx) +{ + // This minimal lift intentionally reuses the existing ThreadStore traversal + // and StackWalkFrames as a best-effort source for managed thread state. + // The later strict-safety slices replace this with the signal-safe thread + // registry and pre-published frame snapshots. + Thread* pCrashThread = GetThreadAsyncSafe(); + bool crashThreadHandled = false; + + // Emit the crashing thread first so the report keeps the most important + // thread even if later enumeration is incomplete. + if (pCrashThread != NULL) + { + uint64_t crashOsId = static_cast(pCrashThread->GetOSThreadId()); + if (crashOsId == crashingTid) + { + threadCallback(crashOsId, 1, ctx); + + WalkContext walkContext = { frameCallback, ctx }; + pCrashThread->StackWalkFrames(FrameCallbackAdapter, &walkContext, + QUICKUNWIND | FUNCTIONSONLY | ALLOW_ASYNC_STACK_WALK); + crashThreadHandled = true; + } + } + + Thread* pThread = ThreadStore::GetThreadList(NULL); + while (pThread != NULL) + { + if (crashThreadHandled && pThread == pCrashThread) + { + pThread = ThreadStore::GetThreadList(pThread); + continue; + } + + uint64_t osThreadId = static_cast(pThread->GetOSThreadId()); + if (osThreadId == 0) + { + pThread = ThreadStore::GetThreadList(pThread); + continue; + } + + bool isCrashThread = !crashThreadHandled && osThreadId == crashingTid; + threadCallback(osThreadId, isCrashThread ? 1 : 0, ctx); + if (isCrashThread) + { + crashThreadHandled = true; + } + + if (pThread->PreemptiveGCDisabled() == FALSE) + { + Frame* pFrame = pThread->GetFrame(); + if (pFrame != NULL && pFrame != FRAME_TOP) + { + WalkContext walkContext = { frameCallback, ctx }; + pThread->StackWalkFrames(FrameCallbackAdapter, &walkContext, + QUICKUNWIND | FUNCTIONSONLY | ALLOW_ASYNC_STACK_WALK); + } + } + + pThread = ThreadStore::GetThreadList(pThread); + } +} + +void +CrashReportRegisterStackWalker() +{ + InProcCrashReportSetCurrentThreadManagedResolver(CrashReportIsCurrentThreadManaged); + InProcCrashReportSetStackWalker(CrashReportWalkStack); + InProcCrashReportSetThreadEnumerator(CrashReportEnumerateThreads); +} + +#endif // HOST_ANDROID diff --git a/src/coreclr/vm/crashreportstackwalker.h b/src/coreclr/vm/crashreportstackwalker.h new file mode 100644 index 00000000000000..07fa546452fedc --- /dev/null +++ b/src/coreclr/vm/crashreportstackwalker.h @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#ifndef __CRASHREPORTSTACKWALKER_H__ +#define __CRASHREPORTSTACKWALKER_H__ + +#ifdef HOST_ANDROID + +void CrashReportRegisterStackWalker(); + +#endif // HOST_ANDROID + +#endif // __CRASHREPORTSTACKWALKER_H__ From ee343dc5d8a686764c5d340fd9aab6f8855fedb6 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 14 Apr 2026 14:17:57 -0400 Subject: [PATCH 05/52] Add crash report frame and module enrichment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/crashreport/inproccrashreporter.cpp | 31 +- .../pal/src/crashreport/moduleenumerator.cpp | 281 ++++++++++++++++-- .../pal/src/crashreport/moduleenumerator.h | 6 + 3 files changed, 284 insertions(+), 34 deletions(-) diff --git a/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp b/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp index c0a79a03a1f1ed..d82a09dfe81de9 100644 --- a/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp @@ -462,6 +462,10 @@ WriteCrashSiteFrameToJson( uint64_t spValue = GetStackPointer(context); char ip[32] = "0x0"; char sp[32] = "0x0"; + uint64_t moduleBase = 0; + char moduleBaseBuffer[32]; + char moduleName[256]; + moduleName[0] = '\0'; FormatHexValue(ip, sizeof(ip), ipValue); FormatHexValue(sp, sizeof(sp), spValue); @@ -470,6 +474,15 @@ WriteCrashSiteFrameToJson( CrashJsonWriteString(writer, "is_managed", "false"); CrashJsonWriteString(writer, "stack_pointer", sp); CrashJsonWriteString(writer, "native_address", ip); + if (CrashModulesTryLookupModuleForAddress(ipValue, &moduleBase, moduleName, sizeof(moduleName))) + { + char imageOffset[32]; + FormatHexValue(moduleBaseBuffer, sizeof(moduleBaseBuffer), moduleBase); + FormatHexValue(imageOffset, sizeof(imageOffset), ipValue - moduleBase); + CrashJsonWriteString(writer, "module_address", moduleBaseBuffer); + CrashJsonWriteString(writer, "native_image_offset", imageOffset); + CrashJsonWriteString(writer, "native_module", moduleName); + } CrashJsonCloseObject(writer); } @@ -522,6 +535,9 @@ JsonFrameCallback( void* ctx) { CrashJsonWriter* writer = reinterpret_cast(ctx); + uint64_t moduleBase = 0; + char nativeModuleName[256]; + nativeModuleName[0] = '\0'; char ipBuffer[32]; char stackPointerBuffer[32]; @@ -537,6 +553,15 @@ JsonFrameCallback( CrashJsonWriteString(writer, "stack_pointer", stackPointerBuffer); CrashJsonWriteString(writer, "native_address", ipBuffer); CrashJsonWriteString(writer, "native_offset", nativeOffsetBuffer); + if (CrashModulesTryLookupModuleForAddress(ip, &moduleBase, nativeModuleName, sizeof(nativeModuleName))) + { + char moduleAddress[32]; + char nativeImageOffset[32]; + FormatHexValue(moduleAddress, sizeof(moduleAddress), moduleBase); + FormatHexValue(nativeImageOffset, sizeof(nativeImageOffset), ip - moduleBase); + CrashJsonWriteString(writer, "module_address", moduleAddress); + CrashJsonWriteString(writer, "native_image_offset", nativeImageOffset); + } if (methodName != NULL) { @@ -553,7 +578,11 @@ JsonFrameCallback( else { CrashJsonWriteString(writer, "is_managed", "false"); - if (moduleName != NULL) + if (nativeModuleName[0] != '\0') + { + CrashJsonWriteString(writer, "native_module", nativeModuleName); + } + else if (moduleName != NULL) { CrashJsonWriteString(writer, "native_module", moduleName); } diff --git a/src/coreclr/pal/src/crashreport/moduleenumerator.cpp b/src/coreclr/pal/src/crashreport/moduleenumerator.cpp index eceee702bdef2e..60963c7fbfd1fd 100644 --- a/src/coreclr/pal/src/crashreport/moduleenumerator.cpp +++ b/src/coreclr/pal/src/crashreport/moduleenumerator.cpp @@ -1,16 +1,34 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// Crash report process-name lookup helper. +// Crash report module lookup helpers. // Parses /proc/self/cmdline and /proc/self/maps using only open/read/close. // No stdio, no sscanf, no heap allocation. #include "moduleenumerator.h" + #include #include #include -typedef void (*ModuleCallback)(const char* filename, void* ctx); +#ifdef __linux__ +#include + +#if UINTPTR_MAX > UINT32_MAX +typedef Elf64_Ehdr ElfNative_Ehdr; +typedef Elf64_Phdr ElfNative_Phdr; +#else +typedef Elf32_Ehdr ElfNative_Ehdr; +typedef Elf32_Phdr ElfNative_Phdr; +#endif +#endif + +typedef void (*ModuleCallback)( + uint64_t startAddr, + uint64_t endAddr, + uint64_t fileOffset, + const char* filename, + void* ctx); struct ProcessNameCtx { @@ -19,11 +37,33 @@ struct ProcessNameCtx int found; }; +struct LookupAddressCtx +{ + uint64_t address; + uint64_t baseAddress; + char* filename; + int filenameLen; + int found; +}; + +static +const char* +ParseHex( + const char* p, + uint64_t* out); + static const char* GetFilename( const char* path); +static +uint64_t +ComputeImageBase( + uint64_t startAddr, + uint64_t endAddr, + uint64_t fileOffset); + void CopyFilename( char* filename, @@ -37,20 +77,67 @@ ParseMapsLine( ModuleCallback callback, void* ctx, char* lastModule, - int lastModuleSize); + int lastModuleSize, + bool deduplicate); static void EnumerateModules( ModuleCallback callback, + void* ctx, + bool deduplicate); + +static +void +ModuleLookupByAddress( + uint64_t startAddr, + uint64_t endAddr, + uint64_t fileOffset, + const char* filename, void* ctx); static void ProcessNameCallback( + uint64_t startAddr, + uint64_t endAddr, + uint64_t fileOffset, const char* filename, void* ctx); +const char* +ParseHex( + const char* p, + uint64_t* out) +{ + uint64_t value = 0; + while (*p != '\0') + { + char c = *p; + if (c >= '0' && c <= '9') + { + value = (value << 4) | static_cast(c - '0'); + } + else if (c >= 'a' && c <= 'f') + { + value = (value << 4) | static_cast(c - 'a' + 10); + } + else if (c >= 'A' && c <= 'F') + { + value = (value << 4) | static_cast(c - 'A' + 10); + } + else + { + break; + } + + p++; + } + + *out = value; + return p; +} + const char* GetFilename( const char* path) @@ -64,6 +151,62 @@ GetFilename( return last; } +uint64_t +ComputeImageBase( + uint64_t startAddr, + uint64_t endAddr, + uint64_t fileOffset) +{ +#ifndef __linux__ + (void)endAddr; + (void)fileOffset; + return startAddr; +#else + if (fileOffset > startAddr) + { + return startAddr; + } + + uint64_t mappedFileBase = startAddr - fileOffset; + if (mappedFileBase > endAddr || sizeof(ElfNative_Ehdr) > endAddr - mappedFileBase) + { + return startAddr; + } + + const ElfNative_Ehdr* ehdr = reinterpret_cast(static_cast(mappedFileBase)); + if (ehdr->e_ident[EI_MAG0] != ELFMAG0 || + ehdr->e_ident[EI_MAG1] != ELFMAG1 || + ehdr->e_ident[EI_MAG2] != ELFMAG2 || + ehdr->e_ident[EI_MAG3] != ELFMAG3) + { + return mappedFileBase; + } + + if (ehdr->e_phentsize != sizeof(ElfNative_Phdr)) + { + return mappedFileBase; + } + + uint64_t availableBytes = endAddr - mappedFileBase; + uint64_t phdrBytes = static_cast(ehdr->e_phnum) * sizeof(ElfNative_Phdr); + if (ehdr->e_phoff > availableBytes || phdrBytes > availableBytes - ehdr->e_phoff) + { + return mappedFileBase; + } + + const ElfNative_Phdr* phdrs = reinterpret_cast(static_cast(mappedFileBase + ehdr->e_phoff)); + for (int i = 0; i < ehdr->e_phnum; i++) + { + if (phdrs[i].p_type == PT_LOAD && phdrs[i].p_offset == 0) + { + return mappedFileBase - phdrs[i].p_vaddr; + } + } + + return mappedFileBase; +#endif +} + void CopyFilename( char* filename, @@ -90,14 +233,19 @@ ParseMapsLine( ModuleCallback callback, void* ctx, char* lastModule, - int lastModuleSize) + int lastModuleSize, + bool deduplicate) { - const char* p = line; - while (*p != '\0' && *p != ' ') + uint64_t startAddr = 0; + const char* p = ParseHex(line, &startAddr); + if (*p != '-') { - p++; + return; } + p++; + uint64_t endAddr = 0; + p = ParseHex(p, &endAddr); if (*p != ' ') { return; @@ -109,59 +257,73 @@ ParseMapsLine( { p++; } - - // We only care about named executable image mappings because they can - // identify the current process image when /proc/self/cmdline is unavailable. - if (*p != ' ' || (p - permissions <= 2) || permissions[2] != 'x') + if (*p != ' ') { return; } + int executable = (p - permissions > 2) && permissions[2] == 'x'; p++; - int spacesToSkip = 3; - while (*p != '\0' && spacesToSkip > 0) + while (*p == ' ') { - if (*p == ' ') - { - while (*p == ' ') - { - p++; - } - spacesToSkip--; - } - else - { - p++; - } + p++; } - if (spacesToSkip != 0 || *p == '\0' || *p == '\n' || *p == '[') + uint64_t fileOffset = 0; + p = ParseHex(p, &fileOffset); + while (*p == ' ') + { + p++; + } + while (*p != '\0' && *p != ' ') + { + p++; + } + while (*p == ' ') + { + p++; + } + while (*p != '\0' && *p != ' ') + { + p++; + } + while (*p == ' ') + { + p++; + } + + if (!executable || *p == '\0' || *p == '\n' || *p == '[') { return; } char pathname[256]; int pathnameLen = 0; - while (p[pathnameLen] != '\0' && p[pathnameLen] != '\n' && pathnameLen < sizeof(pathname) - 1) + while (p[pathnameLen] != '\0' && p[pathnameLen] != '\n' && pathnameLen < static_cast(sizeof(pathname)) - 1) { pathname[pathnameLen] = p[pathnameLen]; pathnameLen++; } pathname[pathnameLen] = '\0'; - if (strcmp(pathname, lastModule) == 0) + if (deduplicate && strcmp(pathname, lastModule) == 0) { return; } - CopyFilename(lastModule, lastModuleSize, pathname); - callback(GetFilename(pathname), ctx); + if (deduplicate) + { + CopyFilename(lastModule, lastModuleSize, pathname); + } + + callback(startAddr, endAddr, fileOffset, GetFilename(pathname), ctx); } void EnumerateModules( ModuleCallback callback, - void* ctx) + void* ctx, + bool deduplicate) { // Walk the live executable mappings as a crash-time fallback for process // name resolution without depending on loader APIs or heap allocation. @@ -184,7 +346,7 @@ EnumerateModules( if (readBuf[i] == '\n') { lineBuf[linePos] = '\0'; - ParseMapsLine(lineBuf, callback, ctx, lastModule, sizeof(lastModule)); + ParseMapsLine(lineBuf, callback, ctx, lastModule, sizeof(lastModule), deduplicate); linePos = 0; } else if (linePos < sizeof(lineBuf) - 1) @@ -197,11 +359,64 @@ EnumerateModules( close(fd); } +void +ModuleLookupByAddress( + uint64_t startAddr, + uint64_t endAddr, + uint64_t fileOffset, + const char* filename, + void* ctx) +{ + LookupAddressCtx* lookup = reinterpret_cast(ctx); + if (lookup->found || lookup->address < startAddr || lookup->address >= endAddr) + { + return; + } + + lookup->baseAddress = ComputeImageBase(startAddr, endAddr, fileOffset); + lookup->found = 1; + + if (lookup->filename != NULL && lookup->filenameLen > 0) + { + CopyFilename(lookup->filename, lookup->filenameLen, filename); + } +} + +int +CrashModulesTryLookupModuleForAddress( + uint64_t address, + uint64_t* baseAddress, + char* filename, + int filenameLen) +{ + LookupAddressCtx ctx = { address, 0, filename, filenameLen, 0 }; + if (filename != NULL && filenameLen > 0) + { + filename[0] = '\0'; + } + + EnumerateModules(ModuleLookupByAddress, &ctx, 0); + + if (ctx.found && baseAddress != NULL) + { + *baseAddress = ctx.baseAddress; + } + + return ctx.found; +} + void ProcessNameCallback( + uint64_t startAddr, + uint64_t endAddr, + uint64_t fileOffset, const char* filename, void* ctx) { + (void)startAddr; + (void)endAddr; + (void)fileOffset; + ProcessNameCtx* processName = (ProcessNameCtx*)ctx; if (processName->found || processName->filename == NULL || processName->filenameLen <= 0) { @@ -245,6 +460,6 @@ CrashModulesTryGetProcessName( // Fall back to the first executable mapping if the kernel cmdline view is // unavailable or empty. ProcessNameCtx ctx = { filename, filenameLen, 0 }; - EnumerateModules(ProcessNameCallback, &ctx); + EnumerateModules(ProcessNameCallback, &ctx, 1); return ctx.found; } diff --git a/src/coreclr/pal/src/crashreport/moduleenumerator.h b/src/coreclr/pal/src/crashreport/moduleenumerator.h index 5360628a259519..5b8e886ebdbed1 100644 --- a/src/coreclr/pal/src/crashreport/moduleenumerator.h +++ b/src/coreclr/pal/src/crashreport/moduleenumerator.h @@ -7,6 +7,12 @@ #pragma once +#include + +// Look up the executable mapping that contains the specified address. +// Returns 1 if a matching module is found, 0 otherwise. +int CrashModulesTryLookupModuleForAddress(uint64_t address, uint64_t* baseAddress, char* filename, int filenameLen); + // Returns the basename of the current process image. Prefers /proc/self/cmdline // and falls back to the first executable mapping in /proc/self/maps. int CrashModulesTryGetProcessName(char* filename, int filenameLen); From 709eadc137956065cc65d980db27ef34845a6bc0 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 14 Apr 2026 14:24:51 -0400 Subject: [PATCH 06/52] Add crash report exception details Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/crashreport/inproccrashreporter.cpp | 97 +++++++++++++++- .../pal/src/crashreport/inproccrashreporter.h | 11 ++ src/coreclr/vm/crashreportstackwalker.cpp | 109 +++++++++++++++++- 3 files changed, 212 insertions(+), 5 deletions(-) diff --git a/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp b/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp index d82a09dfe81de9..61e1d30062c10a 100644 --- a/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp @@ -27,6 +27,7 @@ static CrashJsonWriter s_jsonWriter; // inspection hooks before the later strict-safety hardening slices. static volatile InProcCrashReportIsManagedThreadCallback g_isManagedThreadCallback = NULL; static volatile InProcCrashReportWalkStackCallback g_walkStackCallback = NULL; +static volatile InProcCrashReportGetExceptionCallback g_getExceptionCallback = NULL; static volatile InProcCrashReportEnumerateThreadsCallback g_enumerateThreadsCallback = NULL; struct MultiThreadJsonContext @@ -35,6 +36,9 @@ struct MultiThreadJsonContext void* signalContext; int threadCount; int sawCrashThread; + int hasCrashException; + const char* crashExceptionType; + uint32_t crashExceptionHResult; }; static @@ -109,6 +113,8 @@ void JsonThreadCallback( uint64_t osThreadId, int isCrashThread, + const char* exceptionType, + uint32_t exceptionHResult, void* ctx); static @@ -117,15 +123,31 @@ WriteToLog( const char* msg, int len); +static +const char* +GetExceptionTypeCode( + int signal); + void InProcCrashReportGenerate( int signal, siginfo_t* siginfo, void* context) { - (void)signal; (void)siginfo; + char exTypeBuf[256]; + char exMsgBuf[512]; + uint32_t exHresult = 0; + exTypeBuf[0] = '\0'; + exMsgBuf[0] = '\0'; + + int hasException = 0; + if (g_getExceptionCallback != NULL && signal != SIGSEGV && signal != SIGBUS) + { + hasException = g_getExceptionCallback(exTypeBuf, sizeof(exTypeBuf), exMsgBuf, sizeof(exMsgBuf), &exHresult); + } + CrashJsonInit(&s_jsonWriter); CrashJsonOpenObject(&s_jsonWriter, NULL); @@ -154,7 +176,7 @@ InProcCrashReportGenerate( CrashJsonOpenArray(&s_jsonWriter, "threads"); if (g_enumerateThreadsCallback != NULL) { - MultiThreadJsonContext threadContext = { &s_jsonWriter, context, 0, 0 }; + MultiThreadJsonContext threadContext = { &s_jsonWriter, context, 0, 0, hasException, exTypeBuf, exHresult }; uint64_t crashingTid = static_cast(THREADSilentGetCurrentThreadId()); g_enumerateThreadsCallback(crashingTid, JsonThreadCallback, JsonThreadFrameCallback, &threadContext); @@ -178,6 +200,15 @@ InProcCrashReportGenerate( FormatHexValue(nativeThreadId, sizeof(nativeThreadId), crashingTid); CrashJsonWriteString(&s_jsonWriter, "native_thread_id", nativeThreadId); + if (hasException) + { + char hresultBuffer[32]; + FormatHexValue(hresultBuffer, sizeof(hresultBuffer), exHresult); + + CrashJsonWriteString(&s_jsonWriter, "managed_exception_type", exTypeBuf); + CrashJsonWriteString(&s_jsonWriter, "managed_exception_hresult", hresultBuffer); + } + WriteRegistersToJson(&s_jsonWriter, context); CrashJsonOpenArray(&s_jsonWriter, "stack_frames"); WriteCrashSiteFrameToJson(&s_jsonWriter, context); @@ -198,6 +229,15 @@ InProcCrashReportGenerate( FormatHexValue(nativeThreadId, sizeof(nativeThreadId), crashingTid); CrashJsonWriteString(&s_jsonWriter, "native_thread_id", nativeThreadId); + if (hasException) + { + char hresultBuffer[32]; + FormatHexValue(hresultBuffer, sizeof(hresultBuffer), exHresult); + + CrashJsonWriteString(&s_jsonWriter, "managed_exception_type", exTypeBuf); + CrashJsonWriteString(&s_jsonWriter, "managed_exception_hresult", hresultBuffer); + } + WriteRegistersToJson(&s_jsonWriter, context); CrashJsonOpenArray(&s_jsonWriter, "stack_frames"); WriteCrashSiteFrameToJson(&s_jsonWriter, context); @@ -213,7 +253,7 @@ InProcCrashReportGenerate( CrashJsonCloseObject(&s_jsonWriter); CrashJsonOpenObject(&s_jsonWriter, "parameters"); - CrashJsonWriteString(&s_jsonWriter, "ExceptionType", "0x00000000"); + CrashJsonWriteString(&s_jsonWriter, "ExceptionType", hasException ? "0x05000000" : GetExceptionTypeCode(signal)); #ifdef __APPLE__ CrashJsonWriteString(&s_jsonWriter, "OSVersion", ""); CrashJsonWriteString(&s_jsonWriter, "SystemModel", ""); @@ -240,6 +280,13 @@ InProcCrashReportSetStackWalker( g_walkStackCallback = callback; } +void +InProcCrashReportSetExceptionResolver( + InProcCrashReportGetExceptionCallback callback) +{ + g_getExceptionCallback = callback; +} + void InProcCrashReportSetThreadEnumerator( InProcCrashReportEnumerateThreadsCallback callback) @@ -610,6 +657,8 @@ void JsonThreadCallback( uint64_t osThreadId, int isCrashThread, + const char* exceptionType, + uint32_t exceptionHResult, void* ctx) { MultiThreadJsonContext* threadContext = reinterpret_cast(ctx); @@ -633,6 +682,23 @@ JsonThreadCallback( FormatHexValue(nativeThreadId, sizeof(nativeThreadId), osThreadId); CrashJsonWriteString(threadContext->writer, "native_thread_id", nativeThreadId); + if (isCrashThread && threadContext->hasCrashException) + { + char hresultBuffer[32]; + FormatHexValue(hresultBuffer, sizeof(hresultBuffer), threadContext->crashExceptionHResult); + + CrashJsonWriteString(threadContext->writer, "managed_exception_type", threadContext->crashExceptionType); + CrashJsonWriteString(threadContext->writer, "managed_exception_hresult", hresultBuffer); + } + else if (exceptionType != NULL && exceptionType[0] != '\0') + { + char hresultBuffer[32]; + FormatHexValue(hresultBuffer, sizeof(hresultBuffer), exceptionHResult); + + CrashJsonWriteString(threadContext->writer, "managed_exception_type", exceptionType); + CrashJsonWriteString(threadContext->writer, "managed_exception_hresult", hresultBuffer); + } + if (isCrashThread) { WriteRegistersToJson(threadContext->writer, threadContext->signalContext); @@ -644,3 +710,28 @@ JsonThreadCallback( WriteCrashSiteFrameToJson(threadContext->writer, threadContext->signalContext); } } + +const char* +GetExceptionTypeCode( + int signal) +{ + switch (signal) + { + case SIGSEGV: + return "0x20000000"; + case SIGABRT: + return "0x30000000"; + case SIGBUS: + return "0x60000000"; + case SIGILL: + return "0x50000000"; + case SIGFPE: + return "0x70000000"; + case SIGTRAP: + return "0x03000000"; + case SIGTERM: + return "0x02000000"; + default: + return "0x00000000"; + } +} diff --git a/src/coreclr/pal/src/crashreport/inproccrashreporter.h b/src/coreclr/pal/src/crashreport/inproccrashreporter.h index 1964cf1b41b6de..841ff8ff7d3f80 100644 --- a/src/coreclr/pal/src/crashreport/inproccrashreporter.h +++ b/src/coreclr/pal/src/crashreport/inproccrashreporter.h @@ -34,9 +34,20 @@ typedef void (*InProcCrashReportWalkStackCallback)( void InProcCrashReportSetStackWalker(InProcCrashReportWalkStackCallback callback); +typedef int (*InProcCrashReportGetExceptionCallback)( + char* exceptionTypeBuf, + int exceptionTypeBufSize, + char* exceptionMsgBuf, + int exceptionMsgBufSize, + uint32_t* hresult); + +void InProcCrashReportSetExceptionResolver(InProcCrashReportGetExceptionCallback callback); + typedef void (*InProcCrashReportThreadCallback)( uint64_t osThreadId, int isCrashThread, + const char* exceptionType, + uint32_t exceptionHResult, void* ctx); typedef void (*InProcCrashReportEnumerateThreadsCallback)( diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index afd72cdd95cd5b..b204ab57504f61 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -119,6 +119,102 @@ CrashReportIsCurrentThreadManaged() return GetThreadAsyncSafe() != NULL; } +static +int +CrashReportGetExceptionForThread( + Thread* pThread, + char* exceptionTypeBuf, + int exceptionTypeBufSize, + uint32_t* hresult) +{ + if (exceptionTypeBufSize > 0) + { + exceptionTypeBuf[0] = '\0'; + } + + if (hresult != NULL) + { + *hresult = 0; + } + + OBJECTREF throwable = pThread->GetThrowable(); + if (throwable == NULL) + { + return 0; + } + + MethodTable* pMT = throwable->GetMethodTable(); + if (pMT != NULL) + { + mdTypeDef cl = pMT->GetCl(); + Module* pModule = pMT->GetModule(); + if (pModule != NULL) + { + IMDInternalImport* pImport = pModule->GetMDImport(); + if (pImport != NULL && cl != mdTypeDefNil) + { + LPCUTF8 className = NULL; + LPCUTF8 namespaceName = NULL; + pImport->GetNameOfTypeDef(cl, &className, &namespaceName); + + int index = 0; + if (namespaceName != NULL) + { + while (*namespaceName != '\0' && index < exceptionTypeBufSize - 1) + { + exceptionTypeBuf[index++] = *namespaceName++; + } + } + + if (className != NULL) + { + if (index > 0 && index < exceptionTypeBufSize - 1) + { + exceptionTypeBuf[index++] = '.'; + } + + while (*className != '\0' && index < exceptionTypeBufSize - 1) + { + exceptionTypeBuf[index++] = *className++; + } + } + + exceptionTypeBuf[index] = '\0'; + } + } + } + + if (hresult != NULL) + { + *hresult = static_cast(((EXCEPTIONREF)throwable)->GetHResult()); + } + + return 1; +} + +static +int +CrashReportGetException( + char* exceptionTypeBuf, + int exceptionTypeBufSize, + char* exceptionMsgBuf, + int exceptionMsgBufSize, + uint32_t* hresult) +{ + Thread* pThread = GetThreadAsyncSafe(); + if (pThread == NULL) + { + return 0; + } + + if (exceptionMsgBufSize > 0) + { + exceptionMsgBuf[0] = '\0'; + } + + return CrashReportGetExceptionForThread(pThread, exceptionTypeBuf, exceptionTypeBufSize, hresult); +} + static void CrashReportEnumerateThreads( @@ -141,7 +237,11 @@ CrashReportEnumerateThreads( uint64_t crashOsId = static_cast(pCrashThread->GetOSThreadId()); if (crashOsId == crashingTid) { - threadCallback(crashOsId, 1, ctx); + char exceptionType[256]; + uint32_t hresult = 0; + int hasException = CrashReportGetExceptionForThread(pCrashThread, exceptionType, sizeof(exceptionType), &hresult); + + threadCallback(crashOsId, 1, hasException ? exceptionType : "", hresult, ctx); WalkContext walkContext = { frameCallback, ctx }; pCrashThread->StackWalkFrames(FrameCallbackAdapter, &walkContext, @@ -167,7 +267,11 @@ CrashReportEnumerateThreads( } bool isCrashThread = !crashThreadHandled && osThreadId == crashingTid; - threadCallback(osThreadId, isCrashThread ? 1 : 0, ctx); + char exceptionType[256]; + uint32_t hresult = 0; + int hasException = CrashReportGetExceptionForThread(pThread, exceptionType, sizeof(exceptionType), &hresult); + + threadCallback(osThreadId, isCrashThread ? 1 : 0, hasException ? exceptionType : "", hresult, ctx); if (isCrashThread) { crashThreadHandled = true; @@ -193,6 +297,7 @@ CrashReportRegisterStackWalker() { InProcCrashReportSetCurrentThreadManagedResolver(CrashReportIsCurrentThreadManaged); InProcCrashReportSetStackWalker(CrashReportWalkStack); + InProcCrashReportSetExceptionResolver(CrashReportGetException); InProcCrashReportSetThreadEnumerator(CrashReportEnumerateThreads); } From 6a89b32b6489d520e9fb46a2ff016ea7e05062e6 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 14 Apr 2026 14:35:48 -0400 Subject: [PATCH 07/52] Add createdump frame metadata to crash reports Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/crashreport/inproccrashreporter.cpp | 37 +++++++++++++++- .../pal/src/crashreport/inproccrashreporter.h | 4 ++ src/coreclr/vm/crashreportstackwalker.cpp | 44 ++++++++++++++++++- 3 files changed, 83 insertions(+), 2 deletions(-) diff --git a/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp b/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp index 61e1d30062c10a..1504a808865f1b 100644 --- a/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp @@ -94,6 +94,10 @@ JsonFrameCallback( const char* moduleName, uint32_t nativeOffset, uint32_t token, + uint32_t ilOffset, + uint32_t moduleTimestamp, + uint32_t moduleSize, + const char* moduleGuid, void* ctx); static @@ -106,6 +110,10 @@ JsonThreadFrameCallback( const char* moduleName, uint32_t nativeOffset, uint32_t token, + uint32_t ilOffset, + uint32_t moduleTimestamp, + uint32_t moduleSize, + const char* moduleGuid, void* ctx); static @@ -579,6 +587,10 @@ JsonFrameCallback( const char* moduleName, uint32_t nativeOffset, uint32_t token, + uint32_t ilOffset, + uint32_t moduleTimestamp, + uint32_t moduleSize, + const char* moduleGuid, void* ctx) { CrashJsonWriter* writer = reinterpret_cast(ctx); @@ -590,11 +602,17 @@ JsonFrameCallback( char stackPointerBuffer[32]; char nativeOffsetBuffer[32]; char tokenBuffer[32]; + char ilOffsetBuffer[32]; + char moduleTimestampBuffer[32]; + char moduleSizeBuffer[32]; FormatHexValue(ipBuffer, sizeof(ipBuffer), ip); FormatHexValue(stackPointerBuffer, sizeof(stackPointerBuffer), stackPointer); FormatHexValue(nativeOffsetBuffer, sizeof(nativeOffsetBuffer), nativeOffset); FormatHexValue(tokenBuffer, sizeof(tokenBuffer), token); + FormatHexValue(ilOffsetBuffer, sizeof(ilOffsetBuffer), ilOffset); + FormatHexValue(moduleTimestampBuffer, sizeof(moduleTimestampBuffer), moduleTimestamp); + FormatHexValue(moduleSizeBuffer, sizeof(moduleSizeBuffer), moduleSize); CrashJsonOpenObject(writer, NULL); CrashJsonWriteString(writer, "stack_pointer", stackPointerBuffer); @@ -617,10 +635,23 @@ JsonFrameCallback( CrashJsonWriteString(writer, "method_name", fullName); CrashJsonWriteString(writer, "is_managed", "true"); CrashJsonWriteString(writer, "token", tokenBuffer); + CrashJsonWriteString(writer, "il_offset", ilOffsetBuffer); if (moduleName != NULL) { CrashJsonWriteString(writer, "filename", moduleName); } + if (moduleTimestamp != 0) + { + CrashJsonWriteString(writer, "timestamp", moduleTimestampBuffer); + } + if (moduleSize != 0) + { + CrashJsonWriteString(writer, "sizeofimage", moduleSizeBuffer); + } + if (moduleGuid != NULL && moduleGuid[0] != '\0') + { + CrashJsonWriteString(writer, "guid", moduleGuid); + } } else { @@ -647,10 +678,14 @@ JsonThreadFrameCallback( const char* moduleName, uint32_t nativeOffset, uint32_t token, + uint32_t ilOffset, + uint32_t moduleTimestamp, + uint32_t moduleSize, + const char* moduleGuid, void* ctx) { MultiThreadJsonContext* threadContext = reinterpret_cast(ctx); - JsonFrameCallback(ip, stackPointer, methodName, className, moduleName, nativeOffset, token, threadContext->writer); + JsonFrameCallback(ip, stackPointer, methodName, className, moduleName, nativeOffset, token, ilOffset, moduleTimestamp, moduleSize, moduleGuid, threadContext->writer); } void diff --git a/src/coreclr/pal/src/crashreport/inproccrashreporter.h b/src/coreclr/pal/src/crashreport/inproccrashreporter.h index 841ff8ff7d3f80..3400f39ff74b91 100644 --- a/src/coreclr/pal/src/crashreport/inproccrashreporter.h +++ b/src/coreclr/pal/src/crashreport/inproccrashreporter.h @@ -26,6 +26,10 @@ typedef void (*InProcCrashReportFrameCallback)( const char* moduleName, uint32_t nativeOffset, uint32_t token, + uint32_t ilOffset, + uint32_t moduleTimestamp, + uint32_t moduleSize, + const char* moduleGuid, void* ctx); typedef void (*InProcCrashReportWalkStackCallback)( diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index b204ab57504f61..e7788a6bc5bc16 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -5,7 +5,10 @@ #include "common.h" #include "codeman.h" +#include "dbginterface.h" #include "method.hpp" +#include "peassembly.h" +#include #ifdef HOST_ANDROID @@ -82,6 +85,7 @@ FrameCallbackAdapter( } uint32_t nativeOffset = pCF->HasFaulted() ? 0 : pCF->GetRelOffset(); + uint32_t ilOffset = 0; uint64_t ip = 0; uint64_t stackPointer = 0; PREGDISPLAY pRD = pCF->GetRegisterSet(); @@ -91,7 +95,45 @@ FrameCallbackAdapter( stackPointer = static_cast(GetRegdisplaySP(pRD)); } - ctx->callback(ip, stackPointer, methodName, classNameBuf, moduleName, nativeOffset, static_cast(token), ctx->userCtx); + if (g_pDebugInterface != NULL && pMD != NULL) + { + DWORD resolvedILOffset = 0; + if (g_pDebugInterface->GetILOffsetFromNative( + pMD, + reinterpret_cast(static_cast(ip)), + nativeOffset, + &resolvedILOffset)) + { + ilOffset = resolvedILOffset; + } + } + + uint32_t moduleTimestamp = 0; + uint32_t moduleSize = 0; + char moduleGuid[MINIPAL_GUID_BUFFER_LEN]; + moduleGuid[0] = '\0'; + + if (pModule != NULL) + { + PEAssembly* pPEAssembly = pModule->GetPEAssembly(); + if (pPEAssembly != NULL && pPEAssembly->HasLoadedPEImage()) + { + moduleTimestamp = pPEAssembly->GetLoadedLayout()->GetTimeDateStamp(); + moduleSize = static_cast(pPEAssembly->GetLoadedLayout()->GetSize()); + } + + IMDInternalImport* pImport = pModule->GetMDImport(); + if (pImport != NULL) + { + GUID mvid; + if (SUCCEEDED(pImport->GetScopeProps(NULL, &mvid))) + { + minipal_guid_as_string(mvid, moduleGuid, MINIPAL_GUID_BUFFER_LEN); + } + } + } + + ctx->callback(ip, stackPointer, methodName, classNameBuf, moduleName, nativeOffset, static_cast(token), ilOffset, moduleTimestamp, moduleSize, moduleGuid, ctx->userCtx); return SWA_CONTINUE; } From ba405681defa91ac8559b05798c9900d13016971 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 14 Apr 2026 14:42:59 -0400 Subject: [PATCH 08/52] Write in-proc crash reports to JSON files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/crashreport/inproccrashreporter.cpp | 220 ++++++++++++++++++ .../pal/src/crashreport/inproccrashreporter.h | 5 +- src/coreclr/pal/src/thread/process.cpp | 7 + 3 files changed, 231 insertions(+), 1 deletion(-) diff --git a/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp b/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp index 1504a808865f1b..85ce0f3260c7a4 100644 --- a/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp @@ -10,6 +10,8 @@ #include "moduleenumerator.h" #include "pal/thread.hpp" +#include +#include #include #include #include @@ -29,6 +31,9 @@ static volatile InProcCrashReportIsManagedThreadCallback g_isManagedThreadCallba static volatile InProcCrashReportWalkStackCallback g_walkStackCallback = NULL; static volatile InProcCrashReportGetExceptionCallback g_getExceptionCallback = NULL; static volatile InProcCrashReportEnumerateThreadsCallback g_enumerateThreadsCallback = NULL; +static volatile int g_writeReportToFile = 0; +static char g_reportPath[256]; +static char g_defaultReportDirectory[256]; struct MultiThreadJsonContext { @@ -131,6 +136,44 @@ WriteToLog( const char* msg, int len); +static +int +WriteAllToFile( + int fd, + const char* buffer, + int len); + +static +void +AppendChar( + char* buffer, + int bufferSize, + int* pos, + char value); + +static +void +AppendString( + char* buffer, + int bufferSize, + int* pos, + const char* value); + +static +void +AppendUnsignedDecimal( + char* buffer, + int bufferSize, + int* pos, + uint64_t value); + +static +void +TerminateBuffer( + char* buffer, + int bufferSize, + int* pos); + static const char* GetExceptionTypeCode( @@ -272,6 +315,80 @@ InProcCrashReportGenerate( CrashJsonCloseObject(&s_jsonWriter); WriteToLog(CrashJsonGetBuffer(&s_jsonWriter), CrashJsonGetLength(&s_jsonWriter)); + + if (g_writeReportToFile != 0) + { + char reportPath[256]; + int pathLen = 0; + reportPath[0] = '\0'; + + if (g_reportPath[0] != '\0') + { + AppendString(reportPath, sizeof(reportPath), &pathLen, g_reportPath); + AppendString(reportPath, sizeof(reportPath), &pathLen, ".crashreport.json"); + } + else + { + const char* directory = g_defaultReportDirectory[0] != '\0' ? g_defaultReportDirectory : "/tmp"; + + AppendString(reportPath, sizeof(reportPath), &pathLen, directory); + if (pathLen > 0 && reportPath[pathLen - 1] != '/') + { + AppendChar(reportPath, sizeof(reportPath), &pathLen, '/'); + } + AppendString(reportPath, sizeof(reportPath), &pathLen, "dotnet_crash_"); + AppendUnsignedDecimal(reportPath, sizeof(reportPath), &pathLen, static_cast(getpid())); + AppendString(reportPath, sizeof(reportPath), &pathLen, ".crashreport.json"); + } + + TerminateBuffer(reportPath, sizeof(reportPath), &pathLen); + + int fd = open(reportPath, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd != -1) + { + int writeSucceeded = WriteAllToFile(fd, CrashJsonGetBuffer(&s_jsonWriter), CrashJsonGetLength(&s_jsonWriter)) && + WriteAllToFile(fd, "\n", 1); + + if (close(fd) != 0 || !writeSucceeded) + { + unlink(reportPath); + } + } + } +} + +void +InProcCrashReportInitialize( + int writeToFile, + const char* dumpPath, + const char* defaultDirectory) +{ + g_reportPath[0] = '\0'; + if (dumpPath != NULL) + { + int index = 0; + while (dumpPath[index] != '\0' && index < static_cast(sizeof(g_reportPath)) - 1) + { + g_reportPath[index] = dumpPath[index]; + index++; + } + g_reportPath[index] = '\0'; + } + + g_defaultReportDirectory[0] = '\0'; + if (defaultDirectory != NULL) + { + int index = 0; + while (defaultDirectory[index] != '\0' && index < static_cast(sizeof(g_defaultReportDirectory)) - 1) + { + g_defaultReportDirectory[index] = defaultDirectory[index]; + index++; + } + g_defaultReportDirectory[index] = '\0'; + } + + __sync_synchronize(); + g_writeReportToFile = writeToFile; } void @@ -352,6 +469,109 @@ WriteToLog( #endif } +int +WriteAllToFile( + int fd, + const char* buffer, + int len) +{ + int totalWritten = 0; + while (totalWritten < len) + { + ssize_t written = write(fd, buffer + totalWritten, len - totalWritten); + if (written > 0) + { + totalWritten += static_cast(written); + continue; + } + + if (written == -1 && errno == EINTR) + { + continue; + } + + return 0; + } + + return 1; +} + +void +AppendChar( + char* buffer, + int bufferSize, + int* pos, + char value) +{ + if (*pos < bufferSize - 1) + { + buffer[*pos] = value; + (*pos)++; + } +} + +void +AppendString( + char* buffer, + int bufferSize, + int* pos, + const char* value) +{ + if (value == NULL) + { + return; + } + + while (*value != '\0' && *pos < bufferSize - 1) + { + buffer[*pos] = *value; + (*pos)++; + value++; + } +} + +void +AppendUnsignedDecimal( + char* buffer, + int bufferSize, + int* pos, + uint64_t value) +{ + char reverse[32]; + int reversePos = 0; + + if (value == 0) + { + AppendChar(buffer, bufferSize, pos, '0'); + return; + } + + while (value != 0 && reversePos < static_cast(sizeof(reverse))) + { + reverse[reversePos++] = static_cast('0' + (value % 10)); + value /= 10; + } + + while (reversePos > 0) + { + AppendChar(buffer, bufferSize, pos, reverse[--reversePos]); + } +} + +void +TerminateBuffer( + char* buffer, + int bufferSize, + int* pos) +{ + if (*pos >= bufferSize) + { + *pos = bufferSize - 1; + } + + buffer[*pos] = '\0'; +} + void GetVersionString( char* buffer, diff --git a/src/coreclr/pal/src/crashreport/inproccrashreporter.h b/src/coreclr/pal/src/crashreport/inproccrashreporter.h index 3400f39ff74b91..d732553970c0bb 100644 --- a/src/coreclr/pal/src/crashreport/inproccrashreporter.h +++ b/src/coreclr/pal/src/crashreport/inproccrashreporter.h @@ -3,13 +3,16 @@ // In-proc crash report generation. // -// Emits a minimal createdump-shaped JSON payload to logcat / stderr. +// Emits a minimal createdump-shaped JSON payload to logcat / stderr and an +// optional *.crashreport.json file on disk. #pragma once #include #include +void InProcCrashReportInitialize(int writeToFile, const char* dumpPath, const char* defaultDirectory); + // Generate an in-proc crash report. Called from PROCCreateCrashDumpIfEnabled. // All arguments come from the signal handler and are signal-safe to read. void InProcCrashReportGenerate(int signal, siginfo_t* siginfo, void* context); diff --git a/src/coreclr/pal/src/thread/process.cpp b/src/coreclr/pal/src/thread/process.cpp index 63350be39420f4..4fae93efb19cf9 100644 --- a/src/coreclr/pal/src/thread/process.cpp +++ b/src/coreclr/pal/src/thread/process.cpp @@ -2640,7 +2640,14 @@ PROCAbortInitialize() bool enableCrashReportOnly = enabledReportOnlyCfg.IsSet() && enabledReportOnlyCfg.TryAsInteger(10, reportOnlyEnabled) && reportOnlyEnabled == 1; #ifdef HOST_ANDROID + const char* defaultReportDirectory = getenv("HOME"); + if (defaultReportDirectory == nullptr || defaultReportDirectory[0] == '\0') + { + defaultReportDirectory = getenv("TMPDIR"); + } + g_inProcCrashReportEnabled = enableMiniDump || enableCrashReport || enableCrashReportOnly; + InProcCrashReportInitialize(g_inProcCrashReportEnabled ? 1 : 0, dumpName, defaultReportDirectory); #endif if (enableMiniDump) From 34a7545da3902466227e4e2e6943ee600f7fb6ed Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 14 Apr 2026 14:50:22 -0400 Subject: [PATCH 09/52] Serialize in-proc crash report generation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/pal/src/crashreport/inproccrashreporter.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp b/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp index 85ce0f3260c7a4..9a8ce933e051de 100644 --- a/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp @@ -185,6 +185,12 @@ InProcCrashReportGenerate( siginfo_t* siginfo, void* context) { + static volatile int s_generating = 0; + if (__sync_val_compare_and_swap(&s_generating, 0, 1) != 0) + { + return; + } + (void)siginfo; char exTypeBuf[256]; From 8f3cffe1ce8a3e6f06844c52731fad58c01603fb Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 15 Apr 2026 09:55:40 -0400 Subject: [PATCH 10/52] Move in-proc crashreport code under debug Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/debug/CMakeLists.txt | 3 +++ src/coreclr/debug/crashreport/CMakeLists.txt | 12 ++++++++++++ .../src => debug}/crashreport/crashjsonwriter.cpp | 0 .../{pal/src => debug}/crashreport/crashjsonwriter.h | 0 .../crashreport/inproccrashreporter.cpp | 7 ++++--- .../src => debug}/crashreport/inproccrashreporter.h | 0 .../src => debug}/crashreport/moduleenumerator.cpp | 0 .../src => debug}/crashreport/moduleenumerator.h | 0 src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt | 4 ++++ src/coreclr/pal/src/CMakeLists.txt | 10 ++-------- src/coreclr/pal/src/thread/process.cpp | 4 ++-- src/coreclr/vm/CMakeLists.txt | 1 + src/coreclr/vm/crashreportstackwalker.cpp | 2 +- 13 files changed, 29 insertions(+), 14 deletions(-) create mode 100644 src/coreclr/debug/crashreport/CMakeLists.txt rename src/coreclr/{pal/src => debug}/crashreport/crashjsonwriter.cpp (100%) rename src/coreclr/{pal/src => debug}/crashreport/crashjsonwriter.h (100%) rename src/coreclr/{pal/src => debug}/crashreport/inproccrashreporter.cpp (99%) rename src/coreclr/{pal/src => debug}/crashreport/inproccrashreporter.h (100%) rename src/coreclr/{pal/src => debug}/crashreport/moduleenumerator.cpp (100%) rename src/coreclr/{pal/src => debug}/crashreport/moduleenumerator.h (100%) diff --git a/src/coreclr/debug/CMakeLists.txt b/src/coreclr/debug/CMakeLists.txt index 5a0a420346882f..c8fb2407550d90 100644 --- a/src/coreclr/debug/CMakeLists.txt +++ b/src/coreclr/debug/CMakeLists.txt @@ -7,6 +7,9 @@ include_directories(${RUNTIME_DIR}) add_subdirectory(daccess) add_subdirectory(ee) add_subdirectory(di) +if(CLR_CMAKE_TARGET_ANDROID) + add_subdirectory(crashreport) +endif() if(CLR_CMAKE_HOST_WIN32) add_subdirectory(createdump) endif(CLR_CMAKE_HOST_WIN32) diff --git a/src/coreclr/debug/crashreport/CMakeLists.txt b/src/coreclr/debug/crashreport/CMakeLists.txt new file mode 100644 index 00000000000000..142b64613c079e --- /dev/null +++ b/src/coreclr/debug/crashreport/CMakeLists.txt @@ -0,0 +1,12 @@ +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +include_directories(${CLR_ARTIFACTS_OBJ_DIR}) +include_directories(${CLR_DIR}) + +set(CRASHREPORT_SOURCES + crashjsonwriter.cpp + inproccrashreporter.cpp + moduleenumerator.cpp +) + +add_library(inproccrashreport OBJECT ${CRASHREPORT_SOURCES}) diff --git a/src/coreclr/pal/src/crashreport/crashjsonwriter.cpp b/src/coreclr/debug/crashreport/crashjsonwriter.cpp similarity index 100% rename from src/coreclr/pal/src/crashreport/crashjsonwriter.cpp rename to src/coreclr/debug/crashreport/crashjsonwriter.cpp diff --git a/src/coreclr/pal/src/crashreport/crashjsonwriter.h b/src/coreclr/debug/crashreport/crashjsonwriter.h similarity index 100% rename from src/coreclr/pal/src/crashreport/crashjsonwriter.h rename to src/coreclr/debug/crashreport/crashjsonwriter.h diff --git a/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp similarity index 99% rename from src/coreclr/pal/src/crashreport/inproccrashreporter.cpp rename to src/coreclr/debug/crashreport/inproccrashreporter.cpp index 9a8ce933e051de..53c9115d471a81 100644 --- a/src/coreclr/pal/src/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -8,7 +8,6 @@ #include "inproccrashreporter.h" #include "crashjsonwriter.h" #include "moduleenumerator.h" -#include "pal/thread.hpp" #include #include @@ -20,6 +19,8 @@ #include #endif +#include + // Include the .NET version string instead of linking because it is "static". #include "_version.c" @@ -234,7 +235,7 @@ InProcCrashReportGenerate( if (g_enumerateThreadsCallback != NULL) { MultiThreadJsonContext threadContext = { &s_jsonWriter, context, 0, 0, hasException, exTypeBuf, exHresult }; - uint64_t crashingTid = static_cast(THREADSilentGetCurrentThreadId()); + uint64_t crashingTid = static_cast(minipal_get_current_thread_id()); g_enumerateThreadsCallback(crashingTid, JsonThreadCallback, JsonThreadFrameCallback, &threadContext); @@ -275,7 +276,7 @@ InProcCrashReportGenerate( } else { - uint64_t crashingTid = static_cast(THREADSilentGetCurrentThreadId()); + uint64_t crashingTid = static_cast(minipal_get_current_thread_id()); CrashJsonOpenObject(&s_jsonWriter, NULL); CrashJsonWriteString(&s_jsonWriter, "is_managed", diff --git a/src/coreclr/pal/src/crashreport/inproccrashreporter.h b/src/coreclr/debug/crashreport/inproccrashreporter.h similarity index 100% rename from src/coreclr/pal/src/crashreport/inproccrashreporter.h rename to src/coreclr/debug/crashreport/inproccrashreporter.h diff --git a/src/coreclr/pal/src/crashreport/moduleenumerator.cpp b/src/coreclr/debug/crashreport/moduleenumerator.cpp similarity index 100% rename from src/coreclr/pal/src/crashreport/moduleenumerator.cpp rename to src/coreclr/debug/crashreport/moduleenumerator.cpp diff --git a/src/coreclr/pal/src/crashreport/moduleenumerator.h b/src/coreclr/debug/crashreport/moduleenumerator.h similarity index 100% rename from src/coreclr/pal/src/crashreport/moduleenumerator.h rename to src/coreclr/debug/crashreport/moduleenumerator.h diff --git a/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt b/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt index c0cf0a1ff4176b..84db354cfb949f 100644 --- a/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt +++ b/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt @@ -183,6 +183,10 @@ endif() target_link_libraries(coreclr_static PUBLIC ${CORECLR_LIBRARIES} ${CORECLR_STATIC_CLRJIT_STATIC} ${CORECLR_STATIC_CLRINTERPRETER_STATIC} cee_wks_core ${CEE_WKS_STATIC} ${FOUNDATION}) target_compile_definitions(coreclr_static PUBLIC CORECLR_EMBEDDED) +if(TARGET inproccrashreport) + target_sources(coreclr_static PRIVATE $) +endif() + if (CLR_CMAKE_HOST_ANDROID) target_link_libraries(coreclr PUBLIC log) endif() diff --git a/src/coreclr/pal/src/CMakeLists.txt b/src/coreclr/pal/src/CMakeLists.txt index 14799c9f1a945b..982ce0b1a1fef1 100644 --- a/src/coreclr/pal/src/CMakeLists.txt +++ b/src/coreclr/pal/src/CMakeLists.txt @@ -37,6 +37,7 @@ endif(CORECLR_SET_RPATH) # Include directories include_directories(include) +include_directories(${CLR_DIR}) # Compile options @@ -206,14 +207,6 @@ set(SOURCES thread/threadsusp.cpp ) -if(CLR_CMAKE_TARGET_ANDROID) - list(APPEND SOURCES - crashreport/crashjsonwriter.cpp - crashreport/inproccrashreporter.cpp - crashreport/moduleenumerator.cpp - ) -endif() - set_source_files_properties( com/guid.cpp PROPERTIES @@ -233,6 +226,7 @@ add_library(coreclrpal_objects add_library(coreclrpal STATIC $ + $<$:$> ${LIBUNWIND_OBJECTS} ) diff --git a/src/coreclr/pal/src/thread/process.cpp b/src/coreclr/pal/src/thread/process.cpp index 4fae93efb19cf9..a844a3584bc9ab 100644 --- a/src/coreclr/pal/src/thread/process.cpp +++ b/src/coreclr/pal/src/thread/process.cpp @@ -36,7 +36,7 @@ SET_DEFAULT_DEBUG_CHANNEL(PROCESS); // some headers have code with asserts, so d #include #ifdef HOST_ANDROID -#include "crashreport/inproccrashreporter.h" +#include "debug/crashreport/inproccrashreporter.h" #endif #include @@ -2823,7 +2823,7 @@ PROCIsCrashReportEnabled() --*/ #ifdef HOST_ANDROID #include -#include "crashreport/inproccrashreporter.h" +#include "debug/crashreport/inproccrashreporter.h" VOID PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, void* context, bool serialize) { diff --git a/src/coreclr/vm/CMakeLists.txt b/src/coreclr/vm/CMakeLists.txt index f6facfa7fe9ac5..7548d60c1af64e 100644 --- a/src/coreclr/vm/CMakeLists.txt +++ b/src/coreclr/vm/CMakeLists.txt @@ -4,6 +4,7 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) include_directories(BEFORE ${CMAKE_CURRENT_SOURCE_DIR}) include_directories(${ARCH_SOURCES_DIR}) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../interop/inc) +include_directories(${CLR_DIR}) include_directories(${CLR_SRC_NATIVE_DIR}) include_directories(${RUNTIME_DIR}) diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index e7788a6bc5bc16..446ae9726cdeb9 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -12,7 +12,7 @@ #ifdef HOST_ANDROID -#include "../pal/src/crashreport/inproccrashreporter.h" +#include "debug/crashreport/inproccrashreporter.h" struct WalkContext { From f2cd7761179971d5713171a07c12bd925bfce25c Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 15 Apr 2026 10:05:29 -0400 Subject: [PATCH 11/52] Address in-proc crashreport review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../debug/crashreport/inproccrashreporter.cpp | 20 ++--- .../debug/crashreport/moduleenumerator.cpp | 56 +------------ src/coreclr/pal/src/exception/signal.cpp | 10 +-- src/coreclr/pal/src/thread/process.cpp | 7 +- src/coreclr/vm/crashreportstackwalker.cpp | 81 +++++++++++-------- 5 files changed, 62 insertions(+), 112 deletions(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index 53c9115d471a81..e12cac7a2c901b 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -14,15 +14,15 @@ #include #include #include - -#ifdef __ANDROID__ -#include -#endif - +#include #include // Include the .NET version string instead of linking because it is "static". +#if __has_include("_version.c") #include "_version.c" +#else +static char sccsid[] = "@(#)Version N/A"; +#endif static CrashJsonWriter s_jsonWriter; // These callbacks are published during runtime startup and then only read from @@ -431,7 +431,6 @@ WriteToLog( const char* msg, int len) { -#ifdef __ANDROID__ if (msg == NULL) { return; @@ -446,6 +445,7 @@ WriteToLog( } } +#ifdef __ANDROID__ // Emit long payloads in chunks so the JSON is not truncated by Android's // per-entry log size limit. int offset = 0; @@ -464,15 +464,11 @@ WriteToLog( } buffer[chunk] = '\0'; - // TODO-Async: Prefer Android's async_safe/log.h entrypoints here if they - // become available through the supported NDK surface. __android_log_write - // keeps the crash report visible in logcat, but it doesn't document an - // async-signal-safe contract. - __android_log_write(ANDROID_LOG_ERROR, "DOTNET", buffer); + minipal_log_write_error(buffer); offset += chunk; } #else - write(STDERR_FILENO, msg, len); + (void)WriteAllToFile(STDERR_FILENO, msg, len); #endif } diff --git a/src/coreclr/debug/crashreport/moduleenumerator.cpp b/src/coreclr/debug/crashreport/moduleenumerator.cpp index 60963c7fbfd1fd..c23122b81880f8 100644 --- a/src/coreclr/debug/crashreport/moduleenumerator.cpp +++ b/src/coreclr/debug/crashreport/moduleenumerator.cpp @@ -11,18 +11,6 @@ #include #include -#ifdef __linux__ -#include - -#if UINTPTR_MAX > UINT32_MAX -typedef Elf64_Ehdr ElfNative_Ehdr; -typedef Elf64_Phdr ElfNative_Phdr; -#else -typedef Elf32_Ehdr ElfNative_Ehdr; -typedef Elf32_Phdr ElfNative_Phdr; -#endif -#endif - typedef void (*ModuleCallback)( uint64_t startAddr, uint64_t endAddr, @@ -157,54 +145,14 @@ ComputeImageBase( uint64_t endAddr, uint64_t fileOffset) { -#ifndef __linux__ (void)endAddr; - (void)fileOffset; - return startAddr; -#else - if (fileOffset > startAddr) - { - return startAddr; - } - uint64_t mappedFileBase = startAddr - fileOffset; - if (mappedFileBase > endAddr || sizeof(ElfNative_Ehdr) > endAddr - mappedFileBase) + if (fileOffset > startAddr) { return startAddr; } - const ElfNative_Ehdr* ehdr = reinterpret_cast(static_cast(mappedFileBase)); - if (ehdr->e_ident[EI_MAG0] != ELFMAG0 || - ehdr->e_ident[EI_MAG1] != ELFMAG1 || - ehdr->e_ident[EI_MAG2] != ELFMAG2 || - ehdr->e_ident[EI_MAG3] != ELFMAG3) - { - return mappedFileBase; - } - - if (ehdr->e_phentsize != sizeof(ElfNative_Phdr)) - { - return mappedFileBase; - } - - uint64_t availableBytes = endAddr - mappedFileBase; - uint64_t phdrBytes = static_cast(ehdr->e_phnum) * sizeof(ElfNative_Phdr); - if (ehdr->e_phoff > availableBytes || phdrBytes > availableBytes - ehdr->e_phoff) - { - return mappedFileBase; - } - - const ElfNative_Phdr* phdrs = reinterpret_cast(static_cast(mappedFileBase + ehdr->e_phoff)); - for (int i = 0; i < ehdr->e_phnum; i++) - { - if (phdrs[i].p_type == PT_LOAD && phdrs[i].p_offset == 0) - { - return mappedFileBase - phdrs[i].p_vaddr; - } - } - - return mappedFileBase; -#endif + return startAddr - fileOffset; } void diff --git a/src/coreclr/pal/src/exception/signal.cpp b/src/coreclr/pal/src/exception/signal.cpp index 2f2620b430aba7..a15a76618fabf2 100644 --- a/src/coreclr/pal/src/exception/signal.cpp +++ b/src/coreclr/pal/src/exception/signal.cpp @@ -471,10 +471,7 @@ static void invoke_previous_action(struct sigaction* action, int code, siginfo_t if (g_crash_report_before_signal_chaining) { PROCNotifyProcessShutdown(IsRunningOnAlternateStack(context)); - if (!PROCIsCrashReportEnabled()) - { - PROCLogManagedCallstackForSignal(code); - } + PROCLogManagedCallstackForSignal(code); PROCCreateCrashDumpIfEnabled(code, siginfo, context, true); } @@ -495,10 +492,7 @@ static void invoke_previous_action(struct sigaction* action, int code, siginfo_t { PROCNotifyProcessShutdown(IsRunningOnAlternateStack(context)); - if (!PROCIsCrashReportEnabled()) - { - PROCLogManagedCallstackForSignal(code); - } + PROCLogManagedCallstackForSignal(code); PROCCreateCrashDumpIfEnabled(code, siginfo, context, true); } } diff --git a/src/coreclr/pal/src/thread/process.cpp b/src/coreclr/pal/src/thread/process.cpp index a844a3584bc9ab..d9b2e759be89c0 100644 --- a/src/coreclr/pal/src/thread/process.cpp +++ b/src/coreclr/pal/src/thread/process.cpp @@ -2646,8 +2646,10 @@ PROCAbortInitialize() defaultReportDirectory = getenv("TMPDIR"); } - g_inProcCrashReportEnabled = enableMiniDump || enableCrashReport || enableCrashReportOnly; - InProcCrashReportInitialize(g_inProcCrashReportEnabled ? 1 : 0, dumpName, defaultReportDirectory); + const bool crashReportEnabled = enableCrashReport || enableCrashReportOnly; + const bool writeCrashReportToFile = dumpName != nullptr && dumpName[0] != '\0'; + g_inProcCrashReportEnabled = crashReportEnabled; + InProcCrashReportInitialize(writeCrashReportToFile ? 1 : 0, dumpName, defaultReportDirectory); #endif if (enableMiniDump) @@ -2823,7 +2825,6 @@ PROCIsCrashReportEnabled() --*/ #ifdef HOST_ANDROID #include -#include "debug/crashreport/inproccrashreporter.h" VOID PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, void* context, bool serialize) { diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index 446ae9726cdeb9..f24efbc296314a 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -179,59 +179,72 @@ CrashReportGetExceptionForThread( *hresult = 0; } - OBJECTREF throwable = pThread->GetThrowable(); - if (throwable == NULL) + if (!pThread->PreemptiveGCDisabled()) { return 0; } - MethodTable* pMT = throwable->GetMethodTable(); - if (pMT != NULL) + int result = 0; + + GCX_COOP(); + + OBJECTREF throwable = pThread->GetThrowable(); + GCPROTECT_BEGIN(throwable); + + if (throwable != NULL) { - mdTypeDef cl = pMT->GetCl(); - Module* pModule = pMT->GetModule(); - if (pModule != NULL) + MethodTable* pMT = throwable->GetMethodTable(); + if (pMT != NULL) { - IMDInternalImport* pImport = pModule->GetMDImport(); - if (pImport != NULL && cl != mdTypeDefNil) + mdTypeDef cl = pMT->GetCl(); + Module* pModule = pMT->GetModule(); + if (pModule != NULL) { - LPCUTF8 className = NULL; - LPCUTF8 namespaceName = NULL; - pImport->GetNameOfTypeDef(cl, &className, &namespaceName); - - int index = 0; - if (namespaceName != NULL) + IMDInternalImport* pImport = pModule->GetMDImport(); + if (pImport != NULL && cl != mdTypeDefNil) { - while (*namespaceName != '\0' && index < exceptionTypeBufSize - 1) - { - exceptionTypeBuf[index++] = *namespaceName++; - } - } + LPCUTF8 className = NULL; + LPCUTF8 namespaceName = NULL; + pImport->GetNameOfTypeDef(cl, &className, &namespaceName); - if (className != NULL) - { - if (index > 0 && index < exceptionTypeBufSize - 1) + int index = 0; + if (namespaceName != NULL) { - exceptionTypeBuf[index++] = '.'; + while (*namespaceName != '\0' && index < exceptionTypeBufSize - 1) + { + exceptionTypeBuf[index++] = *namespaceName++; + } } - while (*className != '\0' && index < exceptionTypeBufSize - 1) + if (className != NULL) { - exceptionTypeBuf[index++] = *className++; + if (index > 0 && index < exceptionTypeBufSize - 1) + { + exceptionTypeBuf[index++] = '.'; + } + + while (*className != '\0' && index < exceptionTypeBufSize - 1) + { + exceptionTypeBuf[index++] = *className++; + } } - } - exceptionTypeBuf[index] = '\0'; + exceptionTypeBuf[index] = '\0'; + } } } - } - if (hresult != NULL) - { - *hresult = static_cast(((EXCEPTIONREF)throwable)->GetHResult()); + if (hresult != NULL) + { + *hresult = static_cast(((EXCEPTIONREF)throwable)->GetHResult()); + } + + result = 1; } - return 1; + GCPROTECT_END(); + + return result; } static @@ -267,8 +280,6 @@ CrashReportEnumerateThreads( { // This minimal lift intentionally reuses the existing ThreadStore traversal // and StackWalkFrames as a best-effort source for managed thread state. - // The later strict-safety slices replace this with the signal-safe thread - // registry and pre-published frame snapshots. Thread* pCrashThread = GetThreadAsyncSafe(); bool crashThreadHandled = false; From 7fa6a2d99aab316de386345abc01728de0965213 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 15 Apr 2026 13:33:53 -0400 Subject: [PATCH 12/52] Trim parity-only native crashreport fields Remove the /proc-based native address enrichment that was only being used for createdump-style parity, while keeping the top-level process_name information. This reduces crash-path work and keeps the minimal Android in-proc report focused on the managed diagnostic data reviewers asked for. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../debug/crashreport/inproccrashreporter.cpp | 32 +------- .../debug/crashreport/moduleenumerator.cpp | 80 +------------------ .../debug/crashreport/moduleenumerator.h | 4 - 3 files changed, 2 insertions(+), 114 deletions(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index e12cac7a2c901b..a7b692f4bc4b40 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -740,10 +740,6 @@ WriteCrashSiteFrameToJson( uint64_t spValue = GetStackPointer(context); char ip[32] = "0x0"; char sp[32] = "0x0"; - uint64_t moduleBase = 0; - char moduleBaseBuffer[32]; - char moduleName[256]; - moduleName[0] = '\0'; FormatHexValue(ip, sizeof(ip), ipValue); FormatHexValue(sp, sizeof(sp), spValue); @@ -752,15 +748,6 @@ WriteCrashSiteFrameToJson( CrashJsonWriteString(writer, "is_managed", "false"); CrashJsonWriteString(writer, "stack_pointer", sp); CrashJsonWriteString(writer, "native_address", ip); - if (CrashModulesTryLookupModuleForAddress(ipValue, &moduleBase, moduleName, sizeof(moduleName))) - { - char imageOffset[32]; - FormatHexValue(moduleBaseBuffer, sizeof(moduleBaseBuffer), moduleBase); - FormatHexValue(imageOffset, sizeof(imageOffset), ipValue - moduleBase); - CrashJsonWriteString(writer, "module_address", moduleBaseBuffer); - CrashJsonWriteString(writer, "native_image_offset", imageOffset); - CrashJsonWriteString(writer, "native_module", moduleName); - } CrashJsonCloseObject(writer); } @@ -817,10 +804,6 @@ JsonFrameCallback( void* ctx) { CrashJsonWriter* writer = reinterpret_cast(ctx); - uint64_t moduleBase = 0; - char nativeModuleName[256]; - nativeModuleName[0] = '\0'; - char ipBuffer[32]; char stackPointerBuffer[32]; char nativeOffsetBuffer[32]; @@ -841,15 +824,6 @@ JsonFrameCallback( CrashJsonWriteString(writer, "stack_pointer", stackPointerBuffer); CrashJsonWriteString(writer, "native_address", ipBuffer); CrashJsonWriteString(writer, "native_offset", nativeOffsetBuffer); - if (CrashModulesTryLookupModuleForAddress(ip, &moduleBase, nativeModuleName, sizeof(nativeModuleName))) - { - char moduleAddress[32]; - char nativeImageOffset[32]; - FormatHexValue(moduleAddress, sizeof(moduleAddress), moduleBase); - FormatHexValue(nativeImageOffset, sizeof(nativeImageOffset), ip - moduleBase); - CrashJsonWriteString(writer, "module_address", moduleAddress); - CrashJsonWriteString(writer, "native_image_offset", nativeImageOffset); - } if (methodName != NULL) { @@ -879,11 +853,7 @@ JsonFrameCallback( else { CrashJsonWriteString(writer, "is_managed", "false"); - if (nativeModuleName[0] != '\0') - { - CrashJsonWriteString(writer, "native_module", nativeModuleName); - } - else if (moduleName != NULL) + if (moduleName != NULL) { CrashJsonWriteString(writer, "native_module", moduleName); } diff --git a/src/coreclr/debug/crashreport/moduleenumerator.cpp b/src/coreclr/debug/crashreport/moduleenumerator.cpp index c23122b81880f8..df2bd423834d51 100644 --- a/src/coreclr/debug/crashreport/moduleenumerator.cpp +++ b/src/coreclr/debug/crashreport/moduleenumerator.cpp @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// Crash report module lookup helpers. +// Crash report process-name lookup helper. // Parses /proc/self/cmdline and /proc/self/maps using only open/read/close. // No stdio, no sscanf, no heap allocation. @@ -25,15 +25,6 @@ struct ProcessNameCtx int found; }; -struct LookupAddressCtx -{ - uint64_t address; - uint64_t baseAddress; - char* filename; - int filenameLen; - int found; -}; - static const char* ParseHex( @@ -45,13 +36,6 @@ const char* GetFilename( const char* path); -static -uint64_t -ComputeImageBase( - uint64_t startAddr, - uint64_t endAddr, - uint64_t fileOffset); - void CopyFilename( char* filename, @@ -139,22 +123,6 @@ GetFilename( return last; } -uint64_t -ComputeImageBase( - uint64_t startAddr, - uint64_t endAddr, - uint64_t fileOffset) -{ - (void)endAddr; - - if (fileOffset > startAddr) - { - return startAddr; - } - - return startAddr - fileOffset; -} - void CopyFilename( char* filename, @@ -307,52 +275,6 @@ EnumerateModules( close(fd); } -void -ModuleLookupByAddress( - uint64_t startAddr, - uint64_t endAddr, - uint64_t fileOffset, - const char* filename, - void* ctx) -{ - LookupAddressCtx* lookup = reinterpret_cast(ctx); - if (lookup->found || lookup->address < startAddr || lookup->address >= endAddr) - { - return; - } - - lookup->baseAddress = ComputeImageBase(startAddr, endAddr, fileOffset); - lookup->found = 1; - - if (lookup->filename != NULL && lookup->filenameLen > 0) - { - CopyFilename(lookup->filename, lookup->filenameLen, filename); - } -} - -int -CrashModulesTryLookupModuleForAddress( - uint64_t address, - uint64_t* baseAddress, - char* filename, - int filenameLen) -{ - LookupAddressCtx ctx = { address, 0, filename, filenameLen, 0 }; - if (filename != NULL && filenameLen > 0) - { - filename[0] = '\0'; - } - - EnumerateModules(ModuleLookupByAddress, &ctx, 0); - - if (ctx.found && baseAddress != NULL) - { - *baseAddress = ctx.baseAddress; - } - - return ctx.found; -} - void ProcessNameCallback( uint64_t startAddr, diff --git a/src/coreclr/debug/crashreport/moduleenumerator.h b/src/coreclr/debug/crashreport/moduleenumerator.h index 5b8e886ebdbed1..fedca82f36710c 100644 --- a/src/coreclr/debug/crashreport/moduleenumerator.h +++ b/src/coreclr/debug/crashreport/moduleenumerator.h @@ -9,10 +9,6 @@ #include -// Look up the executable mapping that contains the specified address. -// Returns 1 if a matching module is found, 0 otherwise. -int CrashModulesTryLookupModuleForAddress(uint64_t address, uint64_t* baseAddress, char* filename, int filenameLen); - // Returns the basename of the current process image. Prefers /proc/self/cmdline // and falls back to the first executable mapping in /proc/self/maps. int CrashModulesTryGetProcessName(char* filename, int filenameLen); From fd71ac74414f35c7bcd0ea0bca1f8bde12ff49fc Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 15 Apr 2026 14:12:06 -0400 Subject: [PATCH 13/52] Inline crashreport process name lookup Remove the now-unneeded moduleenumerator helper and resolve process_name directly in the in-proc crash reporter via /proc/self/cmdline with a simple /proc/self/exe fallback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/debug/crashreport/CMakeLists.txt | 1 - .../debug/crashreport/inproccrashreporter.cpp | 106 +++++- .../debug/crashreport/moduleenumerator.cpp | 335 ------------------ .../debug/crashreport/moduleenumerator.h | 14 - 4 files changed, 104 insertions(+), 352 deletions(-) delete mode 100644 src/coreclr/debug/crashreport/moduleenumerator.cpp delete mode 100644 src/coreclr/debug/crashreport/moduleenumerator.h diff --git a/src/coreclr/debug/crashreport/CMakeLists.txt b/src/coreclr/debug/crashreport/CMakeLists.txt index 142b64613c079e..27ddfa084fa339 100644 --- a/src/coreclr/debug/crashreport/CMakeLists.txt +++ b/src/coreclr/debug/crashreport/CMakeLists.txt @@ -6,7 +6,6 @@ include_directories(${CLR_DIR}) set(CRASHREPORT_SOURCES crashjsonwriter.cpp inproccrashreporter.cpp - moduleenumerator.cpp ) add_library(inproccrashreport OBJECT ${CRASHREPORT_SOURCES}) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index a7b692f4bc4b40..a7520fddd1856b 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -7,7 +7,6 @@ #include "inproccrashreporter.h" #include "crashjsonwriter.h" -#include "moduleenumerator.h" #include #include @@ -90,6 +89,24 @@ BuildMethodName( const char* className, const char* methodName); +static +const char* +GetFilename( + const char* path); + +static +void +CopyString( + char* buffer, + int bufferSize, + const char* value); + +static +int +TryGetProcessName( + char* filename, + int filenameLen); + static void JsonFrameCallback( @@ -226,7 +243,7 @@ InProcCrashReportGenerate( CrashJsonCloseObject(&s_jsonWriter); char processName[256]; - if (CrashModulesTryGetProcessName(processName, sizeof(processName))) + if (TryGetProcessName(processName, sizeof(processName))) { CrashJsonWriteString(&s_jsonWriter, "process_name", processName); } @@ -788,6 +805,91 @@ BuildMethodName( buffer[index] = '\0'; } +const char* +GetFilename( + const char* path) +{ + const char* last = path; + for (const char* p = path; *p != '\0'; p++) + { + if (*p == '/') + { + last = p + 1; + } + } + + return last; +} + +void +CopyString( + char* buffer, + int bufferSize, + const char* value) +{ + if (buffer == NULL || bufferSize <= 0) + { + return; + } + + if (value == NULL) + { + buffer[0] = '\0'; + return; + } + + int index = 0; + while (value[index] != '\0' && index < bufferSize - 1) + { + buffer[index] = value[index]; + index++; + } + + buffer[index] = '\0'; +} + +int +TryGetProcessName( + char* filename, + int filenameLen) +{ + if (filename == NULL || filenameLen <= 0) + { + return 0; + } + + filename[0] = '\0'; + + int fd = open("/proc/self/cmdline", O_RDONLY); + if (fd != -1) + { + char cmdline[256]; + ssize_t bytesRead = read(fd, cmdline, sizeof(cmdline) - 1); + close(fd); + + if (bytesRead > 0) + { + cmdline[bytesRead] = '\0'; + CopyString(filename, filenameLen, GetFilename(cmdline)); + if (filename[0] != '\0') + { + return 1; + } + } + } + + char exePath[256]; + ssize_t pathLength = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1); + if (pathLength > 0) + { + exePath[pathLength] = '\0'; + CopyString(filename, filenameLen, GetFilename(exePath)); + return filename[0] != '\0'; + } + + return 0; +} + void JsonFrameCallback( uint64_t ip, diff --git a/src/coreclr/debug/crashreport/moduleenumerator.cpp b/src/coreclr/debug/crashreport/moduleenumerator.cpp deleted file mode 100644 index df2bd423834d51..00000000000000 --- a/src/coreclr/debug/crashreport/moduleenumerator.cpp +++ /dev/null @@ -1,335 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -// Crash report process-name lookup helper. -// Parses /proc/self/cmdline and /proc/self/maps using only open/read/close. -// No stdio, no sscanf, no heap allocation. - -#include "moduleenumerator.h" - -#include -#include -#include - -typedef void (*ModuleCallback)( - uint64_t startAddr, - uint64_t endAddr, - uint64_t fileOffset, - const char* filename, - void* ctx); - -struct ProcessNameCtx -{ - char* filename; - int filenameLen; - int found; -}; - -static -const char* -ParseHex( - const char* p, - uint64_t* out); - -static -const char* -GetFilename( - const char* path); - -void -CopyFilename( - char* filename, - int filenameLen, - const char* source); - -static -void -ParseMapsLine( - const char* line, - ModuleCallback callback, - void* ctx, - char* lastModule, - int lastModuleSize, - bool deduplicate); - -static -void -EnumerateModules( - ModuleCallback callback, - void* ctx, - bool deduplicate); - -static -void -ModuleLookupByAddress( - uint64_t startAddr, - uint64_t endAddr, - uint64_t fileOffset, - const char* filename, - void* ctx); - -static -void -ProcessNameCallback( - uint64_t startAddr, - uint64_t endAddr, - uint64_t fileOffset, - const char* filename, - void* ctx); - -const char* -ParseHex( - const char* p, - uint64_t* out) -{ - uint64_t value = 0; - while (*p != '\0') - { - char c = *p; - if (c >= '0' && c <= '9') - { - value = (value << 4) | static_cast(c - '0'); - } - else if (c >= 'a' && c <= 'f') - { - value = (value << 4) | static_cast(c - 'a' + 10); - } - else if (c >= 'A' && c <= 'F') - { - value = (value << 4) | static_cast(c - 'A' + 10); - } - else - { - break; - } - - p++; - } - - *out = value; - return p; -} - -const char* -GetFilename( - const char* path) -{ - const char* last = path; - for (const char* p = path; *p; p++) - { - if (*p == '/') - last = p + 1; - } - return last; -} - -void -CopyFilename( - char* filename, - int filenameLen, - const char* source) -{ - if (filename == NULL || filenameLen <= 0 || source == NULL) - { - return; - } - - int len = 0; - while (source[len] && len < filenameLen - 1) - { - filename[len] = source[len]; - len++; - } - filename[len] = '\0'; -} - -void -ParseMapsLine( - const char* line, - ModuleCallback callback, - void* ctx, - char* lastModule, - int lastModuleSize, - bool deduplicate) -{ - uint64_t startAddr = 0; - const char* p = ParseHex(line, &startAddr); - if (*p != '-') - { - return; - } - p++; - - uint64_t endAddr = 0; - p = ParseHex(p, &endAddr); - if (*p != ' ') - { - return; - } - p++; - - const char* permissions = p; - while (*p != '\0' && *p != ' ') - { - p++; - } - if (*p != ' ') - { - return; - } - int executable = (p - permissions > 2) && permissions[2] == 'x'; - p++; - - while (*p == ' ') - { - p++; - } - - uint64_t fileOffset = 0; - p = ParseHex(p, &fileOffset); - while (*p == ' ') - { - p++; - } - while (*p != '\0' && *p != ' ') - { - p++; - } - while (*p == ' ') - { - p++; - } - while (*p != '\0' && *p != ' ') - { - p++; - } - while (*p == ' ') - { - p++; - } - - if (!executable || *p == '\0' || *p == '\n' || *p == '[') - { - return; - } - - char pathname[256]; - int pathnameLen = 0; - while (p[pathnameLen] != '\0' && p[pathnameLen] != '\n' && pathnameLen < static_cast(sizeof(pathname)) - 1) - { - pathname[pathnameLen] = p[pathnameLen]; - pathnameLen++; - } - pathname[pathnameLen] = '\0'; - - if (deduplicate && strcmp(pathname, lastModule) == 0) - { - return; - } - - if (deduplicate) - { - CopyFilename(lastModule, lastModuleSize, pathname); - } - - callback(startAddr, endAddr, fileOffset, GetFilename(pathname), ctx); -} - -void -EnumerateModules( - ModuleCallback callback, - void* ctx, - bool deduplicate) -{ - // Walk the live executable mappings as a crash-time fallback for process - // name resolution without depending on loader APIs or heap allocation. - int fd = open("/proc/self/maps", O_RDONLY); - if (fd == -1) - { - return; - } - - char readBuf[4096]; - char lineBuf[512]; - int linePos = 0; - char lastModule[256] = { 0 }; - ssize_t bytesRead = 0; - - while ((bytesRead = read(fd, readBuf, sizeof(readBuf))) > 0) - { - for (int i = 0; i < bytesRead; i++) - { - if (readBuf[i] == '\n') - { - lineBuf[linePos] = '\0'; - ParseMapsLine(lineBuf, callback, ctx, lastModule, sizeof(lastModule), deduplicate); - linePos = 0; - } - else if (linePos < sizeof(lineBuf) - 1) - { - lineBuf[linePos++] = readBuf[i]; - } - } - } - - close(fd); -} - -void -ProcessNameCallback( - uint64_t startAddr, - uint64_t endAddr, - uint64_t fileOffset, - const char* filename, - void* ctx) -{ - (void)startAddr; - (void)endAddr; - (void)fileOffset; - - ProcessNameCtx* processName = (ProcessNameCtx*)ctx; - if (processName->found || processName->filename == NULL || processName->filenameLen <= 0) - { - return; - } - - CopyFilename(processName->filename, processName->filenameLen, filename); - processName->found = processName->filename[0] != '\0'; -} - -int -CrashModulesTryGetProcessName( - char* filename, - int filenameLen) -{ - if (filename == NULL || filenameLen <= 0) - { - return 0; - } - - filename[0] = '\0'; - - int fd = open("/proc/self/cmdline", O_RDONLY); - if (fd != -1) - { - char cmdline[256]; - ssize_t bytesRead = read(fd, cmdline, sizeof(cmdline) - 1); - close(fd); - - if (bytesRead > 0) - { - cmdline[bytesRead] = '\0'; - CopyFilename(filename, filenameLen, GetFilename(cmdline)); - if (filename[0] != '\0') - { - return 1; - } - } - } - - // Fall back to the first executable mapping if the kernel cmdline view is - // unavailable or empty. - ProcessNameCtx ctx = { filename, filenameLen, 0 }; - EnumerateModules(ProcessNameCallback, &ctx, 1); - return ctx.found; -} diff --git a/src/coreclr/debug/crashreport/moduleenumerator.h b/src/coreclr/debug/crashreport/moduleenumerator.h deleted file mode 100644 index fedca82f36710c..00000000000000 --- a/src/coreclr/debug/crashreport/moduleenumerator.h +++ /dev/null @@ -1,14 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -// Crash report process-name lookup helper. -// Uses only open/read/close and manual parsing so it remains usable from -// crash-time code without stdio or heap allocation. - -#pragma once - -#include - -// Returns the basename of the current process image. Prefers /proc/self/cmdline -// and falls back to the first executable mapping in /proc/self/maps. -int CrashModulesTryGetProcessName(char* filename, int filenameLen); From 9461ae2c48c2403243790a8e87e9da2c12a2d49d Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 15 Apr 2026 22:16:27 -0400 Subject: [PATCH 14/52] Stream in-proc crash report JSON output Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../debug/crashreport/crashjsonwriter.cpp | 94 ++++-- .../debug/crashreport/crashjsonwriter.h | 25 +- .../debug/crashreport/inproccrashreporter.cpp | 275 +++++++----------- 3 files changed, 187 insertions(+), 207 deletions(-) diff --git a/src/coreclr/debug/crashreport/crashjsonwriter.cpp b/src/coreclr/debug/crashreport/crashjsonwriter.cpp index f89d11bb2ecdc0..633628b9147917 100644 --- a/src/coreclr/debug/crashreport/crashjsonwriter.cpp +++ b/src/coreclr/debug/crashreport/crashjsonwriter.cpp @@ -1,12 +1,12 @@ // 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 JSON writer implementation. -// Every function here uses only stack variables and the pre-allocated buffer. -// No malloc, no stdio, no locks — safe to call from a signal handler. +// Streaming JSON writer implementation for crash reports. #include "crashjsonwriter.h" +#include + static int CrashJsonAppend( @@ -20,6 +20,11 @@ CrashJsonAppendStr( CrashJsonWriter* w, const char* str); +static +int +CrashJsonFlush( + CrashJsonWriter* w); + static char ToHexChar( @@ -38,10 +43,15 @@ CrashJsonWriteEscapedString( void CrashJsonInit( - CrashJsonWriter* w) + CrashJsonWriter* w, + CrashJsonOutputCallback outputCallback, + void* outputContext) { w->pos = 0; w->commaNeeded = false; + w->writeFailed = false; + w->outputCallback = outputCallback; + w->outputContext = outputContext; w->buffer[0] = '\0'; } @@ -103,37 +113,82 @@ CrashJsonWriteString( CrashJsonWriteEscapedString(w, value); } +void +CrashJsonFinish( + CrashJsonWriter* w) +{ + (void)CrashJsonFlush(w); +} + int -CrashJsonGetLength( +CrashJsonHasFailed( CrashJsonWriter* w) { - return w->pos; + return w->writeFailed ? 1 : 0; } -const char* -CrashJsonGetBuffer( +int +CrashJsonFlush( CrashJsonWriter* w) { - w->buffer[w->pos] = '\0'; - return w->buffer; + if (w->writeFailed) + { + return 0; + } + + if (w->pos == 0) + { + return 1; + } + + if (w->outputCallback != NULL && !w->outputCallback(w->buffer, w->pos, w->outputContext)) + { + w->writeFailed = true; + return 0; + } + + w->pos = 0; + w->buffer[0] = '\0'; + return 1; } -// Append raw bytes to buffer. Returns 0 if out of space. int CrashJsonAppend( CrashJsonWriter* w, const char* str, int len) { - if (w->pos + len >= CRASH_JSON_BUFFER_SIZE - 16) + if (w->writeFailed || str == NULL || len < 0) + { return 0; + } + + if (len == 0) + { + return 1; + } - for (int i = 0; i < len; i++) + int offset = 0; + while (offset < len) { - w->buffer[w->pos + i] = str[i]; + int remaining = (CRASH_JSON_BUFFER_SIZE - 1) - w->pos; + if (remaining == 0 && !CrashJsonFlush(w)) + { + return 0; + } + + remaining = (CRASH_JSON_BUFFER_SIZE - 1) - w->pos; + int chunk = len - offset; + if (chunk > remaining) + { + chunk = remaining; + } + + memcpy(w->buffer + w->pos, str + offset, static_cast(chunk)); + w->pos += chunk; + offset += chunk; } - w->pos += len; return 1; } @@ -142,11 +197,12 @@ CrashJsonAppendStr( CrashJsonWriter* w, const char* str) { - int len = 0; - while (str[len]) - len++; + if (str == NULL) + { + return 0; + } - return CrashJsonAppend(w, str, len); + return CrashJsonAppend(w, str, static_cast(strlen(str))); } char diff --git a/src/coreclr/debug/crashreport/crashjsonwriter.h b/src/coreclr/debug/crashreport/crashjsonwriter.h index bde4dcf788d132..3cdfdca38388b1 100644 --- a/src/coreclr/debug/crashreport/crashjsonwriter.h +++ b/src/coreclr/debug/crashreport/crashjsonwriter.h @@ -1,32 +1,35 @@ // 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 JSON writer for crash reports. -// Writes to a pre-allocated fixed-size buffer using only signal-safe operations. -// No malloc, no stdio, no locks. +// Bounded JSON writer for crash reports. +// Streams content through a small fixed-size buffer using bounded low-level +// string and memory operations so file output does not require materializing +// the whole report at once. #pragma once #include -// Fixed buffer size for the JSON crash report. -// 32KB leaves room for multiple thread/frame entries while staying heap-free. -#define CRASH_JSON_BUFFER_SIZE (32 * 1024) // 32KB +typedef int (*CrashJsonOutputCallback)(const char* buffer, int len, void* ctx); + +// Small streaming buffer used when serializing the crash report JSON. +#define CRASH_JSON_BUFFER_SIZE (4 * 1024) struct CrashJsonWriter { char buffer[CRASH_JSON_BUFFER_SIZE]; int pos; bool commaNeeded; - - // All methods below are async-signal-safe (no malloc, no locks) + bool writeFailed; + CrashJsonOutputCallback outputCallback; + void* outputContext; }; -void CrashJsonInit(CrashJsonWriter* w); +void CrashJsonInit(CrashJsonWriter* w, CrashJsonOutputCallback outputCallback, void* outputContext); void CrashJsonOpenObject(CrashJsonWriter* w, const char* key); void CrashJsonCloseObject(CrashJsonWriter* w); void CrashJsonOpenArray(CrashJsonWriter* w, const char* key); void CrashJsonCloseArray(CrashJsonWriter* w); void CrashJsonWriteString(CrashJsonWriter* w, const char* key, const char* value); -int CrashJsonGetLength(CrashJsonWriter* w); -const char* CrashJsonGetBuffer(CrashJsonWriter* w); +void CrashJsonFinish(CrashJsonWriter* w); +int CrashJsonHasFailed(CrashJsonWriter* w); diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index a7520fddd1856b..a4489b7b363207 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -46,6 +47,13 @@ struct MultiThreadJsonContext uint32_t crashExceptionHResult; }; +struct CrashReportOutputContext +{ + int fd; + int writeToLog; + int writeFailed; +}; + static void GetVersionString( @@ -162,35 +170,19 @@ WriteAllToFile( int len); static -void -AppendChar( - char* buffer, - int bufferSize, - int* pos, - char value); - -static -void -AppendString( - char* buffer, - int bufferSize, - int* pos, - const char* value); - -static -void -AppendUnsignedDecimal( - char* buffer, - int bufferSize, - int* pos, - uint64_t value); +int +WriteCrashReportChunk( + const char* buffer, + int len, + void* ctx); static -void -TerminateBuffer( +int +BuildReportPath( char* buffer, int bufferSize, - int* pos); + const char* dumpPath, + const char* defaultDirectory); static const char* @@ -223,7 +215,23 @@ InProcCrashReportGenerate( hasException = g_getExceptionCallback(exTypeBuf, sizeof(exTypeBuf), exMsgBuf, sizeof(exMsgBuf), &exHresult); } - CrashJsonInit(&s_jsonWriter); + char reportPath[256]; + reportPath[0] = '\0'; + + int fd = -1; + if (g_writeReportToFile != 0 && BuildReportPath(reportPath, sizeof(reportPath), g_reportPath, g_defaultReportDirectory)) + { + fd = open(reportPath, O_WRONLY | O_CREAT | O_TRUNC, 0644); + } + + CrashReportOutputContext outputContext = + { + fd, + fd == -1 ? 1 : 0, + 0 + }; + + CrashJsonInit(&s_jsonWriter, WriteCrashReportChunk, &outputContext); CrashJsonOpenObject(&s_jsonWriter, NULL); CrashJsonOpenObject(&s_jsonWriter, "payload"); @@ -337,46 +345,17 @@ InProcCrashReportGenerate( CrashJsonCloseObject(&s_jsonWriter); CrashJsonCloseObject(&s_jsonWriter); + CrashJsonFinish(&s_jsonWriter); - WriteToLog(CrashJsonGetBuffer(&s_jsonWriter), CrashJsonGetLength(&s_jsonWriter)); - - if (g_writeReportToFile != 0) + if (fd != -1) { - char reportPath[256]; - int pathLen = 0; - reportPath[0] = '\0'; + int writeSucceeded = !CrashJsonHasFailed(&s_jsonWriter) && + outputContext.writeFailed == 0 && + WriteAllToFile(fd, "\n", 1); - if (g_reportPath[0] != '\0') - { - AppendString(reportPath, sizeof(reportPath), &pathLen, g_reportPath); - AppendString(reportPath, sizeof(reportPath), &pathLen, ".crashreport.json"); - } - else + if (close(fd) != 0 || !writeSucceeded) { - const char* directory = g_defaultReportDirectory[0] != '\0' ? g_defaultReportDirectory : "/tmp"; - - AppendString(reportPath, sizeof(reportPath), &pathLen, directory); - if (pathLen > 0 && reportPath[pathLen - 1] != '/') - { - AppendChar(reportPath, sizeof(reportPath), &pathLen, '/'); - } - AppendString(reportPath, sizeof(reportPath), &pathLen, "dotnet_crash_"); - AppendUnsignedDecimal(reportPath, sizeof(reportPath), &pathLen, static_cast(getpid())); - AppendString(reportPath, sizeof(reportPath), &pathLen, ".crashreport.json"); - } - - TerminateBuffer(reportPath, sizeof(reportPath), &pathLen); - - int fd = open(reportPath, O_WRONLY | O_CREAT | O_TRUNC, 0644); - if (fd != -1) - { - int writeSucceeded = WriteAllToFile(fd, CrashJsonGetBuffer(&s_jsonWriter), CrashJsonGetLength(&s_jsonWriter)) && - WriteAllToFile(fd, "\n", 1); - - if (close(fd) != 0 || !writeSucceeded) - { - unlink(reportPath); - } + unlink(reportPath); } } } @@ -387,29 +366,8 @@ InProcCrashReportInitialize( const char* dumpPath, const char* defaultDirectory) { - g_reportPath[0] = '\0'; - if (dumpPath != NULL) - { - int index = 0; - while (dumpPath[index] != '\0' && index < static_cast(sizeof(g_reportPath)) - 1) - { - g_reportPath[index] = dumpPath[index]; - index++; - } - g_reportPath[index] = '\0'; - } - - g_defaultReportDirectory[0] = '\0'; - if (defaultDirectory != NULL) - { - int index = 0; - while (defaultDirectory[index] != '\0' && index < static_cast(sizeof(g_defaultReportDirectory)) - 1) - { - g_defaultReportDirectory[index] = defaultDirectory[index]; - index++; - } - g_defaultReportDirectory[index] = '\0'; - } + CopyString(g_reportPath, sizeof(g_reportPath), dumpPath); + CopyString(g_defaultReportDirectory, sizeof(g_defaultReportDirectory), defaultDirectory); __sync_synchronize(); g_writeReportToFile = writeToFile; @@ -455,11 +413,7 @@ WriteToLog( if (len < 0) { - len = 0; - while (msg[len] != '\0') - { - len++; - } + len = static_cast(strlen(msg)); } #ifdef __ANDROID__ @@ -475,11 +429,7 @@ WriteToLog( } char buffer[3001]; - for (int i = 0; i < chunk; i++) - { - buffer[i] = msg[offset + i]; - } - + memcpy(buffer, msg + offset, static_cast(chunk)); buffer[chunk] = '\0'; minipal_log_write_error(buffer); offset += chunk; @@ -516,80 +466,58 @@ WriteAllToFile( return 1; } -void -AppendChar( - char* buffer, - int bufferSize, - int* pos, - char value) +int +WriteCrashReportChunk( + const char* buffer, + int len, + void* ctx) { - if (*pos < bufferSize - 1) + CrashReportOutputContext* outputContext = reinterpret_cast(ctx); + if (outputContext == NULL) { - buffer[*pos] = value; - (*pos)++; + return 0; } -} -void -AppendString( - char* buffer, - int bufferSize, - int* pos, - const char* value) -{ - if (value == NULL) + if (outputContext->writeToLog != 0) { - return; + WriteToLog(buffer, len); } - while (*value != '\0' && *pos < bufferSize - 1) + if (outputContext->fd != -1 && !WriteAllToFile(outputContext->fd, buffer, len)) { - buffer[*pos] = *value; - (*pos)++; - value++; + outputContext->writeFailed = 1; + return 0; } + + return 1; } -void -AppendUnsignedDecimal( +int +BuildReportPath( char* buffer, int bufferSize, - int* pos, - uint64_t value) + const char* dumpPath, + const char* defaultDirectory) { - char reverse[32]; - int reversePos = 0; - - if (value == 0) - { - AppendChar(buffer, bufferSize, pos, '0'); - return; - } - - while (value != 0 && reversePos < static_cast(sizeof(reverse))) - { - reverse[reversePos++] = static_cast('0' + (value % 10)); - value /= 10; - } - - while (reversePos > 0) + if (buffer == NULL || bufferSize <= 0) { - AppendChar(buffer, bufferSize, pos, reverse[--reversePos]); + return 0; } -} -void -TerminateBuffer( - char* buffer, - int bufferSize, - int* pos) -{ - if (*pos >= bufferSize) + const char* directory = (defaultDirectory != NULL && defaultDirectory[0] != '\0') ? defaultDirectory : "/tmp"; + if (dumpPath != NULL && dumpPath[0] != '\0') { - *pos = bufferSize - 1; + int written = snprintf(buffer, static_cast(bufferSize), "%s.crashreport.json", dumpPath); + return written > 0 && written < bufferSize; } - buffer[*pos] = '\0'; + size_t directoryLength = strnlen(directory, static_cast(bufferSize)); + const char* separator = (directoryLength > 0 && directory[directoryLength - 1] == '/') ? "" : "/"; + int written = snprintf(buffer, static_cast(bufferSize), "%s%sdotnet_crash_%u.crashreport.json", + directory, + separator, + static_cast(getpid())); + return written > 0 && written < bufferSize; } void @@ -616,15 +544,16 @@ GetVersionString( { return; } + version += sizeof(versionPrefix) - 1; - int index = 0; - while (version[index] != '\0' && index < bufferSize - 2) + size_t copied = strnlen(version, static_cast(bufferSize - 2)); + if (copied != 0) { - buffer[index] = version[index]; - index++; + memcpy(buffer, version, copied); } + int index = static_cast(copied); buffer[index++] = ' '; buffer[index] = '\0'; } @@ -780,29 +709,22 @@ BuildMethodName( return; } - int index = 0; - if (className != NULL) + if (className != NULL && methodName != NULL) { - while (*className != '\0' && index < bufferSize - 1) - { - buffer[index++] = *className++; - } + (void)snprintf(buffer, static_cast(bufferSize), "%s.%s", className, methodName); } - - if (methodName != NULL) + else if (className != NULL) { - if (index > 0 && index < bufferSize - 1) - { - buffer[index++] = '.'; - } - - while (*methodName != '\0' && index < bufferSize - 1) - { - buffer[index++] = *methodName++; - } + (void)snprintf(buffer, static_cast(bufferSize), "%s", className); + } + else if (methodName != NULL) + { + (void)snprintf(buffer, static_cast(bufferSize), "%s", methodName); + } + else + { + buffer[0] = '\0'; } - - buffer[index] = '\0'; } const char* @@ -838,14 +760,13 @@ CopyString( return; } - int index = 0; - while (value[index] != '\0' && index < bufferSize - 1) + size_t copied = strnlen(value, static_cast(bufferSize - 1)); + if (copied != 0) { - buffer[index] = value[index]; - index++; + memcpy(buffer, value, copied); } - buffer[index] = '\0'; + buffer[copied] = '\0'; } int From 778c276e9717d2b2278530f7e03eefb4064f58d8 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 15 Apr 2026 23:02:30 -0400 Subject: [PATCH 15/52] Only walk the crashing thread in crash reports Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/crashreportstackwalker.cpp | 39 +++++++++++++---------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index f24efbc296314a..194771a6196928 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -95,6 +95,11 @@ FrameCallbackAdapter( stackPointer = static_cast(GetRegdisplaySP(pRD)); } + if (ip == 0 && stackPointer == 0) + { + return SWA_CONTINUE; + } + if (g_pDebugInterface != NULL && pMD != NULL) { DWORD resolvedILOffset = 0; @@ -139,12 +144,12 @@ FrameCallbackAdapter( static void -CrashReportWalkStack( +CrashReportWalkThread( + Thread* pThread, InProcCrashReportFrameCallback frameCallback, void* ctx) { - Thread* pThread = GetThreadAsyncSafe(); - if (pThread == NULL) + if (pThread == NULL || frameCallback == NULL) { return; } @@ -154,6 +159,15 @@ CrashReportWalkStack( QUICKUNWIND | FUNCTIONSONLY | ALLOW_ASYNC_STACK_WALK); } +static +void +CrashReportWalkStack( + InProcCrashReportFrameCallback frameCallback, + void* ctx) +{ + CrashReportWalkThread(GetThreadAsyncSafe(), frameCallback, ctx); +} + static int CrashReportIsCurrentThreadManaged() @@ -179,6 +193,7 @@ CrashReportGetExceptionForThread( *hresult = 0; } + // Only inspect the managed throwable when the thread is already in cooperative mode. if (!pThread->PreemptiveGCDisabled()) { return 0; @@ -296,9 +311,7 @@ CrashReportEnumerateThreads( threadCallback(crashOsId, 1, hasException ? exceptionType : "", hresult, ctx); - WalkContext walkContext = { frameCallback, ctx }; - pCrashThread->StackWalkFrames(FrameCallbackAdapter, &walkContext, - QUICKUNWIND | FUNCTIONSONLY | ALLOW_ASYNC_STACK_WALK); + CrashReportWalkThread(pCrashThread, frameCallback, ctx); crashThreadHandled = true; } } @@ -327,19 +340,11 @@ CrashReportEnumerateThreads( threadCallback(osThreadId, isCrashThread ? 1 : 0, hasException ? exceptionType : "", hresult, ctx); if (isCrashThread) { + CrashReportWalkThread(pThread, frameCallback, ctx); crashThreadHandled = true; } - - if (pThread->PreemptiveGCDisabled() == FALSE) - { - Frame* pFrame = pThread->GetFrame(); - if (pFrame != NULL && pFrame != FRAME_TOP) - { - WalkContext walkContext = { frameCallback, ctx }; - pThread->StackWalkFrames(FrameCallbackAdapter, &walkContext, - QUICKUNWIND | FUNCTIONSONLY | ALLOW_ASYNC_STACK_WALK); - } - } + // Avoid walking live non-crashing threads here. Stack walking a running + // thread without suspending it is unreliable and can destabilize crash reporting. pThread = ThreadStore::GetThreadList(pThread); } From 15fad2ac34120afd2399b5748812caeff1e6cfb1 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 15 Apr 2026 23:55:57 -0400 Subject: [PATCH 16/52] Write in-proc crash reports to file only Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../debug/crashreport/inproccrashreporter.cpp | 83 ++++--------------- .../debug/crashreport/inproccrashreporter.h | 4 +- src/coreclr/pal/src/thread/process.cpp | 2 +- 3 files changed, 21 insertions(+), 68 deletions(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index a4489b7b363207..a1736fed0a236a 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -3,7 +3,7 @@ // In-proc crash report generator. // -// Emits a createdump-shaped JSON skeleton to logcat / stderr. +// Streams a createdump-shaped JSON skeleton to a crashreport.json file. #include "inproccrashreporter.h" #include "crashjsonwriter.h" @@ -14,7 +14,6 @@ #include #include #include -#include #include // Include the .NET version string instead of linking because it is "static". @@ -50,7 +49,6 @@ struct MultiThreadJsonContext struct CrashReportOutputContext { int fd; - int writeToLog; int writeFailed; }; @@ -156,13 +154,6 @@ JsonThreadCallback( uint32_t exceptionHResult, void* ctx); -static -void -WriteToLog( - const char* msg, - int len); - -static int WriteAllToFile( int fd, @@ -195,9 +186,24 @@ InProcCrashReportGenerate( siginfo_t* siginfo, void* context) { + char reportPath[256]; + reportPath[0] = '\0'; + + if (g_writeReportToFile == 0 || !BuildReportPath(reportPath, sizeof(reportPath), g_reportPath, g_defaultReportDirectory)) + { + return; + } + + int fd = open(reportPath, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd == -1) + { + return; + } + static volatile int s_generating = 0; if (__sync_val_compare_and_swap(&s_generating, 0, 1) != 0) { + close(fd); return; } @@ -215,19 +221,9 @@ InProcCrashReportGenerate( hasException = g_getExceptionCallback(exTypeBuf, sizeof(exTypeBuf), exMsgBuf, sizeof(exMsgBuf), &exHresult); } - char reportPath[256]; - reportPath[0] = '\0'; - - int fd = -1; - if (g_writeReportToFile != 0 && BuildReportPath(reportPath, sizeof(reportPath), g_reportPath, g_defaultReportDirectory)) - { - fd = open(reportPath, O_WRONLY | O_CREAT | O_TRUNC, 0644); - } - CrashReportOutputContext outputContext = { fd, - fd == -1 ? 1 : 0, 0 }; @@ -401,44 +397,6 @@ InProcCrashReportSetThreadEnumerator( g_enumerateThreadsCallback = callback; } -void -WriteToLog( - const char* msg, - int len) -{ - if (msg == NULL) - { - return; - } - - if (len < 0) - { - len = static_cast(strlen(msg)); - } - -#ifdef __ANDROID__ - // Emit long payloads in chunks so the JSON is not truncated by Android's - // per-entry log size limit. - int offset = 0; - while (offset < len) - { - int chunk = len - offset; - if (chunk > 3000) - { - chunk = 3000; - } - - char buffer[3001]; - memcpy(buffer, msg + offset, static_cast(chunk)); - buffer[chunk] = '\0'; - minipal_log_write_error(buffer); - offset += chunk; - } -#else - (void)WriteAllToFile(STDERR_FILENO, msg, len); -#endif -} - int WriteAllToFile( int fd, @@ -473,17 +431,12 @@ WriteCrashReportChunk( void* ctx) { CrashReportOutputContext* outputContext = reinterpret_cast(ctx); - if (outputContext == NULL) + if (outputContext == NULL || outputContext->fd == -1) { return 0; } - if (outputContext->writeToLog != 0) - { - WriteToLog(buffer, len); - } - - if (outputContext->fd != -1 && !WriteAllToFile(outputContext->fd, buffer, len)) + if (!WriteAllToFile(outputContext->fd, buffer, len)) { outputContext->writeFailed = 1; return 0; diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.h b/src/coreclr/debug/crashreport/inproccrashreporter.h index d732553970c0bb..d27c2902d7d7d7 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.h +++ b/src/coreclr/debug/crashreport/inproccrashreporter.h @@ -3,8 +3,8 @@ // In-proc crash report generation. // -// Emits a minimal createdump-shaped JSON payload to logcat / stderr and an -// optional *.crashreport.json file on disk. +// Emits a minimal createdump-shaped JSON payload to a *.crashreport.json file +// on disk. #pragma once diff --git a/src/coreclr/pal/src/thread/process.cpp b/src/coreclr/pal/src/thread/process.cpp index d9b2e759be89c0..ab69d9b5fe5a49 100644 --- a/src/coreclr/pal/src/thread/process.cpp +++ b/src/coreclr/pal/src/thread/process.cpp @@ -2647,7 +2647,7 @@ PROCAbortInitialize() } const bool crashReportEnabled = enableCrashReport || enableCrashReportOnly; - const bool writeCrashReportToFile = dumpName != nullptr && dumpName[0] != '\0'; + const bool writeCrashReportToFile = crashReportEnabled; g_inProcCrashReportEnabled = crashReportEnabled; InProcCrashReportInitialize(writeCrashReportToFile ? 1 : 0, dumpName, defaultReportDirectory); #endif From e7b642f596a11666cbd8e20f90de42bc5162b843 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Thu, 16 Apr 2026 17:21:20 -0400 Subject: [PATCH 17/52] Address crash report review feedback Move re-entrancy guard before file open to avoid truncating on contention. Tighten crash report file permissions to 0600. Expand %%/%p/%d dump name templates in crash report path. Remove unused PROCIsCrashReportEnabled declaration and definition. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../debug/crashreport/inproccrashreporter.cpp | 94 +++++++++++++++++-- src/coreclr/pal/src/include/pal/process.h | 1 - src/coreclr/pal/src/thread/process.cpp | 10 -- 3 files changed, 84 insertions(+), 21 deletions(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index a1736fed0a236a..008d5fb229c95b 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -186,6 +186,12 @@ InProcCrashReportGenerate( siginfo_t* siginfo, void* context) { + static volatile int s_generating = 0; + if (__sync_val_compare_and_swap(&s_generating, 0, 1) != 0) + { + return; + } + char reportPath[256]; reportPath[0] = '\0'; @@ -194,19 +200,12 @@ InProcCrashReportGenerate( return; } - int fd = open(reportPath, O_WRONLY | O_CREAT | O_TRUNC, 0644); + int fd = open(reportPath, O_WRONLY | O_CREAT | O_TRUNC, 0600); if (fd == -1) { return; } - static volatile int s_generating = 0; - if (__sync_val_compare_and_swap(&s_generating, 0, 1) != 0) - { - close(fd); - return; - } - (void)siginfo; char exTypeBuf[256]; @@ -445,6 +444,72 @@ WriteCrashReportChunk( return 1; } +// Expand a subset of the coredump template patterns used by createdump's +// FormatDumpName: %% %p %d (PID). Other specifiers are passed through +// literally since the remaining createdump patterns (%e, %h, %t) are not +// meaningful for in-proc crash reports. +static +int +ExpandDumpTemplate( + char* buffer, + int bufferSize, + const char* pattern) +{ + if (buffer == NULL || bufferSize <= 0 || pattern == NULL) + { + return 0; + } + + int pos = 0; + unsigned pid = static_cast(getpid()); + + while (*pattern != '\0' && pos < bufferSize - 1) + { + if (*pattern == '%') + { + pattern++; + if (*pattern == '%') + { + buffer[pos++] = '%'; + } + else if (*pattern == 'p' || *pattern == 'd') + { + char pidBuf[16]; + int pidLen = snprintf(pidBuf, sizeof(pidBuf), "%u", pid); + if (pidLen > 0 && pos + pidLen < bufferSize) + { + memcpy(buffer + pos, pidBuf, static_cast(pidLen)); + pos += pidLen; + } + } + else + { + // Unknown specifier — pass through literally. + if (pos < bufferSize - 1) + { + buffer[pos++] = '%'; + } + if (*pattern != '\0' && pos < bufferSize - 1) + { + buffer[pos++] = *pattern; + } + } + + if (*pattern != '\0') + { + pattern++; + } + } + else + { + buffer[pos++] = *pattern++; + } + } + + buffer[pos] = '\0'; + return pos; +} + int BuildReportPath( char* buffer, @@ -457,13 +522,22 @@ BuildReportPath( return 0; } - const char* directory = (defaultDirectory != NULL && defaultDirectory[0] != '\0') ? defaultDirectory : "/tmp"; if (dumpPath != NULL && dumpPath[0] != '\0') { - int written = snprintf(buffer, static_cast(bufferSize), "%s.crashreport.json", dumpPath); + // Expand template patterns in the configured dump path, then append + // the crashreport.json suffix. + char expanded[256]; + int expandedLen = ExpandDumpTemplate(expanded, sizeof(expanded), dumpPath); + if (expandedLen <= 0) + { + return 0; + } + + int written = snprintf(buffer, static_cast(bufferSize), "%s.crashreport.json", expanded); return written > 0 && written < bufferSize; } + const char* directory = (defaultDirectory != NULL && defaultDirectory[0] != '\0') ? defaultDirectory : "/tmp"; size_t directoryLength = strnlen(directory, static_cast(bufferSize)); const char* separator = (directoryLength > 0 && directory[directoryLength - 1] == '/') ? "" : "/"; int written = snprintf(buffer, static_cast(bufferSize), "%s%sdotnet_crash_%u.crashreport.json", diff --git a/src/coreclr/pal/src/include/pal/process.h b/src/coreclr/pal/src/include/pal/process.h index 0b7f84e002552d..e3f26bde875a03 100644 --- a/src/coreclr/pal/src/include/pal/process.h +++ b/src/coreclr/pal/src/include/pal/process.h @@ -157,7 +157,6 @@ VOID PROCNotifyProcessShutdown(bool isExecutingOnAltStack = false); (no return value) --*/ VOID PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, void* context, bool serialize); -BOOL PROCIsCrashReportEnabled(); /*++ Function: diff --git a/src/coreclr/pal/src/thread/process.cpp b/src/coreclr/pal/src/thread/process.cpp index ab69d9b5fe5a49..894b638074069a 100644 --- a/src/coreclr/pal/src/thread/process.cpp +++ b/src/coreclr/pal/src/thread/process.cpp @@ -2798,16 +2798,6 @@ PROCLogManagedCallstackForSignal(int signal) } } -BOOL -PROCIsCrashReportEnabled() -{ -#ifdef HOST_ANDROID - return g_inProcCrashReportEnabled ? TRUE : FALSE; -#else - return FALSE; -#endif -} - /*++ Function: PROCCreateCrashDumpIfEnabled From fa6479782282535a4b7bc7f695957f4a4e26046a Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Thu, 16 Apr 2026 15:56:58 -0400 Subject: [PATCH 18/52] Defer crash report initialization to EE startup Move crash report configuration reading from PROCAbortInitialize (PAL init) to CrashReportRegisterStackWalker (EE startup) because Android sets DOTNET_* environment variables via JNI after PAL init. Add PROCEnableInProcCrashReport so the VM can arm the crash reporter flag at the correct time. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/pal/src/include/pal/process.h | 4 +++ src/coreclr/pal/src/thread/process.cpp | 19 ++++-------- src/coreclr/vm/crashreportstackwalker.cpp | 37 +++++++++++++++++++++++ 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/coreclr/pal/src/include/pal/process.h b/src/coreclr/pal/src/include/pal/process.h index e3f26bde875a03..6c2f3c1077cc04 100644 --- a/src/coreclr/pal/src/include/pal/process.h +++ b/src/coreclr/pal/src/include/pal/process.h @@ -172,6 +172,10 @@ VOID PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, void* context, --*/ VOID PROCLogManagedCallstackForSignal(int signal); +#ifdef HOST_ANDROID +void PROCEnableInProcCrashReport(); +#endif + #ifdef __cplusplus } #endif // __cplusplus diff --git a/src/coreclr/pal/src/thread/process.cpp b/src/coreclr/pal/src/thread/process.cpp index 894b638074069a..8e1f72373c5c69 100644 --- a/src/coreclr/pal/src/thread/process.cpp +++ b/src/coreclr/pal/src/thread/process.cpp @@ -2639,19 +2639,6 @@ PROCAbortInitialize() DWORD reportOnlyEnabled = 0; bool enableCrashReportOnly = enabledReportOnlyCfg.IsSet() && enabledReportOnlyCfg.TryAsInteger(10, reportOnlyEnabled) && reportOnlyEnabled == 1; -#ifdef HOST_ANDROID - const char* defaultReportDirectory = getenv("HOME"); - if (defaultReportDirectory == nullptr || defaultReportDirectory[0] == '\0') - { - defaultReportDirectory = getenv("TMPDIR"); - } - - const bool crashReportEnabled = enableCrashReport || enableCrashReportOnly; - const bool writeCrashReportToFile = crashReportEnabled; - g_inProcCrashReportEnabled = crashReportEnabled; - InProcCrashReportInitialize(writeCrashReportToFile ? 1 : 0, dumpName, defaultReportDirectory); -#endif - if (enableMiniDump) { CLRConfigNoCache dmpLogToFileCfg = CLRConfigNoCache::Get("CreateDumpLogToFile", /*noprefix*/ false, &getenv); @@ -2815,6 +2802,12 @@ PROCLogManagedCallstackForSignal(int signal) --*/ #ifdef HOST_ANDROID #include +void +PROCEnableInProcCrashReport() +{ + g_inProcCrashReportEnabled = true; +} + VOID PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, void* context, bool serialize) { diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index 194771a6196928..72730d567f5c72 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -8,12 +8,15 @@ #include "dbginterface.h" #include "method.hpp" #include "peassembly.h" +#include #include #ifdef HOST_ANDROID #include "debug/crashreport/inproccrashreporter.h" +extern "C" void PROCEnableInProcCrashReport(); + struct WalkContext { InProcCrashReportFrameCallback callback; @@ -353,6 +356,40 @@ CrashReportEnumerateThreads( void CrashReportRegisterStackWalker() { + // Read crash report configuration here rather than in PROCAbortInitialize + // because on Android the DOTNET_* environment variables are set via JNI + // after PAL_Initialize has already run. + CLRConfigNoCache enabledReportCfg = CLRConfigNoCache::Get("EnableCrashReport", /*noprefix*/ false, &getenv); + DWORD reportEnabled = 0; + bool enableCrashReport = enabledReportCfg.IsSet() && enabledReportCfg.TryAsInteger(10, reportEnabled) && reportEnabled == 1; + + CLRConfigNoCache enabledReportOnlyCfg = CLRConfigNoCache::Get("EnableCrashReportOnly", /*noprefix*/ false, &getenv); + DWORD reportOnlyEnabled = 0; + bool enableCrashReportOnly = enabledReportOnlyCfg.IsSet() && enabledReportOnlyCfg.TryAsInteger(10, reportOnlyEnabled) && reportOnlyEnabled == 1; + + if (!enableCrashReport && !enableCrashReportOnly) + { + return; + } + + CLRConfigNoCache dmpNameCfg = CLRConfigNoCache::Get("DbgMiniDumpName", /*noprefix*/ false, &getenv); + const char* dumpName = dmpNameCfg.IsSet() ? dmpNameCfg.AsString() : nullptr; + + const char* defaultReportDirectory = getenv("HOME"); + if (defaultReportDirectory == nullptr || defaultReportDirectory[0] == '\0') + { + defaultReportDirectory = getenv("TMPDIR"); + } + if (defaultReportDirectory == nullptr || defaultReportDirectory[0] == '\0') + { + defaultReportDirectory = "/data/local/tmp"; + } + + InProcCrashReportInitialize(1, dumpName, defaultReportDirectory); + + // Set the PAL flag so PROCCreateCrashDumpIfEnabled knows to call the reporter. + PROCEnableInProcCrashReport(); + InProcCrashReportSetCurrentThreadManagedResolver(CrashReportIsCurrentThreadManaged); InProcCrashReportSetStackWalker(CrashReportWalkStack); InProcCrashReportSetExceptionResolver(CrashReportGetException); From 26ee461e34676696fb99d6fd71bca7c1f6aecb19 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Thu, 16 Apr 2026 17:04:04 -0400 Subject: [PATCH 19/52] Suspend non-crashing threads before walking stacks Install a SIGUSR2 handler at crash reporter init time that, when armed, parks threads on a pipe read. At crash time, send SIGUSR2 via tgkill to every non-crashing managed thread, wait briefly for them to park, walk all stacks, then close the pipe to release. This approach is self-contained in crashreportstackwalker.cpp and does not modify the PAL's activation signal handler. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/crashreportstackwalker.cpp | 115 +++++++++++++++++++++- 1 file changed, 110 insertions(+), 5 deletions(-) diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index 72730d567f5c72..383517e17ac38f 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -14,9 +14,70 @@ #ifdef HOST_ANDROID #include "debug/crashreport/inproccrashreporter.h" +#include +#include +#include extern "C" void PROCEnableInProcCrashReport(); +// --------------------------------------------------------------------------- +// Crash-time thread suspension using a dedicated signal and pipe. +// +// At init time we install a handler for SIGUSR2 that, when armed, parks +// the receiving thread on a pipe read. At crash time the reporter arms +// the gate, sends SIGUSR2 to every non-crashing managed thread, waits +// briefly, walks stacks, then closes the pipe to release everyone. +// +// This approach keeps the PAL's activation signal handler untouched. +// --------------------------------------------------------------------------- +static volatile int s_crashSuspendArmed = 0; +static int s_crashResumePipe[2] = { -1, -1 }; + +static void CrashSuspendSignalHandler(int sig, siginfo_t* info, void* context) +{ + (void)sig; + (void)info; + (void)context; + + if (!__atomic_load_n(&s_crashSuspendArmed, __ATOMIC_ACQUIRE)) + return; + + // Block until the crash reporter closes the write end. + // read() is async-signal-safe. + char buf; + while (read(s_crashResumePipe[0], &buf, 1) == -1 && errno == EINTR) + ; +} + +static void CrashSuspendInstallHandler() +{ + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_sigaction = CrashSuspendSignalHandler; + sa.sa_flags = SA_SIGINFO | SA_RESTART; + sigemptyset(&sa.sa_mask); + sigaction(SIGUSR2, &sa, NULL); +} + +static void CrashSuspendArm() +{ + if (pipe(s_crashResumePipe) != 0) + { + s_crashResumePipe[0] = -1; + s_crashResumePipe[1] = -1; + } + __atomic_store_n(&s_crashSuspendArmed, 1, __ATOMIC_RELEASE); +} + +static void CrashSuspendRelease() +{ + if (s_crashResumePipe[1] != -1) + { + close(s_crashResumePipe[1]); + s_crashResumePipe[1] = -1; + } +} + struct WalkContext { InProcCrashReportFrameCallback callback; @@ -288,6 +349,34 @@ CrashReportGetException( return CrashReportGetExceptionForThread(pThread, exceptionTypeBuf, exceptionTypeBufSize, hresult); } +// Suspend non-crashing threads so their managed stacks can be walked +// reliably. Sends SIGUSR2 to every non-crashing managed thread; the +// handler (installed at init) parks them on a pipe read. +static +void +CrashReportSuspendThreads(Thread* pCrashThread) +{ + CrashSuspendArm(); + + pid_t pid = getpid(); + Thread* pThread = ThreadStore::GetThreadList(NULL); + while (pThread != NULL) + { + if (pThread != pCrashThread) + { + DWORD tid = pThread->GetOSThreadId(); + if (tid != 0) + { + syscall(SYS_tgkill, pid, static_cast(tid), SIGUSR2); + } + } + pThread = ThreadStore::GetThreadList(pThread); + } + + // Brief wait for threads to park in the signal handler. + usleep(50000); +} + static void CrashReportEnumerateThreads( @@ -296,11 +385,11 @@ CrashReportEnumerateThreads( InProcCrashReportFrameCallback frameCallback, void* ctx) { - // This minimal lift intentionally reuses the existing ThreadStore traversal - // and StackWalkFrames as a best-effort source for managed thread state. Thread* pCrashThread = GetThreadAsyncSafe(); bool crashThreadHandled = false; + CrashReportSuspendThreads(pCrashThread); + // Emit the crashing thread first so the report keeps the most important // thread even if later enumeration is incomplete. if (pCrashThread != NULL) @@ -338,19 +427,32 @@ CrashReportEnumerateThreads( bool isCrashThread = !crashThreadHandled && osThreadId == crashingTid; char exceptionType[256]; uint32_t hresult = 0; - int hasException = CrashReportGetExceptionForThread(pThread, exceptionType, sizeof(exceptionType), &hresult); + int hasException = 0; + + if (isCrashThread) + { + hasException = CrashReportGetExceptionForThread(pThread, exceptionType, sizeof(exceptionType), &hresult); + } threadCallback(osThreadId, isCrashThread ? 1 : 0, hasException ? exceptionType : "", hresult, ctx); + if (isCrashThread) { CrashReportWalkThread(pThread, frameCallback, ctx); crashThreadHandled = true; } - // Avoid walking live non-crashing threads here. Stack walking a running - // thread without suspending it is unreliable and can destabilize crash reporting. + else + { + // Non-crashing threads have been parked by the crash-suspend + // signal handler. Their managed stacks are frozen and safe + // to walk regardless of their original GC mode. + CrashReportWalkThread(pThread, frameCallback, ctx); + } pThread = ThreadStore::GetThreadList(pThread); } + + CrashSuspendRelease(); } void @@ -390,6 +492,9 @@ CrashReportRegisterStackWalker() // Set the PAL flag so PROCCreateCrashDumpIfEnabled knows to call the reporter. PROCEnableInProcCrashReport(); + // Install the SIGUSR2 handler for crash-time thread suspension. + CrashSuspendInstallHandler(); + InProcCrashReportSetCurrentThreadManagedResolver(CrashReportIsCurrentThreadManaged); InProcCrashReportSetStackWalker(CrashReportWalkStack); InProcCrashReportSetExceptionResolver(CrashReportGetException); From 423fe61cc0da694021f7c8a0211bee44b7a24291 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 17 Apr 2026 14:34:45 -0400 Subject: [PATCH 20/52] Revert "Suspend non-crashing threads before walking stacks" This reverts commit 26ee461e34676696fb99d6fd71bca7c1f6aecb19. PR #126916 feedback from @jkotas and @lateralusX converged on using the runtime's regular suspension APIs instead of a bespoke SIGUSR2-based park. The managed stackwalker is only robust to walk from runtime-known safe points, which a dedicated signal handler parking threads on a pipe read cannot guarantee. A follow-up commit reintroduces non-crashing thread walking using ThreadSuspend::SuspendEE / RestartEE. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/crashreportstackwalker.cpp | 115 +--------------------- 1 file changed, 5 insertions(+), 110 deletions(-) diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index 383517e17ac38f..72730d567f5c72 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -14,70 +14,9 @@ #ifdef HOST_ANDROID #include "debug/crashreport/inproccrashreporter.h" -#include -#include -#include extern "C" void PROCEnableInProcCrashReport(); -// --------------------------------------------------------------------------- -// Crash-time thread suspension using a dedicated signal and pipe. -// -// At init time we install a handler for SIGUSR2 that, when armed, parks -// the receiving thread on a pipe read. At crash time the reporter arms -// the gate, sends SIGUSR2 to every non-crashing managed thread, waits -// briefly, walks stacks, then closes the pipe to release everyone. -// -// This approach keeps the PAL's activation signal handler untouched. -// --------------------------------------------------------------------------- -static volatile int s_crashSuspendArmed = 0; -static int s_crashResumePipe[2] = { -1, -1 }; - -static void CrashSuspendSignalHandler(int sig, siginfo_t* info, void* context) -{ - (void)sig; - (void)info; - (void)context; - - if (!__atomic_load_n(&s_crashSuspendArmed, __ATOMIC_ACQUIRE)) - return; - - // Block until the crash reporter closes the write end. - // read() is async-signal-safe. - char buf; - while (read(s_crashResumePipe[0], &buf, 1) == -1 && errno == EINTR) - ; -} - -static void CrashSuspendInstallHandler() -{ - struct sigaction sa; - memset(&sa, 0, sizeof(sa)); - sa.sa_sigaction = CrashSuspendSignalHandler; - sa.sa_flags = SA_SIGINFO | SA_RESTART; - sigemptyset(&sa.sa_mask); - sigaction(SIGUSR2, &sa, NULL); -} - -static void CrashSuspendArm() -{ - if (pipe(s_crashResumePipe) != 0) - { - s_crashResumePipe[0] = -1; - s_crashResumePipe[1] = -1; - } - __atomic_store_n(&s_crashSuspendArmed, 1, __ATOMIC_RELEASE); -} - -static void CrashSuspendRelease() -{ - if (s_crashResumePipe[1] != -1) - { - close(s_crashResumePipe[1]); - s_crashResumePipe[1] = -1; - } -} - struct WalkContext { InProcCrashReportFrameCallback callback; @@ -349,34 +288,6 @@ CrashReportGetException( return CrashReportGetExceptionForThread(pThread, exceptionTypeBuf, exceptionTypeBufSize, hresult); } -// Suspend non-crashing threads so their managed stacks can be walked -// reliably. Sends SIGUSR2 to every non-crashing managed thread; the -// handler (installed at init) parks them on a pipe read. -static -void -CrashReportSuspendThreads(Thread* pCrashThread) -{ - CrashSuspendArm(); - - pid_t pid = getpid(); - Thread* pThread = ThreadStore::GetThreadList(NULL); - while (pThread != NULL) - { - if (pThread != pCrashThread) - { - DWORD tid = pThread->GetOSThreadId(); - if (tid != 0) - { - syscall(SYS_tgkill, pid, static_cast(tid), SIGUSR2); - } - } - pThread = ThreadStore::GetThreadList(pThread); - } - - // Brief wait for threads to park in the signal handler. - usleep(50000); -} - static void CrashReportEnumerateThreads( @@ -385,11 +296,11 @@ CrashReportEnumerateThreads( InProcCrashReportFrameCallback frameCallback, void* ctx) { + // This minimal lift intentionally reuses the existing ThreadStore traversal + // and StackWalkFrames as a best-effort source for managed thread state. Thread* pCrashThread = GetThreadAsyncSafe(); bool crashThreadHandled = false; - CrashReportSuspendThreads(pCrashThread); - // Emit the crashing thread first so the report keeps the most important // thread even if later enumeration is incomplete. if (pCrashThread != NULL) @@ -427,32 +338,19 @@ CrashReportEnumerateThreads( bool isCrashThread = !crashThreadHandled && osThreadId == crashingTid; char exceptionType[256]; uint32_t hresult = 0; - int hasException = 0; - - if (isCrashThread) - { - hasException = CrashReportGetExceptionForThread(pThread, exceptionType, sizeof(exceptionType), &hresult); - } + int hasException = CrashReportGetExceptionForThread(pThread, exceptionType, sizeof(exceptionType), &hresult); threadCallback(osThreadId, isCrashThread ? 1 : 0, hasException ? exceptionType : "", hresult, ctx); - if (isCrashThread) { CrashReportWalkThread(pThread, frameCallback, ctx); crashThreadHandled = true; } - else - { - // Non-crashing threads have been parked by the crash-suspend - // signal handler. Their managed stacks are frozen and safe - // to walk regardless of their original GC mode. - CrashReportWalkThread(pThread, frameCallback, ctx); - } + // Avoid walking live non-crashing threads here. Stack walking a running + // thread without suspending it is unreliable and can destabilize crash reporting. pThread = ThreadStore::GetThreadList(pThread); } - - CrashSuspendRelease(); } void @@ -492,9 +390,6 @@ CrashReportRegisterStackWalker() // Set the PAL flag so PROCCreateCrashDumpIfEnabled knows to call the reporter. PROCEnableInProcCrashReport(); - // Install the SIGUSR2 handler for crash-time thread suspension. - CrashSuspendInstallHandler(); - InProcCrashReportSetCurrentThreadManagedResolver(CrashReportIsCurrentThreadManaged); InProcCrashReportSetStackWalker(CrashReportWalkStack); InProcCrashReportSetExceptionResolver(CrashReportGetException); From 63b7d34ae8e3f8dd1305c646d9f2cd1c65b221b0 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 17 Apr 2026 14:36:04 -0400 Subject: [PATCH 21/52] Suspend non-crashing threads via SuspendEE for crash reporting Reintroduce multi-thread stack walking for the in-proc crash reporter, this time using the runtime's regular suspension path (ThreadSuspend::SuspendEE / RestartEE) instead of a bespoke SIGUSR2 park. The managed stackwalker is only robust when threads are at runtime-known safe points, which SuspendEE guarantees. Per reviewer guidance (see PR #126916 discussion), the reporter takes the standard stackwalker locks rather than attempting a lock-free variant. To avoid deadlocking when the runtime itself is in a compromised state, a reactive safety heuristic gates the suspension attempt: if a fatal error already occurred on the GC thread, a GC is in progress, the crashing thread is a GC special thread, or the crashing thread already holds the thread store lock, the reporter falls back to walking only the crashing thread. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/crashreportstackwalker.cpp | 95 ++++++++++++++++------- 1 file changed, 66 insertions(+), 29 deletions(-) diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index 72730d567f5c72..5a35491508d249 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -14,6 +14,8 @@ #ifdef HOST_ANDROID #include "debug/crashreport/inproccrashreporter.h" +#include "threadsuspend.h" +#include "gcenv.h" extern "C" void PROCEnableInProcCrashReport(); @@ -288,6 +290,54 @@ CrashReportGetException( return CrashReportGetExceptionForThread(pThread, exceptionTypeBuf, exceptionTypeBufSize, hresult); } +// Suspend non-crashing managed threads via SuspendEE so their stacks +// can be walked from runtime-known safe points. SuspendEE acquires the +// thread store lock and waits for every other managed thread to reach a +// safe point (and for any in-progress GC to complete), so skip it when +// a known pre-condition would prevent forward progress: +// +// * g_fFatalErrorOccurredOnGCThread: GC thread faulted mid-GC, so GC +// will never finish and SuspendEE's GC wait would hang. +// * GCHeapUtilities::IsGCInProgress(): a GC is already running; if it +// is wedged (common in runtime-internal crashes) SuspendEE hangs. +// * IsGCSpecialThread(): we are a GC thread ourselves; the GC wait +// would wait on us. +// * ThreadStore::HoldingThreadStore(pCrashThread): SuspendEE's +// LockThreadStore asserts the holder is unknown, so it would +// assert-fail in checked builds (undefined in release). +// +// The crash reporter is best-effort; on hang the Android watchdog +// kills the process and we keep whatever crash report JSON was flushed +// beforehand. +static bool s_runtimeSuspendedForCrashReport = false; + +static +void +CrashReportSuspendThreads(Thread* pCrashThread) +{ + if (g_fFatalErrorOccurredOnGCThread + || GCHeapUtilities::IsGCInProgress() + || IsGCSpecialThread() + || ThreadStore::HoldingThreadStore(pCrashThread)) + { + return; + } + + ThreadSuspend::SuspendEE(ThreadSuspend::SUSPEND_OTHER); + s_runtimeSuspendedForCrashReport = true; +} + +static +void +CrashReportResumeThreads() +{ + if (s_runtimeSuspendedForCrashReport) + { + s_runtimeSuspendedForCrashReport = false; + ThreadSuspend::RestartEE(FALSE /* bFinishedGC */, TRUE /* SuspendSucceeded */); + } +} + static void CrashReportEnumerateThreads( @@ -296,10 +346,9 @@ CrashReportEnumerateThreads( InProcCrashReportFrameCallback frameCallback, void* ctx) { - // This minimal lift intentionally reuses the existing ThreadStore traversal - // and StackWalkFrames as a best-effort source for managed thread state. Thread* pCrashThread = GetThreadAsyncSafe(); - bool crashThreadHandled = false; + + CrashReportSuspendThreads(pCrashThread); // Emit the crashing thread first so the report keeps the most important // thread even if later enumeration is incomplete. @@ -315,42 +364,30 @@ CrashReportEnumerateThreads( threadCallback(crashOsId, 1, hasException ? exceptionType : "", hresult, ctx); CrashReportWalkThread(pCrashThread, frameCallback, ctx); - crashThreadHandled = true; } } - Thread* pThread = ThreadStore::GetThreadList(NULL); - while (pThread != NULL) + // Walk the remaining managed threads only when the runtime was + // successfully suspended; otherwise the walker is not guaranteed + // to be at a safe point for them. + if (s_runtimeSuspendedForCrashReport) { - if (crashThreadHandled && pThread == pCrashThread) - { - pThread = ThreadStore::GetThreadList(pThread); - continue; - } - - uint64_t osThreadId = static_cast(pThread->GetOSThreadId()); - if (osThreadId == 0) + Thread* pThread = NULL; + while ((pThread = ThreadStore::GetThreadList(pThread)) != NULL) { - pThread = ThreadStore::GetThreadList(pThread); - continue; - } + if (pThread == pCrashThread) + continue; - bool isCrashThread = !crashThreadHandled && osThreadId == crashingTid; - char exceptionType[256]; - uint32_t hresult = 0; - int hasException = CrashReportGetExceptionForThread(pThread, exceptionType, sizeof(exceptionType), &hresult); + uint64_t osThreadId = static_cast(pThread->GetOSThreadId()); + if (osThreadId == 0 || osThreadId == crashingTid) + continue; - threadCallback(osThreadId, isCrashThread ? 1 : 0, hasException ? exceptionType : "", hresult, ctx); - if (isCrashThread) - { + threadCallback(osThreadId, 0, "", 0, ctx); CrashReportWalkThread(pThread, frameCallback, ctx); - crashThreadHandled = true; } - // Avoid walking live non-crashing threads here. Stack walking a running - // thread without suspending it is unreliable and can destabilize crash reporting. - - pThread = ThreadStore::GetThreadList(pThread); } + + CrashReportResumeThreads(); } void From fb38795d6b8965e94a02dfcacd190c1e682b888b Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 17 Apr 2026 13:26:32 -0400 Subject: [PATCH 22/52] Flush crash report JSON per thread for incremental persistence Expose CrashJsonFlush in the crash report JSON writer header and call it from the multi-thread enumeration path after each thread's object is closed, plus once more after the final thread. This writes each walked thread to the on-disk crash report as soon as it is fully serialized, so a later hang or secondary fault during the crash-reporting path does not lose threads that have already been captured. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/debug/crashreport/crashjsonwriter.cpp | 5 ----- src/coreclr/debug/crashreport/crashjsonwriter.h | 1 + src/coreclr/debug/crashreport/inproccrashreporter.cpp | 7 +++++++ 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/coreclr/debug/crashreport/crashjsonwriter.cpp b/src/coreclr/debug/crashreport/crashjsonwriter.cpp index 633628b9147917..11a50389041852 100644 --- a/src/coreclr/debug/crashreport/crashjsonwriter.cpp +++ b/src/coreclr/debug/crashreport/crashjsonwriter.cpp @@ -20,11 +20,6 @@ CrashJsonAppendStr( CrashJsonWriter* w, const char* str); -static -int -CrashJsonFlush( - CrashJsonWriter* w); - static char ToHexChar( diff --git a/src/coreclr/debug/crashreport/crashjsonwriter.h b/src/coreclr/debug/crashreport/crashjsonwriter.h index 3cdfdca38388b1..e4a940ba3741cc 100644 --- a/src/coreclr/debug/crashreport/crashjsonwriter.h +++ b/src/coreclr/debug/crashreport/crashjsonwriter.h @@ -32,4 +32,5 @@ void CrashJsonOpenArray(CrashJsonWriter* w, const char* key); void CrashJsonCloseArray(CrashJsonWriter* w); void CrashJsonWriteString(CrashJsonWriter* w, const char* key, const char* value); void CrashJsonFinish(CrashJsonWriter* w); +int CrashJsonFlush(CrashJsonWriter* w); int CrashJsonHasFailed(CrashJsonWriter* w); diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index 008d5fb229c95b..94a2b9be867dde 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -265,6 +265,11 @@ InProcCrashReportGenerate( // enumeration callback. CrashJsonCloseArray(&s_jsonWriter); CrashJsonCloseObject(&s_jsonWriter); + + // Flush the final thread so it reaches the crash report file + // even if any later work (e.g. synthesizing a crash thread + // fallback) hangs or faults. + (void)CrashJsonFlush(&s_jsonWriter); } if (threadContext.threadCount == 0 || !threadContext.sawCrashThread) @@ -944,6 +949,8 @@ JsonThreadCallback( { CrashJsonCloseArray(threadContext->writer); CrashJsonCloseObject(threadContext->writer); + + (void)CrashJsonFlush(threadContext->writer); } if (isCrashThread) From 1e674fd5f99a2d7d19ea37751ed0049e14bd9535 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 17 Apr 2026 16:27:51 -0400 Subject: [PATCH 23/52] Require DbgMiniDumpName for in-proc crash report file output The in-proc crash reporter previously fell back to HOME/TMPDIR/ /data/local/tmp when DbgMiniDumpName was unset, always emitting a crash report file. Gate the file emission on DbgMiniDumpName being configured, and when it is only a filename, place the report under TMPDIR (or /tmp) so it lands in a writable location. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../debug/crashreport/inproccrashreporter.cpp | 43 +++++-------------- .../debug/crashreport/inproccrashreporter.h | 2 +- src/coreclr/vm/crashreportstackwalker.cpp | 28 +++++++++--- 3 files changed, 32 insertions(+), 41 deletions(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index 94a2b9be867dde..82e3edf2c4b8e5 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -31,9 +31,7 @@ static volatile InProcCrashReportIsManagedThreadCallback g_isManagedThreadCallba static volatile InProcCrashReportWalkStackCallback g_walkStackCallback = NULL; static volatile InProcCrashReportGetExceptionCallback g_getExceptionCallback = NULL; static volatile InProcCrashReportEnumerateThreadsCallback g_enumerateThreadsCallback = NULL; -static volatile int g_writeReportToFile = 0; static char g_reportPath[256]; -static char g_defaultReportDirectory[256]; struct MultiThreadJsonContext { @@ -172,8 +170,7 @@ int BuildReportPath( char* buffer, int bufferSize, - const char* dumpPath, - const char* defaultDirectory); + const char* dumpPath); static const char* @@ -195,7 +192,7 @@ InProcCrashReportGenerate( char reportPath[256]; reportPath[0] = '\0'; - if (g_writeReportToFile == 0 || !BuildReportPath(reportPath, sizeof(reportPath), g_reportPath, g_defaultReportDirectory)) + if (g_reportPath[0] == '\0' || !BuildReportPath(reportPath, sizeof(reportPath), g_reportPath)) { return; } @@ -362,15 +359,10 @@ InProcCrashReportGenerate( void InProcCrashReportInitialize( - int writeToFile, - const char* dumpPath, - const char* defaultDirectory) + const char* dumpPath) { CopyString(g_reportPath, sizeof(g_reportPath), dumpPath); - CopyString(g_defaultReportDirectory, sizeof(g_defaultReportDirectory), defaultDirectory); - __sync_synchronize(); - g_writeReportToFile = writeToFile; } void @@ -519,36 +511,21 @@ int BuildReportPath( char* buffer, int bufferSize, - const char* dumpPath, - const char* defaultDirectory) + const char* dumpPath) { - if (buffer == NULL || bufferSize <= 0) + if (buffer == NULL || bufferSize <= 0 || dumpPath == NULL || dumpPath[0] == '\0') { return 0; } - if (dumpPath != NULL && dumpPath[0] != '\0') + char expanded[256]; + int expandedLen = ExpandDumpTemplate(expanded, sizeof(expanded), dumpPath); + if (expandedLen <= 0) { - // Expand template patterns in the configured dump path, then append - // the crashreport.json suffix. - char expanded[256]; - int expandedLen = ExpandDumpTemplate(expanded, sizeof(expanded), dumpPath); - if (expandedLen <= 0) - { - return 0; - } - - int written = snprintf(buffer, static_cast(bufferSize), "%s.crashreport.json", expanded); - return written > 0 && written < bufferSize; + return 0; } - const char* directory = (defaultDirectory != NULL && defaultDirectory[0] != '\0') ? defaultDirectory : "/tmp"; - size_t directoryLength = strnlen(directory, static_cast(bufferSize)); - const char* separator = (directoryLength > 0 && directory[directoryLength - 1] == '/') ? "" : "/"; - int written = snprintf(buffer, static_cast(bufferSize), "%s%sdotnet_crash_%u.crashreport.json", - directory, - separator, - static_cast(getpid())); + int written = snprintf(buffer, static_cast(bufferSize), "%s.crashreport.json", expanded); return written > 0 && written < bufferSize; } diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.h b/src/coreclr/debug/crashreport/inproccrashreporter.h index d27c2902d7d7d7..45a7c4e13ff3ed 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.h +++ b/src/coreclr/debug/crashreport/inproccrashreporter.h @@ -11,7 +11,7 @@ #include #include -void InProcCrashReportInitialize(int writeToFile, const char* dumpPath, const char* defaultDirectory); +void InProcCrashReportInitialize(const char* dumpPath); // Generate an in-proc crash report. Called from PROCCreateCrashDumpIfEnabled. // All arguments come from the signal handler and are signal-safe to read. diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index 5a35491508d249..98a7614538c044 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -411,18 +411,32 @@ CrashReportRegisterStackWalker() CLRConfigNoCache dmpNameCfg = CLRConfigNoCache::Get("DbgMiniDumpName", /*noprefix*/ false, &getenv); const char* dumpName = dmpNameCfg.IsSet() ? dmpNameCfg.AsString() : nullptr; - - const char* defaultReportDirectory = getenv("HOME"); - if (defaultReportDirectory == nullptr || defaultReportDirectory[0] == '\0') + if (dumpName == nullptr || dumpName[0] == '\0') { - defaultReportDirectory = getenv("TMPDIR"); + return; } - if (defaultReportDirectory == nullptr || defaultReportDirectory[0] == '\0') + + // If DbgMiniDumpName is just a filename (no directory component), write + // the crash report under TMPDIR / /tmp so it lands somewhere writable. + char dumpPathBuf[256]; + if (strchr(dumpName, '/') == nullptr) { - defaultReportDirectory = "/data/local/tmp"; + const char* tmpDir = getenv("TMPDIR"); + if (tmpDir == nullptr || tmpDir[0] == '\0') + { + tmpDir = "/tmp"; + } + size_t tmpLen = strlen(tmpDir); + const char* separator = (tmpLen > 0 && tmpDir[tmpLen - 1] == '/') ? "" : "/"; + int written = snprintf(dumpPathBuf, sizeof(dumpPathBuf), "%s%s%s", tmpDir, separator, dumpName); + if (written <= 0 || static_cast(written) >= sizeof(dumpPathBuf)) + { + return; + } + dumpName = dumpPathBuf; } - InProcCrashReportInitialize(1, dumpName, defaultReportDirectory); + InProcCrashReportInitialize(dumpName); // Set the PAL flag so PROCCreateCrashDumpIfEnabled knows to call the reporter. PROCEnableInProcCrashReport(); From bc0ce40937252f0fec717487f3fa331d5316b36b Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 17 Apr 2026 16:29:46 -0400 Subject: [PATCH 24/52] Emit signal number instead of Windows-shaped ExceptionType The in-proc crash report initially mirrored createdump's schema byte-for-byte, including an 'ExceptionType' field that encoded the signal as a synthetic Windows exception code. Per reviewer feedback, ExceptionType is a Windows concept that does not carry meaningful information for Unix crash reports; emit the raw signal number instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../debug/crashreport/inproccrashreporter.cpp | 34 ++----------------- 1 file changed, 3 insertions(+), 31 deletions(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index 82e3edf2c4b8e5..5eee85e993b4fd 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -172,11 +172,6 @@ BuildReportPath( int bufferSize, const char* dumpPath); -static -const char* -GetExceptionTypeCode( - int signal); - void InProcCrashReportGenerate( int signal, @@ -333,7 +328,9 @@ InProcCrashReportGenerate( CrashJsonCloseObject(&s_jsonWriter); CrashJsonOpenObject(&s_jsonWriter, "parameters"); - CrashJsonWriteString(&s_jsonWriter, "ExceptionType", hasException ? "0x05000000" : GetExceptionTypeCode(signal)); + char signalBuf[16]; + (void)snprintf(signalBuf, sizeof(signalBuf), "%d", signal); + CrashJsonWriteString(&s_jsonWriter, "signal", signalBuf); #ifdef __APPLE__ CrashJsonWriteString(&s_jsonWriter, "OSVersion", ""); CrashJsonWriteString(&s_jsonWriter, "SystemModel", ""); @@ -972,28 +969,3 @@ JsonThreadCallback( WriteCrashSiteFrameToJson(threadContext->writer, threadContext->signalContext); } } - -const char* -GetExceptionTypeCode( - int signal) -{ - switch (signal) - { - case SIGSEGV: - return "0x20000000"; - case SIGABRT: - return "0x30000000"; - case SIGBUS: - return "0x60000000"; - case SIGILL: - return "0x50000000"; - case SIGFPE: - return "0x70000000"; - case SIGTRAP: - return "0x03000000"; - case SIGTERM: - return "0x02000000"; - default: - return "0x00000000"; - } -} From 73c4feaea6ac6fba805a7f5139fa2b20c10fd0d8 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 17 Apr 2026 18:04:15 -0400 Subject: [PATCH 25/52] Rename crashreportstackwalker.h include guard Avoid the reserved __CRASHREPORTSTACKWALKER_H__ identifier (leading double underscores are reserved to the implementation in C/C++) by switching to CRASHREPORTSTACKWALKER_H. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/crashreportstackwalker.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/coreclr/vm/crashreportstackwalker.h b/src/coreclr/vm/crashreportstackwalker.h index 07fa546452fedc..0beaae6077e92c 100644 --- a/src/coreclr/vm/crashreportstackwalker.h +++ b/src/coreclr/vm/crashreportstackwalker.h @@ -1,8 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#ifndef __CRASHREPORTSTACKWALKER_H__ -#define __CRASHREPORTSTACKWALKER_H__ +#ifndef CRASHREPORTSTACKWALKER_H +#define CRASHREPORTSTACKWALKER_H #ifdef HOST_ANDROID @@ -10,4 +10,4 @@ void CrashReportRegisterStackWalker(); #endif // HOST_ANDROID -#endif // __CRASHREPORTSTACKWALKER_H__ +#endif // CRASHREPORTSTACKWALKER_H From ef7da7afa00c325f735b5bccff5be957554e054f Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 17 Apr 2026 18:04:25 -0400 Subject: [PATCH 26/52] Scope CLR_DIR include to Android builds The include_directories(${CLR_DIR}) entries added to VM and PAL were only needed to resolve the Android-only debug/crashreport/inproccrashreporter.h header. Wrap them in CLR_CMAKE_TARGET_ANDROID guards so the CLR source root isn't globally added to the include search path on other platforms. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/pal/src/CMakeLists.txt | 4 +++- src/coreclr/vm/CMakeLists.txt | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/coreclr/pal/src/CMakeLists.txt b/src/coreclr/pal/src/CMakeLists.txt index 982ce0b1a1fef1..f5f83c9dbe4206 100644 --- a/src/coreclr/pal/src/CMakeLists.txt +++ b/src/coreclr/pal/src/CMakeLists.txt @@ -37,7 +37,9 @@ endif(CORECLR_SET_RPATH) # Include directories include_directories(include) -include_directories(${CLR_DIR}) +if(CLR_CMAKE_TARGET_ANDROID) + include_directories(${CLR_DIR}) +endif(CLR_CMAKE_TARGET_ANDROID) # Compile options diff --git a/src/coreclr/vm/CMakeLists.txt b/src/coreclr/vm/CMakeLists.txt index 7548d60c1af64e..9662b1f5a8df3f 100644 --- a/src/coreclr/vm/CMakeLists.txt +++ b/src/coreclr/vm/CMakeLists.txt @@ -4,7 +4,9 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) include_directories(BEFORE ${CMAKE_CURRENT_SOURCE_DIR}) include_directories(${ARCH_SOURCES_DIR}) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../interop/inc) -include_directories(${CLR_DIR}) +if(CLR_CMAKE_TARGET_ANDROID) + include_directories(${CLR_DIR}) +endif(CLR_CMAKE_TARGET_ANDROID) include_directories(${CLR_SRC_NATIVE_DIR}) include_directories(${RUNTIME_DIR}) From 791b693b89517f1c137db1174e284ba1a849babc Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 17 Apr 2026 18:17:38 -0400 Subject: [PATCH 27/52] Publish g_inProcCrashReportEnabled through Volatile g_inProcCrashReportEnabled is written once during EE startup (via PROCEnableInProcCrashReport) and then read from the fatal-signal path in PROCCreateCrashDumpIfEnabled. A plain bool is not a defined synchronization primitive between normal code and an async signal handler. Publish it through Volatile to match the existing pattern used for other signal-path globals in process.cpp (g_logManagedCallstackForSignalCallback, g_shutdownCallback, g_createdumpCallback). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/pal/src/thread/process.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/coreclr/pal/src/thread/process.cpp b/src/coreclr/pal/src/thread/process.cpp index 8e1f72373c5c69..ddf59095f17077 100644 --- a/src/coreclr/pal/src/thread/process.cpp +++ b/src/coreclr/pal/src/thread/process.cpp @@ -195,7 +195,10 @@ Volatile g_logManagedCallstackForSignalC const char* g_argvCreateDump[MAX_ARGV_ENTRIES] = { nullptr }; #ifdef HOST_ANDROID -static bool g_inProcCrashReportEnabled = false; +// Read from the fatal-signal path (PROCCreateCrashDumpIfEnabled) and written +// once during startup (PROCEnableInProcCrashReport); use Volatile to +// match the signal-path publication of g_logManagedCallstackForSignalCallback. +static Volatile g_inProcCrashReportEnabled = false; #endif // From 70a40a3032dd85e420dae371de4192fa500ec749 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 17 Apr 2026 18:17:49 -0400 Subject: [PATCH 28/52] Revert unnecessary PROCAbortInitialize refactor An earlier version of the in-proc crash reporter hooked its initialization into PROCAbortInitialize, which required reading DbgMiniDumpName, EnableCrashReport, and EnableCrashReportOnly both inside and outside the DbgEnableMiniDump block. That initialization now lives in CrashReportRegisterStackWalker, so those config reads are only consumed inside the existing enableMiniDump branch. Restore the original structure (reads stay inside the if-block) to keep the diff minimal. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/pal/src/thread/process.cpp | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/coreclr/pal/src/thread/process.cpp b/src/coreclr/pal/src/thread/process.cpp index ddf59095f17077..af6269fe0bdbd1 100644 --- a/src/coreclr/pal/src/thread/process.cpp +++ b/src/coreclr/pal/src/thread/process.cpp @@ -2629,21 +2629,11 @@ PROCAbortInitialize() CLRConfigNoCache enabledCfg = CLRConfigNoCache::Get("DbgEnableMiniDump", /*noprefix*/ false, &getenv); DWORD enabled = 0; - bool enableMiniDump = enabledCfg.IsSet() && enabledCfg.TryAsInteger(10, enabled) && enabled != 0; - - CLRConfigNoCache dmpNameCfg = CLRConfigNoCache::Get("DbgMiniDumpName", /*noprefix*/ false, &getenv); - const char* dumpName = dmpNameCfg.IsSet() ? dmpNameCfg.AsString() : nullptr; - - CLRConfigNoCache enabledReportCfg = CLRConfigNoCache::Get("EnableCrashReport", /*noprefix*/ false, &getenv); - DWORD reportEnabled = 0; - bool enableCrashReport = enabledReportCfg.IsSet() && enabledReportCfg.TryAsInteger(10, reportEnabled) && reportEnabled == 1; - - CLRConfigNoCache enabledReportOnlyCfg = CLRConfigNoCache::Get("EnableCrashReportOnly", /*noprefix*/ false, &getenv); - DWORD reportOnlyEnabled = 0; - bool enableCrashReportOnly = enabledReportOnlyCfg.IsSet() && enabledReportOnlyCfg.TryAsInteger(10, reportOnlyEnabled) && reportOnlyEnabled == 1; - - if (enableMiniDump) + if (enabledCfg.IsSet() && enabledCfg.TryAsInteger(10, enabled) && enabled) { + CLRConfigNoCache dmpNameCfg = CLRConfigNoCache::Get("DbgMiniDumpName", /*noprefix*/ false, &getenv); + const char* dumpName = dmpNameCfg.IsSet() ? dmpNameCfg.AsString() : nullptr; + CLRConfigNoCache dmpLogToFileCfg = CLRConfigNoCache::Get("CreateDumpLogToFile", /*noprefix*/ false, &getenv); const char* logFilePath = dmpLogToFileCfg.IsSet() ? dmpLogToFileCfg.AsString() : nullptr; @@ -2671,11 +2661,15 @@ PROCAbortInitialize() { flags |= GenerateDumpFlagsVerboseLoggingEnabled; } - if (enableCrashReport) + CLRConfigNoCache enabledReportCfg = CLRConfigNoCache::Get("EnableCrashReport", /*noprefix*/ false, &getenv); + val = 0; + if (enabledReportCfg.IsSet() && enabledReportCfg.TryAsInteger(10, val) && val == 1) { flags |= GenerateDumpFlagsCrashReportEnabled; } - if (enableCrashReportOnly) + CLRConfigNoCache enabledReportOnlyCfg = CLRConfigNoCache::Get("EnableCrashReportOnly", /*noprefix*/ false, &getenv); + val = 0; + if (enabledReportOnlyCfg.IsSet() && enabledReportOnlyCfg.TryAsInteger(10, val) && val == 1) { flags |= GenerateDumpFlagsCrashReportOnlyEnabled; } From 69d3bc76d8c67e8eca6238f241756770a54129ec Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Mon, 20 Apr 2026 09:25:01 -0400 Subject: [PATCH 29/52] Include process id in in-proc crash report Surfaces the crashing process id alongside the existing process_name field so the JSON crash report identifies which process produced it without relying on the report filename. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/debug/crashreport/inproccrashreporter.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index 5eee85e993b4fd..e7d35f1dd68234 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -243,6 +243,10 @@ InProcCrashReportGenerate( CrashJsonWriteString(&s_jsonWriter, "process_name", processName); } + char pidBuf[16]; + (void)snprintf(pidBuf, sizeof(pidBuf), "%u", static_cast(getpid())); + CrashJsonWriteString(&s_jsonWriter, "pid", pidBuf); + CrashJsonOpenArray(&s_jsonWriter, "threads"); if (g_enumerateThreadsCallback != NULL) { From 7002ce7808b5fb961f8b6cb3f2310429bcc3a795 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Mon, 20 Apr 2026 09:26:35 -0400 Subject: [PATCH 30/52] Latch CrashJsonWriter failure on invalid append input If a caller ever passes NULL or a negative length to CrashJsonAppend mid-document, silently returning 0 would leave the JSON output syntactically broken while subsequent writes continued to emit content. Trip writeFailed in those paths so subsequent writes become no-ops, matching how the writer already behaves when the output callback reports an I/O failure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/debug/crashreport/crashjsonwriter.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/coreclr/debug/crashreport/crashjsonwriter.cpp b/src/coreclr/debug/crashreport/crashjsonwriter.cpp index 11a50389041852..15b96a14bb7a94 100644 --- a/src/coreclr/debug/crashreport/crashjsonwriter.cpp +++ b/src/coreclr/debug/crashreport/crashjsonwriter.cpp @@ -153,11 +153,20 @@ CrashJsonAppend( const char* str, int len) { - if (w->writeFailed || str == NULL || len < 0) + if (w->writeFailed) { return 0; } + if (str == NULL || len < 0) + { + // Invalid input mid-document would corrupt the JSON. Latch the + // failure so subsequent writes become no-ops, matching the + // behavior when the output callback reports an I/O failure. + w->writeFailed = true; + return 0; + } + if (len == 0) { return 1; @@ -194,6 +203,7 @@ CrashJsonAppendStr( { if (str == NULL) { + w->writeFailed = true; return 0; } From ae55a01cdd83c0420283bc7e47572d86aa9ec196 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Mon, 20 Apr 2026 12:21:41 -0400 Subject: [PATCH 31/52] Drop unused exception message plumbing from in-proc crash report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The message buffer threaded through the exception-resolver callback was populated but never emitted — createdump intentionally writes only managed_exception_type, managed_exception_hresult, and the exception object pointer, relying on the dump file itself for the throwable's Message. The in-proc reporter matched that schema from the start, so the message buffer, its accompanying callback parameters, and the null- initialization on both sides were dead code. Remove them so the callback surface matches what the report actually writes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/debug/crashreport/inproccrashreporter.cpp | 4 +--- src/coreclr/debug/crashreport/inproccrashreporter.h | 2 -- src/coreclr/vm/crashreportstackwalker.cpp | 7 ------- 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index e7d35f1dd68234..f6ddee77a66667 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -201,15 +201,13 @@ InProcCrashReportGenerate( (void)siginfo; char exTypeBuf[256]; - char exMsgBuf[512]; uint32_t exHresult = 0; exTypeBuf[0] = '\0'; - exMsgBuf[0] = '\0'; int hasException = 0; if (g_getExceptionCallback != NULL && signal != SIGSEGV && signal != SIGBUS) { - hasException = g_getExceptionCallback(exTypeBuf, sizeof(exTypeBuf), exMsgBuf, sizeof(exMsgBuf), &exHresult); + hasException = g_getExceptionCallback(exTypeBuf, sizeof(exTypeBuf), &exHresult); } CrashReportOutputContext outputContext = diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.h b/src/coreclr/debug/crashreport/inproccrashreporter.h index 45a7c4e13ff3ed..51896d10372573 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.h +++ b/src/coreclr/debug/crashreport/inproccrashreporter.h @@ -44,8 +44,6 @@ void InProcCrashReportSetStackWalker(InProcCrashReportWalkStackCallback callback typedef int (*InProcCrashReportGetExceptionCallback)( char* exceptionTypeBuf, int exceptionTypeBufSize, - char* exceptionMsgBuf, - int exceptionMsgBufSize, uint32_t* hresult); void InProcCrashReportSetExceptionResolver(InProcCrashReportGetExceptionCallback callback); diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index 98a7614538c044..b67e19ed7920e5 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -272,8 +272,6 @@ int CrashReportGetException( char* exceptionTypeBuf, int exceptionTypeBufSize, - char* exceptionMsgBuf, - int exceptionMsgBufSize, uint32_t* hresult) { Thread* pThread = GetThreadAsyncSafe(); @@ -282,11 +280,6 @@ CrashReportGetException( return 0; } - if (exceptionMsgBufSize > 0) - { - exceptionMsgBuf[0] = '\0'; - } - return CrashReportGetExceptionForThread(pThread, exceptionTypeBuf, exceptionTypeBufSize, hresult); } From 518e4f6bebc70de452b2140c4c67e11a7b76954c Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Mon, 20 Apr 2026 20:32:58 -0400 Subject: [PATCH 32/52] Register in-proc crash report callbacks before enabling PAL flag The PAL's PROCCreateCrashDumpIfEnabled observes g_inProcCrashReportEnabled to decide whether to dispatch to InProcCrashReportGenerate. Previously the registration sequence set that flag before the VM callback pointers were published, so a fatal signal in the narrow startup window would have run the reporter with unset callbacks and produced a degraded report. Swap the order so PROCEnableInProcCrashReport is the last step, ensuring any observer of the PAL flag also sees all four VM callbacks already set. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/crashreportstackwalker.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index b67e19ed7920e5..4fd060217ffab3 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -431,13 +431,14 @@ CrashReportRegisterStackWalker() InProcCrashReportInitialize(dumpName); - // Set the PAL flag so PROCCreateCrashDumpIfEnabled knows to call the reporter. - PROCEnableInProcCrashReport(); - InProcCrashReportSetCurrentThreadManagedResolver(CrashReportIsCurrentThreadManaged); InProcCrashReportSetStackWalker(CrashReportWalkStack); InProcCrashReportSetExceptionResolver(CrashReportGetException); InProcCrashReportSetThreadEnumerator(CrashReportEnumerateThreads); + + // Enable the PAL flag last so PROCCreateCrashDumpIfEnabled only observes + // the reporter as enabled after all VM callbacks are registered. + PROCEnableInProcCrashReport(); } #endif // HOST_ANDROID From 0fb7526ac25e2fc74753b92c20cde6aebb18d913 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 21 Apr 2026 13:43:47 -0400 Subject: [PATCH 33/52] Drop redundant include_directories in crashreport CMakeLists CLR_ARTIFACTS_OBJ_DIR is already added globally by src/coreclr/CMakeLists.txt, and no source file under debug/crashreport/ uses CLR_DIR-relative includes. The consumers (pal/src and vm) already add CLR_DIR themselves (guarded). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/debug/crashreport/CMakeLists.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/coreclr/debug/crashreport/CMakeLists.txt b/src/coreclr/debug/crashreport/CMakeLists.txt index 27ddfa084fa339..b917b6ebe3ed9e 100644 --- a/src/coreclr/debug/crashreport/CMakeLists.txt +++ b/src/coreclr/debug/crashreport/CMakeLists.txt @@ -1,8 +1,5 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) -include_directories(${CLR_ARTIFACTS_OBJ_DIR}) -include_directories(${CLR_DIR}) - set(CRASHREPORT_SOURCES crashjsonwriter.cpp inproccrashreporter.cpp From 1d488bd98fd58766465f168b5396c301e91c19a4 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 21 Apr 2026 18:42:44 -0400 Subject: [PATCH 34/52] Adopt C++ style and idioms in crash report sources Address review feedback on the in-proc crash reporter by aligning the code with idioms used by neighboring coreclr/debug/createdump sources: - Use `bool` for boolean-valued returns, parameters, and fields (crash report callbacks, `isCrashThread`, `writeFailed`, `sawCrashThread`, `hasCrashException`) rather than `int`. - Use `size_t` for sizes, lengths, and buffer offsets; drop unreachable negative-length branches and redundant casts. - Use `nullptr` and `static_cast()` instead of `NULL` and C-style casts. - Convert `CrashJsonWriter`, `MultiThreadJsonContext`, and `CrashReportOutputContext` from POD structs with free-function callbacks into classes with member functions, `m_`-prefixed fields, constructors with member-initializer lists, and deleted copy constructor and assignment operator. Signal-handler callbacks are exposed as static trampoline methods so they keep their C ABI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../debug/crashreport/crashjsonwriter.cpp | 223 +++---- .../debug/crashreport/crashjsonwriter.h | 56 +- .../debug/crashreport/inproccrashreporter.cpp | 572 ++++++++++-------- .../debug/crashreport/inproccrashreporter.h | 8 +- src/coreclr/vm/crashreportstackwalker.cpp | 109 ++-- 5 files changed, 519 insertions(+), 449 deletions(-) diff --git a/src/coreclr/debug/crashreport/crashjsonwriter.cpp b/src/coreclr/debug/crashreport/crashjsonwriter.cpp index 15b96a14bb7a94..a944a8aea2f24d 100644 --- a/src/coreclr/debug/crashreport/crashjsonwriter.cpp +++ b/src/coreclr/debug/crashreport/crashjsonwriter.cpp @@ -7,266 +7,229 @@ #include -static -int -CrashJsonAppend( - CrashJsonWriter* w, - const char* str, - int len); - -static -int -CrashJsonAppendStr( - CrashJsonWriter* w, - const char* str); - static char ToHexChar( unsigned value); -static -void -CrashJsonWriteSeparator( - CrashJsonWriter* w); - -static -void -CrashJsonWriteEscapedString( - CrashJsonWriter* w, - const char* str); - void -CrashJsonInit( - CrashJsonWriter* w, +CrashJsonWriter::Init( CrashJsonOutputCallback outputCallback, void* outputContext) { - w->pos = 0; - w->commaNeeded = false; - w->writeFailed = false; - w->outputCallback = outputCallback; - w->outputContext = outputContext; - w->buffer[0] = '\0'; + m_pos = 0; + m_commaNeeded = false; + m_writeFailed = false; + m_outputCallback = outputCallback; + m_outputContext = outputContext; + m_buffer[0] = '\0'; } void -CrashJsonOpenObject( - CrashJsonWriter* w, +CrashJsonWriter::OpenObject( const char* key) { - CrashJsonWriteSeparator(w); - if (key != NULL) + WriteSeparator(); + if (key != nullptr) { - CrashJsonWriteEscapedString(w, key); - CrashJsonAppendStr(w, ": "); + WriteEscapedString(key); + AppendStr(": "); } - CrashJsonAppendStr(w, "{"); - w->commaNeeded = false; + AppendStr("{"); + m_commaNeeded = false; } void -CrashJsonCloseObject( - CrashJsonWriter* w) +CrashJsonWriter::CloseObject() { - CrashJsonAppendStr(w, "}"); - w->commaNeeded = true; + AppendStr("}"); + m_commaNeeded = true; } void -CrashJsonOpenArray( - CrashJsonWriter* w, +CrashJsonWriter::OpenArray( const char* key) { - CrashJsonWriteSeparator(w); - if (key != NULL) + WriteSeparator(); + if (key != nullptr) { - CrashJsonWriteEscapedString(w, key); - CrashJsonAppendStr(w, ": "); + WriteEscapedString(key); + AppendStr(": "); } - CrashJsonAppendStr(w, "["); - w->commaNeeded = false; + AppendStr("["); + m_commaNeeded = false; } void -CrashJsonCloseArray( - CrashJsonWriter* w) +CrashJsonWriter::CloseArray() { - CrashJsonAppendStr(w, "]"); - w->commaNeeded = true; + AppendStr("]"); + m_commaNeeded = true; } void -CrashJsonWriteString( - CrashJsonWriter* w, +CrashJsonWriter::WriteString( const char* key, const char* value) { - CrashJsonWriteSeparator(w); - CrashJsonWriteEscapedString(w, key); - CrashJsonAppendStr(w, ": "); - CrashJsonWriteEscapedString(w, value); + WriteSeparator(); + WriteEscapedString(key); + AppendStr(": "); + WriteEscapedString(value); } void -CrashJsonFinish( - CrashJsonWriter* w) +CrashJsonWriter::Finish() { - (void)CrashJsonFlush(w); + (void)Flush(); } -int -CrashJsonHasFailed( - CrashJsonWriter* w) +bool +CrashJsonWriter::HasFailed() const { - return w->writeFailed ? 1 : 0; + return m_writeFailed; } -int -CrashJsonFlush( - CrashJsonWriter* w) +bool +CrashJsonWriter::Flush() { - if (w->writeFailed) + if (m_writeFailed) { - return 0; + return false; } - if (w->pos == 0) + if (m_pos == 0) { - return 1; + return true; } - if (w->outputCallback != NULL && !w->outputCallback(w->buffer, w->pos, w->outputContext)) + if (m_outputCallback != nullptr && !m_outputCallback(m_buffer, m_pos, m_outputContext)) { - w->writeFailed = true; - return 0; + m_writeFailed = true; + return false; } - w->pos = 0; - w->buffer[0] = '\0'; - return 1; + m_pos = 0; + m_buffer[0] = '\0'; + return true; } -int -CrashJsonAppend( - CrashJsonWriter* w, +bool +CrashJsonWriter::Append( const char* str, - int len) + size_t len) { - if (w->writeFailed) + if (m_writeFailed) { - return 0; + return false; } - if (str == NULL || len < 0) + if (str == nullptr) { // Invalid input mid-document would corrupt the JSON. Latch the // failure so subsequent writes become no-ops, matching the // behavior when the output callback reports an I/O failure. - w->writeFailed = true; - return 0; + m_writeFailed = true; + return false; } if (len == 0) { - return 1; + return true; } - int offset = 0; + size_t offset = 0; while (offset < len) { - int remaining = (CRASH_JSON_BUFFER_SIZE - 1) - w->pos; - if (remaining == 0 && !CrashJsonFlush(w)) + size_t remaining = (CRASH_JSON_BUFFER_SIZE - 1) - m_pos; + if (remaining == 0 && !Flush()) { - return 0; + return false; } - remaining = (CRASH_JSON_BUFFER_SIZE - 1) - w->pos; - int chunk = len - offset; + remaining = (CRASH_JSON_BUFFER_SIZE - 1) - m_pos; + size_t chunk = len - offset; if (chunk > remaining) { chunk = remaining; } - memcpy(w->buffer + w->pos, str + offset, static_cast(chunk)); - w->pos += chunk; + memcpy(m_buffer + m_pos, str + offset, chunk); + m_pos += chunk; offset += chunk; } - return 1; + return true; } -int -CrashJsonAppendStr( - CrashJsonWriter* w, +bool +CrashJsonWriter::AppendStr( const char* str) { - if (str == NULL) + if (str == nullptr) { - w->writeFailed = true; - return 0; + m_writeFailed = true; + return false; } - return CrashJsonAppend(w, str, static_cast(strlen(str))); + return Append(str, strlen(str)); } char ToHexChar( unsigned value) { - return (value < 10) ? (char)('0' + value) : (char)('a' + (value - 10)); + return (value < 10) ? static_cast('0' + value) : static_cast('a' + (value - 10)); } void -CrashJsonWriteSeparator( - CrashJsonWriter* w) +CrashJsonWriter::WriteSeparator() { - if (w->commaNeeded) - CrashJsonAppendStr(w, ","); + if (m_commaNeeded) + AppendStr(","); - w->commaNeeded = true; + m_commaNeeded = true; } // Escape a string value for JSON. Handles \, ", and control characters. void -CrashJsonWriteEscapedString( - CrashJsonWriter* w, +CrashJsonWriter::WriteEscapedString( const char* str) { - CrashJsonAppendStr(w, "\""); - if (str != NULL) + AppendStr("\""); + if (str != nullptr) { - for (int i = 0; str[i]; i++) + for (size_t i = 0; str[i]; i++) { char c = str[i]; if (c == '"') - CrashJsonAppendStr(w, "\\\""); + AppendStr("\\\""); else if (c == '\\') - CrashJsonAppendStr(w, "\\\\"); + AppendStr("\\\\"); else if (c == '\n') - CrashJsonAppendStr(w, "\\n"); + AppendStr("\\n"); else if (c == '\r') - CrashJsonAppendStr(w, "\\r"); + AppendStr("\\r"); else if (c == '\t') - CrashJsonAppendStr(w, "\\t"); - else if ((unsigned char)c < 0x20) + AppendStr("\\t"); + else if (static_cast(c) < 0x20) { char esc[7]; esc[0] = '\\'; esc[1] = 'u'; esc[2] = '0'; esc[3] = '0'; - esc[4] = ToHexChar(((unsigned char)c >> 4) & 0xF); - esc[5] = ToHexChar((unsigned char)c & 0xF); + esc[4] = ToHexChar((static_cast(c) >> 4) & 0xF); + esc[5] = ToHexChar(static_cast(c) & 0xF); esc[6] = '\0'; - CrashJsonAppendStr(w, esc); + AppendStr(esc); } else { - CrashJsonAppend(w, &c, 1); + Append(&c, 1); } } } - CrashJsonAppendStr(w, "\""); + AppendStr("\""); } diff --git a/src/coreclr/debug/crashreport/crashjsonwriter.h b/src/coreclr/debug/crashreport/crashjsonwriter.h index e4a940ba3741cc..e94889cb3df376 100644 --- a/src/coreclr/debug/crashreport/crashjsonwriter.h +++ b/src/coreclr/debug/crashreport/crashjsonwriter.h @@ -10,27 +10,47 @@ #include -typedef int (*CrashJsonOutputCallback)(const char* buffer, int len, void* ctx); +typedef bool (*CrashJsonOutputCallback)(const char* buffer, size_t len, void* ctx); // Small streaming buffer used when serializing the crash report JSON. #define CRASH_JSON_BUFFER_SIZE (4 * 1024) -struct CrashJsonWriter +class CrashJsonWriter { - char buffer[CRASH_JSON_BUFFER_SIZE]; - int pos; - bool commaNeeded; - bool writeFailed; - CrashJsonOutputCallback outputCallback; - void* outputContext; -}; +public: + CrashJsonWriter() + : m_pos(0), + m_commaNeeded(false), + m_writeFailed(false), + m_outputCallback(nullptr), + m_outputContext(nullptr) + { + m_buffer[0] = '\0'; + } + + CrashJsonWriter(const CrashJsonWriter&) = delete; + CrashJsonWriter& operator=(const CrashJsonWriter&) = delete; + + void Init(CrashJsonOutputCallback outputCallback, void* outputContext); + void OpenObject(const char* key); + void CloseObject(); + void OpenArray(const char* key); + void CloseArray(); + void WriteString(const char* key, const char* value); + void Finish(); + bool Flush(); + bool HasFailed() const; -void CrashJsonInit(CrashJsonWriter* w, CrashJsonOutputCallback outputCallback, void* outputContext); -void CrashJsonOpenObject(CrashJsonWriter* w, const char* key); -void CrashJsonCloseObject(CrashJsonWriter* w); -void CrashJsonOpenArray(CrashJsonWriter* w, const char* key); -void CrashJsonCloseArray(CrashJsonWriter* w); -void CrashJsonWriteString(CrashJsonWriter* w, const char* key, const char* value); -void CrashJsonFinish(CrashJsonWriter* w); -int CrashJsonFlush(CrashJsonWriter* w); -int CrashJsonHasFailed(CrashJsonWriter* w); +private: + bool Append(const char* str, size_t len); + bool AppendStr(const char* str); + void WriteSeparator(); + void WriteEscapedString(const char* str); + + char m_buffer[CRASH_JSON_BUFFER_SIZE]; + size_t m_pos; + bool m_commaNeeded; + bool m_writeFailed; + CrashJsonOutputCallback m_outputCallback; + void* m_outputContext; +}; diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index f6ddee77a66667..0dc62276797190 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -27,40 +27,123 @@ static CrashJsonWriter s_jsonWriter; // These callbacks are published during runtime startup and then only read from // the crash path; this minimal branch intentionally reuses the existing VM // inspection hooks before the later strict-safety hardening slices. -static volatile InProcCrashReportIsManagedThreadCallback g_isManagedThreadCallback = NULL; -static volatile InProcCrashReportWalkStackCallback g_walkStackCallback = NULL; -static volatile InProcCrashReportGetExceptionCallback g_getExceptionCallback = NULL; -static volatile InProcCrashReportEnumerateThreadsCallback g_enumerateThreadsCallback = NULL; +static volatile InProcCrashReportIsManagedThreadCallback g_isManagedThreadCallback = nullptr; +static volatile InProcCrashReportWalkStackCallback g_walkStackCallback = nullptr; +static volatile InProcCrashReportGetExceptionCallback g_getExceptionCallback = nullptr; +static volatile InProcCrashReportEnumerateThreadsCallback g_enumerateThreadsCallback = nullptr; static char g_reportPath[256]; -struct MultiThreadJsonContext +class MultiThreadJsonContext { - CrashJsonWriter* writer; - void* signalContext; - int threadCount; - int sawCrashThread; - int hasCrashException; - const char* crashExceptionType; - uint32_t crashExceptionHResult; +public: + MultiThreadJsonContext( + CrashJsonWriter* writer, + void* signalContext, + bool hasCrashException, + const char* crashExceptionType, + uint32_t crashExceptionHResult) + : m_writer(writer), + m_signalContext(signalContext), + m_threadCount(0), + m_sawCrashThread(false), + m_hasCrashException(hasCrashException), + m_crashExceptionType(crashExceptionType), + m_crashExceptionHResult(crashExceptionHResult) + { + } + + MultiThreadJsonContext(const MultiThreadJsonContext&) = delete; + MultiThreadJsonContext& operator=(const MultiThreadJsonContext&) = delete; + + size_t ThreadCount() const { return m_threadCount; } + bool SawCrashThread() const { return m_sawCrashThread; } + CrashJsonWriter* Writer() const { return m_writer; } + + void OnThread( + uint64_t osThreadId, + bool isCrashThread, + const char* exceptionType, + uint32_t exceptionHResult); + + void OnFrame( + 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 ThreadCallback( + uint64_t osThreadId, + bool isCrashThread, + const char* exceptionType, + uint32_t exceptionHResult, + void* ctx); + + static void FrameCallback( + 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); + +private: + CrashJsonWriter* m_writer; + void* m_signalContext; + size_t m_threadCount; + bool m_sawCrashThread; + bool m_hasCrashException; + const char* m_crashExceptionType; + uint32_t m_crashExceptionHResult; }; -struct CrashReportOutputContext +class CrashReportOutputContext { - int fd; - int writeFailed; +public: + explicit CrashReportOutputContext(int fd) + : m_fd(fd), + m_writeFailed(false) + { + } + + CrashReportOutputContext(const CrashReportOutputContext&) = delete; + CrashReportOutputContext& operator=(const CrashReportOutputContext&) = delete; + + int Fd() const { return m_fd; } + bool WriteFailed() const { return m_writeFailed; } + + bool HandleChunk(const char* buffer, size_t len); + + static bool ChunkCallback(const char* buffer, size_t len, void* ctx); + +private: + int m_fd; + bool m_writeFailed; }; static void GetVersionString( char* buffer, - int bufferSize); + size_t bufferSize); static void FormatHexValue( char* buffer, - int bufferSize, + size_t bufferSize, uint64_t value); static @@ -89,7 +172,7 @@ static void BuildMethodName( char* buffer, - int bufferSize, + size_t bufferSize, const char* className, const char* methodName); @@ -102,14 +185,14 @@ static void CopyString( char* buffer, - int bufferSize, + size_t bufferSize, const char* value); static -int +bool TryGetProcessName( char* filename, - int filenameLen); + size_t filenameLen); static void @@ -127,49 +210,17 @@ JsonFrameCallback( const char* moduleGuid, void* ctx); -static -void -JsonThreadFrameCallback( - 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 -void -JsonThreadCallback( - uint64_t osThreadId, - int isCrashThread, - const char* exceptionType, - uint32_t exceptionHResult, - void* ctx); - -int +bool WriteAllToFile( int fd, const char* buffer, - int len); - -static -int -WriteCrashReportChunk( - const char* buffer, - int len, - void* ctx); + size_t len); static -int +bool BuildReportPath( char* buffer, - int bufferSize, + size_t bufferSize, const char* dumpPath); void @@ -204,149 +255,145 @@ InProcCrashReportGenerate( uint32_t exHresult = 0; exTypeBuf[0] = '\0'; - int hasException = 0; - if (g_getExceptionCallback != NULL && signal != SIGSEGV && signal != SIGBUS) + bool hasException = false; + if (g_getExceptionCallback != nullptr && signal != SIGSEGV && signal != SIGBUS) { hasException = g_getExceptionCallback(exTypeBuf, sizeof(exTypeBuf), &exHresult); } - CrashReportOutputContext outputContext = - { - fd, - 0 - }; + CrashReportOutputContext outputContext(fd); - CrashJsonInit(&s_jsonWriter, WriteCrashReportChunk, &outputContext); + s_jsonWriter.Init(&CrashReportOutputContext::ChunkCallback, &outputContext); - CrashJsonOpenObject(&s_jsonWriter, NULL); - CrashJsonOpenObject(&s_jsonWriter, "payload"); - CrashJsonWriteString(&s_jsonWriter, "protocol_version", "1.0.0"); + s_jsonWriter.OpenObject(nullptr); + s_jsonWriter.OpenObject("payload"); + s_jsonWriter.WriteString("protocol_version", "1.0.0"); - CrashJsonOpenObject(&s_jsonWriter, "configuration"); + s_jsonWriter.OpenObject("configuration"); #if defined(__x86_64__) - CrashJsonWriteString(&s_jsonWriter, "architecture", "amd64"); + s_jsonWriter.WriteString("architecture", "amd64"); #elif defined(__aarch64__) - CrashJsonWriteString(&s_jsonWriter, "architecture", "arm64"); + s_jsonWriter.WriteString("architecture", "arm64"); #elif defined(__arm__) - CrashJsonWriteString(&s_jsonWriter, "architecture", "arm"); + s_jsonWriter.WriteString("architecture", "arm"); #endif char version[sizeof(sccsid) + 1]; GetVersionString(version, sizeof(version)); - CrashJsonWriteString(&s_jsonWriter, "version", version); - CrashJsonCloseObject(&s_jsonWriter); + s_jsonWriter.WriteString("version", version); + s_jsonWriter.CloseObject(); char processName[256]; if (TryGetProcessName(processName, sizeof(processName))) { - CrashJsonWriteString(&s_jsonWriter, "process_name", processName); + s_jsonWriter.WriteString("process_name", processName); } char pidBuf[16]; (void)snprintf(pidBuf, sizeof(pidBuf), "%u", static_cast(getpid())); - CrashJsonWriteString(&s_jsonWriter, "pid", pidBuf); + s_jsonWriter.WriteString("pid", pidBuf); - CrashJsonOpenArray(&s_jsonWriter, "threads"); - if (g_enumerateThreadsCallback != NULL) + s_jsonWriter.OpenArray("threads"); + if (g_enumerateThreadsCallback != nullptr) { - MultiThreadJsonContext threadContext = { &s_jsonWriter, context, 0, 0, hasException, exTypeBuf, exHresult }; + MultiThreadJsonContext threadContext(&s_jsonWriter, context, hasException, exTypeBuf, exHresult); uint64_t crashingTid = static_cast(minipal_get_current_thread_id()); - g_enumerateThreadsCallback(crashingTid, JsonThreadCallback, JsonThreadFrameCallback, &threadContext); + g_enumerateThreadsCallback(crashingTid, &MultiThreadJsonContext::ThreadCallback, &MultiThreadJsonContext::FrameCallback, &threadContext); - if (threadContext.threadCount > 0) + if (threadContext.ThreadCount() > 0) { // Close the last thread's stack_frames + object opened by the // enumeration callback. - CrashJsonCloseArray(&s_jsonWriter); - CrashJsonCloseObject(&s_jsonWriter); + s_jsonWriter.CloseArray(); + s_jsonWriter.CloseObject(); // Flush the final thread so it reaches the crash report file // even if any later work (e.g. synthesizing a crash thread // fallback) hangs or faults. - (void)CrashJsonFlush(&s_jsonWriter); + (void)s_jsonWriter.Flush(); } - if (threadContext.threadCount == 0 || !threadContext.sawCrashThread) + if (threadContext.ThreadCount() == 0 || !threadContext.SawCrashThread()) { - CrashJsonOpenObject(&s_jsonWriter, NULL); - CrashJsonWriteString(&s_jsonWriter, "is_managed", - g_isManagedThreadCallback != NULL && g_isManagedThreadCallback() ? "true" : "false"); - CrashJsonWriteString(&s_jsonWriter, "crashed", "true"); + s_jsonWriter.OpenObject(nullptr); + s_jsonWriter.WriteString("is_managed", + g_isManagedThreadCallback != nullptr && g_isManagedThreadCallback() ? "true" : "false"); + s_jsonWriter.WriteString("crashed", "true"); char nativeThreadId[32]; FormatHexValue(nativeThreadId, sizeof(nativeThreadId), crashingTid); - CrashJsonWriteString(&s_jsonWriter, "native_thread_id", nativeThreadId); + s_jsonWriter.WriteString("native_thread_id", nativeThreadId); if (hasException) { char hresultBuffer[32]; FormatHexValue(hresultBuffer, sizeof(hresultBuffer), exHresult); - CrashJsonWriteString(&s_jsonWriter, "managed_exception_type", exTypeBuf); - CrashJsonWriteString(&s_jsonWriter, "managed_exception_hresult", hresultBuffer); + s_jsonWriter.WriteString("managed_exception_type", exTypeBuf); + s_jsonWriter.WriteString("managed_exception_hresult", hresultBuffer); } WriteRegistersToJson(&s_jsonWriter, context); - CrashJsonOpenArray(&s_jsonWriter, "stack_frames"); + s_jsonWriter.OpenArray("stack_frames"); WriteCrashSiteFrameToJson(&s_jsonWriter, context); - CrashJsonCloseArray(&s_jsonWriter); - CrashJsonCloseObject(&s_jsonWriter); + s_jsonWriter.CloseArray(); + s_jsonWriter.CloseObject(); } } else { uint64_t crashingTid = static_cast(minipal_get_current_thread_id()); - CrashJsonOpenObject(&s_jsonWriter, NULL); - CrashJsonWriteString(&s_jsonWriter, "is_managed", - g_isManagedThreadCallback != NULL && g_isManagedThreadCallback() ? "true" : "false"); - CrashJsonWriteString(&s_jsonWriter, "crashed", "true"); + s_jsonWriter.OpenObject(nullptr); + s_jsonWriter.WriteString("is_managed", + g_isManagedThreadCallback != nullptr && g_isManagedThreadCallback() ? "true" : "false"); + s_jsonWriter.WriteString("crashed", "true"); char nativeThreadId[32]; FormatHexValue(nativeThreadId, sizeof(nativeThreadId), crashingTid); - CrashJsonWriteString(&s_jsonWriter, "native_thread_id", nativeThreadId); + s_jsonWriter.WriteString("native_thread_id", nativeThreadId); if (hasException) { char hresultBuffer[32]; FormatHexValue(hresultBuffer, sizeof(hresultBuffer), exHresult); - CrashJsonWriteString(&s_jsonWriter, "managed_exception_type", exTypeBuf); - CrashJsonWriteString(&s_jsonWriter, "managed_exception_hresult", hresultBuffer); + s_jsonWriter.WriteString("managed_exception_type", exTypeBuf); + s_jsonWriter.WriteString("managed_exception_hresult", hresultBuffer); } WriteRegistersToJson(&s_jsonWriter, context); - CrashJsonOpenArray(&s_jsonWriter, "stack_frames"); + s_jsonWriter.OpenArray("stack_frames"); WriteCrashSiteFrameToJson(&s_jsonWriter, context); - if (g_walkStackCallback != NULL) + if (g_walkStackCallback != nullptr) { g_walkStackCallback(JsonFrameCallback, &s_jsonWriter); } - CrashJsonCloseArray(&s_jsonWriter); - CrashJsonCloseObject(&s_jsonWriter); + s_jsonWriter.CloseArray(); + s_jsonWriter.CloseObject(); } - CrashJsonCloseArray(&s_jsonWriter); + s_jsonWriter.CloseArray(); - CrashJsonCloseObject(&s_jsonWriter); + s_jsonWriter.CloseObject(); - CrashJsonOpenObject(&s_jsonWriter, "parameters"); + s_jsonWriter.OpenObject("parameters"); char signalBuf[16]; (void)snprintf(signalBuf, sizeof(signalBuf), "%d", signal); - CrashJsonWriteString(&s_jsonWriter, "signal", signalBuf); + s_jsonWriter.WriteString("signal", signalBuf); #ifdef __APPLE__ - CrashJsonWriteString(&s_jsonWriter, "OSVersion", ""); - CrashJsonWriteString(&s_jsonWriter, "SystemModel", ""); - CrashJsonWriteString(&s_jsonWriter, "SystemManufacturer", "apple"); + s_jsonWriter.WriteString("OSVersion", ""); + s_jsonWriter.WriteString("SystemModel", ""); + s_jsonWriter.WriteString("SystemManufacturer", "apple"); #endif - CrashJsonCloseObject(&s_jsonWriter); + s_jsonWriter.CloseObject(); - CrashJsonCloseObject(&s_jsonWriter); - CrashJsonFinish(&s_jsonWriter); + s_jsonWriter.CloseObject(); + s_jsonWriter.Finish(); if (fd != -1) { - int writeSucceeded = !CrashJsonHasFailed(&s_jsonWriter) && - outputContext.writeFailed == 0 && + bool writeSucceeded = !s_jsonWriter.HasFailed() && + !outputContext.WriteFailed() && WriteAllToFile(fd, "\n", 1); if (close(fd) != 0 || !writeSucceeded) @@ -392,19 +439,19 @@ InProcCrashReportSetThreadEnumerator( g_enumerateThreadsCallback = callback; } -int +bool WriteAllToFile( int fd, const char* buffer, - int len) + size_t len) { - int totalWritten = 0; + size_t totalWritten = 0; while (totalWritten < len) { ssize_t written = write(fd, buffer + totalWritten, len - totalWritten); if (written > 0) { - totalWritten += static_cast(written); + totalWritten += static_cast(written); continue; } @@ -413,31 +460,44 @@ WriteAllToFile( continue; } - return 0; + return false; } - return 1; + return true; } -int -WriteCrashReportChunk( +bool +CrashReportOutputContext::HandleChunk( const char* buffer, - int len, - void* ctx) + size_t len) { - CrashReportOutputContext* outputContext = reinterpret_cast(ctx); - if (outputContext == NULL || outputContext->fd == -1) + if (m_fd == -1) { - return 0; + return false; } - if (!WriteAllToFile(outputContext->fd, buffer, len)) + if (!WriteAllToFile(m_fd, buffer, len)) { - outputContext->writeFailed = 1; - return 0; + m_writeFailed = true; + return false; + } + + return true; +} + +bool +CrashReportOutputContext::ChunkCallback( + const char* buffer, + size_t len, + void* ctx) +{ + CrashReportOutputContext* outputContext = reinterpret_cast(ctx); + if (outputContext == nullptr) + { + return false; } - return 1; + return outputContext->HandleChunk(buffer, len); } // Expand a subset of the coredump template patterns used by createdump's @@ -445,21 +505,21 @@ WriteCrashReportChunk( // literally since the remaining createdump patterns (%e, %h, %t) are not // meaningful for in-proc crash reports. static -int +size_t ExpandDumpTemplate( char* buffer, - int bufferSize, + size_t bufferSize, const char* pattern) { - if (buffer == NULL || bufferSize <= 0 || pattern == NULL) + if (buffer == nullptr || bufferSize == 0 || pattern == nullptr) { return 0; } - int pos = 0; + size_t pos = 0; unsigned pid = static_cast(getpid()); - while (*pattern != '\0' && pos < bufferSize - 1) + while (*pattern != '\0' && pos + 1 < bufferSize) { if (*pattern == '%') { @@ -472,20 +532,20 @@ ExpandDumpTemplate( { char pidBuf[16]; int pidLen = snprintf(pidBuf, sizeof(pidBuf), "%u", pid); - if (pidLen > 0 && pos + pidLen < bufferSize) + if (pidLen > 0 && pos + static_cast(pidLen) < bufferSize) { memcpy(buffer + pos, pidBuf, static_cast(pidLen)); - pos += pidLen; + pos += static_cast(pidLen); } } else { // Unknown specifier — pass through literally. - if (pos < bufferSize - 1) + if (pos + 1 < bufferSize) { buffer[pos++] = '%'; } - if (*pattern != '\0' && pos < bufferSize - 1) + if (*pattern != '\0' && pos + 1 < bufferSize) { buffer[pos++] = *pattern; } @@ -506,34 +566,34 @@ ExpandDumpTemplate( return pos; } -int +bool BuildReportPath( char* buffer, - int bufferSize, + size_t bufferSize, const char* dumpPath) { - if (buffer == NULL || bufferSize <= 0 || dumpPath == NULL || dumpPath[0] == '\0') + if (buffer == nullptr || bufferSize == 0 || dumpPath == nullptr || dumpPath[0] == '\0') { - return 0; + return false; } char expanded[256]; - int expandedLen = ExpandDumpTemplate(expanded, sizeof(expanded), dumpPath); - if (expandedLen <= 0) + size_t expandedLen = ExpandDumpTemplate(expanded, sizeof(expanded), dumpPath); + if (expandedLen == 0) { - return 0; + return false; } - int written = snprintf(buffer, static_cast(bufferSize), "%s.crashreport.json", expanded); - return written > 0 && written < bufferSize; + int written = snprintf(buffer, bufferSize, "%s.crashreport.json", expanded); + return written > 0 && static_cast(written) < bufferSize; } void GetVersionString( char* buffer, - int bufferSize) + size_t bufferSize) { - if (buffer == NULL || bufferSize <= 0) + if (buffer == nullptr || bufferSize == 0) { return; } @@ -555,24 +615,23 @@ GetVersionString( version += sizeof(versionPrefix) - 1; - size_t copied = strnlen(version, static_cast(bufferSize - 2)); + size_t copied = strnlen(version, bufferSize - 2); if (copied != 0) { memcpy(buffer, version, copied); } - int index = static_cast(copied); - buffer[index++] = ' '; - buffer[index] = '\0'; + buffer[copied] = ' '; + buffer[copied + 1] = '\0'; } void FormatHexValue( char* buffer, - int bufferSize, + size_t bufferSize, uint64_t value) { - if (buffer == NULL || bufferSize <= 0) + if (buffer == nullptr || bufferSize == 0) { return; } @@ -593,16 +652,16 @@ FormatHexValue( buffer[1] = 'x'; char reverse[16]; - int reverseLength = 0; + size_t reverseLength = 0; do { - int digit = static_cast(value & 0xf); + unsigned digit = static_cast(value & 0xf); reverse[reverseLength++] = static_cast(digit < 10 ? ('0' + digit) : ('a' + digit - 10)); value >>= 4; - } while (value != 0 && reverseLength < static_cast(sizeof(reverse))); + } while (value != 0 && reverseLength < sizeof(reverse)); - int index = 2; - while (reverseLength > 0 && index < bufferSize - 1) + size_t index = 2; + while (reverseLength > 0 && index + 1 < bufferSize) { buffer[index++] = reverse[--reverseLength]; } @@ -624,7 +683,7 @@ WriteRegistersToJson( FormatHexValue(ip, sizeof(ip), ipValue); FormatHexValue(sp, sizeof(sp), spValue); - if (context != NULL) + if (context != nullptr) { ucontext_t* ucontext = reinterpret_cast(context); #if defined(__x86_64__) @@ -636,18 +695,18 @@ WriteRegistersToJson( #endif } - CrashJsonOpenObject(writer, "ctx"); - CrashJsonWriteString(writer, "IP", ip); - CrashJsonWriteString(writer, "SP", sp); - CrashJsonWriteString(writer, "BP", bp); - CrashJsonCloseObject(writer); + writer->OpenObject("ctx"); + writer->WriteString("IP", ip); + writer->WriteString("SP", sp); + writer->WriteString("BP", bp); + writer->CloseObject(); } uint64_t GetInstructionPointer( void* context) { - if (context == NULL) + if (context == nullptr) { return 0; } @@ -668,7 +727,7 @@ uint64_t GetStackPointer( void* context) { - if (context == NULL) + if (context == nullptr) { return 0; } @@ -698,36 +757,36 @@ WriteCrashSiteFrameToJson( FormatHexValue(ip, sizeof(ip), ipValue); FormatHexValue(sp, sizeof(sp), spValue); - CrashJsonOpenObject(writer, NULL); - CrashJsonWriteString(writer, "is_managed", "false"); - CrashJsonWriteString(writer, "stack_pointer", sp); - CrashJsonWriteString(writer, "native_address", ip); - CrashJsonCloseObject(writer); + writer->OpenObject(nullptr); + writer->WriteString("is_managed", "false"); + writer->WriteString("stack_pointer", sp); + writer->WriteString("native_address", ip); + writer->CloseObject(); } void BuildMethodName( char* buffer, - int bufferSize, + size_t bufferSize, const char* className, const char* methodName) { - if (buffer == NULL || bufferSize <= 0) + if (buffer == nullptr || bufferSize == 0) { return; } - if (className != NULL && methodName != NULL) + if (className != nullptr && methodName != nullptr) { - (void)snprintf(buffer, static_cast(bufferSize), "%s.%s", className, methodName); + (void)snprintf(buffer, bufferSize, "%s.%s", className, methodName); } - else if (className != NULL) + else if (className != nullptr) { - (void)snprintf(buffer, static_cast(bufferSize), "%s", className); + (void)snprintf(buffer, bufferSize, "%s", className); } - else if (methodName != NULL) + else if (methodName != nullptr) { - (void)snprintf(buffer, static_cast(bufferSize), "%s", methodName); + (void)snprintf(buffer, bufferSize, "%s", methodName); } else { @@ -754,21 +813,21 @@ GetFilename( void CopyString( char* buffer, - int bufferSize, + size_t bufferSize, const char* value) { - if (buffer == NULL || bufferSize <= 0) + if (buffer == nullptr || bufferSize == 0) { return; } - if (value == NULL) + if (value == nullptr) { buffer[0] = '\0'; return; } - size_t copied = strnlen(value, static_cast(bufferSize - 1)); + size_t copied = strnlen(value, bufferSize - 1); if (copied != 0) { memcpy(buffer, value, copied); @@ -777,14 +836,14 @@ CopyString( buffer[copied] = '\0'; } -int +bool TryGetProcessName( char* filename, - int filenameLen) + size_t filenameLen) { - if (filename == NULL || filenameLen <= 0) + if (filename == nullptr || filenameLen == 0) { - return 0; + return false; } filename[0] = '\0'; @@ -802,7 +861,7 @@ TryGetProcessName( CopyString(filename, filenameLen, GetFilename(cmdline)); if (filename[0] != '\0') { - return 1; + return true; } } } @@ -816,7 +875,7 @@ TryGetProcessName( return filename[0] != '\0'; } - return 0; + return false; } void @@ -851,50 +910,67 @@ JsonFrameCallback( FormatHexValue(moduleTimestampBuffer, sizeof(moduleTimestampBuffer), moduleTimestamp); FormatHexValue(moduleSizeBuffer, sizeof(moduleSizeBuffer), moduleSize); - CrashJsonOpenObject(writer, NULL); - CrashJsonWriteString(writer, "stack_pointer", stackPointerBuffer); - CrashJsonWriteString(writer, "native_address", ipBuffer); - CrashJsonWriteString(writer, "native_offset", nativeOffsetBuffer); + writer->OpenObject(nullptr); + writer->WriteString("stack_pointer", stackPointerBuffer); + writer->WriteString("native_address", ipBuffer); + writer->WriteString("native_offset", nativeOffsetBuffer); - if (methodName != NULL) + if (methodName != nullptr) { char fullName[256]; BuildMethodName(fullName, sizeof(fullName), className, methodName); - CrashJsonWriteString(writer, "method_name", fullName); - CrashJsonWriteString(writer, "is_managed", "true"); - CrashJsonWriteString(writer, "token", tokenBuffer); - CrashJsonWriteString(writer, "il_offset", ilOffsetBuffer); - if (moduleName != NULL) + writer->WriteString("method_name", fullName); + writer->WriteString("is_managed", "true"); + writer->WriteString("token", tokenBuffer); + writer->WriteString("il_offset", ilOffsetBuffer); + if (moduleName != nullptr) { - CrashJsonWriteString(writer, "filename", moduleName); + writer->WriteString("filename", moduleName); } if (moduleTimestamp != 0) { - CrashJsonWriteString(writer, "timestamp", moduleTimestampBuffer); + writer->WriteString("timestamp", moduleTimestampBuffer); } if (moduleSize != 0) { - CrashJsonWriteString(writer, "sizeofimage", moduleSizeBuffer); + writer->WriteString("sizeofimage", moduleSizeBuffer); } - if (moduleGuid != NULL && moduleGuid[0] != '\0') + if (moduleGuid != nullptr && moduleGuid[0] != '\0') { - CrashJsonWriteString(writer, "guid", moduleGuid); + writer->WriteString("guid", moduleGuid); } } else { - CrashJsonWriteString(writer, "is_managed", "false"); - if (moduleName != NULL) + writer->WriteString("is_managed", "false"); + if (moduleName != nullptr) { - CrashJsonWriteString(writer, "native_module", moduleName); + writer->WriteString("native_module", moduleName); } } - CrashJsonCloseObject(writer); + writer->CloseObject(); +} + +void +MultiThreadJsonContext::OnFrame( + 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) +{ + JsonFrameCallback(ip, stackPointer, methodName, className, moduleName, nativeOffset, token, ilOffset, moduleTimestamp, moduleSize, moduleGuid, m_writer); } void -JsonThreadFrameCallback( +MultiThreadJsonContext::FrameCallback( uint64_t ip, uint64_t stackPointer, const char* methodName, @@ -908,66 +984,74 @@ JsonThreadFrameCallback( const char* moduleGuid, void* ctx) { - MultiThreadJsonContext* threadContext = reinterpret_cast(ctx); - JsonFrameCallback(ip, stackPointer, methodName, className, moduleName, nativeOffset, token, ilOffset, moduleTimestamp, moduleSize, moduleGuid, threadContext->writer); + reinterpret_cast(ctx)->OnFrame(ip, stackPointer, methodName, className, moduleName, nativeOffset, token, ilOffset, moduleTimestamp, moduleSize, moduleGuid); } void -JsonThreadCallback( +MultiThreadJsonContext::OnThread( uint64_t osThreadId, - int isCrashThread, + bool isCrashThread, const char* exceptionType, - uint32_t exceptionHResult, - void* ctx) + uint32_t exceptionHResult) { - MultiThreadJsonContext* threadContext = reinterpret_cast(ctx); - if (threadContext->threadCount > 0) + if (m_threadCount > 0) { - CrashJsonCloseArray(threadContext->writer); - CrashJsonCloseObject(threadContext->writer); + m_writer->CloseArray(); + m_writer->CloseObject(); - (void)CrashJsonFlush(threadContext->writer); + (void)m_writer->Flush(); } if (isCrashThread) { - threadContext->sawCrashThread = 1; + m_sawCrashThread = true; } - threadContext->threadCount++; + m_threadCount++; - CrashJsonOpenObject(threadContext->writer, NULL); - CrashJsonWriteString(threadContext->writer, "is_managed", "true"); - CrashJsonWriteString(threadContext->writer, "crashed", isCrashThread ? "true" : "false"); + m_writer->OpenObject(nullptr); + m_writer->WriteString("is_managed", "true"); + m_writer->WriteString("crashed", isCrashThread ? "true" : "false"); char nativeThreadId[32]; FormatHexValue(nativeThreadId, sizeof(nativeThreadId), osThreadId); - CrashJsonWriteString(threadContext->writer, "native_thread_id", nativeThreadId); + m_writer->WriteString("native_thread_id", nativeThreadId); - if (isCrashThread && threadContext->hasCrashException) + if (isCrashThread && m_hasCrashException) { char hresultBuffer[32]; - FormatHexValue(hresultBuffer, sizeof(hresultBuffer), threadContext->crashExceptionHResult); + FormatHexValue(hresultBuffer, sizeof(hresultBuffer), m_crashExceptionHResult); - CrashJsonWriteString(threadContext->writer, "managed_exception_type", threadContext->crashExceptionType); - CrashJsonWriteString(threadContext->writer, "managed_exception_hresult", hresultBuffer); + m_writer->WriteString("managed_exception_type", m_crashExceptionType); + m_writer->WriteString("managed_exception_hresult", hresultBuffer); } - else if (exceptionType != NULL && exceptionType[0] != '\0') + else if (exceptionType != nullptr && exceptionType[0] != '\0') { char hresultBuffer[32]; FormatHexValue(hresultBuffer, sizeof(hresultBuffer), exceptionHResult); - CrashJsonWriteString(threadContext->writer, "managed_exception_type", exceptionType); - CrashJsonWriteString(threadContext->writer, "managed_exception_hresult", hresultBuffer); + m_writer->WriteString("managed_exception_type", exceptionType); + m_writer->WriteString("managed_exception_hresult", hresultBuffer); } if (isCrashThread) { - WriteRegistersToJson(threadContext->writer, threadContext->signalContext); + WriteRegistersToJson(m_writer, m_signalContext); } - CrashJsonOpenArray(threadContext->writer, "stack_frames"); + m_writer->OpenArray("stack_frames"); if (isCrashThread) { - WriteCrashSiteFrameToJson(threadContext->writer, threadContext->signalContext); + WriteCrashSiteFrameToJson(m_writer, m_signalContext); } } + +void +MultiThreadJsonContext::ThreadCallback( + uint64_t osThreadId, + bool isCrashThread, + const char* exceptionType, + uint32_t exceptionHResult, + void* ctx) +{ + reinterpret_cast(ctx)->OnThread(osThreadId, isCrashThread, exceptionType, exceptionHResult); +} diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.h b/src/coreclr/debug/crashreport/inproccrashreporter.h index 51896d10372573..0edf2b6d93317f 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.h +++ b/src/coreclr/debug/crashreport/inproccrashreporter.h @@ -17,7 +17,7 @@ void InProcCrashReportInitialize(const char* dumpPath); // All arguments come from the signal handler and are signal-safe to read. void InProcCrashReportGenerate(int signal, siginfo_t* siginfo, void* context); -typedef int (*InProcCrashReportIsManagedThreadCallback)(); +typedef bool (*InProcCrashReportIsManagedThreadCallback)(); void InProcCrashReportSetCurrentThreadManagedResolver(InProcCrashReportIsManagedThreadCallback callback); @@ -41,16 +41,16 @@ typedef void (*InProcCrashReportWalkStackCallback)( void InProcCrashReportSetStackWalker(InProcCrashReportWalkStackCallback callback); -typedef int (*InProcCrashReportGetExceptionCallback)( +typedef bool (*InProcCrashReportGetExceptionCallback)( char* exceptionTypeBuf, - int exceptionTypeBufSize, + size_t exceptionTypeBufSize, uint32_t* hresult); void InProcCrashReportSetExceptionResolver(InProcCrashReportGetExceptionCallback callback); typedef void (*InProcCrashReportThreadCallback)( uint64_t osThreadId, - int isCrashThread, + bool isCrashThread, const char* exceptionType, uint32_t exceptionHResult, void* ctx); diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index 4fd060217ffab3..5d46a1acd8d3f8 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -33,7 +33,7 @@ FrameCallbackAdapter( { WalkContext* ctx = static_cast(pData); MethodDesc* pMD = pCF->GetFunction(); - if (pMD == NULL) + if (pMD == nullptr) { return SWA_CONTINUE; } @@ -41,49 +41,49 @@ FrameCallbackAdapter( LPCUTF8 methodName = pMD->GetName(); mdMethodDef token = pMD->GetMemberDef(); - LPCUTF8 className = NULL; - LPCUTF8 namespaceName = NULL; + LPCUTF8 className = nullptr; + LPCUTF8 namespaceName = nullptr; MethodTable* pMT = pMD->GetMethodTable(); - if (pMT != NULL) + if (pMT != nullptr) { mdTypeDef cl = pMT->GetCl(); IMDInternalImport* pImport = pMD->GetMDImport(); - if (pImport != NULL && cl != mdTypeDefNil) + if (pImport != nullptr && cl != mdTypeDefNil) { pImport->GetNameOfTypeDef(cl, &className, &namespaceName); } } char classNameBuf[256] = { 0 }; - int index = 0; - if (namespaceName != NULL) + size_t index = 0; + if (namespaceName != nullptr) { - while (*namespaceName != '\0' && index < static_cast(sizeof(classNameBuf)) - 1) + while (*namespaceName != '\0' && index + 1 < sizeof(classNameBuf)) { classNameBuf[index++] = *namespaceName++; } } - if (className != NULL) + if (className != nullptr) { - if (index > 0 && index < static_cast(sizeof(classNameBuf)) - 1) + if (index > 0 && index + 1 < sizeof(classNameBuf)) { classNameBuf[index++] = '.'; } - while (*className != '\0' && index < static_cast(sizeof(classNameBuf)) - 1) + while (*className != '\0' && index + 1 < sizeof(classNameBuf)) { classNameBuf[index++] = *className++; } } classNameBuf[index] = '\0'; - const char* moduleName = NULL; + const char* moduleName = nullptr; Module* pModule = pMD->GetModule(); - if (pModule != NULL) + if (pModule != nullptr) { Assembly* pAssembly = pModule->GetAssembly(); - if (pAssembly != NULL) + if (pAssembly != nullptr) { moduleName = pAssembly->GetSimpleName(); } @@ -94,7 +94,7 @@ FrameCallbackAdapter( uint64_t ip = 0; uint64_t stackPointer = 0; PREGDISPLAY pRD = pCF->GetRegisterSet(); - if (pRD != NULL) + if (pRD != nullptr) { ip = static_cast(GetControlPC(pRD)); stackPointer = static_cast(GetRegdisplaySP(pRD)); @@ -105,7 +105,7 @@ FrameCallbackAdapter( return SWA_CONTINUE; } - if (g_pDebugInterface != NULL && pMD != NULL) + if (g_pDebugInterface != nullptr && pMD != nullptr) { DWORD resolvedILOffset = 0; if (g_pDebugInterface->GetILOffsetFromNative( @@ -123,20 +123,20 @@ FrameCallbackAdapter( char moduleGuid[MINIPAL_GUID_BUFFER_LEN]; moduleGuid[0] = '\0'; - if (pModule != NULL) + if (pModule != nullptr) { PEAssembly* pPEAssembly = pModule->GetPEAssembly(); - if (pPEAssembly != NULL && pPEAssembly->HasLoadedPEImage()) + if (pPEAssembly != nullptr && pPEAssembly->HasLoadedPEImage()) { moduleTimestamp = pPEAssembly->GetLoadedLayout()->GetTimeDateStamp(); moduleSize = static_cast(pPEAssembly->GetLoadedLayout()->GetSize()); } IMDInternalImport* pImport = pModule->GetMDImport(); - if (pImport != NULL) + if (pImport != nullptr) { GUID mvid; - if (SUCCEEDED(pImport->GetScopeProps(NULL, &mvid))) + if (SUCCEEDED(pImport->GetScopeProps(nullptr, &mvid))) { minipal_guid_as_string(mvid, moduleGuid, MINIPAL_GUID_BUFFER_LEN); } @@ -154,7 +154,7 @@ CrashReportWalkThread( InProcCrashReportFrameCallback frameCallback, void* ctx) { - if (pThread == NULL || frameCallback == NULL) + if (pThread == nullptr || frameCallback == nullptr) { return; } @@ -174,18 +174,18 @@ CrashReportWalkStack( } static -int +bool CrashReportIsCurrentThreadManaged() { - return GetThreadAsyncSafe() != NULL; + return GetThreadAsyncSafe() != nullptr; } static -int +bool CrashReportGetExceptionForThread( Thread* pThread, char* exceptionTypeBuf, - int exceptionTypeBufSize, + size_t exceptionTypeBufSize, uint32_t* hresult) { if (exceptionTypeBufSize > 0) @@ -193,7 +193,7 @@ CrashReportGetExceptionForThread( exceptionTypeBuf[0] = '\0'; } - if (hresult != NULL) + if (hresult != nullptr) { *hresult = 0; } @@ -201,65 +201,68 @@ CrashReportGetExceptionForThread( // Only inspect the managed throwable when the thread is already in cooperative mode. if (!pThread->PreemptiveGCDisabled()) { - return 0; + return false; } - int result = 0; + bool result = false; GCX_COOP(); OBJECTREF throwable = pThread->GetThrowable(); GCPROTECT_BEGIN(throwable); - if (throwable != NULL) + if (throwable != nullptr) { MethodTable* pMT = throwable->GetMethodTable(); - if (pMT != NULL) + if (pMT != nullptr) { mdTypeDef cl = pMT->GetCl(); Module* pModule = pMT->GetModule(); - if (pModule != NULL) + if (pModule != nullptr) { IMDInternalImport* pImport = pModule->GetMDImport(); - if (pImport != NULL && cl != mdTypeDefNil) + if (pImport != nullptr && cl != mdTypeDefNil) { - LPCUTF8 className = NULL; - LPCUTF8 namespaceName = NULL; + LPCUTF8 className = nullptr; + LPCUTF8 namespaceName = nullptr; pImport->GetNameOfTypeDef(cl, &className, &namespaceName); - int index = 0; - if (namespaceName != NULL) + size_t index = 0; + if (namespaceName != nullptr) { - while (*namespaceName != '\0' && index < exceptionTypeBufSize - 1) + while (*namespaceName != '\0' && index + 1 < exceptionTypeBufSize) { exceptionTypeBuf[index++] = *namespaceName++; } } - if (className != NULL) + if (className != nullptr) { - if (index > 0 && index < exceptionTypeBufSize - 1) + if (index > 0 && index + 1 < exceptionTypeBufSize) { exceptionTypeBuf[index++] = '.'; } - while (*className != '\0' && index < exceptionTypeBufSize - 1) + while (*className != '\0' && index + 1 < exceptionTypeBufSize) { exceptionTypeBuf[index++] = *className++; } } - exceptionTypeBuf[index] = '\0'; + if (exceptionTypeBufSize > 0) + { + exceptionTypeBuf[index] = '\0'; + } } } } - if (hresult != NULL) + if (hresult != nullptr) { *hresult = static_cast(((EXCEPTIONREF)throwable)->GetHResult()); } - result = 1; + result = true; } GCPROTECT_END(); @@ -268,16 +271,16 @@ CrashReportGetExceptionForThread( } static -int +bool CrashReportGetException( char* exceptionTypeBuf, - int exceptionTypeBufSize, + size_t exceptionTypeBufSize, uint32_t* hresult) { Thread* pThread = GetThreadAsyncSafe(); - if (pThread == NULL) + if (pThread == nullptr) { - return 0; + return false; } return CrashReportGetExceptionForThread(pThread, exceptionTypeBuf, exceptionTypeBufSize, hresult); @@ -345,16 +348,16 @@ CrashReportEnumerateThreads( // Emit the crashing thread first so the report keeps the most important // thread even if later enumeration is incomplete. - if (pCrashThread != NULL) + if (pCrashThread != nullptr) { uint64_t crashOsId = static_cast(pCrashThread->GetOSThreadId()); if (crashOsId == crashingTid) { char exceptionType[256]; uint32_t hresult = 0; - int hasException = CrashReportGetExceptionForThread(pCrashThread, exceptionType, sizeof(exceptionType), &hresult); + bool hasException = CrashReportGetExceptionForThread(pCrashThread, exceptionType, sizeof(exceptionType), &hresult); - threadCallback(crashOsId, 1, hasException ? exceptionType : "", hresult, ctx); + threadCallback(crashOsId, true, hasException ? exceptionType : "", hresult, ctx); CrashReportWalkThread(pCrashThread, frameCallback, ctx); } @@ -365,8 +368,8 @@ CrashReportEnumerateThreads( // to be at a safe point for them. if (s_runtimeSuspendedForCrashReport) { - Thread* pThread = NULL; - while ((pThread = ThreadStore::GetThreadList(pThread)) != NULL) + Thread* pThread = nullptr; + while ((pThread = ThreadStore::GetThreadList(pThread)) != nullptr) { if (pThread == pCrashThread) continue; @@ -375,7 +378,7 @@ CrashReportEnumerateThreads( if (osThreadId == 0 || osThreadId == crashingTid) continue; - threadCallback(osThreadId, 0, "", 0, ctx); + threadCallback(osThreadId, false, "", 0, ctx); CrashReportWalkThread(pThread, frameCallback, ctx); } } From 42ed634ad0777ae5c6704c39469df197e7535195 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 21 Apr 2026 19:18:58 -0400 Subject: [PATCH 35/52] Use C++ alias and constexpr in crash report headers Replace the remaining C-ish constructs in the in-proc crash report headers with C++ equivalents so the public surface matches the rest of the now-class-based implementation: - `typedef T (*Fn)(...)` function-pointer typedefs replaced with `using Fn = T (*)(...)` aliases. - `#define CRASH_JSON_BUFFER_SIZE (4 * 1024)` replaced with `static constexpr size_t CRASH_JSON_BUFFER_SIZE = 4 * 1024` so the buffer size is a typed constant rather than a macro. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/debug/crashreport/crashjsonwriter.h | 4 ++-- src/coreclr/debug/crashreport/inproccrashreporter.h | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/coreclr/debug/crashreport/crashjsonwriter.h b/src/coreclr/debug/crashreport/crashjsonwriter.h index e94889cb3df376..92c3b6debcaff0 100644 --- a/src/coreclr/debug/crashreport/crashjsonwriter.h +++ b/src/coreclr/debug/crashreport/crashjsonwriter.h @@ -10,10 +10,10 @@ #include -typedef bool (*CrashJsonOutputCallback)(const char* buffer, size_t len, void* ctx); +using CrashJsonOutputCallback = bool (*)(const char* buffer, size_t len, void* ctx); // Small streaming buffer used when serializing the crash report JSON. -#define CRASH_JSON_BUFFER_SIZE (4 * 1024) +static constexpr size_t CRASH_JSON_BUFFER_SIZE = 4 * 1024; class CrashJsonWriter { diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.h b/src/coreclr/debug/crashreport/inproccrashreporter.h index 0edf2b6d93317f..a0f8b41f14fa80 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.h +++ b/src/coreclr/debug/crashreport/inproccrashreporter.h @@ -17,11 +17,11 @@ void InProcCrashReportInitialize(const char* dumpPath); // All arguments come from the signal handler and are signal-safe to read. void InProcCrashReportGenerate(int signal, siginfo_t* siginfo, void* context); -typedef bool (*InProcCrashReportIsManagedThreadCallback)(); +using InProcCrashReportIsManagedThreadCallback = bool (*)(); void InProcCrashReportSetCurrentThreadManagedResolver(InProcCrashReportIsManagedThreadCallback callback); -typedef void (*InProcCrashReportFrameCallback)( +using InProcCrashReportFrameCallback = void (*)( uint64_t ip, uint64_t stackPointer, const char* methodName, @@ -35,27 +35,27 @@ typedef void (*InProcCrashReportFrameCallback)( const char* moduleGuid, void* ctx); -typedef void (*InProcCrashReportWalkStackCallback)( +using InProcCrashReportWalkStackCallback = void (*)( InProcCrashReportFrameCallback frameCallback, void* ctx); void InProcCrashReportSetStackWalker(InProcCrashReportWalkStackCallback callback); -typedef bool (*InProcCrashReportGetExceptionCallback)( +using InProcCrashReportGetExceptionCallback = bool (*)( char* exceptionTypeBuf, size_t exceptionTypeBufSize, uint32_t* hresult); void InProcCrashReportSetExceptionResolver(InProcCrashReportGetExceptionCallback callback); -typedef void (*InProcCrashReportThreadCallback)( +using InProcCrashReportThreadCallback = void (*)( uint64_t osThreadId, bool isCrashThread, const char* exceptionType, uint32_t exceptionHResult, void* ctx); -typedef void (*InProcCrashReportEnumerateThreadsCallback)( +using InProcCrashReportEnumerateThreadsCallback = void (*)( uint64_t crashingTid, InProcCrashReportThreadCallback threadCallback, InProcCrashReportFrameCallback frameCallback, From 31ebacacee5c714b3350a557f6a9ea354577d5f8 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 22 Apr 2026 14:22:15 -0400 Subject: [PATCH 36/52] Rename in-proc crash report writer and scrub orphan comments Adopt reviewer-preferred names for the crash report internals and tighten comments that no longer match the shape of the code: - Rename CrashJsonWriter to SignalSafeJsonWriter (and the matching crashjsonwriter.{h,cpp} files to signalsafejsonwriter.{h,cpp}) so the name documents the async-signal-safety contract the class provides. Add a class-level comment spelling out that contract. - Rename HasFailed to HasError so the method name describes the state being queried rather than past behavior. - Rename MultiThreadJsonContext to ThreadEnumerationContext so the helper's role in the per-thread enumeration is immediately obvious. - Rename WriteAllToFile to WriteToFile; "All" was redundant given the function already loops until everything is written or EINTR/EAGAIN resolve. - Tag every CloseObject/CloseArray call with a trailing // scope_name comment so readers can match closes to their opens at a glance (payload, configuration, threads, stack_frames, thread, parameters, ctx, frame, root). - Remove two orphan comments that described earlier branch/slice language rather than the code itself. No behavior changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/debug/crashreport/CMakeLists.txt | 2 +- .../debug/crashreport/inproccrashreporter.cpp | 90 +++++++++---------- ...sonwriter.cpp => signalsafejsonwriter.cpp} | 28 +++--- ...ashjsonwriter.h => signalsafejsonwriter.h} | 13 +-- src/coreclr/pal/src/exception/signal.cpp | 1 + 5 files changed, 66 insertions(+), 68 deletions(-) rename src/coreclr/debug/crashreport/{crashjsonwriter.cpp => signalsafejsonwriter.cpp} (89%) rename src/coreclr/debug/crashreport/{crashjsonwriter.h => signalsafejsonwriter.h} (80%) diff --git a/src/coreclr/debug/crashreport/CMakeLists.txt b/src/coreclr/debug/crashreport/CMakeLists.txt index b917b6ebe3ed9e..9a13917fa0adf1 100644 --- a/src/coreclr/debug/crashreport/CMakeLists.txt +++ b/src/coreclr/debug/crashreport/CMakeLists.txt @@ -1,7 +1,7 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CRASHREPORT_SOURCES - crashjsonwriter.cpp + signalsafejsonwriter.cpp inproccrashreporter.cpp ) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index 0dc62276797190..707b962bf98104 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -6,7 +6,7 @@ // Streams a createdump-shaped JSON skeleton to a crashreport.json file. #include "inproccrashreporter.h" -#include "crashjsonwriter.h" +#include "signalsafejsonwriter.h" #include #include @@ -23,21 +23,18 @@ static char sccsid[] = "@(#)Version N/A"; #endif -static CrashJsonWriter s_jsonWriter; -// These callbacks are published during runtime startup and then only read from -// the crash path; this minimal branch intentionally reuses the existing VM -// inspection hooks before the later strict-safety hardening slices. +static SignalSafeJsonWriter s_jsonWriter; static volatile InProcCrashReportIsManagedThreadCallback g_isManagedThreadCallback = nullptr; static volatile InProcCrashReportWalkStackCallback g_walkStackCallback = nullptr; static volatile InProcCrashReportGetExceptionCallback g_getExceptionCallback = nullptr; static volatile InProcCrashReportEnumerateThreadsCallback g_enumerateThreadsCallback = nullptr; static char g_reportPath[256]; -class MultiThreadJsonContext +class ThreadEnumerationContext { public: - MultiThreadJsonContext( - CrashJsonWriter* writer, + ThreadEnumerationContext( + SignalSafeJsonWriter* writer, void* signalContext, bool hasCrashException, const char* crashExceptionType, @@ -52,12 +49,12 @@ class MultiThreadJsonContext { } - MultiThreadJsonContext(const MultiThreadJsonContext&) = delete; - MultiThreadJsonContext& operator=(const MultiThreadJsonContext&) = delete; + ThreadEnumerationContext(const ThreadEnumerationContext&) = delete; + ThreadEnumerationContext& operator=(const ThreadEnumerationContext&) = delete; size_t ThreadCount() const { return m_threadCount; } bool SawCrashThread() const { return m_sawCrashThread; } - CrashJsonWriter* Writer() const { return m_writer; } + SignalSafeJsonWriter* Writer() const { return m_writer; } void OnThread( uint64_t osThreadId, @@ -100,7 +97,7 @@ class MultiThreadJsonContext void* ctx); private: - CrashJsonWriter* m_writer; + SignalSafeJsonWriter* m_writer; void* m_signalContext; size_t m_threadCount; bool m_sawCrashThread; @@ -149,7 +146,7 @@ FormatHexValue( static void WriteRegistersToJson( - CrashJsonWriter* writer, + SignalSafeJsonWriter* writer, void* context); static @@ -165,7 +162,7 @@ GetStackPointer( static void WriteCrashSiteFrameToJson( - CrashJsonWriter* writer, + SignalSafeJsonWriter* writer, void* context); static @@ -211,7 +208,7 @@ JsonFrameCallback( void* ctx); bool -WriteAllToFile( +WriteToFile( int fd, const char* buffer, size_t len); @@ -280,7 +277,7 @@ InProcCrashReportGenerate( char version[sizeof(sccsid) + 1]; GetVersionString(version, sizeof(version)); s_jsonWriter.WriteString("version", version); - s_jsonWriter.CloseObject(); + s_jsonWriter.CloseObject(); // configuration char processName[256]; if (TryGetProcessName(processName, sizeof(processName))) @@ -295,17 +292,17 @@ InProcCrashReportGenerate( s_jsonWriter.OpenArray("threads"); if (g_enumerateThreadsCallback != nullptr) { - MultiThreadJsonContext threadContext(&s_jsonWriter, context, hasException, exTypeBuf, exHresult); + ThreadEnumerationContext threadContext(&s_jsonWriter, context, hasException, exTypeBuf, exHresult); uint64_t crashingTid = static_cast(minipal_get_current_thread_id()); - g_enumerateThreadsCallback(crashingTid, &MultiThreadJsonContext::ThreadCallback, &MultiThreadJsonContext::FrameCallback, &threadContext); + g_enumerateThreadsCallback(crashingTid, &ThreadEnumerationContext::ThreadCallback, &ThreadEnumerationContext::FrameCallback, &threadContext); if (threadContext.ThreadCount() > 0) { // Close the last thread's stack_frames + object opened by the // enumeration callback. - s_jsonWriter.CloseArray(); - s_jsonWriter.CloseObject(); + s_jsonWriter.CloseArray(); // stack_frames + s_jsonWriter.CloseObject(); // thread // Flush the final thread so it reaches the crash report file // even if any later work (e.g. synthesizing a crash thread @@ -336,8 +333,8 @@ InProcCrashReportGenerate( WriteRegistersToJson(&s_jsonWriter, context); s_jsonWriter.OpenArray("stack_frames"); WriteCrashSiteFrameToJson(&s_jsonWriter, context); - s_jsonWriter.CloseArray(); - s_jsonWriter.CloseObject(); + s_jsonWriter.CloseArray(); // stack_frames + s_jsonWriter.CloseObject(); // thread } } else @@ -369,12 +366,12 @@ InProcCrashReportGenerate( { g_walkStackCallback(JsonFrameCallback, &s_jsonWriter); } - s_jsonWriter.CloseArray(); - s_jsonWriter.CloseObject(); + s_jsonWriter.CloseArray(); // stack_frames + s_jsonWriter.CloseObject(); // thread } - s_jsonWriter.CloseArray(); + s_jsonWriter.CloseArray(); // threads - s_jsonWriter.CloseObject(); + s_jsonWriter.CloseObject(); // payload s_jsonWriter.OpenObject("parameters"); char signalBuf[16]; @@ -385,16 +382,16 @@ InProcCrashReportGenerate( s_jsonWriter.WriteString("SystemModel", ""); s_jsonWriter.WriteString("SystemManufacturer", "apple"); #endif - s_jsonWriter.CloseObject(); + s_jsonWriter.CloseObject(); // parameters - s_jsonWriter.CloseObject(); + s_jsonWriter.CloseObject(); // root s_jsonWriter.Finish(); if (fd != -1) { - bool writeSucceeded = !s_jsonWriter.HasFailed() && + bool writeSucceeded = !s_jsonWriter.HasError() && !outputContext.WriteFailed() && - WriteAllToFile(fd, "\n", 1); + WriteToFile(fd, "\n", 1); if (close(fd) != 0 || !writeSucceeded) { @@ -440,7 +437,7 @@ InProcCrashReportSetThreadEnumerator( } bool -WriteAllToFile( +WriteToFile( int fd, const char* buffer, size_t len) @@ -476,7 +473,7 @@ CrashReportOutputContext::HandleChunk( return false; } - if (!WriteAllToFile(m_fd, buffer, len)) + if (!WriteToFile(m_fd, buffer, len)) { m_writeFailed = true; return false; @@ -670,10 +667,9 @@ FormatHexValue( void WriteRegistersToJson( - CrashJsonWriter* writer, + SignalSafeJsonWriter* writer, void* context) { - // Only the crashing thread has a reliable signal context in this slice. uint64_t ipValue = GetInstructionPointer(context); uint64_t spValue = GetStackPointer(context); char ip[32] = "0x0"; @@ -699,7 +695,7 @@ WriteRegistersToJson( writer->WriteString("IP", ip); writer->WriteString("SP", sp); writer->WriteString("BP", bp); - writer->CloseObject(); + writer->CloseObject(); // ctx } uint64_t @@ -746,7 +742,7 @@ GetStackPointer( void WriteCrashSiteFrameToJson( - CrashJsonWriter* writer, + SignalSafeJsonWriter* writer, void* context) { uint64_t ipValue = GetInstructionPointer(context); @@ -761,7 +757,7 @@ WriteCrashSiteFrameToJson( writer->WriteString("is_managed", "false"); writer->WriteString("stack_pointer", sp); writer->WriteString("native_address", ip); - writer->CloseObject(); + writer->CloseObject(); // frame } void @@ -893,7 +889,7 @@ JsonFrameCallback( const char* moduleGuid, void* ctx) { - CrashJsonWriter* writer = reinterpret_cast(ctx); + SignalSafeJsonWriter* writer = reinterpret_cast(ctx); char ipBuffer[32]; char stackPointerBuffer[32]; char nativeOffsetBuffer[32]; @@ -949,11 +945,11 @@ JsonFrameCallback( } } - writer->CloseObject(); + writer->CloseObject(); // frame } void -MultiThreadJsonContext::OnFrame( +ThreadEnumerationContext::OnFrame( uint64_t ip, uint64_t stackPointer, const char* methodName, @@ -970,7 +966,7 @@ MultiThreadJsonContext::OnFrame( } void -MultiThreadJsonContext::FrameCallback( +ThreadEnumerationContext::FrameCallback( uint64_t ip, uint64_t stackPointer, const char* methodName, @@ -984,11 +980,11 @@ MultiThreadJsonContext::FrameCallback( const char* moduleGuid, void* ctx) { - reinterpret_cast(ctx)->OnFrame(ip, stackPointer, methodName, className, moduleName, nativeOffset, token, ilOffset, moduleTimestamp, moduleSize, moduleGuid); + reinterpret_cast(ctx)->OnFrame(ip, stackPointer, methodName, className, moduleName, nativeOffset, token, ilOffset, moduleTimestamp, moduleSize, moduleGuid); } void -MultiThreadJsonContext::OnThread( +ThreadEnumerationContext::OnThread( uint64_t osThreadId, bool isCrashThread, const char* exceptionType, @@ -996,8 +992,8 @@ MultiThreadJsonContext::OnThread( { if (m_threadCount > 0) { - m_writer->CloseArray(); - m_writer->CloseObject(); + m_writer->CloseArray(); // stack_frames + m_writer->CloseObject(); // thread (void)m_writer->Flush(); } @@ -1046,12 +1042,12 @@ MultiThreadJsonContext::OnThread( } void -MultiThreadJsonContext::ThreadCallback( +ThreadEnumerationContext::ThreadCallback( uint64_t osThreadId, bool isCrashThread, const char* exceptionType, uint32_t exceptionHResult, void* ctx) { - reinterpret_cast(ctx)->OnThread(osThreadId, isCrashThread, exceptionType, exceptionHResult); + reinterpret_cast(ctx)->OnThread(osThreadId, isCrashThread, exceptionType, exceptionHResult); } diff --git a/src/coreclr/debug/crashreport/crashjsonwriter.cpp b/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp similarity index 89% rename from src/coreclr/debug/crashreport/crashjsonwriter.cpp rename to src/coreclr/debug/crashreport/signalsafejsonwriter.cpp index a944a8aea2f24d..3fb64c9e5de352 100644 --- a/src/coreclr/debug/crashreport/crashjsonwriter.cpp +++ b/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp @@ -3,7 +3,7 @@ // Streaming JSON writer implementation for crash reports. -#include "crashjsonwriter.h" +#include "signalsafejsonwriter.h" #include @@ -13,7 +13,7 @@ ToHexChar( unsigned value); void -CrashJsonWriter::Init( +SignalSafeJsonWriter::Init( CrashJsonOutputCallback outputCallback, void* outputContext) { @@ -26,7 +26,7 @@ CrashJsonWriter::Init( } void -CrashJsonWriter::OpenObject( +SignalSafeJsonWriter::OpenObject( const char* key) { WriteSeparator(); @@ -40,14 +40,14 @@ CrashJsonWriter::OpenObject( } void -CrashJsonWriter::CloseObject() +SignalSafeJsonWriter::CloseObject() { AppendStr("}"); m_commaNeeded = true; } void -CrashJsonWriter::OpenArray( +SignalSafeJsonWriter::OpenArray( const char* key) { WriteSeparator(); @@ -61,14 +61,14 @@ CrashJsonWriter::OpenArray( } void -CrashJsonWriter::CloseArray() +SignalSafeJsonWriter::CloseArray() { AppendStr("]"); m_commaNeeded = true; } void -CrashJsonWriter::WriteString( +SignalSafeJsonWriter::WriteString( const char* key, const char* value) { @@ -79,19 +79,19 @@ CrashJsonWriter::WriteString( } void -CrashJsonWriter::Finish() +SignalSafeJsonWriter::Finish() { (void)Flush(); } bool -CrashJsonWriter::HasFailed() const +SignalSafeJsonWriter::HasError() const { return m_writeFailed; } bool -CrashJsonWriter::Flush() +SignalSafeJsonWriter::Flush() { if (m_writeFailed) { @@ -115,7 +115,7 @@ CrashJsonWriter::Flush() } bool -CrashJsonWriter::Append( +SignalSafeJsonWriter::Append( const char* str, size_t len) { @@ -163,7 +163,7 @@ CrashJsonWriter::Append( } bool -CrashJsonWriter::AppendStr( +SignalSafeJsonWriter::AppendStr( const char* str) { if (str == nullptr) @@ -183,7 +183,7 @@ ToHexChar( } void -CrashJsonWriter::WriteSeparator() +SignalSafeJsonWriter::WriteSeparator() { if (m_commaNeeded) AppendStr(","); @@ -193,7 +193,7 @@ CrashJsonWriter::WriteSeparator() // Escape a string value for JSON. Handles \, ", and control characters. void -CrashJsonWriter::WriteEscapedString( +SignalSafeJsonWriter::WriteEscapedString( const char* str) { AppendStr("\""); diff --git a/src/coreclr/debug/crashreport/crashjsonwriter.h b/src/coreclr/debug/crashreport/signalsafejsonwriter.h similarity index 80% rename from src/coreclr/debug/crashreport/crashjsonwriter.h rename to src/coreclr/debug/crashreport/signalsafejsonwriter.h index 92c3b6debcaff0..9708d1ba20bbc2 100644 --- a/src/coreclr/debug/crashreport/crashjsonwriter.h +++ b/src/coreclr/debug/crashreport/signalsafejsonwriter.h @@ -4,7 +4,8 @@ // Bounded JSON writer for crash reports. // Streams content through a small fixed-size buffer using bounded low-level // string and memory operations so file output does not require materializing -// the whole report at once. +// the whole report at once. All public members are async-signal-safe: no +// heap allocation, no stdio, no locale or variadic formatting. #pragma once @@ -15,10 +16,10 @@ using CrashJsonOutputCallback = bool (*)(const char* buffer, size_t len, void* c // Small streaming buffer used when serializing the crash report JSON. static constexpr size_t CRASH_JSON_BUFFER_SIZE = 4 * 1024; -class CrashJsonWriter +class SignalSafeJsonWriter { public: - CrashJsonWriter() + SignalSafeJsonWriter() : m_pos(0), m_commaNeeded(false), m_writeFailed(false), @@ -28,8 +29,8 @@ class CrashJsonWriter m_buffer[0] = '\0'; } - CrashJsonWriter(const CrashJsonWriter&) = delete; - CrashJsonWriter& operator=(const CrashJsonWriter&) = delete; + SignalSafeJsonWriter(const SignalSafeJsonWriter&) = delete; + SignalSafeJsonWriter& operator=(const SignalSafeJsonWriter&) = delete; void Init(CrashJsonOutputCallback outputCallback, void* outputContext); void OpenObject(const char* key); @@ -39,7 +40,7 @@ class CrashJsonWriter void WriteString(const char* key, const char* value); void Finish(); bool Flush(); - bool HasFailed() const; + bool HasError() const; private: bool Append(const char* str, size_t len); diff --git a/src/coreclr/pal/src/exception/signal.cpp b/src/coreclr/pal/src/exception/signal.cpp index a15a76618fabf2..c27b6e6f92006b 100644 --- a/src/coreclr/pal/src/exception/signal.cpp +++ b/src/coreclr/pal/src/exception/signal.cpp @@ -471,6 +471,7 @@ static void invoke_previous_action(struct sigaction* action, int code, siginfo_t if (g_crash_report_before_signal_chaining) { PROCNotifyProcessShutdown(IsRunningOnAlternateStack(context)); + PROCLogManagedCallstackForSignal(code); PROCCreateCrashDumpIfEnabled(code, siginfo, context, true); } From 4d1d2e3f5572ffeceb7ae2a5e64eecaa6af80085 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 22 Apr 2026 14:38:52 -0400 Subject: [PATCH 37/52] Introduce FEATURE_INPROC_CRASHREPORT build flag Replace the hardcoded CLR_CMAKE_TARGET_ANDROID gate around the in-proc crash reporter code (debug/crashreport, crashreportstackwalker, and the PAL signal dispatcher hook in process.cpp) with a new FEATURE_INPROC_CRASHREPORT build flag. The flag defaults to 1 on Android and 0 elsewhere, keeping today's Android-only behavior while making it straightforward to extend the feature to iOS, macOS, and Linux later without further gating churn. The feature variable and its compile definition are set before add_subdirectory(pal) so the PAL translation units (process.cpp) see the define alongside the VM and debug/crashreport subdirectories. The logcat-prelude plumbing (PAL_SetLogManagedCallstackForSignalCallback and EEPolicy::LogManagedCallstackForSignal) stays under HOST_ANDROID since it is Android-specific platform behavior independent of the in-proc crash reporter; only CrashReportRegisterStackWalker is gated on FEATURE_INPROC_CRASHREPORT. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/CMakeLists.txt | 7 +++++++ src/coreclr/clrfeatures.cmake | 8 ++++++++ src/coreclr/debug/CMakeLists.txt | 2 +- src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt | 2 +- src/coreclr/pal/src/CMakeLists.txt | 2 +- src/coreclr/pal/src/include/pal/process.h | 2 +- src/coreclr/pal/src/thread/process.cpp | 6 +++--- src/coreclr/vm/CMakeLists.txt | 7 ++++++- src/coreclr/vm/ceemain.cpp | 10 ++++++++-- src/coreclr/vm/crashreportstackwalker.cpp | 4 ++-- src/coreclr/vm/crashreportstackwalker.h | 4 ++-- 11 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/coreclr/CMakeLists.txt b/src/coreclr/CMakeLists.txt index 3806f9319c27f7..0f74561d551240 100644 --- a/src/coreclr/CMakeLists.txt +++ b/src/coreclr/CMakeLists.txt @@ -109,6 +109,13 @@ if(CLR_CMAKE_HOST_UNIX) add_linker_flag(-Wl,-z,notext) endif() + # PAL consumes FEATURE_INPROC_CRASHREPORT, so the feature variable must be + # populated and the compile definition added before PAL is processed. + include(clrfeatures.cmake) + if(FEATURE_INPROC_CRASHREPORT) + add_compile_definitions(FEATURE_INPROC_CRASHREPORT) + endif() + add_subdirectory(pal) else() if(CLR_CMAKE_TARGET_UNIX) diff --git a/src/coreclr/clrfeatures.cmake b/src/coreclr/clrfeatures.cmake index f8e32929136086..dbf3747a2e478f 100644 --- a/src/coreclr/clrfeatures.cmake +++ b/src/coreclr/clrfeatures.cmake @@ -67,6 +67,14 @@ if(NOT DEFINED FEATURE_SINGLE_FILE_DIAGNOSTICS) set(FEATURE_SINGLE_FILE_DIAGNOSTICS 1) endif(NOT DEFINED FEATURE_SINGLE_FILE_DIAGNOSTICS) +if(NOT DEFINED FEATURE_INPROC_CRASHREPORT) + if(CLR_CMAKE_TARGET_ANDROID) + set(FEATURE_INPROC_CRASHREPORT 1) + else() + set(FEATURE_INPROC_CRASHREPORT 0) + endif() +endif(NOT DEFINED FEATURE_INPROC_CRASHREPORT) + if ((CLR_CMAKE_TARGET_WIN32 OR CLR_CMAKE_TARGET_UNIX) AND NOT CLR_CMAKE_TARGET_ARCH_WASM) set(FEATURE_COMWRAPPERS 1) endif() diff --git a/src/coreclr/debug/CMakeLists.txt b/src/coreclr/debug/CMakeLists.txt index c8fb2407550d90..acfd2fa7fe7c23 100644 --- a/src/coreclr/debug/CMakeLists.txt +++ b/src/coreclr/debug/CMakeLists.txt @@ -7,7 +7,7 @@ include_directories(${RUNTIME_DIR}) add_subdirectory(daccess) add_subdirectory(ee) add_subdirectory(di) -if(CLR_CMAKE_TARGET_ANDROID) +if(FEATURE_INPROC_CRASHREPORT) add_subdirectory(crashreport) endif() if(CLR_CMAKE_HOST_WIN32) diff --git a/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt b/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt index 84db354cfb949f..c2494a0ec246c8 100644 --- a/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt +++ b/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt @@ -183,7 +183,7 @@ endif() target_link_libraries(coreclr_static PUBLIC ${CORECLR_LIBRARIES} ${CORECLR_STATIC_CLRJIT_STATIC} ${CORECLR_STATIC_CLRINTERPRETER_STATIC} cee_wks_core ${CEE_WKS_STATIC} ${FOUNDATION}) target_compile_definitions(coreclr_static PUBLIC CORECLR_EMBEDDED) -if(TARGET inproccrashreport) +if(FEATURE_INPROC_CRASHREPORT) target_sources(coreclr_static PRIVATE $) endif() diff --git a/src/coreclr/pal/src/CMakeLists.txt b/src/coreclr/pal/src/CMakeLists.txt index f5f83c9dbe4206..733e42b0c8b819 100644 --- a/src/coreclr/pal/src/CMakeLists.txt +++ b/src/coreclr/pal/src/CMakeLists.txt @@ -228,7 +228,7 @@ add_library(coreclrpal_objects add_library(coreclrpal STATIC $ - $<$:$> + $<$:$> ${LIBUNWIND_OBJECTS} ) diff --git a/src/coreclr/pal/src/include/pal/process.h b/src/coreclr/pal/src/include/pal/process.h index 6c2f3c1077cc04..65d3cab030fa1c 100644 --- a/src/coreclr/pal/src/include/pal/process.h +++ b/src/coreclr/pal/src/include/pal/process.h @@ -172,7 +172,7 @@ VOID PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, void* context, --*/ VOID PROCLogManagedCallstackForSignal(int signal); -#ifdef HOST_ANDROID +#ifdef FEATURE_INPROC_CRASHREPORT void PROCEnableInProcCrashReport(); #endif diff --git a/src/coreclr/pal/src/thread/process.cpp b/src/coreclr/pal/src/thread/process.cpp index af6269fe0bdbd1..733e0dec5c648c 100644 --- a/src/coreclr/pal/src/thread/process.cpp +++ b/src/coreclr/pal/src/thread/process.cpp @@ -35,7 +35,7 @@ SET_DEFAULT_DEBUG_CHANNEL(PROCESS); // some headers have code with asserts, so d #include #include -#ifdef HOST_ANDROID +#ifdef FEATURE_INPROC_CRASHREPORT #include "debug/crashreport/inproccrashreporter.h" #endif @@ -194,7 +194,7 @@ Volatile g_logManagedCallstackForSignalC #define MAX_ARGV_ENTRIES 32 const char* g_argvCreateDump[MAX_ARGV_ENTRIES] = { nullptr }; -#ifdef HOST_ANDROID +#ifdef FEATURE_INPROC_CRASHREPORT // Read from the fatal-signal path (PROCCreateCrashDumpIfEnabled) and written // once during startup (PROCEnableInProcCrashReport); use Volatile to // match the signal-path publication of g_logManagedCallstackForSignalCallback. @@ -2797,7 +2797,7 @@ PROCLogManagedCallstackForSignal(int signal) (no return value) --*/ -#ifdef HOST_ANDROID +#ifdef FEATURE_INPROC_CRASHREPORT #include void PROCEnableInProcCrashReport() diff --git a/src/coreclr/vm/CMakeLists.txt b/src/coreclr/vm/CMakeLists.txt index 9662b1f5a8df3f..606724e792a503 100644 --- a/src/coreclr/vm/CMakeLists.txt +++ b/src/coreclr/vm/CMakeLists.txt @@ -313,7 +313,6 @@ set(VM_SOURCES_WKS comutilnative.cpp coreassemblyspec.cpp corelib.cpp # true - crashreportstackwalker.cpp customattribute.cpp custommarshalerinfo.cpp autotrace.cpp @@ -565,6 +564,12 @@ if(FEATURE_OBJCMARSHAL) ) endif(FEATURE_OBJCMARSHAL) +if(FEATURE_INPROC_CRASHREPORT) + list(APPEND VM_SOURCES_WKS + crashreportstackwalker.cpp + ) +endif(FEATURE_INPROC_CRASHREPORT) + list(APPEND VM_SOURCES_WKS interoplibinterface_java.cpp ) diff --git a/src/coreclr/vm/ceemain.cpp b/src/coreclr/vm/ceemain.cpp index 6b36a675474f91..206965cea9bcb1 100644 --- a/src/coreclr/vm/ceemain.cpp +++ b/src/coreclr/vm/ceemain.cpp @@ -116,7 +116,6 @@ // boxing this describes this feature. #include "common.h" -#include "crashreportstackwalker.h" #include "vars.hpp" #include "log.h" @@ -207,6 +206,10 @@ #include "gdbjit.h" #endif // FEATURE_GDBJIT +#ifdef FEATURE_INPROC_CRASHREPORT +#include "crashreportstackwalker.h" +#endif // FEATURE_INPROC_CRASHREPORT + #include "genanalysis.h" HRESULT EEStartup(); @@ -697,9 +700,12 @@ void EEStartupHelper() #ifdef HOST_ANDROID PAL_SetLogManagedCallstackForSignalCallback(EEPolicy::LogManagedCallstackForSignal); - CrashReportRegisterStackWalker(); #endif // HOST_ANDROID +#ifdef FEATURE_INPROC_CRASHREPORT + CrashReportRegisterStackWalker(); +#endif // FEATURE_INPROC_CRASHREPORT + #ifdef STRESS_LOG if (CLRConfig::GetConfigValue(CLRConfig::UNSUPPORTED_StressLog, g_pConfig->StressLog()) != 0) { unsigned facilities = CLRConfig::GetConfigValue(CLRConfig::INTERNAL_LogFacility, LF_ALL); diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index 5d46a1acd8d3f8..d33ac1e2b75a36 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -11,7 +11,7 @@ #include #include -#ifdef HOST_ANDROID +#ifdef FEATURE_INPROC_CRASHREPORT #include "debug/crashreport/inproccrashreporter.h" #include "threadsuspend.h" @@ -444,4 +444,4 @@ CrashReportRegisterStackWalker() PROCEnableInProcCrashReport(); } -#endif // HOST_ANDROID +#endif // FEATURE_INPROC_CRASHREPORT diff --git a/src/coreclr/vm/crashreportstackwalker.h b/src/coreclr/vm/crashreportstackwalker.h index 0beaae6077e92c..45604dafd293df 100644 --- a/src/coreclr/vm/crashreportstackwalker.h +++ b/src/coreclr/vm/crashreportstackwalker.h @@ -4,10 +4,10 @@ #ifndef CRASHREPORTSTACKWALKER_H #define CRASHREPORTSTACKWALKER_H -#ifdef HOST_ANDROID +#ifdef FEATURE_INPROC_CRASHREPORT void CrashReportRegisterStackWalker(); -#endif // HOST_ANDROID +#endif // FEATURE_INPROC_CRASHREPORT #endif // CRASHREPORTSTACKWALKER_H From 4f751fd285237650dbfe3417ba147cae73ef923f Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 22 Apr 2026 14:50:14 -0400 Subject: [PATCH 38/52] Consolidate in-proc crash reporter initialization and crash entry Replace the two separate VM-facing entry points (InProcCrashReportInitialize called directly into the reporter, and PROCEnableInProcCrashReport exported from the PAL) with a single PAL entry point PROCInitializeInProcCrashReport that initializes the reporter and publishes the enabled flag. The VM now makes one call after registering its callbacks. Rename the signal-path entry from InProcCrashReportGenerate to CreateInProcCrashReport and extract ThreadEnumerationContext::EndEnumeration and EmitSynthesizedCrashThread helpers so the top-level flow in the signal handler stays short and the fallback-thread path is not duplicated. Cache the process name at initialization time (g_processName) instead of reading /proc/self/cmdline / /proc/self/exe from the signal handler, drop the now-redundant __sync_synchronize in the init path (the Volatile PAL flag provides the publication fence), and drop the dead #ifdef __APPLE__ parameters block (the feature is Android-only today). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../debug/crashreport/inproccrashreporter.cpp | 156 +++++++++--------- .../debug/crashreport/inproccrashreporter.h | 4 +- src/coreclr/pal/src/include/pal/process.h | 2 +- src/coreclr/pal/src/thread/process.cpp | 10 +- src/coreclr/vm/crashreportstackwalker.cpp | 11 +- 5 files changed, 96 insertions(+), 87 deletions(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index 707b962bf98104..672cfe3ad5d920 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -29,6 +29,7 @@ static volatile InProcCrashReportWalkStackCallback g_walkStackCallback = nullptr static volatile InProcCrashReportGetExceptionCallback g_getExceptionCallback = nullptr; static volatile InProcCrashReportEnumerateThreadsCallback g_enumerateThreadsCallback = nullptr; static char g_reportPath[256]; +static char g_processName[256]; class ThreadEnumerationContext { @@ -56,6 +57,12 @@ class ThreadEnumerationContext bool SawCrashThread() const { return m_sawCrashThread; } SignalSafeJsonWriter* Writer() const { return m_writer; } + // Close the per-thread stack_frames + thread objects opened by OnThread + // for the final thread in the enumeration, and flush the writer so the + // thread list reaches the crash report file even if later work (e.g. + // synthesizing a fallback crash thread) hangs or faults. + void EndEnumeration(); + void OnThread( uint64_t osThreadId, bool isCrashThread, @@ -220,8 +227,17 @@ BuildReportPath( size_t bufferSize, const char* dumpPath); +static +void +EmitSynthesizedCrashThread( + void* context, + bool hasException, + const char* crashExceptionType, + uint32_t crashExceptionHResult, + bool walkStack); + void -InProcCrashReportGenerate( +CreateInProcCrashReport( int signal, siginfo_t* siginfo, void* context) @@ -279,10 +295,9 @@ InProcCrashReportGenerate( s_jsonWriter.WriteString("version", version); s_jsonWriter.CloseObject(); // configuration - char processName[256]; - if (TryGetProcessName(processName, sizeof(processName))) + if (g_processName[0] != '\0') { - s_jsonWriter.WriteString("process_name", processName); + s_jsonWriter.WriteString("process_name", g_processName); } char pidBuf[16]; @@ -297,77 +312,16 @@ InProcCrashReportGenerate( g_enumerateThreadsCallback(crashingTid, &ThreadEnumerationContext::ThreadCallback, &ThreadEnumerationContext::FrameCallback, &threadContext); - if (threadContext.ThreadCount() > 0) - { - // Close the last thread's stack_frames + object opened by the - // enumeration callback. - s_jsonWriter.CloseArray(); // stack_frames - s_jsonWriter.CloseObject(); // thread - - // Flush the final thread so it reaches the crash report file - // even if any later work (e.g. synthesizing a crash thread - // fallback) hangs or faults. - (void)s_jsonWriter.Flush(); - } + threadContext.EndEnumeration(); if (threadContext.ThreadCount() == 0 || !threadContext.SawCrashThread()) { - s_jsonWriter.OpenObject(nullptr); - s_jsonWriter.WriteString("is_managed", - g_isManagedThreadCallback != nullptr && g_isManagedThreadCallback() ? "true" : "false"); - s_jsonWriter.WriteString("crashed", "true"); - - char nativeThreadId[32]; - FormatHexValue(nativeThreadId, sizeof(nativeThreadId), crashingTid); - s_jsonWriter.WriteString("native_thread_id", nativeThreadId); - - if (hasException) - { - char hresultBuffer[32]; - FormatHexValue(hresultBuffer, sizeof(hresultBuffer), exHresult); - - s_jsonWriter.WriteString("managed_exception_type", exTypeBuf); - s_jsonWriter.WriteString("managed_exception_hresult", hresultBuffer); - } - - WriteRegistersToJson(&s_jsonWriter, context); - s_jsonWriter.OpenArray("stack_frames"); - WriteCrashSiteFrameToJson(&s_jsonWriter, context); - s_jsonWriter.CloseArray(); // stack_frames - s_jsonWriter.CloseObject(); // thread + EmitSynthesizedCrashThread(context, hasException, exTypeBuf, exHresult, /*walkStack*/ false); } } else { - uint64_t crashingTid = static_cast(minipal_get_current_thread_id()); - - s_jsonWriter.OpenObject(nullptr); - s_jsonWriter.WriteString("is_managed", - g_isManagedThreadCallback != nullptr && g_isManagedThreadCallback() ? "true" : "false"); - s_jsonWriter.WriteString("crashed", "true"); - - char nativeThreadId[32]; - FormatHexValue(nativeThreadId, sizeof(nativeThreadId), crashingTid); - s_jsonWriter.WriteString("native_thread_id", nativeThreadId); - - if (hasException) - { - char hresultBuffer[32]; - FormatHexValue(hresultBuffer, sizeof(hresultBuffer), exHresult); - - s_jsonWriter.WriteString("managed_exception_type", exTypeBuf); - s_jsonWriter.WriteString("managed_exception_hresult", hresultBuffer); - } - - WriteRegistersToJson(&s_jsonWriter, context); - s_jsonWriter.OpenArray("stack_frames"); - WriteCrashSiteFrameToJson(&s_jsonWriter, context); - if (g_walkStackCallback != nullptr) - { - g_walkStackCallback(JsonFrameCallback, &s_jsonWriter); - } - s_jsonWriter.CloseArray(); // stack_frames - s_jsonWriter.CloseObject(); // thread + EmitSynthesizedCrashThread(context, hasException, exTypeBuf, exHresult, /*walkStack*/ true); } s_jsonWriter.CloseArray(); // threads @@ -377,11 +331,6 @@ InProcCrashReportGenerate( char signalBuf[16]; (void)snprintf(signalBuf, sizeof(signalBuf), "%d", signal); s_jsonWriter.WriteString("signal", signalBuf); -#ifdef __APPLE__ - s_jsonWriter.WriteString("OSVersion", ""); - s_jsonWriter.WriteString("SystemModel", ""); - s_jsonWriter.WriteString("SystemManufacturer", "apple"); -#endif s_jsonWriter.CloseObject(); // parameters s_jsonWriter.CloseObject(); // root @@ -401,11 +350,11 @@ InProcCrashReportGenerate( } void -InProcCrashReportInitialize( +InitializeInProcCrashReport( const char* dumpPath) { CopyString(g_reportPath, sizeof(g_reportPath), dumpPath); - __sync_synchronize(); + (void)TryGetProcessName(g_processName, sizeof(g_processName)); } void @@ -1051,3 +1000,60 @@ ThreadEnumerationContext::ThreadCallback( { reinterpret_cast(ctx)->OnThread(osThreadId, isCrashThread, exceptionType, exceptionHResult); } + +void +ThreadEnumerationContext::EndEnumeration() +{ + if (m_threadCount == 0) + { + return; + } + + // Close the last thread's stack_frames + thread objects opened by OnThread. + m_writer->CloseArray(); // stack_frames + m_writer->CloseObject(); // thread + + // Flush the final thread so it reaches the crash report file even if any + // later work (e.g. synthesizing a crash thread fallback) hangs or faults. + (void)m_writer->Flush(); +} + +static +void +EmitSynthesizedCrashThread( + void* context, + bool hasException, + const char* crashExceptionType, + uint32_t crashExceptionHResult, + bool walkStack) +{ + uint64_t crashingTid = static_cast(minipal_get_current_thread_id()); + + s_jsonWriter.OpenObject(nullptr); + s_jsonWriter.WriteString("is_managed", + g_isManagedThreadCallback != nullptr && g_isManagedThreadCallback() ? "true" : "false"); + s_jsonWriter.WriteString("crashed", "true"); + + char nativeThreadId[32]; + FormatHexValue(nativeThreadId, sizeof(nativeThreadId), crashingTid); + s_jsonWriter.WriteString("native_thread_id", nativeThreadId); + + if (hasException) + { + char hresultBuffer[32]; + FormatHexValue(hresultBuffer, sizeof(hresultBuffer), crashExceptionHResult); + + s_jsonWriter.WriteString("managed_exception_type", crashExceptionType); + s_jsonWriter.WriteString("managed_exception_hresult", hresultBuffer); + } + + WriteRegistersToJson(&s_jsonWriter, context); + s_jsonWriter.OpenArray("stack_frames"); + WriteCrashSiteFrameToJson(&s_jsonWriter, context); + if (walkStack && g_walkStackCallback != nullptr) + { + g_walkStackCallback(JsonFrameCallback, &s_jsonWriter); + } + s_jsonWriter.CloseArray(); // stack_frames + s_jsonWriter.CloseObject(); // thread +} diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.h b/src/coreclr/debug/crashreport/inproccrashreporter.h index a0f8b41f14fa80..ff4ef3cbf2baaa 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.h +++ b/src/coreclr/debug/crashreport/inproccrashreporter.h @@ -11,11 +11,11 @@ #include #include -void InProcCrashReportInitialize(const char* dumpPath); +void InitializeInProcCrashReport(const char* dumpPath); // Generate an in-proc crash report. Called from PROCCreateCrashDumpIfEnabled. // All arguments come from the signal handler and are signal-safe to read. -void InProcCrashReportGenerate(int signal, siginfo_t* siginfo, void* context); +void CreateInProcCrashReport(int signal, siginfo_t* siginfo, void* context); using InProcCrashReportIsManagedThreadCallback = bool (*)(); diff --git a/src/coreclr/pal/src/include/pal/process.h b/src/coreclr/pal/src/include/pal/process.h index 65d3cab030fa1c..c407d55c0fbf5c 100644 --- a/src/coreclr/pal/src/include/pal/process.h +++ b/src/coreclr/pal/src/include/pal/process.h @@ -173,7 +173,7 @@ VOID PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, void* context, VOID PROCLogManagedCallstackForSignal(int signal); #ifdef FEATURE_INPROC_CRASHREPORT -void PROCEnableInProcCrashReport(); +void PROCInitializeInProcCrashReport(const char* dumpPath); #endif #ifdef __cplusplus diff --git a/src/coreclr/pal/src/thread/process.cpp b/src/coreclr/pal/src/thread/process.cpp index 733e0dec5c648c..c45c42a952dc9e 100644 --- a/src/coreclr/pal/src/thread/process.cpp +++ b/src/coreclr/pal/src/thread/process.cpp @@ -196,7 +196,7 @@ const char* g_argvCreateDump[MAX_ARGV_ENTRIES] = { nullptr }; #ifdef FEATURE_INPROC_CRASHREPORT // Read from the fatal-signal path (PROCCreateCrashDumpIfEnabled) and written -// once during startup (PROCEnableInProcCrashReport); use Volatile to +// once during startup (PROCInitializeInProcCrashReport); use Volatile to // match the signal-path publication of g_logManagedCallstackForSignalCallback. static Volatile g_inProcCrashReportEnabled = false; #endif @@ -2800,8 +2800,12 @@ PROCLogManagedCallstackForSignal(int signal) #ifdef FEATURE_INPROC_CRASHREPORT #include void -PROCEnableInProcCrashReport() +PROCInitializeInProcCrashReport(const char* dumpPath) { + InitializeInProcCrashReport(dumpPath); + + // Publish last so PROCCreateCrashDumpIfEnabled only observes the reporter + // as enabled after the crashreport path (and any other state) is set. g_inProcCrashReportEnabled = true; } @@ -2814,7 +2818,7 @@ PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, void* context, bool // TODO: Dump stress log into logcat and/or file when enabled? if (g_inProcCrashReportEnabled) { - InProcCrashReportGenerate(signal, siginfo, context); + CreateInProcCrashReport(signal, siginfo, context); } minipal_log_write_fatal("Aborting process.\n"); } diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index d33ac1e2b75a36..8f4fc86a547a53 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -17,7 +17,7 @@ #include "threadsuspend.h" #include "gcenv.h" -extern "C" void PROCEnableInProcCrashReport(); +extern "C" void PROCInitializeInProcCrashReport(const char* dumpPath); struct WalkContext { @@ -432,16 +432,15 @@ CrashReportRegisterStackWalker() dumpName = dumpPathBuf; } - InProcCrashReportInitialize(dumpName); - InProcCrashReportSetCurrentThreadManagedResolver(CrashReportIsCurrentThreadManaged); InProcCrashReportSetStackWalker(CrashReportWalkStack); InProcCrashReportSetExceptionResolver(CrashReportGetException); InProcCrashReportSetThreadEnumerator(CrashReportEnumerateThreads); - // Enable the PAL flag last so PROCCreateCrashDumpIfEnabled only observes - // the reporter as enabled after all VM callbacks are registered. - PROCEnableInProcCrashReport(); + // Initialize and enable the PAL side last so PROCCreateCrashDumpIfEnabled + // only observes the reporter as enabled after all VM callbacks are + // registered. + PROCInitializeInProcCrashReport(dumpName); } #endif // FEATURE_INPROC_CRASHREPORT From 99aaf825af1a8eda9837c0fca14b195a678f1e37 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 22 Apr 2026 14:54:09 -0400 Subject: [PATCH 39/52] Polish SignalSafeJsonWriter API and buffer handling Refine the async-signal-safe JSON writer surface and internals: - Add keyless OpenObject()/OpenArray() overloads so callers writing array elements no longer pass OpenObject(nullptr). - Add a private AppendChar(char) helper and use it in place of AppendStr for single-character punctuation ('{', '}', '[', ']', ',', '"') to avoid the strlen/memcpy overhead of the string-based path. - Change Finish() to return bool so callers can fold the success check into their existing write-success predicate; inproccrashreporter now uses s_jsonWriter.Finish() directly instead of a separate HasError() call. - Drop the null-terminator invariant on the buffer: the buffer is consumed purely as a (ptr, len) chunk, so reserving and writing the trailing '\0' is unnecessary. Simplifies buffer math from CRASH_JSON_BUFFER_SIZE - 1 to CRASH_JSON_BUFFER_SIZE, and hoists the 'remaining' computation. - Remove the now-unused HasError() accessor. - Move ToHexChar to the top of the translation unit to match the standard file-static helper placement. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../debug/crashreport/inproccrashreporter.cpp | 13 ++- .../crashreport/signalsafejsonwriter.cpp | 80 +++++++++++-------- .../debug/crashreport/signalsafejsonwriter.h | 7 +- 3 files changed, 57 insertions(+), 43 deletions(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index 672cfe3ad5d920..c037afeeefd83c 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -278,7 +278,7 @@ CreateInProcCrashReport( s_jsonWriter.Init(&CrashReportOutputContext::ChunkCallback, &outputContext); - s_jsonWriter.OpenObject(nullptr); + s_jsonWriter.OpenObject(); s_jsonWriter.OpenObject("payload"); s_jsonWriter.WriteString("protocol_version", "1.0.0"); @@ -334,11 +334,10 @@ CreateInProcCrashReport( s_jsonWriter.CloseObject(); // parameters s_jsonWriter.CloseObject(); // root - s_jsonWriter.Finish(); if (fd != -1) { - bool writeSucceeded = !s_jsonWriter.HasError() && + bool writeSucceeded = s_jsonWriter.Finish() && !outputContext.WriteFailed() && WriteToFile(fd, "\n", 1); @@ -702,7 +701,7 @@ WriteCrashSiteFrameToJson( FormatHexValue(ip, sizeof(ip), ipValue); FormatHexValue(sp, sizeof(sp), spValue); - writer->OpenObject(nullptr); + writer->OpenObject(); writer->WriteString("is_managed", "false"); writer->WriteString("stack_pointer", sp); writer->WriteString("native_address", ip); @@ -855,7 +854,7 @@ JsonFrameCallback( FormatHexValue(moduleTimestampBuffer, sizeof(moduleTimestampBuffer), moduleTimestamp); FormatHexValue(moduleSizeBuffer, sizeof(moduleSizeBuffer), moduleSize); - writer->OpenObject(nullptr); + writer->OpenObject(); writer->WriteString("stack_pointer", stackPointerBuffer); writer->WriteString("native_address", ipBuffer); writer->WriteString("native_offset", nativeOffsetBuffer); @@ -953,7 +952,7 @@ ThreadEnumerationContext::OnThread( } m_threadCount++; - m_writer->OpenObject(nullptr); + m_writer->OpenObject(); m_writer->WriteString("is_managed", "true"); m_writer->WriteString("crashed", isCrashThread ? "true" : "false"); @@ -1029,7 +1028,7 @@ EmitSynthesizedCrashThread( { uint64_t crashingTid = static_cast(minipal_get_current_thread_id()); - s_jsonWriter.OpenObject(nullptr); + s_jsonWriter.OpenObject(); s_jsonWriter.WriteString("is_managed", g_isManagedThreadCallback != nullptr && g_isManagedThreadCallback() ? "true" : "false"); s_jsonWriter.WriteString("crashed", "true"); diff --git a/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp b/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp index 3fb64c9e5de352..1eac7d8ce2512d 100644 --- a/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp +++ b/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp @@ -9,8 +9,10 @@ static char -ToHexChar( - unsigned value); +ToHexChar(unsigned value) +{ + return (value < 10) ? static_cast('0' + value) : static_cast('a' + (value - 10)); +} void SignalSafeJsonWriter::Init( @@ -22,7 +24,6 @@ SignalSafeJsonWriter::Init( m_writeFailed = false; m_outputCallback = outputCallback; m_outputContext = outputContext; - m_buffer[0] = '\0'; } void @@ -35,14 +36,20 @@ SignalSafeJsonWriter::OpenObject( WriteEscapedString(key); AppendStr(": "); } - AppendStr("{"); + AppendChar('{'); m_commaNeeded = false; } +void +SignalSafeJsonWriter::OpenObject() +{ + OpenObject(nullptr); +} + void SignalSafeJsonWriter::CloseObject() { - AppendStr("}"); + AppendChar('}'); m_commaNeeded = true; } @@ -56,14 +63,20 @@ SignalSafeJsonWriter::OpenArray( WriteEscapedString(key); AppendStr(": "); } - AppendStr("["); + AppendChar('['); m_commaNeeded = false; } +void +SignalSafeJsonWriter::OpenArray() +{ + OpenArray(nullptr); +} + void SignalSafeJsonWriter::CloseArray() { - AppendStr("]"); + AppendChar(']'); m_commaNeeded = true; } @@ -78,16 +91,10 @@ SignalSafeJsonWriter::WriteString( WriteEscapedString(value); } -void -SignalSafeJsonWriter::Finish() -{ - (void)Flush(); -} - bool -SignalSafeJsonWriter::HasError() const +SignalSafeJsonWriter::Finish() { - return m_writeFailed; + return Flush(); } bool @@ -110,7 +117,6 @@ SignalSafeJsonWriter::Flush() } m_pos = 0; - m_buffer[0] = '\0'; return true; } @@ -141,13 +147,12 @@ SignalSafeJsonWriter::Append( size_t offset = 0; while (offset < len) { - size_t remaining = (CRASH_JSON_BUFFER_SIZE - 1) - m_pos; - if (remaining == 0 && !Flush()) + if (m_pos == CRASH_JSON_BUFFER_SIZE && !Flush()) { return false; } - remaining = (CRASH_JSON_BUFFER_SIZE - 1) - m_pos; + size_t remaining = CRASH_JSON_BUFFER_SIZE - m_pos; size_t chunk = len - offset; if (chunk > remaining) { @@ -162,6 +167,23 @@ SignalSafeJsonWriter::Append( return true; } +bool +SignalSafeJsonWriter::AppendChar(char c) +{ + if (m_writeFailed) + { + return false; + } + + if (m_pos == CRASH_JSON_BUFFER_SIZE && !Flush()) + { + return false; + } + + m_buffer[m_pos++] = c; + return true; +} + bool SignalSafeJsonWriter::AppendStr( const char* str) @@ -175,18 +197,11 @@ SignalSafeJsonWriter::AppendStr( return Append(str, strlen(str)); } -char -ToHexChar( - unsigned value) -{ - return (value < 10) ? static_cast('0' + value) : static_cast('a' + (value - 10)); -} - void SignalSafeJsonWriter::WriteSeparator() { if (m_commaNeeded) - AppendStr(","); + AppendChar(','); m_commaNeeded = true; } @@ -196,7 +211,7 @@ void SignalSafeJsonWriter::WriteEscapedString( const char* str) { - AppendStr("\""); + AppendChar('"'); if (str != nullptr) { for (size_t i = 0; str[i]; i++) @@ -214,22 +229,21 @@ SignalSafeJsonWriter::WriteEscapedString( AppendStr("\\t"); else if (static_cast(c) < 0x20) { - char esc[7]; + char esc[6]; esc[0] = '\\'; esc[1] = 'u'; esc[2] = '0'; esc[3] = '0'; esc[4] = ToHexChar((static_cast(c) >> 4) & 0xF); esc[5] = ToHexChar(static_cast(c) & 0xF); - esc[6] = '\0'; - AppendStr(esc); + Append(esc, sizeof(esc)); } else { - Append(&c, 1); + AppendChar(c); } } } - AppendStr("\""); + AppendChar('"'); } diff --git a/src/coreclr/debug/crashreport/signalsafejsonwriter.h b/src/coreclr/debug/crashreport/signalsafejsonwriter.h index 9708d1ba20bbc2..1ba22624c90eca 100644 --- a/src/coreclr/debug/crashreport/signalsafejsonwriter.h +++ b/src/coreclr/debug/crashreport/signalsafejsonwriter.h @@ -26,7 +26,6 @@ class SignalSafeJsonWriter m_outputCallback(nullptr), m_outputContext(nullptr) { - m_buffer[0] = '\0'; } SignalSafeJsonWriter(const SignalSafeJsonWriter&) = delete; @@ -34,16 +33,18 @@ class SignalSafeJsonWriter void Init(CrashJsonOutputCallback outputCallback, void* outputContext); void OpenObject(const char* key); + void OpenObject(); void CloseObject(); void OpenArray(const char* key); + void OpenArray(); void CloseArray(); void WriteString(const char* key, const char* value); - void Finish(); + bool Finish(); bool Flush(); - bool HasError() const; private: bool Append(const char* str, size_t len); + bool AppendChar(char c); bool AppendStr(const char* str); void WriteSeparator(); void WriteEscapedString(const char* str); From bee63f9b4355826aa4e5a63f69b01ceb97e4f484 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 22 Apr 2026 14:58:09 -0400 Subject: [PATCH 40/52] Polish in-proc crash reporter helpers Reduce stack footprint and factor out repeated logic in the signal-path helpers: - JsonFrameCallback: replace seven 32-byte scratch buffers with a single reused scratch buffer. WriteString copies its value into the writer's buffer before the next format call, so one scratch slot suffices. Trims ~192 bytes off the signal-handler stack footprint per frame. - WriteThreadToJson: hoist the hresultBuffer scratch out of the two mutually-exclusive branches so both reuse a single buffer. - Extract GetFramePointer alongside GetInstructionPointer/GetStackPointer so WriteRegistersToJson no longer inlines a per-arch #ifdef block. - Add an async-signal-safety doc comment on FormatHexValue. - WriteToFile: validate fd and buffer up front instead of relying on write() to fail. - Replace raw getpid() calls with the CoreCLR PAL GetCurrentProcessId() helper so the PID helper is cross-platform consistent with the rest of CoreCLR. - crashreportstackwalker.cpp: extract BuildTypeName helper so CrashReportGetExceptionForThread no longer nests six levels deep to concatenate namespace + '.' + class name into the caller's buffer. --- .../debug/crashreport/inproccrashreporter.cpp | 100 +++++++++++------- src/coreclr/vm/crashreportstackwalker.cpp | 67 +++++++----- 2 files changed, 103 insertions(+), 64 deletions(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index c037afeeefd83c..7c1d5b54e264c5 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -8,6 +8,8 @@ #include "inproccrashreporter.h" #include "signalsafejsonwriter.h" +#include "pal.h" + #include #include #include @@ -166,6 +168,11 @@ uint64_t GetStackPointer( void* context); +static +uint64_t +GetFramePointer( + void* context); + static void WriteCrashSiteFrameToJson( @@ -301,7 +308,7 @@ CreateInProcCrashReport( } char pidBuf[16]; - (void)snprintf(pidBuf, sizeof(pidBuf), "%u", static_cast(getpid())); + (void)snprintf(pidBuf, sizeof(pidBuf), "%u", static_cast(GetCurrentProcessId())); s_jsonWriter.WriteString("pid", pidBuf); s_jsonWriter.OpenArray("threads"); @@ -390,6 +397,11 @@ WriteToFile( const char* buffer, size_t len) { + if (fd < 0 || buffer == nullptr) + { + return false; + } + size_t totalWritten = 0; while (totalWritten < len) { @@ -462,7 +474,7 @@ ExpandDumpTemplate( } size_t pos = 0; - unsigned pid = static_cast(getpid()); + unsigned pid = static_cast(GetCurrentProcessId()); while (*pattern != '\0' && pos + 1 < bufferSize) { @@ -570,6 +582,10 @@ GetVersionString( buffer[copied + 1] = '\0'; } +// Formats a 64-bit value as a lowercase hexadecimal C string with a "0x" +// prefix into |buffer|. The output is always null-terminated provided +// |bufferSize| > 0. This helper is async-signal-safe: it performs no +// allocation, locking, or TLS access. void FormatHexValue( char* buffer, @@ -620,24 +636,14 @@ WriteRegistersToJson( { uint64_t ipValue = GetInstructionPointer(context); uint64_t spValue = GetStackPointer(context); + uint64_t bpValue = GetFramePointer(context); char ip[32] = "0x0"; char sp[32] = "0x0"; char bp[32] = "0x0"; FormatHexValue(ip, sizeof(ip), ipValue); FormatHexValue(sp, sizeof(sp), spValue); - - if (context != nullptr) - { - ucontext_t* ucontext = reinterpret_cast(context); -#if defined(__x86_64__) - FormatHexValue(bp, sizeof(bp), static_cast(ucontext->uc_mcontext.gregs[REG_RBP])); -#elif defined(__aarch64__) - FormatHexValue(bp, sizeof(bp), static_cast(ucontext->uc_mcontext.regs[29])); -#elif defined(__arm__) - FormatHexValue(bp, sizeof(bp), static_cast(ucontext->uc_mcontext.arm_fp)); -#endif - } + FormatHexValue(bp, sizeof(bp), bpValue); writer->OpenObject("ctx"); writer->WriteString("IP", ip); @@ -688,6 +694,27 @@ GetStackPointer( #endif } +uint64_t +GetFramePointer( + void* context) +{ + if (context == nullptr) + { + return 0; + } + + ucontext_t* ucontext = reinterpret_cast(context); +#if defined(__x86_64__) + return static_cast(ucontext->uc_mcontext.gregs[REG_RBP]); +#elif defined(__aarch64__) + return static_cast(ucontext->uc_mcontext.regs[29]); +#elif defined(__arm__) + return static_cast(ucontext->uc_mcontext.arm_fp); +#else + return 0; +#endif +} + void WriteCrashSiteFrameToJson( SignalSafeJsonWriter* writer, @@ -838,26 +865,20 @@ JsonFrameCallback( void* ctx) { SignalSafeJsonWriter* writer = reinterpret_cast(ctx); - char ipBuffer[32]; - char stackPointerBuffer[32]; - char nativeOffsetBuffer[32]; - char tokenBuffer[32]; - char ilOffsetBuffer[32]; - char moduleTimestampBuffer[32]; - char moduleSizeBuffer[32]; - - FormatHexValue(ipBuffer, sizeof(ipBuffer), ip); - FormatHexValue(stackPointerBuffer, sizeof(stackPointerBuffer), stackPointer); - FormatHexValue(nativeOffsetBuffer, sizeof(nativeOffsetBuffer), nativeOffset); - FormatHexValue(tokenBuffer, sizeof(tokenBuffer), token); - FormatHexValue(ilOffsetBuffer, sizeof(ilOffsetBuffer), ilOffset); - FormatHexValue(moduleTimestampBuffer, sizeof(moduleTimestampBuffer), moduleTimestamp); - FormatHexValue(moduleSizeBuffer, sizeof(moduleSizeBuffer), moduleSize); + + // Reuse a single scratch buffer for hex formatting: WriteString copies the + // value into the writer's buffer before we format the next field, so we + // don't need one scratch buffer per hex field. Keeps the signal-handler + // stack footprint down. + char scratch[32]; writer->OpenObject(); - writer->WriteString("stack_pointer", stackPointerBuffer); - writer->WriteString("native_address", ipBuffer); - writer->WriteString("native_offset", nativeOffsetBuffer); + FormatHexValue(scratch, sizeof(scratch), stackPointer); + writer->WriteString("stack_pointer", scratch); + FormatHexValue(scratch, sizeof(scratch), ip); + writer->WriteString("native_address", scratch); + FormatHexValue(scratch, sizeof(scratch), nativeOffset); + writer->WriteString("native_offset", scratch); if (methodName != nullptr) { @@ -865,19 +886,23 @@ JsonFrameCallback( BuildMethodName(fullName, sizeof(fullName), className, methodName); writer->WriteString("method_name", fullName); writer->WriteString("is_managed", "true"); - writer->WriteString("token", tokenBuffer); - writer->WriteString("il_offset", ilOffsetBuffer); + FormatHexValue(scratch, sizeof(scratch), token); + writer->WriteString("token", scratch); + FormatHexValue(scratch, sizeof(scratch), ilOffset); + writer->WriteString("il_offset", scratch); if (moduleName != nullptr) { writer->WriteString("filename", moduleName); } if (moduleTimestamp != 0) { - writer->WriteString("timestamp", moduleTimestampBuffer); + FormatHexValue(scratch, sizeof(scratch), moduleTimestamp); + writer->WriteString("timestamp", scratch); } if (moduleSize != 0) { - writer->WriteString("sizeofimage", moduleSizeBuffer); + FormatHexValue(scratch, sizeof(scratch), moduleSize); + writer->WriteString("sizeofimage", scratch); } if (moduleGuid != nullptr && moduleGuid[0] != '\0') { @@ -960,9 +985,9 @@ ThreadEnumerationContext::OnThread( FormatHexValue(nativeThreadId, sizeof(nativeThreadId), osThreadId); m_writer->WriteString("native_thread_id", nativeThreadId); + char hresultBuffer[32]; if (isCrashThread && m_hasCrashException) { - char hresultBuffer[32]; FormatHexValue(hresultBuffer, sizeof(hresultBuffer), m_crashExceptionHResult); m_writer->WriteString("managed_exception_type", m_crashExceptionType); @@ -970,7 +995,6 @@ ThreadEnumerationContext::OnThread( } else if (exceptionType != nullptr && exceptionType[0] != '\0') { - char hresultBuffer[32]; FormatHexValue(hresultBuffer, sizeof(hresultBuffer), exceptionHResult); m_writer->WriteString("managed_exception_type", exceptionType); diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index 8f4fc86a547a53..454c3f99a3121c 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -180,6 +180,46 @@ CrashReportIsCurrentThreadManaged() return GetThreadAsyncSafe() != nullptr; } +// Copy a type's namespace-qualified name (namespace + '.' + class) into +// |buffer|, truncating if needed. Always null-terminates when bufferSize > 0. +static +void +BuildTypeName( + char* buffer, + size_t bufferSize, + LPCUTF8 namespaceName, + LPCUTF8 className) +{ + if (bufferSize == 0) + { + return; + } + + size_t index = 0; + if (namespaceName != nullptr) + { + while (*namespaceName != '\0' && index + 1 < bufferSize) + { + buffer[index++] = *namespaceName++; + } + } + + if (className != nullptr) + { + if (index > 0 && index + 1 < bufferSize) + { + buffer[index++] = '.'; + } + + while (*className != '\0' && index + 1 < bufferSize) + { + buffer[index++] = *className++; + } + } + + buffer[index] = '\0'; +} + static bool CrashReportGetExceptionForThread( @@ -227,32 +267,7 @@ CrashReportGetExceptionForThread( LPCUTF8 namespaceName = nullptr; pImport->GetNameOfTypeDef(cl, &className, &namespaceName); - size_t index = 0; - if (namespaceName != nullptr) - { - while (*namespaceName != '\0' && index + 1 < exceptionTypeBufSize) - { - exceptionTypeBuf[index++] = *namespaceName++; - } - } - - if (className != nullptr) - { - if (index > 0 && index + 1 < exceptionTypeBufSize) - { - exceptionTypeBuf[index++] = '.'; - } - - while (*className != '\0' && index + 1 < exceptionTypeBufSize) - { - exceptionTypeBuf[index++] = *className++; - } - } - - if (exceptionTypeBufSize > 0) - { - exceptionTypeBuf[index] = '\0'; - } + BuildTypeName(exceptionTypeBuf, exceptionTypeBufSize, namespaceName, className); } } } From bf26555d56f4a596368e2296fd8efe3111a80b90 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 22 Apr 2026 19:51:49 -0400 Subject: [PATCH 41/52] Use InterlockedCompareExchange for one-shot reentry guard in crash reporter The crash report signal handler was guarded against re-entry by a plain write. This replaces it with `InterlockedCompareExchange` from pal.h, which matches the compare-exchange pattern used throughout CoreCLR for one-shot initialization flags. Addresses PR #126916 feedback: `You should probably use CoreCLR helpers for compare exchange.` --- src/coreclr/debug/crashreport/inproccrashreporter.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index 7c1d5b54e264c5..08304733cc2808 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -249,8 +249,8 @@ CreateInProcCrashReport( siginfo_t* siginfo, void* context) { - static volatile int s_generating = 0; - if (__sync_val_compare_and_swap(&s_generating, 0, 1) != 0) + static LONG s_generating = 0; + if (InterlockedCompareExchange(&s_generating, 1, 0) != 0) { return; } From 4a12ae27f1462a5e88239ebbaf88ff36013fc994 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 22 Apr 2026 19:52:37 -0400 Subject: [PATCH 42/52] Replace snprintf in crash reporter signal and startup paths with AS-safe formatters The signal-path formatting in inproccrashreporter.cpp now uses hand-rolled async-signal-safe helpers (FormatHexValue, FormatUnsignedDecimal) instead of snprintf, which is not async-signal-safe. Also replaces the startup-time path concatenation in crashreportstackwalker.cpp (used when DbgMiniDumpName is a bare filename) with manual memcpy-based concatenation, removing the remaining snprintf usage from the crash reporter code paths for consistency. Addresses PR #126916 feedback on snprintf usage from lateralusX. --- .../debug/crashreport/inproccrashreporter.cpp | 146 ++++++++++++++++-- src/coreclr/vm/crashreportstackwalker.cpp | 9 +- 2 files changed, 141 insertions(+), 14 deletions(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index 08304733cc2808..3f537352bfa166 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -12,7 +12,6 @@ #include #include -#include #include #include #include @@ -152,6 +151,28 @@ FormatHexValue( 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); + +static +bool +AppendString( + char* buffer, + size_t bufferSize, + size_t* pos, + const char* value); + static void WriteRegistersToJson( @@ -308,7 +329,7 @@ CreateInProcCrashReport( } char pidBuf[16]; - (void)snprintf(pidBuf, sizeof(pidBuf), "%u", static_cast(GetCurrentProcessId())); + (void)FormatUnsignedDecimal(pidBuf, sizeof(pidBuf), static_cast(GetCurrentProcessId())); s_jsonWriter.WriteString("pid", pidBuf); s_jsonWriter.OpenArray("threads"); @@ -336,7 +357,7 @@ CreateInProcCrashReport( s_jsonWriter.OpenObject("parameters"); char signalBuf[16]; - (void)snprintf(signalBuf, sizeof(signalBuf), "%d", signal); + (void)FormatSignedDecimal(signalBuf, sizeof(signalBuf), static_cast(signal)); s_jsonWriter.WriteString("signal", signalBuf); s_jsonWriter.CloseObject(); // parameters @@ -488,11 +509,11 @@ ExpandDumpTemplate( else if (*pattern == 'p' || *pattern == 'd') { char pidBuf[16]; - int pidLen = snprintf(pidBuf, sizeof(pidBuf), "%u", pid); - if (pidLen > 0 && pos + static_cast(pidLen) < bufferSize) + size_t pidLen = FormatUnsignedDecimal(pidBuf, sizeof(pidBuf), pid); + if (pidLen > 0 && pos + pidLen < bufferSize) { - memcpy(buffer + pos, pidBuf, static_cast(pidLen)); - pos += static_cast(pidLen); + memcpy(buffer + pos, pidBuf, pidLen); + pos += pidLen; } } else @@ -541,8 +562,16 @@ BuildReportPath( return false; } - int written = snprintf(buffer, bufferSize, "%s.crashreport.json", expanded); - return written > 0 && static_cast(written) < bufferSize; + size_t pos = 0; + if (!AppendString(buffer, bufferSize, &pos, expanded)) + { + return false; + } + if (!AppendString(buffer, bufferSize, &pos, ".crashreport.json")) + { + return false; + } + return true; } void @@ -629,6 +658,96 @@ FormatHexValue( buffer[index] = '\0'; } +// Formats an unsigned value as decimal into |buffer|. Returns the number of +// characters written (not counting the null terminator). Always +// null-terminates when bufferSize > 0. Async-signal-safe. +size_t +FormatUnsignedDecimal( + char* buffer, + size_t bufferSize, + uint64_t value) +{ + if (buffer == nullptr || bufferSize == 0) + { + return 0; + } + + char reverse[20]; // enough for UINT64_MAX + size_t reverseLength = 0; + do + { + reverse[reverseLength++] = static_cast('0' + (value % 10)); + value /= 10; + } while (value != 0 && reverseLength < sizeof(reverse)); + + size_t pos = 0; + while (reverseLength > 0 && pos + 1 < bufferSize) + { + buffer[pos++] = reverse[--reverseLength]; + } + buffer[pos] = '\0'; + return pos; +} + +// Formats a signed value as decimal into |buffer|. Returns the number of +// characters written (not counting the null terminator). Handles INT64_MIN +// correctly via unsigned negation. Always null-terminates when +// bufferSize > 0. Async-signal-safe. +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 < 2) + { + 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); + return written == 0 ? 0 : written + 1; +} + +// Appends |value| to |buffer| at *|pos|, advancing *|pos|, while leaving +// room for a trailing null terminator. Always null-terminates when +// bufferSize > 0. Returns true iff the full value was appended. +// Async-signal-safe. +bool +AppendString( + char* buffer, + size_t bufferSize, + size_t* pos, + const char* value) +{ + if (buffer == nullptr || pos == nullptr || value == nullptr || bufferSize == 0) + { + return false; + } + + size_t p = *pos; + while (*value != '\0' && p + 1 < bufferSize) + { + buffer[p++] = *value++; + } + buffer[p] = '\0'; + *pos = p; + return *value == '\0'; +} + void WriteRegistersToJson( SignalSafeJsonWriter* writer, @@ -749,15 +868,18 @@ BuildMethodName( if (className != nullptr && methodName != nullptr) { - (void)snprintf(buffer, bufferSize, "%s.%s", className, methodName); + size_t pos = 0; + AppendString(buffer, bufferSize, &pos, className); + AppendString(buffer, bufferSize, &pos, "."); + AppendString(buffer, bufferSize, &pos, methodName); } else if (className != nullptr) { - (void)snprintf(buffer, bufferSize, "%s", className); + CopyString(buffer, bufferSize, className); } else if (methodName != nullptr) { - (void)snprintf(buffer, bufferSize, "%s", methodName); + CopyString(buffer, bufferSize, methodName); } else { diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index 454c3f99a3121c..454966c7205fad 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -439,11 +439,16 @@ CrashReportRegisterStackWalker() } size_t tmpLen = strlen(tmpDir); const char* separator = (tmpLen > 0 && tmpDir[tmpLen - 1] == '/') ? "" : "/"; - int written = snprintf(dumpPathBuf, sizeof(dumpPathBuf), "%s%s%s", tmpDir, separator, dumpName); - if (written <= 0 || static_cast(written) >= sizeof(dumpPathBuf)) + size_t sepLen = strlen(separator); + size_t dumpLen = strlen(dumpName); + if (tmpLen + sepLen + dumpLen + 1 > sizeof(dumpPathBuf)) { return; } + memcpy(dumpPathBuf, tmpDir, tmpLen); + memcpy(dumpPathBuf + tmpLen, separator, sepLen); + memcpy(dumpPathBuf + tmpLen + sepLen, dumpName, dumpLen); + dumpPathBuf[tmpLen + sepLen + dumpLen] = '\0'; dumpName = dumpPathBuf; } From 7fde5fa368327d29274d7b1180ecc2a018c1369a Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 22 Apr 2026 20:05:07 -0400 Subject: [PATCH 43/52] Encapsulate in-proc crash reporter state in InProcCrashReporter class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapse the free-function surface and file-scope state (s_jsonWriter, g_*Callback, g_reportPath, g_processName) behind a single InProcCrashReporter singleton with: - InProcCrashReporterSettings struct that bundles the report path and the four VM callbacks captured at startup - InProcCrashReporter::Initialize(settings) — one call replaces the four per-callback setters plus the old InitializeInProcCrashReport - InProcCrashReporter::CreateReport(signal, siginfo, context) — replaces the CreateInProcCrashReport free function The PAL shim in process.cpp forwards to InProcCrashReporter::GetInstance() and still owns the g_inProcCrashReportEnabled publishing boundary so signal-handler dispatch remains ordered after Initialize completes. The VM startup path builds one settings struct and passes it through PROCInitializeInProcCrashReport in a single call. Helper free functions inside the crash reporter translation unit (FormatHexValue, CopyString, WriteRegistersToJson, JsonFrameCallback, etc.) stay as file-scope statics because they are pure functions with no state to encapsulate; only the methods that touch the singleton's members (CreateReport, Initialize, EmitSynthesizedCrashThread) are members. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../debug/crashreport/inproccrashreporter.cpp | 145 +++++++----------- .../debug/crashreport/inproccrashreporter.h | 55 +++++-- src/coreclr/pal/src/include/pal/process.h | 9 +- src/coreclr/pal/src/thread/process.cpp | 6 +- src/coreclr/vm/crashreportstackwalker.cpp | 14 +- 5 files changed, 114 insertions(+), 115 deletions(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index 3f537352bfa166..eee23f40e9c816 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -24,14 +24,6 @@ static char sccsid[] = "@(#)Version N/A"; #endif -static SignalSafeJsonWriter s_jsonWriter; -static volatile InProcCrashReportIsManagedThreadCallback g_isManagedThreadCallback = nullptr; -static volatile InProcCrashReportWalkStackCallback g_walkStackCallback = nullptr; -static volatile InProcCrashReportGetExceptionCallback g_getExceptionCallback = nullptr; -static volatile InProcCrashReportEnumerateThreadsCallback g_enumerateThreadsCallback = nullptr; -static char g_reportPath[256]; -static char g_processName[256]; - class ThreadEnumerationContext { public: @@ -255,17 +247,8 @@ BuildReportPath( size_t bufferSize, const char* dumpPath); -static -void -EmitSynthesizedCrashThread( - void* context, - bool hasException, - const char* crashExceptionType, - uint32_t crashExceptionHResult, - bool walkStack); - void -CreateInProcCrashReport( +InProcCrashReporter::CreateReport( int signal, siginfo_t* siginfo, void* context) @@ -279,7 +262,7 @@ CreateInProcCrashReport( char reportPath[256]; reportPath[0] = '\0'; - if (g_reportPath[0] == '\0' || !BuildReportPath(reportPath, sizeof(reportPath), g_reportPath)) + if (m_reportPath[0] == '\0' || !BuildReportPath(reportPath, sizeof(reportPath), m_reportPath)) { return; } @@ -297,48 +280,48 @@ CreateInProcCrashReport( exTypeBuf[0] = '\0'; bool hasException = false; - if (g_getExceptionCallback != nullptr && signal != SIGSEGV && signal != SIGBUS) + if (m_getExceptionCallback != nullptr && signal != SIGSEGV && signal != SIGBUS) { - hasException = g_getExceptionCallback(exTypeBuf, sizeof(exTypeBuf), &exHresult); + hasException = m_getExceptionCallback(exTypeBuf, sizeof(exTypeBuf), &exHresult); } CrashReportOutputContext outputContext(fd); - s_jsonWriter.Init(&CrashReportOutputContext::ChunkCallback, &outputContext); + m_jsonWriter.Init(&CrashReportOutputContext::ChunkCallback, &outputContext); - s_jsonWriter.OpenObject(); - s_jsonWriter.OpenObject("payload"); - s_jsonWriter.WriteString("protocol_version", "1.0.0"); + m_jsonWriter.OpenObject(); + m_jsonWriter.OpenObject("payload"); + m_jsonWriter.WriteString("protocol_version", "1.0.0"); - s_jsonWriter.OpenObject("configuration"); + m_jsonWriter.OpenObject("configuration"); #if defined(__x86_64__) - s_jsonWriter.WriteString("architecture", "amd64"); + m_jsonWriter.WriteString("architecture", "amd64"); #elif defined(__aarch64__) - s_jsonWriter.WriteString("architecture", "arm64"); + m_jsonWriter.WriteString("architecture", "arm64"); #elif defined(__arm__) - s_jsonWriter.WriteString("architecture", "arm"); + m_jsonWriter.WriteString("architecture", "arm"); #endif char version[sizeof(sccsid) + 1]; GetVersionString(version, sizeof(version)); - s_jsonWriter.WriteString("version", version); - s_jsonWriter.CloseObject(); // configuration + m_jsonWriter.WriteString("version", version); + m_jsonWriter.CloseObject(); // configuration - if (g_processName[0] != '\0') + if (m_processName[0] != '\0') { - s_jsonWriter.WriteString("process_name", g_processName); + m_jsonWriter.WriteString("process_name", m_processName); } char pidBuf[16]; (void)FormatUnsignedDecimal(pidBuf, sizeof(pidBuf), static_cast(GetCurrentProcessId())); - s_jsonWriter.WriteString("pid", pidBuf); + m_jsonWriter.WriteString("pid", pidBuf); - s_jsonWriter.OpenArray("threads"); - if (g_enumerateThreadsCallback != nullptr) + m_jsonWriter.OpenArray("threads"); + if (m_enumerateThreadsCallback != nullptr) { - ThreadEnumerationContext threadContext(&s_jsonWriter, context, hasException, exTypeBuf, exHresult); + ThreadEnumerationContext threadContext(&m_jsonWriter, context, hasException, exTypeBuf, exHresult); uint64_t crashingTid = static_cast(minipal_get_current_thread_id()); - g_enumerateThreadsCallback(crashingTid, &ThreadEnumerationContext::ThreadCallback, &ThreadEnumerationContext::FrameCallback, &threadContext); + m_enumerateThreadsCallback(crashingTid, &ThreadEnumerationContext::ThreadCallback, &ThreadEnumerationContext::FrameCallback, &threadContext); threadContext.EndEnumeration(); @@ -351,21 +334,21 @@ CreateInProcCrashReport( { EmitSynthesizedCrashThread(context, hasException, exTypeBuf, exHresult, /*walkStack*/ true); } - s_jsonWriter.CloseArray(); // threads + m_jsonWriter.CloseArray(); // threads - s_jsonWriter.CloseObject(); // payload + m_jsonWriter.CloseObject(); // payload - s_jsonWriter.OpenObject("parameters"); + m_jsonWriter.OpenObject("parameters"); char signalBuf[16]; (void)FormatSignedDecimal(signalBuf, sizeof(signalBuf), static_cast(signal)); - s_jsonWriter.WriteString("signal", signalBuf); - s_jsonWriter.CloseObject(); // parameters + m_jsonWriter.WriteString("signal", signalBuf); + m_jsonWriter.CloseObject(); // parameters - s_jsonWriter.CloseObject(); // root + m_jsonWriter.CloseObject(); // root if (fd != -1) { - bool writeSucceeded = s_jsonWriter.Finish() && + bool writeSucceeded = m_jsonWriter.Finish() && !outputContext.WriteFailed() && WriteToFile(fd, "\n", 1); @@ -376,40 +359,23 @@ CreateInProcCrashReport( } } -void -InitializeInProcCrashReport( - const char* dumpPath) +InProcCrashReporter& +InProcCrashReporter::GetInstance() { - CopyString(g_reportPath, sizeof(g_reportPath), dumpPath); - (void)TryGetProcessName(g_processName, sizeof(g_processName)); + static InProcCrashReporter s_instance; + return s_instance; } void -InProcCrashReportSetCurrentThreadManagedResolver( - InProcCrashReportIsManagedThreadCallback callback) +InProcCrashReporter::Initialize( + const InProcCrashReporterSettings& settings) { - g_isManagedThreadCallback = callback; -} - -void -InProcCrashReportSetStackWalker( - InProcCrashReportWalkStackCallback callback) -{ - g_walkStackCallback = callback; -} - -void -InProcCrashReportSetExceptionResolver( - InProcCrashReportGetExceptionCallback callback) -{ - g_getExceptionCallback = callback; -} - -void -InProcCrashReportSetThreadEnumerator( - InProcCrashReportEnumerateThreadsCallback callback) -{ - g_enumerateThreadsCallback = callback; + m_isManagedThreadCallback = settings.isManagedThreadCallback; + m_walkStackCallback = settings.walkStackCallback; + m_getExceptionCallback = settings.getExceptionCallback; + m_enumerateThreadsCallback = settings.enumerateThreadsCallback; + CopyString(m_reportPath, sizeof(m_reportPath), settings.reportPath); + (void)TryGetProcessName(m_processName, sizeof(m_processName)); } bool @@ -1163,9 +1129,8 @@ ThreadEnumerationContext::EndEnumeration() (void)m_writer->Flush(); } -static void -EmitSynthesizedCrashThread( +InProcCrashReporter::EmitSynthesizedCrashThread( void* context, bool hasException, const char* crashExceptionType, @@ -1174,31 +1139,31 @@ EmitSynthesizedCrashThread( { uint64_t crashingTid = static_cast(minipal_get_current_thread_id()); - s_jsonWriter.OpenObject(); - s_jsonWriter.WriteString("is_managed", - g_isManagedThreadCallback != nullptr && g_isManagedThreadCallback() ? "true" : "false"); - s_jsonWriter.WriteString("crashed", "true"); + m_jsonWriter.OpenObject(); + m_jsonWriter.WriteString("is_managed", + m_isManagedThreadCallback != nullptr && m_isManagedThreadCallback() ? "true" : "false"); + m_jsonWriter.WriteString("crashed", "true"); char nativeThreadId[32]; FormatHexValue(nativeThreadId, sizeof(nativeThreadId), crashingTid); - s_jsonWriter.WriteString("native_thread_id", nativeThreadId); + m_jsonWriter.WriteString("native_thread_id", nativeThreadId); if (hasException) { char hresultBuffer[32]; FormatHexValue(hresultBuffer, sizeof(hresultBuffer), crashExceptionHResult); - s_jsonWriter.WriteString("managed_exception_type", crashExceptionType); - s_jsonWriter.WriteString("managed_exception_hresult", hresultBuffer); + m_jsonWriter.WriteString("managed_exception_type", crashExceptionType); + m_jsonWriter.WriteString("managed_exception_hresult", hresultBuffer); } - WriteRegistersToJson(&s_jsonWriter, context); - s_jsonWriter.OpenArray("stack_frames"); - WriteCrashSiteFrameToJson(&s_jsonWriter, context); - if (walkStack && g_walkStackCallback != nullptr) + WriteRegistersToJson(&m_jsonWriter, context); + m_jsonWriter.OpenArray("stack_frames"); + WriteCrashSiteFrameToJson(&m_jsonWriter, context); + if (walkStack && m_walkStackCallback != nullptr) { - g_walkStackCallback(JsonFrameCallback, &s_jsonWriter); + m_walkStackCallback(JsonFrameCallback, &m_jsonWriter); } - s_jsonWriter.CloseArray(); // stack_frames - s_jsonWriter.CloseObject(); // thread + 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 ff4ef3cbf2baaa..87e6f28b8d2aba 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.h +++ b/src/coreclr/debug/crashreport/inproccrashreporter.h @@ -11,16 +11,10 @@ #include #include -void InitializeInProcCrashReport(const char* dumpPath); - -// Generate an in-proc crash report. Called from PROCCreateCrashDumpIfEnabled. -// All arguments come from the signal handler and are signal-safe to read. -void CreateInProcCrashReport(int signal, siginfo_t* siginfo, void* context); +#include "signalsafejsonwriter.h" using InProcCrashReportIsManagedThreadCallback = bool (*)(); -void InProcCrashReportSetCurrentThreadManagedResolver(InProcCrashReportIsManagedThreadCallback callback); - using InProcCrashReportFrameCallback = void (*)( uint64_t ip, uint64_t stackPointer, @@ -39,15 +33,11 @@ using InProcCrashReportWalkStackCallback = void (*)( InProcCrashReportFrameCallback frameCallback, void* ctx); -void InProcCrashReportSetStackWalker(InProcCrashReportWalkStackCallback callback); - using InProcCrashReportGetExceptionCallback = bool (*)( char* exceptionTypeBuf, size_t exceptionTypeBufSize, uint32_t* hresult); -void InProcCrashReportSetExceptionResolver(InProcCrashReportGetExceptionCallback callback); - using InProcCrashReportThreadCallback = void (*)( uint64_t osThreadId, bool isCrashThread, @@ -61,4 +51,45 @@ using InProcCrashReportEnumerateThreadsCallback = void (*)( InProcCrashReportFrameCallback frameCallback, void* ctx); -void InProcCrashReportSetThreadEnumerator(InProcCrashReportEnumerateThreadsCallback callback); +struct InProcCrashReporterSettings +{ + const char* reportPath; + InProcCrashReportIsManagedThreadCallback isManagedThreadCallback; + InProcCrashReportWalkStackCallback walkStackCallback; + InProcCrashReportGetExceptionCallback getExceptionCallback; + InProcCrashReportEnumerateThreadsCallback enumerateThreadsCallback; +}; + +class InProcCrashReporter +{ +public: + static InProcCrashReporter& GetInstance(); + + // Capture configuration and the crash-report template path. Must be called + // before the PAL enables signal-handler dispatch to CreateReport. + void Initialize(const InProcCrashReporterSettings& settings); + + // Generate an in-proc crash report. Called from PROCCreateCrashDumpIfEnabled. + // All arguments come from the signal handler and are signal-safe to read. + void CreateReport(int signal, siginfo_t* siginfo, void* context); + +private: + InProcCrashReporter() = default; + InProcCrashReporter(const InProcCrashReporter&) = delete; + InProcCrashReporter& operator=(const InProcCrashReporter&) = delete; + + void EmitSynthesizedCrashThread( + void* context, + bool hasException, + const char* crashExceptionType, + uint32_t crashExceptionHResult, + bool walkStack); + + SignalSafeJsonWriter m_jsonWriter; + InProcCrashReportIsManagedThreadCallback m_isManagedThreadCallback = nullptr; + InProcCrashReportWalkStackCallback m_walkStackCallback = nullptr; + InProcCrashReportGetExceptionCallback m_getExceptionCallback = nullptr; + InProcCrashReportEnumerateThreadsCallback m_enumerateThreadsCallback = nullptr; + char m_reportPath[256] = {}; + char m_processName[256] = {}; +}; diff --git a/src/coreclr/pal/src/include/pal/process.h b/src/coreclr/pal/src/include/pal/process.h index c407d55c0fbf5c..95649546cf1f31 100644 --- a/src/coreclr/pal/src/include/pal/process.h +++ b/src/coreclr/pal/src/include/pal/process.h @@ -172,13 +172,14 @@ VOID PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, void* context, --*/ VOID PROCLogManagedCallstackForSignal(int signal); -#ifdef FEATURE_INPROC_CRASHREPORT -void PROCInitializeInProcCrashReport(const char* dumpPath); -#endif - #ifdef __cplusplus } #endif // __cplusplus +#ifdef FEATURE_INPROC_CRASHREPORT +struct InProcCrashReporterSettings; +void PROCInitializeInProcCrashReport(const InProcCrashReporterSettings& settings); +#endif + #endif //PAL_PROCESS_H_ diff --git a/src/coreclr/pal/src/thread/process.cpp b/src/coreclr/pal/src/thread/process.cpp index c45c42a952dc9e..5e2edc4bc0c7f7 100644 --- a/src/coreclr/pal/src/thread/process.cpp +++ b/src/coreclr/pal/src/thread/process.cpp @@ -2800,9 +2800,9 @@ PROCLogManagedCallstackForSignal(int signal) #ifdef FEATURE_INPROC_CRASHREPORT #include void -PROCInitializeInProcCrashReport(const char* dumpPath) +PROCInitializeInProcCrashReport(const InProcCrashReporterSettings& settings) { - InitializeInProcCrashReport(dumpPath); + InProcCrashReporter::GetInstance().Initialize(settings); // Publish last so PROCCreateCrashDumpIfEnabled only observes the reporter // as enabled after the crashreport path (and any other state) is set. @@ -2818,7 +2818,7 @@ PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, void* context, bool // TODO: Dump stress log into logcat and/or file when enabled? if (g_inProcCrashReportEnabled) { - CreateInProcCrashReport(signal, siginfo, context); + InProcCrashReporter::GetInstance().CreateReport(signal, siginfo, context); } minipal_log_write_fatal("Aborting process.\n"); } diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index 454966c7205fad..d1589cc4f91cd4 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -17,7 +17,7 @@ #include "threadsuspend.h" #include "gcenv.h" -extern "C" void PROCInitializeInProcCrashReport(const char* dumpPath); +void PROCInitializeInProcCrashReport(const InProcCrashReporterSettings& settings); struct WalkContext { @@ -452,15 +452,17 @@ CrashReportRegisterStackWalker() dumpName = dumpPathBuf; } - InProcCrashReportSetCurrentThreadManagedResolver(CrashReportIsCurrentThreadManaged); - InProcCrashReportSetStackWalker(CrashReportWalkStack); - InProcCrashReportSetExceptionResolver(CrashReportGetException); - InProcCrashReportSetThreadEnumerator(CrashReportEnumerateThreads); + InProcCrashReporterSettings settings = {}; + settings.reportPath = dumpName; + settings.isManagedThreadCallback = CrashReportIsCurrentThreadManaged; + settings.walkStackCallback = CrashReportWalkStack; + settings.getExceptionCallback = CrashReportGetException; + settings.enumerateThreadsCallback = CrashReportEnumerateThreads; // Initialize and enable the PAL side last so PROCCreateCrashDumpIfEnabled // only observes the reporter as enabled after all VM callbacks are // registered. - PROCInitializeInProcCrashReport(dumpName); + PROCInitializeInProcCrashReport(settings); } #endif // FEATURE_INPROC_CRASHREPORT From d24f300268596601e42455241d89f347f7fdad43 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 22 Apr 2026 20:20:34 -0400 Subject: [PATCH 44/52] Hook in-proc crash reporter into coreclrpal via target_sources Move the coreclrpal wiring for the in-proc crash reporter objects from pal/src/CMakeLists.txt into the crashreport subdir itself, so that the PAL CMakeLists.txt does not need to know about an optional feature defined elsewhere in the tree. The enclosing add_subdirectory guard in debug/CMakeLists.txt already gates on FEATURE_INPROC_CRASHREPORT, so the target_sources call only runs when the feature is on. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/debug/crashreport/CMakeLists.txt | 2 ++ src/coreclr/pal/src/CMakeLists.txt | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/coreclr/debug/crashreport/CMakeLists.txt b/src/coreclr/debug/crashreport/CMakeLists.txt index 9a13917fa0adf1..f88699a4c6a464 100644 --- a/src/coreclr/debug/crashreport/CMakeLists.txt +++ b/src/coreclr/debug/crashreport/CMakeLists.txt @@ -6,3 +6,5 @@ set(CRASHREPORT_SOURCES ) add_library(inproccrashreport OBJECT ${CRASHREPORT_SOURCES}) + +target_sources(coreclrpal PRIVATE $) diff --git a/src/coreclr/pal/src/CMakeLists.txt b/src/coreclr/pal/src/CMakeLists.txt index 733e42b0c8b819..0d422ff87522be 100644 --- a/src/coreclr/pal/src/CMakeLists.txt +++ b/src/coreclr/pal/src/CMakeLists.txt @@ -228,7 +228,6 @@ add_library(coreclrpal_objects add_library(coreclrpal STATIC $ - $<$:$> ${LIBUNWIND_OBJECTS} ) From e70b43619aa9f9e3e474e46c1e047af173cd2ab0 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 22 Apr 2026 21:10:27 -0400 Subject: [PATCH 45/52] Use named constants for crash report scratch buffer sizes Replace the 256 and 32 magic numbers scattered across the in-proc crash reporter with CRASHREPORT_STRING_BUFFER_SIZE and CRASHREPORT_NUMBER_BUFFER_SIZE constants in inproccrashreporter.h. 256 covers path-like and identifier-like scratch buffers (report paths, process name, type/class names); 32 covers hex/decimal integer formatters (addresses, thread IDs, hresults). No behavior change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../debug/crashreport/inproccrashreporter.cpp | 32 +++++++++---------- .../debug/crashreport/inproccrashreporter.h | 11 +++++-- src/coreclr/vm/crashreportstackwalker.cpp | 6 ++-- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index eee23f40e9c816..9eb99abd5b726a 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -259,7 +259,7 @@ InProcCrashReporter::CreateReport( return; } - char reportPath[256]; + char reportPath[CRASHREPORT_STRING_BUFFER_SIZE]; reportPath[0] = '\0'; if (m_reportPath[0] == '\0' || !BuildReportPath(reportPath, sizeof(reportPath), m_reportPath)) @@ -275,7 +275,7 @@ InProcCrashReporter::CreateReport( (void)siginfo; - char exTypeBuf[256]; + char exTypeBuf[CRASHREPORT_STRING_BUFFER_SIZE]; uint32_t exHresult = 0; exTypeBuf[0] = '\0'; @@ -521,7 +521,7 @@ BuildReportPath( return false; } - char expanded[256]; + char expanded[CRASHREPORT_STRING_BUFFER_SIZE]; size_t expandedLen = ExpandDumpTemplate(expanded, sizeof(expanded), dumpPath); if (expandedLen == 0) { @@ -722,9 +722,9 @@ WriteRegistersToJson( uint64_t ipValue = GetInstructionPointer(context); uint64_t spValue = GetStackPointer(context); uint64_t bpValue = GetFramePointer(context); - char ip[32] = "0x0"; - char sp[32] = "0x0"; - char bp[32] = "0x0"; + char ip[CRASHREPORT_NUMBER_BUFFER_SIZE] = "0x0"; + char sp[CRASHREPORT_NUMBER_BUFFER_SIZE] = "0x0"; + char bp[CRASHREPORT_NUMBER_BUFFER_SIZE] = "0x0"; FormatHexValue(ip, sizeof(ip), ipValue); FormatHexValue(sp, sizeof(sp), spValue); @@ -807,8 +807,8 @@ WriteCrashSiteFrameToJson( { uint64_t ipValue = GetInstructionPointer(context); uint64_t spValue = GetStackPointer(context); - char ip[32] = "0x0"; - char sp[32] = "0x0"; + char ip[CRASHREPORT_NUMBER_BUFFER_SIZE] = "0x0"; + char sp[CRASHREPORT_NUMBER_BUFFER_SIZE] = "0x0"; FormatHexValue(ip, sizeof(ip), ipValue); FormatHexValue(sp, sizeof(sp), spValue); @@ -910,7 +910,7 @@ TryGetProcessName( int fd = open("/proc/self/cmdline", O_RDONLY); if (fd != -1) { - char cmdline[256]; + char cmdline[CRASHREPORT_STRING_BUFFER_SIZE]; ssize_t bytesRead = read(fd, cmdline, sizeof(cmdline) - 1); close(fd); @@ -925,7 +925,7 @@ TryGetProcessName( } } - char exePath[256]; + char exePath[CRASHREPORT_STRING_BUFFER_SIZE]; ssize_t pathLength = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1); if (pathLength > 0) { @@ -958,7 +958,7 @@ JsonFrameCallback( // value into the writer's buffer before we format the next field, so we // don't need one scratch buffer per hex field. Keeps the signal-handler // stack footprint down. - char scratch[32]; + char scratch[CRASHREPORT_NUMBER_BUFFER_SIZE]; writer->OpenObject(); FormatHexValue(scratch, sizeof(scratch), stackPointer); @@ -970,7 +970,7 @@ JsonFrameCallback( if (methodName != nullptr) { - char fullName[256]; + char fullName[CRASHREPORT_STRING_BUFFER_SIZE]; BuildMethodName(fullName, sizeof(fullName), className, methodName); writer->WriteString("method_name", fullName); writer->WriteString("is_managed", "true"); @@ -1069,11 +1069,11 @@ ThreadEnumerationContext::OnThread( m_writer->WriteString("is_managed", "true"); m_writer->WriteString("crashed", isCrashThread ? "true" : "false"); - char nativeThreadId[32]; + char nativeThreadId[CRASHREPORT_NUMBER_BUFFER_SIZE]; FormatHexValue(nativeThreadId, sizeof(nativeThreadId), osThreadId); m_writer->WriteString("native_thread_id", nativeThreadId); - char hresultBuffer[32]; + char hresultBuffer[CRASHREPORT_NUMBER_BUFFER_SIZE]; if (isCrashThread && m_hasCrashException) { FormatHexValue(hresultBuffer, sizeof(hresultBuffer), m_crashExceptionHResult); @@ -1144,13 +1144,13 @@ InProcCrashReporter::EmitSynthesizedCrashThread( m_isManagedThreadCallback != nullptr && m_isManagedThreadCallback() ? "true" : "false"); m_jsonWriter.WriteString("crashed", "true"); - char nativeThreadId[32]; + char nativeThreadId[CRASHREPORT_NUMBER_BUFFER_SIZE]; FormatHexValue(nativeThreadId, sizeof(nativeThreadId), crashingTid); m_jsonWriter.WriteString("native_thread_id", nativeThreadId); if (hasException) { - char hresultBuffer[32]; + char hresultBuffer[CRASHREPORT_NUMBER_BUFFER_SIZE]; FormatHexValue(hresultBuffer, sizeof(hresultBuffer), crashExceptionHResult); m_jsonWriter.WriteString("managed_exception_type", crashExceptionType); diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.h b/src/coreclr/debug/crashreport/inproccrashreporter.h index 87e6f28b8d2aba..b04099076b919a 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.h +++ b/src/coreclr/debug/crashreport/inproccrashreporter.h @@ -13,6 +13,13 @@ #include "signalsafejsonwriter.h" +// Scratch-buffer sizes used throughout the in-proc crash reporter. 256 is +// sized for path-like and identifier-like strings (report paths, process +// name, type/class names). 32 is sized for a single hex-or-decimal integer +// formatted as a C string (addresses, thread IDs, hresults). +static constexpr size_t CRASHREPORT_STRING_BUFFER_SIZE = 256; +static constexpr size_t CRASHREPORT_NUMBER_BUFFER_SIZE = 32; + using InProcCrashReportIsManagedThreadCallback = bool (*)(); using InProcCrashReportFrameCallback = void (*)( @@ -90,6 +97,6 @@ class InProcCrashReporter InProcCrashReportWalkStackCallback m_walkStackCallback = nullptr; InProcCrashReportGetExceptionCallback m_getExceptionCallback = nullptr; InProcCrashReportEnumerateThreadsCallback m_enumerateThreadsCallback = nullptr; - char m_reportPath[256] = {}; - char m_processName[256] = {}; + char m_reportPath[CRASHREPORT_STRING_BUFFER_SIZE] = {}; + char m_processName[CRASHREPORT_STRING_BUFFER_SIZE] = {}; }; diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index d1589cc4f91cd4..70ccdeffb8f485 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -54,7 +54,7 @@ FrameCallbackAdapter( } } - char classNameBuf[256] = { 0 }; + char classNameBuf[CRASHREPORT_STRING_BUFFER_SIZE] = { 0 }; size_t index = 0; if (namespaceName != nullptr) { @@ -368,7 +368,7 @@ CrashReportEnumerateThreads( uint64_t crashOsId = static_cast(pCrashThread->GetOSThreadId()); if (crashOsId == crashingTid) { - char exceptionType[256]; + char exceptionType[CRASHREPORT_STRING_BUFFER_SIZE]; uint32_t hresult = 0; bool hasException = CrashReportGetExceptionForThread(pCrashThread, exceptionType, sizeof(exceptionType), &hresult); @@ -429,7 +429,7 @@ CrashReportRegisterStackWalker() // If DbgMiniDumpName is just a filename (no directory component), write // the crash report under TMPDIR / /tmp so it lands somewhere writable. - char dumpPathBuf[256]; + char dumpPathBuf[CRASHREPORT_STRING_BUFFER_SIZE]; if (strchr(dumpName, '/') == nullptr) { const char* tmpDir = getenv("TMPDIR"); From 92661327754689aebd5d9f2d444fe2f37cfd9ca0 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 22 Apr 2026 21:28:55 -0400 Subject: [PATCH 46/52] Hoist remaining-capacity computation out of SignalSafeJsonWriter::Append loop Compute 'remaining' once before the loop and decrement by 'chunk' each iteration, reseeding to CRASH_JSON_BUFFER_SIZE after a successful Flush(). This is the loop-invariant form the reviewer asked for on the initial C implementation; commit 5 (99aaf825af1) only deduplicated the two in-loop recomputes, it did not actually hoist out of the loop. No behavior change: chunk <= remaining is still structurally guaranteed, so remaining -= chunk cannot underflow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../debug/crashreport/signalsafejsonwriter.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp b/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp index 1eac7d8ce2512d..2728436a2d1980 100644 --- a/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp +++ b/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp @@ -145,14 +145,18 @@ SignalSafeJsonWriter::Append( } size_t offset = 0; + size_t remaining = CRASH_JSON_BUFFER_SIZE - m_pos; while (offset < len) { - if (m_pos == CRASH_JSON_BUFFER_SIZE && !Flush()) + if (remaining == 0) { - return false; + if (!Flush()) + { + return false; + } + remaining = CRASH_JSON_BUFFER_SIZE; } - size_t remaining = CRASH_JSON_BUFFER_SIZE - m_pos; size_t chunk = len - offset; if (chunk > remaining) { @@ -162,6 +166,7 @@ SignalSafeJsonWriter::Append( memcpy(m_buffer + m_pos, str + offset, chunk); m_pos += chunk; offset += chunk; + remaining -= chunk; } return true; From 98ffae0d7e08a8b389916eb3b1d47604fe6403f1 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 22 Apr 2026 21:49:15 -0400 Subject: [PATCH 47/52] Consolidate scratch buffers in crash report thread and process-name helpers Reuse a single scratch[] local for the hex-formatted native thread id and managed-exception-hresult pair in both the managed and native thread crash-site JSON writers, and reuse a single scratch[] local across the /proc/self/cmdline and /proc/self/exe fallbacks in TryGetProcessName. WriteString copies the value into the writer buffer before the next field is formatted, and the cmdline vs. exe paths are sequential, so the separate per-value locals were never simultaneously live. Shrinks signal-handler stack usage without changing behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../debug/crashreport/inproccrashreporter.cpp | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index 9eb99abd5b726a..1f56b19a12eac1 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -907,17 +907,18 @@ TryGetProcessName( filename[0] = '\0'; + char scratch[CRASHREPORT_STRING_BUFFER_SIZE]; + int fd = open("/proc/self/cmdline", O_RDONLY); if (fd != -1) { - char cmdline[CRASHREPORT_STRING_BUFFER_SIZE]; - ssize_t bytesRead = read(fd, cmdline, sizeof(cmdline) - 1); + ssize_t bytesRead = read(fd, scratch, sizeof(scratch) - 1); close(fd); if (bytesRead > 0) { - cmdline[bytesRead] = '\0'; - CopyString(filename, filenameLen, GetFilename(cmdline)); + scratch[bytesRead] = '\0'; + CopyString(filename, filenameLen, GetFilename(scratch)); if (filename[0] != '\0') { return true; @@ -925,12 +926,11 @@ TryGetProcessName( } } - char exePath[CRASHREPORT_STRING_BUFFER_SIZE]; - ssize_t pathLength = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1); + ssize_t pathLength = readlink("/proc/self/exe", scratch, sizeof(scratch) - 1); if (pathLength > 0) { - exePath[pathLength] = '\0'; - CopyString(filename, filenameLen, GetFilename(exePath)); + scratch[pathLength] = '\0'; + CopyString(filename, filenameLen, GetFilename(scratch)); return filename[0] != '\0'; } @@ -1069,24 +1069,23 @@ ThreadEnumerationContext::OnThread( m_writer->WriteString("is_managed", "true"); m_writer->WriteString("crashed", isCrashThread ? "true" : "false"); - char nativeThreadId[CRASHREPORT_NUMBER_BUFFER_SIZE]; - FormatHexValue(nativeThreadId, sizeof(nativeThreadId), osThreadId); - m_writer->WriteString("native_thread_id", nativeThreadId); + char scratch[CRASHREPORT_NUMBER_BUFFER_SIZE]; + FormatHexValue(scratch, sizeof(scratch), osThreadId); + m_writer->WriteString("native_thread_id", scratch); - char hresultBuffer[CRASHREPORT_NUMBER_BUFFER_SIZE]; if (isCrashThread && m_hasCrashException) { - FormatHexValue(hresultBuffer, sizeof(hresultBuffer), m_crashExceptionHResult); + FormatHexValue(scratch, sizeof(scratch), m_crashExceptionHResult); m_writer->WriteString("managed_exception_type", m_crashExceptionType); - m_writer->WriteString("managed_exception_hresult", hresultBuffer); + m_writer->WriteString("managed_exception_hresult", scratch); } else if (exceptionType != nullptr && exceptionType[0] != '\0') { - FormatHexValue(hresultBuffer, sizeof(hresultBuffer), exceptionHResult); + FormatHexValue(scratch, sizeof(scratch), exceptionHResult); m_writer->WriteString("managed_exception_type", exceptionType); - m_writer->WriteString("managed_exception_hresult", hresultBuffer); + m_writer->WriteString("managed_exception_hresult", scratch); } if (isCrashThread) @@ -1144,17 +1143,16 @@ InProcCrashReporter::EmitSynthesizedCrashThread( m_isManagedThreadCallback != nullptr && m_isManagedThreadCallback() ? "true" : "false"); m_jsonWriter.WriteString("crashed", "true"); - char nativeThreadId[CRASHREPORT_NUMBER_BUFFER_SIZE]; - FormatHexValue(nativeThreadId, sizeof(nativeThreadId), crashingTid); - m_jsonWriter.WriteString("native_thread_id", nativeThreadId); + char scratch[CRASHREPORT_NUMBER_BUFFER_SIZE]; + FormatHexValue(scratch, sizeof(scratch), crashingTid); + m_jsonWriter.WriteString("native_thread_id", scratch); if (hasException) { - char hresultBuffer[CRASHREPORT_NUMBER_BUFFER_SIZE]; - FormatHexValue(hresultBuffer, sizeof(hresultBuffer), crashExceptionHResult); + FormatHexValue(scratch, sizeof(scratch), crashExceptionHResult); m_jsonWriter.WriteString("managed_exception_type", crashExceptionType); - m_jsonWriter.WriteString("managed_exception_hresult", hresultBuffer); + m_jsonWriter.WriteString("managed_exception_hresult", scratch); } WriteRegistersToJson(&m_jsonWriter, context); From 97d721d38c43accbd09a213b51862d1be4f245a4 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 22 Apr 2026 22:03:59 -0400 Subject: [PATCH 48/52] Use BuildTypeName helper in FrameCallbackAdapter FrameCallbackAdapter still had an inline namespace+'.'+class builder that duplicated the logic moved to the BuildTypeName helper. Route it through the helper instead so managed-frame and exception-type formatting share the same implementation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/crashreportstackwalker.cpp | 25 +++-------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index 70ccdeffb8f485..b9d5f6ba54480b 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -25,6 +25,8 @@ struct WalkContext void* userCtx; }; +static void BuildTypeName(char* buffer, size_t bufferSize, LPCUTF8 namespaceName, LPCUTF8 className); + static StackWalkAction FrameCallbackAdapter( @@ -55,28 +57,7 @@ FrameCallbackAdapter( } char classNameBuf[CRASHREPORT_STRING_BUFFER_SIZE] = { 0 }; - size_t index = 0; - if (namespaceName != nullptr) - { - while (*namespaceName != '\0' && index + 1 < sizeof(classNameBuf)) - { - classNameBuf[index++] = *namespaceName++; - } - } - - if (className != nullptr) - { - if (index > 0 && index + 1 < sizeof(classNameBuf)) - { - classNameBuf[index++] = '.'; - } - - while (*className != '\0' && index + 1 < sizeof(classNameBuf)) - { - classNameBuf[index++] = *className++; - } - } - classNameBuf[index] = '\0'; + BuildTypeName(classNameBuf, sizeof(classNameBuf), namespaceName, className); const char* moduleName = nullptr; Module* pModule = pMD->GetModule(); From dc19e4672d184ec0c2dfbbd9ced329d20a06dd88 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Mon, 27 Apr 2026 17:37:22 -0400 Subject: [PATCH 49/52] Address PR review feedback (round 2) Bundle of changes addressing reviewer feedback on the in-proc crash reporter (PR #126916). Highlights: - Group file-scope helpers in inproccrashreporter.cpp under a single CrashReportHelpers struct; drop static forward-declaration block. - Fold FormatHexValue / FormatUnsignedDecimal / FormatSignedDecimal helpers onto SignalSafeJsonWriter and add WriteHex / WriteDecimal / WriteSignedDecimal members; eliminate per-call scratch buffers across inproccrashreporter.cpp. - crashreportstackwalker.cpp: rename CrashReportRegisterStackWalker to CrashReportConfigure; have CrashReportSuspendThreads return the suspended state and pass it to CrashReportResumeThreads (drop the static s_runtimeSuspendedForCrashReport flag); wrap GetILOffset call in EX_TRY / EX_CATCH; add CONTRACTL annotations on FrameCallbackAdapter and CrashReportGetExceptionForThread. - PAL callback inversion: register the in-proc crash reporter as a PAL signal callback instead of having PAL know about the reporter type directly; gate the wiring with FEATURE_INPROC_CRASH_REPORT cmake define. - Various smaller review-feedback cleanups consistent with reviewer preferences (e.g. (void)param; for unused parameters). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/CMakeLists.txt | 7 +- src/coreclr/clrdefinitions.cmake | 4 + .../debug/crashreport/inproccrashreporter.cpp | 461 ++++++------------ .../debug/crashreport/inproccrashreporter.h | 7 + .../crashreport/signalsafejsonwriter.cpp | 170 ++++++- .../debug/crashreport/signalsafejsonwriter.h | 50 +- src/coreclr/pal/inc/pal.h | 16 + src/coreclr/pal/src/CMakeLists.txt | 7 +- src/coreclr/pal/src/include/pal/process.h | 5 - src/coreclr/pal/src/thread/process.cpp | 31 +- src/coreclr/vm/CMakeLists.txt | 6 +- src/coreclr/vm/ceemain.cpp | 2 +- src/coreclr/vm/crashreportstackwalker.cpp | 95 ++-- src/coreclr/vm/crashreportstackwalker.h | 2 +- 14 files changed, 453 insertions(+), 410 deletions(-) diff --git a/src/coreclr/CMakeLists.txt b/src/coreclr/CMakeLists.txt index 0f74561d551240..e4d05d1b4f27b4 100644 --- a/src/coreclr/CMakeLists.txt +++ b/src/coreclr/CMakeLists.txt @@ -109,12 +109,9 @@ if(CLR_CMAKE_HOST_UNIX) add_linker_flag(-Wl,-z,notext) endif() - # PAL consumes FEATURE_INPROC_CRASHREPORT, so the feature variable must be - # populated and the compile definition added before PAL is processed. + # FEATURE_INPROC_CRASHREPORT must be visible to PAL sources before + # clrdefinitions.cmake is processed (PAL is added before that include). include(clrfeatures.cmake) - if(FEATURE_INPROC_CRASHREPORT) - add_compile_definitions(FEATURE_INPROC_CRASHREPORT) - endif() add_subdirectory(pal) else() diff --git a/src/coreclr/clrdefinitions.cmake b/src/coreclr/clrdefinitions.cmake index 2b3137b35641a0..cc400c53543ce2 100644 --- a/src/coreclr/clrdefinitions.cmake +++ b/src/coreclr/clrdefinitions.cmake @@ -148,6 +148,10 @@ if(FEATURE_OBJCMARSHAL) add_compile_definitions(FEATURE_OBJCMARSHAL) endif() +if(FEATURE_INPROC_CRASHREPORT) + add_compile_definitions(FEATURE_INPROC_CRASHREPORT) +endif() + add_compile_definitions($<${FEATURE_JAVAMARSHAL}:FEATURE_JAVAMARSHAL>) add_compile_definitions($<$>>:FEATURE_PROFAPI_ATTACH_DETACH>) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index 1f56b19a12eac1..dee87349a75708 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -130,122 +130,82 @@ class CrashReportOutputContext bool m_writeFailed; }; -static -void -GetVersionString( - char* buffer, - size_t bufferSize); - -static -void -FormatHexValue( - char* buffer, - size_t bufferSize, - uint64_t value); - -static -size_t -FormatUnsignedDecimal( - char* buffer, - size_t bufferSize, - uint64_t value); +struct CrashReportHelpers +{ + static void GetVersionString( + char* buffer, + size_t bufferSize); -static -size_t -FormatSignedDecimal( - char* buffer, - size_t bufferSize, - int64_t value); + static bool AppendString( + char* buffer, + size_t bufferSize, + size_t* pos, + const char* value); -static -bool -AppendString( - char* buffer, - size_t bufferSize, - size_t* pos, - const char* value); + static void WriteRegistersToJson( + SignalSafeJsonWriter* writer, + void* context); -static -void -WriteRegistersToJson( - SignalSafeJsonWriter* writer, - void* context); + static uint64_t GetInstructionPointer( + void* context); -static -uint64_t -GetInstructionPointer( - void* context); + static uint64_t GetStackPointer( + void* context); -static -uint64_t -GetStackPointer( - void* context); + static uint64_t GetFramePointer( + void* context); -static -uint64_t -GetFramePointer( - void* context); + static void WriteCrashSiteFrameToJson( + SignalSafeJsonWriter* writer, + void* context); -static -void -WriteCrashSiteFrameToJson( - SignalSafeJsonWriter* writer, - void* context); + static void BuildMethodName( + char* buffer, + size_t bufferSize, + const char* className, + const char* methodName); -static -void -BuildMethodName( - char* buffer, - size_t bufferSize, - const char* className, - const char* methodName); + static const char* GetFilename( + const char* path); -static -const char* -GetFilename( - const char* path); + static void CopyString( + char* buffer, + size_t bufferSize, + const char* value); -static -void -CopyString( - char* buffer, - size_t bufferSize, - const char* value); + static bool TryGetProcessName( + char* filename, + size_t filenameLen); -static -bool -TryGetProcessName( - char* filename, - size_t filenameLen); + static void JsonFrameCallback( + 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 -void -JsonFrameCallback( - uint64_t ip, - uint64_t stackPointer, - const char* methodName, - const char* className, - const char* moduleName, - uint32_t nativeOffset, - uint32_t token, - uint32_t ilOffset, - uint32_t moduleTimestamp, - uint32_t moduleSize, - const char* moduleGuid, - void* ctx); + static bool WriteToFile( + int fd, + const char* buffer, + size_t len); -bool -WriteToFile( - int fd, - const char* buffer, - size_t len); + static bool BuildReportPath( + char* buffer, + size_t bufferSize, + const char* dumpPath); -static -bool -BuildReportPath( - char* buffer, - size_t bufferSize, - const char* dumpPath); + static size_t ExpandDumpTemplate( + char* buffer, + size_t bufferSize, + const char* pattern); +}; void InProcCrashReporter::CreateReport( @@ -262,7 +222,7 @@ InProcCrashReporter::CreateReport( char reportPath[CRASHREPORT_STRING_BUFFER_SIZE]; reportPath[0] = '\0'; - if (m_reportPath[0] == '\0' || !BuildReportPath(reportPath, sizeof(reportPath), m_reportPath)) + if (m_reportPath[0] == '\0' || !CrashReportHelpers::BuildReportPath(reportPath, sizeof(reportPath), m_reportPath)) { return; } @@ -301,8 +261,8 @@ InProcCrashReporter::CreateReport( #elif defined(__arm__) m_jsonWriter.WriteString("architecture", "arm"); #endif - char version[sizeof(sccsid) + 1]; - GetVersionString(version, sizeof(version)); + char version[sizeof(sccsid)]; + CrashReportHelpers::GetVersionString(version, sizeof(version)); m_jsonWriter.WriteString("version", version); m_jsonWriter.CloseObject(); // configuration @@ -311,9 +271,7 @@ InProcCrashReporter::CreateReport( m_jsonWriter.WriteString("process_name", m_processName); } - char pidBuf[16]; - (void)FormatUnsignedDecimal(pidBuf, sizeof(pidBuf), static_cast(GetCurrentProcessId())); - m_jsonWriter.WriteString("pid", pidBuf); + m_jsonWriter.WriteDecimal("pid", static_cast(GetCurrentProcessId())); m_jsonWriter.OpenArray("threads"); if (m_enumerateThreadsCallback != nullptr) @@ -339,9 +297,7 @@ InProcCrashReporter::CreateReport( m_jsonWriter.CloseObject(); // payload m_jsonWriter.OpenObject("parameters"); - char signalBuf[16]; - (void)FormatSignedDecimal(signalBuf, sizeof(signalBuf), static_cast(signal)); - m_jsonWriter.WriteString("signal", signalBuf); + m_jsonWriter.WriteSignedDecimal("signal", static_cast(signal)); m_jsonWriter.CloseObject(); // parameters m_jsonWriter.CloseObject(); // root @@ -350,7 +306,7 @@ InProcCrashReporter::CreateReport( { bool writeSucceeded = m_jsonWriter.Finish() && !outputContext.WriteFailed() && - WriteToFile(fd, "\n", 1); + CrashReportHelpers::WriteToFile(fd, "\n", 1); if (close(fd) != 0 || !writeSucceeded) { @@ -374,12 +330,29 @@ InProcCrashReporter::Initialize( m_walkStackCallback = settings.walkStackCallback; m_getExceptionCallback = settings.getExceptionCallback; m_enumerateThreadsCallback = settings.enumerateThreadsCallback; - CopyString(m_reportPath, sizeof(m_reportPath), settings.reportPath); - (void)TryGetProcessName(m_processName, sizeof(m_processName)); + CrashReportHelpers::CopyString(m_reportPath, sizeof(m_reportPath), settings.reportPath); + (void)CrashReportHelpers::TryGetProcessName(m_processName, sizeof(m_processName)); +} + +static void +InProcCrashReportSignalDispatcher(int signal, void* siginfo, void* context) +{ + InProcCrashReporter::GetInstance().CreateReport(signal, static_cast(siginfo), context); +} + +void +InProcCrashReportInitialize(const InProcCrashReporterSettings& settings) +{ + InProcCrashReporter::GetInstance().Initialize(settings); + + // Register last so PAL only observes the dispatcher after the reporter + // singleton is fully populated (mirrors the publication ordering used by + // PAL_SetLogManagedCallstackForSignalCallback). + PAL_SetInProcCrashReportCallback(&InProcCrashReportSignalDispatcher); } bool -WriteToFile( +CrashReportHelpers::WriteToFile( int fd, const char* buffer, size_t len) @@ -420,7 +393,7 @@ CrashReportOutputContext::HandleChunk( return false; } - if (!WriteToFile(m_fd, buffer, len)) + if (!CrashReportHelpers::WriteToFile(m_fd, buffer, len)) { m_writeFailed = true; return false; @@ -448,9 +421,8 @@ CrashReportOutputContext::ChunkCallback( // FormatDumpName: %% %p %d (PID). Other specifiers are passed through // literally since the remaining createdump patterns (%e, %h, %t) are not // meaningful for in-proc crash reports. -static size_t -ExpandDumpTemplate( +CrashReportHelpers::ExpandDumpTemplate( char* buffer, size_t bufferSize, const char* pattern) @@ -474,13 +446,17 @@ ExpandDumpTemplate( } else if (*pattern == 'p' || *pattern == 'd') { - char pidBuf[16]; - size_t pidLen = FormatUnsignedDecimal(pidBuf, sizeof(pidBuf), pid); - if (pidLen > 0 && pos + pidLen < bufferSize) + char pidBuf[CRASHREPORT_NUMBER_BUFFER_SIZE]; + size_t pidLen = SignalSafeJsonWriter::FormatUnsignedDecimal(pidBuf, sizeof(pidBuf), pid); + if (pidLen == 0 || pos + pidLen >= bufferSize) { - memcpy(buffer + pos, pidBuf, pidLen); - pos += pidLen; + // Not enough room to expand %p/%d; fail rather than emit + // a path missing the PID (which could collide with the + // dump file on disk). + return 0; } + memcpy(buffer + pos, pidBuf, pidLen); + pos += pidLen; } else { @@ -511,7 +487,7 @@ ExpandDumpTemplate( } bool -BuildReportPath( +CrashReportHelpers::BuildReportPath( char* buffer, size_t bufferSize, const char* dumpPath) @@ -541,7 +517,7 @@ BuildReportPath( } void -GetVersionString( +CrashReportHelpers::GetVersionString( char* buffer, size_t bufferSize) { @@ -567,125 +543,13 @@ GetVersionString( version += sizeof(versionPrefix) - 1; - size_t copied = strnlen(version, bufferSize - 2); + size_t copied = strnlen(version, bufferSize - 1); if (copied != 0) { memcpy(buffer, version, copied); } - buffer[copied] = ' '; - buffer[copied + 1] = '\0'; -} - -// Formats a 64-bit value as a lowercase hexadecimal C string with a "0x" -// prefix into |buffer|. The output is always null-terminated provided -// |bufferSize| > 0. This helper is async-signal-safe: it performs no -// allocation, locking, or TLS access. -void -FormatHexValue( - char* buffer, - size_t bufferSize, - uint64_t value) -{ - if (buffer == nullptr || bufferSize == 0) - { - return; - } - - if (bufferSize == 1) - { - buffer[0] = '\0'; - return; - } - - buffer[0] = '0'; - if (bufferSize == 2) - { - buffer[1] = '\0'; - return; - } - - buffer[1] = 'x'; - - char reverse[16]; - 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)); - - size_t index = 2; - while (reverseLength > 0 && index + 1 < bufferSize) - { - buffer[index++] = reverse[--reverseLength]; - } - buffer[index] = '\0'; -} - -// Formats an unsigned value as decimal into |buffer|. Returns the number of -// characters written (not counting the null terminator). Always -// null-terminates when bufferSize > 0. Async-signal-safe. -size_t -FormatUnsignedDecimal( - char* buffer, - size_t bufferSize, - uint64_t value) -{ - if (buffer == nullptr || bufferSize == 0) - { - return 0; - } - - char reverse[20]; // enough for UINT64_MAX - size_t reverseLength = 0; - do - { - reverse[reverseLength++] = static_cast('0' + (value % 10)); - value /= 10; - } while (value != 0 && reverseLength < sizeof(reverse)); - - size_t pos = 0; - while (reverseLength > 0 && pos + 1 < bufferSize) - { - buffer[pos++] = reverse[--reverseLength]; - } - buffer[pos] = '\0'; - return pos; -} - -// Formats a signed value as decimal into |buffer|. Returns the number of -// characters written (not counting the null terminator). Handles INT64_MIN -// correctly via unsigned negation. Always null-terminates when -// bufferSize > 0. Async-signal-safe. -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 < 2) - { - 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); - return written == 0 ? 0 : written + 1; + buffer[copied] = '\0'; } // Appends |value| to |buffer| at *|pos|, advancing *|pos|, while leaving @@ -693,7 +557,7 @@ FormatSignedDecimal( // bufferSize > 0. Returns true iff the full value was appended. // Async-signal-safe. bool -AppendString( +CrashReportHelpers::AppendString( char* buffer, size_t bufferSize, size_t* pos, @@ -715,30 +579,23 @@ AppendString( } void -WriteRegistersToJson( +CrashReportHelpers::WriteRegistersToJson( SignalSafeJsonWriter* writer, void* context) { uint64_t ipValue = GetInstructionPointer(context); uint64_t spValue = GetStackPointer(context); uint64_t bpValue = GetFramePointer(context); - char ip[CRASHREPORT_NUMBER_BUFFER_SIZE] = "0x0"; - char sp[CRASHREPORT_NUMBER_BUFFER_SIZE] = "0x0"; - char bp[CRASHREPORT_NUMBER_BUFFER_SIZE] = "0x0"; - - FormatHexValue(ip, sizeof(ip), ipValue); - FormatHexValue(sp, sizeof(sp), spValue); - FormatHexValue(bp, sizeof(bp), bpValue); writer->OpenObject("ctx"); - writer->WriteString("IP", ip); - writer->WriteString("SP", sp); - writer->WriteString("BP", bp); + writer->WriteHex("IP", ipValue); + writer->WriteHex("SP", spValue); + writer->WriteHex("BP", bpValue); writer->CloseObject(); // ctx } uint64_t -GetInstructionPointer( +CrashReportHelpers::GetInstructionPointer( void* context) { if (context == nullptr) @@ -759,7 +616,7 @@ GetInstructionPointer( } uint64_t -GetStackPointer( +CrashReportHelpers::GetStackPointer( void* context) { if (context == nullptr) @@ -780,7 +637,7 @@ GetStackPointer( } uint64_t -GetFramePointer( +CrashReportHelpers::GetFramePointer( void* context) { if (context == nullptr) @@ -801,27 +658,29 @@ GetFramePointer( } void -WriteCrashSiteFrameToJson( +CrashReportHelpers::WriteCrashSiteFrameToJson( SignalSafeJsonWriter* writer, void* context) { uint64_t ipValue = GetInstructionPointer(context); uint64_t spValue = GetStackPointer(context); - char ip[CRASHREPORT_NUMBER_BUFFER_SIZE] = "0x0"; - char sp[CRASHREPORT_NUMBER_BUFFER_SIZE] = "0x0"; - - FormatHexValue(ip, sizeof(ip), ipValue); - FormatHexValue(sp, sizeof(sp), spValue); writer->OpenObject(); + // Crash-site frame: IP/SP captured directly from the signal's saved + // ucontext_t. It is the instruction the OS interrupted (faulting user + // code, libc abort(), the JIT, etc.) - not a frame inside this reporter. + // Marked native because classifying an arbitrary IP as managed would + // require a JIT lookup we deliberately avoid in the signal handler; + // subsequent frames produced by the managed stack walker carry their + // own is_managed classification. writer->WriteString("is_managed", "false"); - writer->WriteString("stack_pointer", sp); - writer->WriteString("native_address", ip); + writer->WriteHex("stack_pointer", spValue); + writer->WriteHex("native_address", ipValue); writer->CloseObject(); // frame } void -BuildMethodName( +CrashReportHelpers::BuildMethodName( char* buffer, size_t bufferSize, const char* className, @@ -853,14 +712,19 @@ BuildMethodName( } } +// Returns the basename of a POSIX path (the substring after the last '/'). +// The in-proc reporter only consumes /proc paths, so a single POSIX separator +// is sufficient; if this ever needs to handle other separators, extend the +// loop to recognize them as well. const char* -GetFilename( +CrashReportHelpers::GetFilename( const char* path) { + constexpr char POSIX_PATH_SEPARATOR = '/'; const char* last = path; for (const char* p = path; *p != '\0'; p++) { - if (*p == '/') + if (*p == POSIX_PATH_SEPARATOR) { last = p + 1; } @@ -870,7 +734,7 @@ GetFilename( } void -CopyString( +CrashReportHelpers::CopyString( char* buffer, size_t bufferSize, const char* value) @@ -896,7 +760,7 @@ CopyString( } bool -TryGetProcessName( +CrashReportHelpers::TryGetProcessName( char* filename, size_t filenameLen) { @@ -938,7 +802,7 @@ TryGetProcessName( } void -JsonFrameCallback( +CrashReportHelpers::JsonFrameCallback( uint64_t ip, uint64_t stackPointer, const char* methodName, @@ -954,19 +818,10 @@ JsonFrameCallback( { SignalSafeJsonWriter* writer = reinterpret_cast(ctx); - // Reuse a single scratch buffer for hex formatting: WriteString copies the - // value into the writer's buffer before we format the next field, so we - // don't need one scratch buffer per hex field. Keeps the signal-handler - // stack footprint down. - char scratch[CRASHREPORT_NUMBER_BUFFER_SIZE]; - writer->OpenObject(); - FormatHexValue(scratch, sizeof(scratch), stackPointer); - writer->WriteString("stack_pointer", scratch); - FormatHexValue(scratch, sizeof(scratch), ip); - writer->WriteString("native_address", scratch); - FormatHexValue(scratch, sizeof(scratch), nativeOffset); - writer->WriteString("native_offset", scratch); + writer->WriteHex("stack_pointer", stackPointer); + writer->WriteHex("native_address", ip); + writer->WriteHex("native_offset", nativeOffset); if (methodName != nullptr) { @@ -974,23 +829,19 @@ JsonFrameCallback( BuildMethodName(fullName, sizeof(fullName), className, methodName); writer->WriteString("method_name", fullName); writer->WriteString("is_managed", "true"); - FormatHexValue(scratch, sizeof(scratch), token); - writer->WriteString("token", scratch); - FormatHexValue(scratch, sizeof(scratch), ilOffset); - writer->WriteString("il_offset", scratch); + writer->WriteHex("token", token); + writer->WriteHex("il_offset", ilOffset); if (moduleName != nullptr) { writer->WriteString("filename", moduleName); } if (moduleTimestamp != 0) { - FormatHexValue(scratch, sizeof(scratch), moduleTimestamp); - writer->WriteString("timestamp", scratch); + writer->WriteHex("timestamp", moduleTimestamp); } if (moduleSize != 0) { - FormatHexValue(scratch, sizeof(scratch), moduleSize); - writer->WriteString("sizeofimage", scratch); + writer->WriteHex("sizeofimage", moduleSize); } if (moduleGuid != nullptr && moduleGuid[0] != '\0') { @@ -1023,7 +874,7 @@ ThreadEnumerationContext::OnFrame( uint32_t moduleSize, const char* moduleGuid) { - JsonFrameCallback(ip, stackPointer, methodName, className, moduleName, nativeOffset, token, ilOffset, moduleTimestamp, moduleSize, moduleGuid, m_writer); + CrashReportHelpers::JsonFrameCallback(ip, stackPointer, methodName, className, moduleName, nativeOffset, token, ilOffset, moduleTimestamp, moduleSize, moduleGuid, m_writer); } void @@ -1068,35 +919,28 @@ ThreadEnumerationContext::OnThread( m_writer->OpenObject(); m_writer->WriteString("is_managed", "true"); m_writer->WriteString("crashed", isCrashThread ? "true" : "false"); - - char scratch[CRASHREPORT_NUMBER_BUFFER_SIZE]; - FormatHexValue(scratch, sizeof(scratch), osThreadId); - m_writer->WriteString("native_thread_id", scratch); + m_writer->WriteHex("native_thread_id", osThreadId); if (isCrashThread && m_hasCrashException) { - FormatHexValue(scratch, sizeof(scratch), m_crashExceptionHResult); - m_writer->WriteString("managed_exception_type", m_crashExceptionType); - m_writer->WriteString("managed_exception_hresult", scratch); + m_writer->WriteHex("managed_exception_hresult", m_crashExceptionHResult); } else if (exceptionType != nullptr && exceptionType[0] != '\0') { - FormatHexValue(scratch, sizeof(scratch), exceptionHResult); - m_writer->WriteString("managed_exception_type", exceptionType); - m_writer->WriteString("managed_exception_hresult", scratch); + m_writer->WriteHex("managed_exception_hresult", exceptionHResult); } if (isCrashThread) { - WriteRegistersToJson(m_writer, m_signalContext); + CrashReportHelpers::WriteRegistersToJson(m_writer, m_signalContext); } m_writer->OpenArray("stack_frames"); if (isCrashThread) { - WriteCrashSiteFrameToJson(m_writer, m_signalContext); + CrashReportHelpers::WriteCrashSiteFrameToJson(m_writer, m_signalContext); } } @@ -1142,25 +986,20 @@ InProcCrashReporter::EmitSynthesizedCrashThread( m_jsonWriter.WriteString("is_managed", m_isManagedThreadCallback != nullptr && m_isManagedThreadCallback() ? "true" : "false"); m_jsonWriter.WriteString("crashed", "true"); - - char scratch[CRASHREPORT_NUMBER_BUFFER_SIZE]; - FormatHexValue(scratch, sizeof(scratch), crashingTid); - m_jsonWriter.WriteString("native_thread_id", scratch); + m_jsonWriter.WriteHex("native_thread_id", crashingTid); if (hasException) { - FormatHexValue(scratch, sizeof(scratch), crashExceptionHResult); - m_jsonWriter.WriteString("managed_exception_type", crashExceptionType); - m_jsonWriter.WriteString("managed_exception_hresult", scratch); + m_jsonWriter.WriteHex("managed_exception_hresult", crashExceptionHResult); } - WriteRegistersToJson(&m_jsonWriter, context); + CrashReportHelpers::WriteRegistersToJson(&m_jsonWriter, context); m_jsonWriter.OpenArray("stack_frames"); - WriteCrashSiteFrameToJson(&m_jsonWriter, context); + CrashReportHelpers::WriteCrashSiteFrameToJson(&m_jsonWriter, context); if (walkStack && m_walkStackCallback != nullptr) { - m_walkStackCallback(JsonFrameCallback, &m_jsonWriter); + m_walkStackCallback(&CrashReportHelpers::JsonFrameCallback, &m_jsonWriter); } 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 b04099076b919a..351c43c34a7576 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.h +++ b/src/coreclr/debug/crashreport/inproccrashreporter.h @@ -100,3 +100,10 @@ class InProcCrashReporter char m_reportPath[CRASHREPORT_STRING_BUFFER_SIZE] = {}; char m_processName[CRASHREPORT_STRING_BUFFER_SIZE] = {}; }; + +// Free-function entry point used by the runtime to wire the in-proc crash +// reporter into the PAL signal-handler path. Captures `settings` into the +// singleton and registers a signal-safe dispatcher with PAL via +// PAL_SetInProcCrashReportCallback. PAL has no direct dependency on the +// reporter; the only coupling is through this registered callback. +void InProcCrashReportInitialize(const InProcCrashReporterSettings& settings); diff --git a/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp b/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp index 2728436a2d1980..0b8a29076119f5 100644 --- a/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp +++ b/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp @@ -1,10 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// Streaming JSON writer implementation for crash reports. - #include "signalsafejsonwriter.h" +#include #include static @@ -16,7 +15,7 @@ ToHexChar(unsigned value) void SignalSafeJsonWriter::Init( - CrashJsonOutputCallback outputCallback, + SignalSafeJsonOutputCallback outputCallback, void* outputContext) { m_pos = 0; @@ -26,7 +25,7 @@ SignalSafeJsonWriter::Init( m_outputContext = outputContext; } -void +bool SignalSafeJsonWriter::OpenObject( const char* key) { @@ -38,22 +37,24 @@ SignalSafeJsonWriter::OpenObject( } AppendChar('{'); m_commaNeeded = false; + return !m_writeFailed; } -void +bool SignalSafeJsonWriter::OpenObject() { - OpenObject(nullptr); + return OpenObject(nullptr); } -void +bool SignalSafeJsonWriter::CloseObject() { AppendChar('}'); m_commaNeeded = true; + return !m_writeFailed; } -void +bool SignalSafeJsonWriter::OpenArray( const char* key) { @@ -65,22 +66,24 @@ SignalSafeJsonWriter::OpenArray( } AppendChar('['); m_commaNeeded = false; + return !m_writeFailed; } -void +bool SignalSafeJsonWriter::OpenArray() { - OpenArray(nullptr); + return OpenArray(nullptr); } -void +bool SignalSafeJsonWriter::CloseArray() { AppendChar(']'); m_commaNeeded = true; + return !m_writeFailed; } -void +bool SignalSafeJsonWriter::WriteString( const char* key, const char* value) @@ -89,6 +92,7 @@ SignalSafeJsonWriter::WriteString( WriteEscapedString(key); AppendStr(": "); WriteEscapedString(value); + return !m_writeFailed; } bool @@ -145,7 +149,7 @@ SignalSafeJsonWriter::Append( } size_t offset = 0; - size_t remaining = CRASH_JSON_BUFFER_SIZE - m_pos; + size_t remaining = SIGNAL_SAFE_JSON_BUFFER_SIZE - m_pos; while (offset < len) { if (remaining == 0) @@ -154,7 +158,7 @@ SignalSafeJsonWriter::Append( { return false; } - remaining = CRASH_JSON_BUFFER_SIZE; + remaining = SIGNAL_SAFE_JSON_BUFFER_SIZE; } size_t chunk = len - offset; @@ -180,7 +184,7 @@ SignalSafeJsonWriter::AppendChar(char c) return false; } - if (m_pos == CRASH_JSON_BUFFER_SIZE && !Flush()) + if (m_pos == SIGNAL_SAFE_JSON_BUFFER_SIZE && !Flush()) { return false; } @@ -252,3 +256,139 @@ 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; + } + + if (bufferSize < MAX_HEX_FORMAT_BUFFER_SIZE) + { + buffer[0] = '\0'; + return; + } + + buffer[0] = '0'; + buffer[1] = 'x'; + + 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)); + + size_t index = 2; + 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; + } + + if (bufferSize < MAX_UNSIGNED_DECIMAL_BUFFER_SIZE) + { + buffer[0] = '\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)); + + 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 (bufferSize < MAX_SIGNED_DECIMAL_BUFFER_SIZE) + { + buffer[0] = '\0'; + return 0; + } + + if (value >= 0) + { + return FormatUnsignedDecimal(buffer, bufferSize, static_cast(value)); + } + + 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); + return written == 0 ? 0 : written + 1; +} + +bool +SignalSafeJsonWriter::WriteHex( + const char* key, + uint64_t value) +{ + char scratch[MAX_HEX_FORMAT_BUFFER_SIZE]; + FormatHexValue(scratch, sizeof(scratch), value); + return WriteString(key, scratch); +} + +bool +SignalSafeJsonWriter::WriteDecimal( + const char* key, + uint64_t value) +{ + char scratch[MAX_UNSIGNED_DECIMAL_BUFFER_SIZE]; + (void)FormatUnsignedDecimal(scratch, sizeof(scratch), value); + return WriteString(key, scratch); +} + +bool +SignalSafeJsonWriter::WriteSignedDecimal( + const char* key, + int64_t value) +{ + char scratch[MAX_SIGNED_DECIMAL_BUFFER_SIZE]; + (void)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 1ba22624c90eca..44ae1833ecf47f 100644 --- a/src/coreclr/debug/crashreport/signalsafejsonwriter.h +++ b/src/coreclr/debug/crashreport/signalsafejsonwriter.h @@ -1,24 +1,34 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// Bounded JSON writer for crash reports. +// Bounded, signal-safe JSON writer. // Streams content through a small fixed-size buffer using bounded low-level // string and memory operations so file output does not require materializing -// the whole report at once. All public members are async-signal-safe: no +// the whole document at once. All public members are async-signal-safe: no // heap allocation, no stdio, no locale or variadic formatting. #pragma once #include +#include -using CrashJsonOutputCallback = bool (*)(const char* buffer, size_t len, void* ctx); +using SignalSafeJsonOutputCallback = bool (*)(const char* buffer, size_t len, void* ctx); -// Small streaming buffer used when serializing the crash report JSON. -static constexpr size_t CRASH_JSON_BUFFER_SIZE = 4 * 1024; +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), @@ -31,17 +41,27 @@ class SignalSafeJsonWriter SignalSafeJsonWriter(const SignalSafeJsonWriter&) = delete; SignalSafeJsonWriter& operator=(const SignalSafeJsonWriter&) = delete; - void Init(CrashJsonOutputCallback outputCallback, void* outputContext); - void OpenObject(const char* key); - void OpenObject(); - void CloseObject(); - void OpenArray(const char* key); - void OpenArray(); - void CloseArray(); - void WriteString(const char* key, const char* value); + void Init(SignalSafeJsonOutputCallback outputCallback, void* outputContext); + bool OpenObject(const char* key); + bool OpenObject(); + bool CloseObject(); + bool OpenArray(const char* key); + bool OpenArray(); + bool CloseArray(); + bool WriteString(const char* key, const char* value); + bool WriteHex(const char* key, uint64_t value); + bool WriteDecimal(const char* key, uint64_t value); + bool WriteSignedDecimal(const char* key, int64_t value); 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); @@ -49,10 +69,10 @@ class SignalSafeJsonWriter void WriteSeparator(); void WriteEscapedString(const char* str); - char m_buffer[CRASH_JSON_BUFFER_SIZE]; + char m_buffer[SIGNAL_SAFE_JSON_BUFFER_SIZE]; size_t m_pos; bool m_commaNeeded; bool m_writeFailed; - CrashJsonOutputCallback m_outputCallback; + SignalSafeJsonOutputCallback m_outputCallback; void* m_outputContext; }; diff --git a/src/coreclr/pal/inc/pal.h b/src/coreclr/pal/inc/pal.h index d3db65876a2fa0..8c9f6c337dd8f7 100644 --- a/src/coreclr/pal/inc/pal.h +++ b/src/coreclr/pal/inc/pal.h @@ -264,6 +264,22 @@ PALAPI PAL_SetLogManagedCallstackForSignalCallback( IN PLOGMANAGEDCALLSTACKFORSIGNAL_CALLBACK callback); +#ifdef FEATURE_INPROC_CRASHREPORT +/// +/// Callback invoked from the fatal-signal path to write an in-proc crash +/// report. The callback runs inside the signal handler and must therefore +/// be async-signal-safe. siginfo is opaque (siginfo_t*) and context is the +/// raw ucontext_t pointer received by the PAL signal handler. +/// +typedef VOID (*PINPROCCRASHREPORT_CALLBACK)(int signal, void* siginfo, void* context); + +PALIMPORT +VOID +PALAPI +PAL_SetInProcCrashReportCallback( + IN PINPROCCRASHREPORT_CALLBACK callback); +#endif // FEATURE_INPROC_CRASHREPORT + PALIMPORT VOID PALAPI diff --git a/src/coreclr/pal/src/CMakeLists.txt b/src/coreclr/pal/src/CMakeLists.txt index 0d422ff87522be..6a51a318353c8c 100644 --- a/src/coreclr/pal/src/CMakeLists.txt +++ b/src/coreclr/pal/src/CMakeLists.txt @@ -37,9 +37,10 @@ endif(CORECLR_SET_RPATH) # Include directories include_directories(include) -if(CLR_CMAKE_TARGET_ANDROID) - include_directories(${CLR_DIR}) -endif(CLR_CMAKE_TARGET_ANDROID) +if(FEATURE_INPROC_CRASHREPORT) + add_compile_definitions(FEATURE_INPROC_CRASHREPORT) + include_directories(${CLR_DIR}/debug/crashreport) +endif() # Compile options diff --git a/src/coreclr/pal/src/include/pal/process.h b/src/coreclr/pal/src/include/pal/process.h index 95649546cf1f31..e3f26bde875a03 100644 --- a/src/coreclr/pal/src/include/pal/process.h +++ b/src/coreclr/pal/src/include/pal/process.h @@ -176,10 +176,5 @@ VOID PROCLogManagedCallstackForSignal(int signal); } #endif // __cplusplus -#ifdef FEATURE_INPROC_CRASHREPORT -struct InProcCrashReporterSettings; -void PROCInitializeInProcCrashReport(const InProcCrashReporterSettings& settings); -#endif - #endif //PAL_PROCESS_H_ diff --git a/src/coreclr/pal/src/thread/process.cpp b/src/coreclr/pal/src/thread/process.cpp index 5e2edc4bc0c7f7..a72aafcd972b99 100644 --- a/src/coreclr/pal/src/thread/process.cpp +++ b/src/coreclr/pal/src/thread/process.cpp @@ -35,10 +35,6 @@ SET_DEFAULT_DEBUG_CHANNEL(PROCESS); // some headers have code with asserts, so d #include #include -#ifdef FEATURE_INPROC_CRASHREPORT -#include "debug/crashreport/inproccrashreporter.h" -#endif - #include #if HAVE_POLL #include @@ -196,9 +192,11 @@ const char* g_argvCreateDump[MAX_ARGV_ENTRIES] = { nullptr }; #ifdef FEATURE_INPROC_CRASHREPORT // Read from the fatal-signal path (PROCCreateCrashDumpIfEnabled) and written -// once during startup (PROCInitializeInProcCrashReport); use Volatile to -// match the signal-path publication of g_logManagedCallstackForSignalCallback. -static Volatile g_inProcCrashReportEnabled = false; +// once during startup via PAL_SetInProcCrashReportCallback; use Volatile<> +// to match the publication ordering of g_logManagedCallstackForSignalCallback. +// PAL has no direct dependency on the in-proc crash reporter library; the +// reporter registers itself by installing this signal-safe callback. +static Volatile g_inProcCrashReportCallback = nullptr; #endif // @@ -2799,26 +2797,27 @@ PROCLogManagedCallstackForSignal(int signal) --*/ #ifdef FEATURE_INPROC_CRASHREPORT #include -void -PROCInitializeInProcCrashReport(const InProcCrashReporterSettings& settings) +VOID +PALAPI +PAL_SetInProcCrashReportCallback( + IN PINPROCCRASHREPORT_CALLBACK callback) { - InProcCrashReporter::GetInstance().Initialize(settings); - - // Publish last so PROCCreateCrashDumpIfEnabled only observes the reporter - // as enabled after the crashreport path (and any other state) is set. - g_inProcCrashReportEnabled = true; + _ASSERTE(g_inProcCrashReportCallback == nullptr); + g_inProcCrashReportCallback = callback; } VOID PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, void* context, bool serialize) { + (void)serialize; // Preserve context pointer to prevent optimization DoNotOptimize(&context); // TODO: Dump stress log into logcat and/or file when enabled? - if (g_inProcCrashReportEnabled) + PINPROCCRASHREPORT_CALLBACK callback = g_inProcCrashReportCallback; + if (callback != nullptr) { - InProcCrashReporter::GetInstance().CreateReport(signal, siginfo, context); + callback(signal, siginfo, context); } minipal_log_write_fatal("Aborting process.\n"); } diff --git a/src/coreclr/vm/CMakeLists.txt b/src/coreclr/vm/CMakeLists.txt index 606724e792a503..418d41572f51e5 100644 --- a/src/coreclr/vm/CMakeLists.txt +++ b/src/coreclr/vm/CMakeLists.txt @@ -4,9 +4,9 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) include_directories(BEFORE ${CMAKE_CURRENT_SOURCE_DIR}) include_directories(${ARCH_SOURCES_DIR}) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../interop/inc) -if(CLR_CMAKE_TARGET_ANDROID) - include_directories(${CLR_DIR}) -endif(CLR_CMAKE_TARGET_ANDROID) +if(FEATURE_INPROC_CRASHREPORT) + include_directories(${CLR_DIR}/debug/crashreport) +endif(FEATURE_INPROC_CRASHREPORT) include_directories(${CLR_SRC_NATIVE_DIR}) include_directories(${RUNTIME_DIR}) diff --git a/src/coreclr/vm/ceemain.cpp b/src/coreclr/vm/ceemain.cpp index 206965cea9bcb1..7f7bd7015d24a9 100644 --- a/src/coreclr/vm/ceemain.cpp +++ b/src/coreclr/vm/ceemain.cpp @@ -703,7 +703,7 @@ void EEStartupHelper() #endif // HOST_ANDROID #ifdef FEATURE_INPROC_CRASHREPORT - CrashReportRegisterStackWalker(); + CrashReportConfigure(); #endif // FEATURE_INPROC_CRASHREPORT #ifdef STRESS_LOG diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index b9d5f6ba54480b..3150537654255a 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -13,19 +13,17 @@ #ifdef FEATURE_INPROC_CRASHREPORT -#include "debug/crashreport/inproccrashreporter.h" +#include "inproccrashreporter.h" #include "threadsuspend.h" #include "gcenv.h" -void PROCInitializeInProcCrashReport(const InProcCrashReporterSettings& settings); - struct WalkContext { InProcCrashReportFrameCallback callback; void* userCtx; }; -static void BuildTypeName(char* buffer, size_t bufferSize, LPCUTF8 namespaceName, LPCUTF8 className); +static void BuildTypeName(LPUTF8 buffer, size_t bufferSize, LPCUTF8 namespaceName, LPCUTF8 className); static StackWalkAction @@ -33,6 +31,15 @@ FrameCallbackAdapter( CrawlFrame* pCF, VOID* pData) { + CONTRACTL + { + NOTHROW; + GC_NOTRIGGER; + CANNOT_TAKE_LOCK; + MODE_ANY; + } + CONTRACTL_END; + WalkContext* ctx = static_cast(pData); MethodDesc* pMD = pCF->GetFunction(); if (pMD == nullptr) @@ -56,10 +63,10 @@ FrameCallbackAdapter( } } - char classNameBuf[CRASHREPORT_STRING_BUFFER_SIZE] = { 0 }; + char classNameBuf[CRASHREPORT_STRING_BUFFER_SIZE]; BuildTypeName(classNameBuf, sizeof(classNameBuf), namespaceName, className); - const char* moduleName = nullptr; + LPCUTF8 moduleName = nullptr; Module* pModule = pMD->GetModule(); if (pModule != nullptr) { @@ -72,16 +79,16 @@ FrameCallbackAdapter( uint32_t nativeOffset = pCF->HasFaulted() ? 0 : pCF->GetRelOffset(); uint32_t ilOffset = 0; - uint64_t ip = 0; - uint64_t stackPointer = 0; + PCODE ip = (PCODE)0; + TADDR stackPointer = (TADDR)0; PREGDISPLAY pRD = pCF->GetRegisterSet(); if (pRD != nullptr) { - ip = static_cast(GetControlPC(pRD)); - stackPointer = static_cast(GetRegdisplaySP(pRD)); + ip = GetControlPC(pRD); + stackPointer = GetRegdisplaySP(pRD); } - if (ip == 0 && stackPointer == 0) + if (ip == (PCODE)0 && stackPointer == (TADDR)0) { return SWA_CONTINUE; } @@ -89,11 +96,22 @@ FrameCallbackAdapter( if (g_pDebugInterface != nullptr && pMD != nullptr) { DWORD resolvedILOffset = 0; - if (g_pDebugInterface->GetILOffsetFromNative( - pMD, - reinterpret_cast(static_cast(ip)), - nativeOffset, - &resolvedILOffset)) + BOOL haveILOffset = FALSE; + EX_TRY + { + haveILOffset = g_pDebugInterface->GetILOffsetFromNative( + pMD, + reinterpret_cast(ip), + nativeOffset, + &resolvedILOffset); + } + EX_CATCH + { + // Best-effort: if IL-offset resolution throws, leave ilOffset = 0 + // and continue with the native frame metadata we already have. + } + EX_END_CATCH + if (haveILOffset) { ilOffset = resolvedILOffset; } @@ -124,7 +142,8 @@ FrameCallbackAdapter( } } - ctx->callback(ip, stackPointer, methodName, classNameBuf, moduleName, nativeOffset, static_cast(token), ilOffset, moduleTimestamp, moduleSize, moduleGuid, ctx->userCtx); + className = classNameBuf[0] == '\0' ? nullptr : classNameBuf; + ctx->callback(static_cast(ip), static_cast(stackPointer), methodName, className, moduleName, nativeOffset, static_cast(token), ilOffset, moduleTimestamp, moduleSize, moduleGuid, ctx->userCtx); return SWA_CONTINUE; } @@ -166,7 +185,7 @@ CrashReportIsCurrentThreadManaged() static void BuildTypeName( - char* buffer, + LPUTF8 buffer, size_t bufferSize, LPCUTF8 namespaceName, LPCUTF8 className) @@ -209,6 +228,15 @@ CrashReportGetExceptionForThread( size_t exceptionTypeBufSize, uint32_t* hresult) { + CONTRACTL + { + NOTHROW; + GC_NOTRIGGER; + CANNOT_TAKE_LOCK; + MODE_ANY; + } + CONTRACTL_END; + if (exceptionTypeBufSize > 0) { exceptionTypeBuf[0] = '\0'; @@ -301,10 +329,9 @@ CrashReportGetException( // The crash reporter is best-effort; on hang the Android watchdog // kills the process and we keep whatever crash report JSON was flushed // beforehand. -static bool s_runtimeSuspendedForCrashReport = false; static -void +bool CrashReportSuspendThreads(Thread* pCrashThread) { if (g_fFatalErrorOccurredOnGCThread @@ -312,20 +339,19 @@ CrashReportSuspendThreads(Thread* pCrashThread) || IsGCSpecialThread() || ThreadStore::HoldingThreadStore(pCrashThread)) { - return; + return false; } ThreadSuspend::SuspendEE(ThreadSuspend::SUSPEND_OTHER); - s_runtimeSuspendedForCrashReport = true; + return true; } static void -CrashReportResumeThreads() +CrashReportResumeThreads(bool runtimeSuspended) { - if (s_runtimeSuspendedForCrashReport) + if (runtimeSuspended) { - s_runtimeSuspendedForCrashReport = false; ThreadSuspend::RestartEE(FALSE /* bFinishedGC */, TRUE /* SuspendSucceeded */); } } @@ -340,7 +366,7 @@ CrashReportEnumerateThreads( { Thread* pCrashThread = GetThreadAsyncSafe(); - CrashReportSuspendThreads(pCrashThread); + bool runtimeSuspended = CrashReportSuspendThreads(pCrashThread); // Emit the crashing thread first so the report keeps the most important // thread even if later enumeration is incomplete. @@ -362,7 +388,7 @@ CrashReportEnumerateThreads( // Walk the remaining managed threads only when the runtime was // successfully suspended; otherwise the walker is not guaranteed // to be at a safe point for them. - if (s_runtimeSuspendedForCrashReport) + if (runtimeSuspended) { Thread* pThread = nullptr; while ((pThread = ThreadStore::GetThreadList(pThread)) != nullptr) @@ -379,11 +405,11 @@ CrashReportEnumerateThreads( } } - CrashReportResumeThreads(); + CrashReportResumeThreads(runtimeSuspended); } void -CrashReportRegisterStackWalker() +CrashReportConfigure() { // Read crash report configuration here rather than in PROCAbortInitialize // because on Android the DOTNET_* environment variables are set via JNI @@ -411,7 +437,7 @@ CrashReportRegisterStackWalker() // If DbgMiniDumpName is just a filename (no directory component), write // the crash report under TMPDIR / /tmp so it lands somewhere writable. char dumpPathBuf[CRASHREPORT_STRING_BUFFER_SIZE]; - if (strchr(dumpName, '/') == nullptr) + if (strchr(dumpName, DIRECTORY_SEPARATOR_CHAR_A) == nullptr) { const char* tmpDir = getenv("TMPDIR"); if (tmpDir == nullptr || tmpDir[0] == '\0') @@ -419,7 +445,7 @@ CrashReportRegisterStackWalker() tmpDir = "/tmp"; } size_t tmpLen = strlen(tmpDir); - const char* separator = (tmpLen > 0 && tmpDir[tmpLen - 1] == '/') ? "" : "/"; + const char* separator = (tmpLen > 0 && tmpDir[tmpLen - 1] == DIRECTORY_SEPARATOR_CHAR_A) ? "" : DIRECTORY_SEPARATOR_STR_A; size_t sepLen = strlen(separator); size_t dumpLen = strlen(dumpName); if (tmpLen + sepLen + dumpLen + 1 > sizeof(dumpPathBuf)) @@ -440,10 +466,9 @@ CrashReportRegisterStackWalker() settings.getExceptionCallback = CrashReportGetException; settings.enumerateThreadsCallback = CrashReportEnumerateThreads; - // Initialize and enable the PAL side last so PROCCreateCrashDumpIfEnabled - // only observes the reporter as enabled after all VM callbacks are - // registered. - PROCInitializeInProcCrashReport(settings); + // Initialize the reporter and register the PAL signal-path callback last + // so PAL only observes the reporter after all VM callbacks are wired in. + InProcCrashReportInitialize(settings); } #endif // FEATURE_INPROC_CRASHREPORT diff --git a/src/coreclr/vm/crashreportstackwalker.h b/src/coreclr/vm/crashreportstackwalker.h index 45604dafd293df..7afa32eeb71f2f 100644 --- a/src/coreclr/vm/crashreportstackwalker.h +++ b/src/coreclr/vm/crashreportstackwalker.h @@ -6,7 +6,7 @@ #ifdef FEATURE_INPROC_CRASHREPORT -void CrashReportRegisterStackWalker(); +void CrashReportConfigure(); #endif // FEATURE_INPROC_CRASHREPORT From b689874d1d96548d531e2d0f195fe0cf9de00484 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Mon, 27 Apr 2026 17:43:11 -0400 Subject: [PATCH 50/52] Add minipal_get_tempdir helper and migrate VM crash reporter Three places in the runtime independently resolve the system temporary directory: g_get_tmp_dir (eglib), ep_rt_temp_path_get (EventPipe), and the new VM crash reporter added in this PR. lateralusX asked for these to converge on a shared signal-safe helper so cross-platform divergence (Unix TMPDIR/tmp vs Windows TMP/TEMP) and any future hardening live in one place. Add minipal_get_tempdir(buffer, size) under src/native/minipal: - Unix: probes \, falls back to "/tmp/". - Windows: probes %TMP%, then %TEMP%, falls back to "C:\\Temp\\". - Always writes a NUL-terminated path that ends with the platform separator, so callers can append a filename directly. - Uses only getenv / strlen / memcpy, matching the existing in-proc crash reporter contract (no allocation, no syscalls beyond getenv). Migrate the VM crash reporter caller in CrashReportConfigure to use the new helper. The other two pre-existing callers (g_get_tmp_dir, ep_rt_temp_path_get) are intentionally left for follow-up so this change stays scoped and low-risk for the in-proc crash reporter PR. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/crashreportstackwalker.cpp | 20 +++--- src/native/minipal/CMakeLists.txt | 1 + src/native/minipal/tempdir.c | 80 +++++++++++++++++++++++ src/native/minipal/tempdir.h | 42 ++++++++++++ 4 files changed, 131 insertions(+), 12 deletions(-) create mode 100644 src/native/minipal/tempdir.c create mode 100644 src/native/minipal/tempdir.h diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index 3150537654255a..bde6d735c740c9 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -10,6 +10,7 @@ #include "peassembly.h" #include #include +#include #ifdef FEATURE_INPROC_CRASHREPORT @@ -435,27 +436,22 @@ CrashReportConfigure() } // If DbgMiniDumpName is just a filename (no directory component), write - // the crash report under TMPDIR / /tmp so it lands somewhere writable. + // the crash report under the platform temp directory. char dumpPathBuf[CRASHREPORT_STRING_BUFFER_SIZE]; if (strchr(dumpName, DIRECTORY_SEPARATOR_CHAR_A) == nullptr) { - const char* tmpDir = getenv("TMPDIR"); - if (tmpDir == nullptr || tmpDir[0] == '\0') + if (!minipal_get_tempdir(dumpPathBuf, sizeof(dumpPathBuf))) { - tmpDir = "/tmp"; + return; } - size_t tmpLen = strlen(tmpDir); - const char* separator = (tmpLen > 0 && tmpDir[tmpLen - 1] == DIRECTORY_SEPARATOR_CHAR_A) ? "" : DIRECTORY_SEPARATOR_STR_A; - size_t sepLen = strlen(separator); + size_t tmpLen = strlen(dumpPathBuf); size_t dumpLen = strlen(dumpName); - if (tmpLen + sepLen + dumpLen + 1 > sizeof(dumpPathBuf)) + if (tmpLen + dumpLen + 1 > sizeof(dumpPathBuf)) { return; } - memcpy(dumpPathBuf, tmpDir, tmpLen); - memcpy(dumpPathBuf + tmpLen, separator, sepLen); - memcpy(dumpPathBuf + tmpLen + sepLen, dumpName, dumpLen); - dumpPathBuf[tmpLen + sepLen + dumpLen] = '\0'; + memcpy(dumpPathBuf + tmpLen, dumpName, dumpLen); + dumpPathBuf[tmpLen + dumpLen] = '\0'; dumpName = dumpPathBuf; } diff --git a/src/native/minipal/CMakeLists.txt b/src/native/minipal/CMakeLists.txt index d7f9ad5e2ab78a..9977bbb2912f43 100644 --- a/src/native/minipal/CMakeLists.txt +++ b/src/native/minipal/CMakeLists.txt @@ -9,6 +9,7 @@ set(SOURCES random.c debugger.c strings.c + tempdir.c time.c unicodedata.c utf8.c diff --git a/src/native/minipal/tempdir.c b/src/native/minipal/tempdir.c new file mode 100644 index 00000000000000..48b15d96d3c51e --- /dev/null +++ b/src/native/minipal/tempdir.c @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include +#include +#include +#include + +#include "tempdir.h" + +#ifdef _WIN32 +#define MINIPAL_TEMP_SEPARATOR '\\' +#define MINIPAL_TEMP_FALLBACK "C:\\Temp\\" +#else +#define MINIPAL_TEMP_SEPARATOR '/' +#define MINIPAL_TEMP_FALLBACK "/tmp/" +#endif + +static bool minipal_write_tempdir(char* buffer, size_t buffer_size, const char* value) +{ + size_t valueLen = strlen(value); + if (valueLen == 0) + { + return false; + } + + bool needsSeparator = value[valueLen - 1] != MINIPAL_TEMP_SEPARATOR; + size_t required = valueLen + (needsSeparator ? 1u : 0u) + 1u; // +1 for NUL + if (required > buffer_size) + { + return false; + } + + memcpy(buffer, value, valueLen); + if (needsSeparator) + { + buffer[valueLen] = MINIPAL_TEMP_SEPARATOR; + buffer[valueLen + 1] = '\0'; + } + else + { + buffer[valueLen] = '\0'; + } + + return true; +} + +bool minipal_get_tempdir(char* buffer, size_t buffer_size) +{ + if (buffer == NULL || buffer_size == 0) + { + return false; + } + + buffer[0] = '\0'; + + static const char* const envVars[] = + { +#ifdef _WIN32 + "TMP", + "TEMP", +#else + "TMPDIR", +#endif + }; + + for (size_t i = 0; i < sizeof(envVars) / sizeof(envVars[0]); ++i) + { + const char* value = getenv(envVars[i]); + if (value != NULL && value[0] != '\0') + { + if (minipal_write_tempdir(buffer, buffer_size, value)) + { + return true; + } + } + } + + return minipal_write_tempdir(buffer, buffer_size, MINIPAL_TEMP_FALLBACK); +} diff --git a/src/native/minipal/tempdir.h b/src/native/minipal/tempdir.h new file mode 100644 index 00000000000000..5dea032b8f5f42 --- /dev/null +++ b/src/native/minipal/tempdir.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. + +#ifndef HAVE_MINIPAL_TEMPDIR_H +#define HAVE_MINIPAL_TEMPDIR_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Resolve the system temporary directory and write it to @p buffer with a + * trailing platform path separator and NUL terminator. + * + * The lookup order is: + * - Unix: $TMPDIR, otherwise "/tmp/" + * - Windows: %TMP%, then %TEMP%, otherwise "C:\\Temp\\" + * + * The implementation only calls getenv/strlen/memcpy and does no + * heap allocation. POSIX does not list getenv as async-signal-safe, + * so callers reaching this helper from a signal-handler path inherit + * the same in-practice constraints as a direct getenv call (the + * in-proc crash reporter already relies on this). + * + * @param buffer Destination buffer; must be non-NULL. + * @param buffer_size Size of @p buffer in bytes. + * @return true on success; false if @p buffer is NULL, @p buffer_size + * is zero, or the resolved directory plus separator and NUL + * does not fit. On failure @p buffer is left NUL-terminated + * (when @p buffer_size > 0) so callers can fall back without + * reading uninitialized memory. + */ +bool minipal_get_tempdir(char* buffer, size_t buffer_size); + +#ifdef __cplusplus +} +#endif + +#endif // HAVE_MINIPAL_TEMPDIR_H From 72361c05cfe09bf204df785e06943f5d45d9790b Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 28 Apr 2026 13:07:10 -0400 Subject: [PATCH 51/52] Address PR review feedback - Drop the minipal_get_tempdir helper. Resolving a temp directory at signal time is security-sensitive (open-coded TMPDIR/GetTempPath probing has historically been a pitfall, and Android's java.io.tmpdir is not reachable from a signal handler). Trust whatever DbgMiniDumpName the host configured and let fopen decide; this matches createdump's contract for the same setting. - Switch CrashReportHelpers from struct to class to match the established coreclr convention for stateless helper bags (StubHelpers, DynamicHelpers, ImportHelper, UnwindHelpers, etc.). - Capture the process name during reporter init via minipal_getexepath instead of opening /proc/self/cmdline at signal time. Removes the Linux-only TryGetProcessName helper and lets the same code path serve Apple/FreeBSD/Solaris/Haiku/Windows when the feature is ported. - Replace the POSIX_PATH_SEPARATOR alias in inproccrashreporter.cpp with a platform-conditional CRASHREPORT_DIRECTORY_SEPARATOR constant so a future Windows port flips a single branch rather than introducing a new name. - Rename SignalSafeJsonWriter::Write{Hex,Decimal,SignedDecimal} to *AsString. The writer always emits numeric values as quoted JSON strings (matching createdumps WriteValue32/64 byte-for-byte); the new names telegraph that contract instead of suggesting raw JSON numerics. - Drop the dead include_directories(${CLR_DIR}/debug/crashreport) from pal/src/CMakeLists.txt. After the PAL -> reporter callback inversion no PAL source includes any header from that directory. - Drop the redundant include(clrfeatures.cmake) and its load-order comment from src/coreclr/CMakeLists.txt. Once add_compile_definitions(FEATURE_INPROC_CRASHREPORT) was moved into clrdefinitions.cmake (next to the other feature flags), nothing in the top-level CMakeLists reads FEATURE_INPROC_CRASHREPORT before add_subdirectory(pal); PALs own CMakeLists already includes ../clrfeatures.cmake. - Simplify CrashReportResumeThreads to take no arguments and unconditionally restart the EE. Push the "did we successfully suspend?" check up to the single call site, and fold it together with the other-threads enumeration loop so suspend / walk-others / resume share a single guarded block. --- src/coreclr/CMakeLists.txt | 4 - .../debug/crashreport/inproccrashreporter.cpp | 114 +++++++----------- .../crashreport/signalsafejsonwriter.cpp | 6 +- .../debug/crashreport/signalsafejsonwriter.h | 6 +- src/coreclr/pal/src/CMakeLists.txt | 1 - src/coreclr/vm/crashreportstackwalker.cpp | 32 +---- src/native/minipal/CMakeLists.txt | 1 - src/native/minipal/tempdir.c | 80 ------------ src/native/minipal/tempdir.h | 42 ------- 9 files changed, 51 insertions(+), 235 deletions(-) delete mode 100644 src/native/minipal/tempdir.c delete mode 100644 src/native/minipal/tempdir.h diff --git a/src/coreclr/CMakeLists.txt b/src/coreclr/CMakeLists.txt index e4d05d1b4f27b4..3806f9319c27f7 100644 --- a/src/coreclr/CMakeLists.txt +++ b/src/coreclr/CMakeLists.txt @@ -109,10 +109,6 @@ if(CLR_CMAKE_HOST_UNIX) add_linker_flag(-Wl,-z,notext) endif() - # FEATURE_INPROC_CRASHREPORT must be visible to PAL sources before - # clrdefinitions.cmake is processed (PAL is added before that include). - include(clrfeatures.cmake) - add_subdirectory(pal) else() if(CLR_CMAKE_TARGET_UNIX) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index dee87349a75708..52cbd0d255ab3b 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include // Include the .NET version string instead of linking because it is "static". @@ -130,8 +131,9 @@ class CrashReportOutputContext bool m_writeFailed; }; -struct CrashReportHelpers +class CrashReportHelpers { +public: static void GetVersionString( char* buffer, size_t bufferSize); @@ -173,10 +175,6 @@ struct CrashReportHelpers size_t bufferSize, const char* value); - static bool TryGetProcessName( - char* filename, - size_t filenameLen); - static void JsonFrameCallback( uint64_t ip, uint64_t stackPointer, @@ -271,7 +269,7 @@ InProcCrashReporter::CreateReport( m_jsonWriter.WriteString("process_name", m_processName); } - m_jsonWriter.WriteDecimal("pid", static_cast(GetCurrentProcessId())); + m_jsonWriter.WriteDecimalAsString("pid", static_cast(GetCurrentProcessId())); m_jsonWriter.OpenArray("threads"); if (m_enumerateThreadsCallback != nullptr) @@ -297,7 +295,7 @@ InProcCrashReporter::CreateReport( m_jsonWriter.CloseObject(); // payload m_jsonWriter.OpenObject("parameters"); - m_jsonWriter.WriteSignedDecimal("signal", static_cast(signal)); + m_jsonWriter.WriteSignedDecimalAsString("signal", static_cast(signal)); m_jsonWriter.CloseObject(); // parameters m_jsonWriter.CloseObject(); // root @@ -331,7 +329,13 @@ InProcCrashReporter::Initialize( m_getExceptionCallback = settings.getExceptionCallback; m_enumerateThreadsCallback = settings.enumerateThreadsCallback; CrashReportHelpers::CopyString(m_reportPath, sizeof(m_reportPath), settings.reportPath); - (void)CrashReportHelpers::TryGetProcessName(m_processName, sizeof(m_processName)); + + m_processName[0] = '\0'; + if (char* exePath = minipal_getexepath()) + { + CrashReportHelpers::CopyString(m_processName, sizeof(m_processName), CrashReportHelpers::GetFilename(exePath)); + free(exePath); + } } static void @@ -588,9 +592,9 @@ CrashReportHelpers::WriteRegistersToJson( uint64_t bpValue = GetFramePointer(context); writer->OpenObject("ctx"); - writer->WriteHex("IP", ipValue); - writer->WriteHex("SP", spValue); - writer->WriteHex("BP", bpValue); + writer->WriteHexAsString("IP", ipValue); + writer->WriteHexAsString("SP", spValue); + writer->WriteHexAsString("BP", bpValue); writer->CloseObject(); // ctx } @@ -674,8 +678,8 @@ CrashReportHelpers::WriteCrashSiteFrameToJson( // subsequent frames produced by the managed stack walker carry their // own is_managed classification. writer->WriteString("is_managed", "false"); - writer->WriteHex("stack_pointer", spValue); - writer->WriteHex("native_address", ipValue); + writer->WriteHexAsString("stack_pointer", spValue); + writer->WriteHexAsString("native_address", ipValue); writer->CloseObject(); // frame } @@ -712,19 +716,25 @@ CrashReportHelpers::BuildMethodName( } } -// Returns the basename of a POSIX path (the substring after the last '/'). -// The in-proc reporter only consumes /proc paths, so a single POSIX separator -// is sufficient; if this ever needs to handle other separators, extend the -// loop to recognize them as well. +// Returns the basename of a path (the substring after the last directory +// separator). The crash reporter is currently Unix-only via +// FEATURE_INPROC_CRASHREPORT gating, but a future Windows port would need +// a different separator; expose a platform-conditional constant so callers +// don't have to change. +#if defined(_WIN32) +static constexpr char CRASHREPORT_DIRECTORY_SEPARATOR = '\\'; +#else +static constexpr char CRASHREPORT_DIRECTORY_SEPARATOR = '/'; +#endif + const char* CrashReportHelpers::GetFilename( const char* path) { - constexpr char POSIX_PATH_SEPARATOR = '/'; const char* last = path; for (const char* p = path; *p != '\0'; p++) { - if (*p == POSIX_PATH_SEPARATOR) + if (*p == CRASHREPORT_DIRECTORY_SEPARATOR) { last = p + 1; } @@ -759,48 +769,6 @@ CrashReportHelpers::CopyString( buffer[copied] = '\0'; } -bool -CrashReportHelpers::TryGetProcessName( - char* filename, - size_t filenameLen) -{ - if (filename == nullptr || filenameLen == 0) - { - return false; - } - - filename[0] = '\0'; - - char scratch[CRASHREPORT_STRING_BUFFER_SIZE]; - - int fd = open("/proc/self/cmdline", O_RDONLY); - if (fd != -1) - { - ssize_t bytesRead = read(fd, scratch, sizeof(scratch) - 1); - close(fd); - - if (bytesRead > 0) - { - scratch[bytesRead] = '\0'; - CopyString(filename, filenameLen, GetFilename(scratch)); - if (filename[0] != '\0') - { - return true; - } - } - } - - ssize_t pathLength = readlink("/proc/self/exe", scratch, sizeof(scratch) - 1); - if (pathLength > 0) - { - scratch[pathLength] = '\0'; - CopyString(filename, filenameLen, GetFilename(scratch)); - return filename[0] != '\0'; - } - - return false; -} - void CrashReportHelpers::JsonFrameCallback( uint64_t ip, @@ -819,9 +787,9 @@ CrashReportHelpers::JsonFrameCallback( SignalSafeJsonWriter* writer = reinterpret_cast(ctx); writer->OpenObject(); - writer->WriteHex("stack_pointer", stackPointer); - writer->WriteHex("native_address", ip); - writer->WriteHex("native_offset", nativeOffset); + writer->WriteHexAsString("stack_pointer", stackPointer); + writer->WriteHexAsString("native_address", ip); + writer->WriteHexAsString("native_offset", nativeOffset); if (methodName != nullptr) { @@ -829,19 +797,19 @@ CrashReportHelpers::JsonFrameCallback( BuildMethodName(fullName, sizeof(fullName), className, methodName); writer->WriteString("method_name", fullName); writer->WriteString("is_managed", "true"); - writer->WriteHex("token", token); - writer->WriteHex("il_offset", ilOffset); + writer->WriteHexAsString("token", token); + writer->WriteHexAsString("il_offset", ilOffset); if (moduleName != nullptr) { writer->WriteString("filename", moduleName); } if (moduleTimestamp != 0) { - writer->WriteHex("timestamp", moduleTimestamp); + writer->WriteHexAsString("timestamp", moduleTimestamp); } if (moduleSize != 0) { - writer->WriteHex("sizeofimage", moduleSize); + writer->WriteHexAsString("sizeofimage", moduleSize); } if (moduleGuid != nullptr && moduleGuid[0] != '\0') { @@ -919,17 +887,17 @@ ThreadEnumerationContext::OnThread( m_writer->OpenObject(); m_writer->WriteString("is_managed", "true"); m_writer->WriteString("crashed", isCrashThread ? "true" : "false"); - m_writer->WriteHex("native_thread_id", osThreadId); + m_writer->WriteHexAsString("native_thread_id", osThreadId); if (isCrashThread && m_hasCrashException) { m_writer->WriteString("managed_exception_type", m_crashExceptionType); - m_writer->WriteHex("managed_exception_hresult", m_crashExceptionHResult); + m_writer->WriteHexAsString("managed_exception_hresult", m_crashExceptionHResult); } else if (exceptionType != nullptr && exceptionType[0] != '\0') { m_writer->WriteString("managed_exception_type", exceptionType); - m_writer->WriteHex("managed_exception_hresult", exceptionHResult); + m_writer->WriteHexAsString("managed_exception_hresult", exceptionHResult); } if (isCrashThread) @@ -986,12 +954,12 @@ InProcCrashReporter::EmitSynthesizedCrashThread( m_jsonWriter.WriteString("is_managed", m_isManagedThreadCallback != nullptr && m_isManagedThreadCallback() ? "true" : "false"); m_jsonWriter.WriteString("crashed", "true"); - m_jsonWriter.WriteHex("native_thread_id", crashingTid); + m_jsonWriter.WriteHexAsString("native_thread_id", crashingTid); if (hasException) { m_jsonWriter.WriteString("managed_exception_type", crashExceptionType); - m_jsonWriter.WriteHex("managed_exception_hresult", crashExceptionHResult); + m_jsonWriter.WriteHexAsString("managed_exception_hresult", crashExceptionHResult); } CrashReportHelpers::WriteRegistersToJson(&m_jsonWriter, context); diff --git a/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp b/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp index 0b8a29076119f5..4bed326d7cbcda 100644 --- a/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp +++ b/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp @@ -364,7 +364,7 @@ SignalSafeJsonWriter::FormatSignedDecimal( } bool -SignalSafeJsonWriter::WriteHex( +SignalSafeJsonWriter::WriteHexAsString( const char* key, uint64_t value) { @@ -374,7 +374,7 @@ SignalSafeJsonWriter::WriteHex( } bool -SignalSafeJsonWriter::WriteDecimal( +SignalSafeJsonWriter::WriteDecimalAsString( const char* key, uint64_t value) { @@ -384,7 +384,7 @@ SignalSafeJsonWriter::WriteDecimal( } bool -SignalSafeJsonWriter::WriteSignedDecimal( +SignalSafeJsonWriter::WriteSignedDecimalAsString( const char* key, int64_t value) { diff --git a/src/coreclr/debug/crashreport/signalsafejsonwriter.h b/src/coreclr/debug/crashreport/signalsafejsonwriter.h index 44ae1833ecf47f..54eac5dbf6d30d 100644 --- a/src/coreclr/debug/crashreport/signalsafejsonwriter.h +++ b/src/coreclr/debug/crashreport/signalsafejsonwriter.h @@ -49,9 +49,9 @@ class SignalSafeJsonWriter bool OpenArray(); bool CloseArray(); bool WriteString(const char* key, const char* value); - bool WriteHex(const char* key, uint64_t value); - bool WriteDecimal(const char* key, uint64_t value); - bool WriteSignedDecimal(const char* key, int64_t value); + bool WriteHexAsString(const char* key, uint64_t value); + bool WriteDecimalAsString(const char* key, uint64_t value); + bool WriteSignedDecimalAsString(const char* key, int64_t value); bool Finish(); bool Flush(); diff --git a/src/coreclr/pal/src/CMakeLists.txt b/src/coreclr/pal/src/CMakeLists.txt index 6a51a318353c8c..0933924a2a7f88 100644 --- a/src/coreclr/pal/src/CMakeLists.txt +++ b/src/coreclr/pal/src/CMakeLists.txt @@ -39,7 +39,6 @@ endif(CORECLR_SET_RPATH) include_directories(include) if(FEATURE_INPROC_CRASHREPORT) add_compile_definitions(FEATURE_INPROC_CRASHREPORT) - include_directories(${CLR_DIR}/debug/crashreport) endif() # Compile options diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index bde6d735c740c9..22391429424048 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -10,7 +10,6 @@ #include "peassembly.h" #include #include -#include #ifdef FEATURE_INPROC_CRASHREPORT @@ -349,12 +348,9 @@ CrashReportSuspendThreads(Thread* pCrashThread) static void -CrashReportResumeThreads(bool runtimeSuspended) +CrashReportResumeThreads() { - if (runtimeSuspended) - { - ThreadSuspend::RestartEE(FALSE /* bFinishedGC */, TRUE /* SuspendSucceeded */); - } + ThreadSuspend::RestartEE(FALSE /* bFinishedGC */, TRUE /* SuspendSucceeded */); } static @@ -404,9 +400,9 @@ CrashReportEnumerateThreads( threadCallback(osThreadId, false, "", 0, ctx); CrashReportWalkThread(pThread, frameCallback, ctx); } - } - CrashReportResumeThreads(runtimeSuspended); + CrashReportResumeThreads(); + } } void @@ -435,26 +431,6 @@ CrashReportConfigure() return; } - // If DbgMiniDumpName is just a filename (no directory component), write - // the crash report under the platform temp directory. - char dumpPathBuf[CRASHREPORT_STRING_BUFFER_SIZE]; - if (strchr(dumpName, DIRECTORY_SEPARATOR_CHAR_A) == nullptr) - { - if (!minipal_get_tempdir(dumpPathBuf, sizeof(dumpPathBuf))) - { - return; - } - size_t tmpLen = strlen(dumpPathBuf); - size_t dumpLen = strlen(dumpName); - if (tmpLen + dumpLen + 1 > sizeof(dumpPathBuf)) - { - return; - } - memcpy(dumpPathBuf + tmpLen, dumpName, dumpLen); - dumpPathBuf[tmpLen + dumpLen] = '\0'; - dumpName = dumpPathBuf; - } - InProcCrashReporterSettings settings = {}; settings.reportPath = dumpName; settings.isManagedThreadCallback = CrashReportIsCurrentThreadManaged; diff --git a/src/native/minipal/CMakeLists.txt b/src/native/minipal/CMakeLists.txt index 9977bbb2912f43..d7f9ad5e2ab78a 100644 --- a/src/native/minipal/CMakeLists.txt +++ b/src/native/minipal/CMakeLists.txt @@ -9,7 +9,6 @@ set(SOURCES random.c debugger.c strings.c - tempdir.c time.c unicodedata.c utf8.c diff --git a/src/native/minipal/tempdir.c b/src/native/minipal/tempdir.c deleted file mode 100644 index 48b15d96d3c51e..00000000000000 --- a/src/native/minipal/tempdir.c +++ /dev/null @@ -1,80 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#include -#include -#include -#include - -#include "tempdir.h" - -#ifdef _WIN32 -#define MINIPAL_TEMP_SEPARATOR '\\' -#define MINIPAL_TEMP_FALLBACK "C:\\Temp\\" -#else -#define MINIPAL_TEMP_SEPARATOR '/' -#define MINIPAL_TEMP_FALLBACK "/tmp/" -#endif - -static bool minipal_write_tempdir(char* buffer, size_t buffer_size, const char* value) -{ - size_t valueLen = strlen(value); - if (valueLen == 0) - { - return false; - } - - bool needsSeparator = value[valueLen - 1] != MINIPAL_TEMP_SEPARATOR; - size_t required = valueLen + (needsSeparator ? 1u : 0u) + 1u; // +1 for NUL - if (required > buffer_size) - { - return false; - } - - memcpy(buffer, value, valueLen); - if (needsSeparator) - { - buffer[valueLen] = MINIPAL_TEMP_SEPARATOR; - buffer[valueLen + 1] = '\0'; - } - else - { - buffer[valueLen] = '\0'; - } - - return true; -} - -bool minipal_get_tempdir(char* buffer, size_t buffer_size) -{ - if (buffer == NULL || buffer_size == 0) - { - return false; - } - - buffer[0] = '\0'; - - static const char* const envVars[] = - { -#ifdef _WIN32 - "TMP", - "TEMP", -#else - "TMPDIR", -#endif - }; - - for (size_t i = 0; i < sizeof(envVars) / sizeof(envVars[0]); ++i) - { - const char* value = getenv(envVars[i]); - if (value != NULL && value[0] != '\0') - { - if (minipal_write_tempdir(buffer, buffer_size, value)) - { - return true; - } - } - } - - return minipal_write_tempdir(buffer, buffer_size, MINIPAL_TEMP_FALLBACK); -} diff --git a/src/native/minipal/tempdir.h b/src/native/minipal/tempdir.h deleted file mode 100644 index 5dea032b8f5f42..00000000000000 --- a/src/native/minipal/tempdir.h +++ /dev/null @@ -1,42 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#ifndef HAVE_MINIPAL_TEMPDIR_H -#define HAVE_MINIPAL_TEMPDIR_H - -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * Resolve the system temporary directory and write it to @p buffer with a - * trailing platform path separator and NUL terminator. - * - * The lookup order is: - * - Unix: $TMPDIR, otherwise "/tmp/" - * - Windows: %TMP%, then %TEMP%, otherwise "C:\\Temp\\" - * - * The implementation only calls getenv/strlen/memcpy and does no - * heap allocation. POSIX does not list getenv as async-signal-safe, - * so callers reaching this helper from a signal-handler path inherit - * the same in-practice constraints as a direct getenv call (the - * in-proc crash reporter already relies on this). - * - * @param buffer Destination buffer; must be non-NULL. - * @param buffer_size Size of @p buffer in bytes. - * @return true on success; false if @p buffer is NULL, @p buffer_size - * is zero, or the resolved directory plus separator and NUL - * does not fit. On failure @p buffer is left NUL-terminated - * (when @p buffer_size > 0) so callers can fall back without - * reading uninitialized memory. - */ -bool minipal_get_tempdir(char* buffer, size_t buffer_size); - -#ifdef __cplusplus -} -#endif - -#endif // HAVE_MINIPAL_TEMPDIR_H From 239875fed97db166e0fb769f8ca0f169651f6f44 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 28 Apr 2026 14:39:37 -0400 Subject: [PATCH 52/52] Address PR review feedback - Stop propagating FEATURE_INPROC_CRASHREPORT into the PAL. PAL's only coupling to the reporter is the optional signal-safe callback the reporter registers via PAL_SetInProcCrashReportCallback. Make the callback ABI and dispatch unconditional in PAL: if a callback is registered, invoke it; otherwise fall back to the existing out-of-proc createdump path. This drops every #ifdef FEATURE_INPROC_CRASHREPORT inside PAL plus the duplicate add_compile_definitions in pal/src/CMakeLists.txt; the feature flag's only remaining role is gating the host-side reporter library under src/coreclr/debug/crashreport. Also preserves createdump on builds where the feature is enabled but no in-proc callback gets registered. - Drop the absolute-path requirement on DbgMiniDumpName in CrashReportConfigure. Whatever the host configured is what we try to write to; if open() fails, the report is silently skipped. This matches createdump's contract for the same setting (createdump just fopen's whatever path comes out of FormatDumpName). - On Android, prefer /proc/self/cmdline over /proc/self/exe for process_name. Every Android app forks from the Zygote, so /proc/self/exe always resolves to /system/bin/app_process64; /proc/self/cmdline holds the package name set by ActivityThread via PR_SET_NAME / setproctitle, which is what crash diagnostics actually want. Other platforms keep using minipal_getexepath unchanged. - Simplify CrashReportEnumerateThreads: fold the other-threads enumeration loop and the resume-EE call into a single if (runtimeSuspended) block. --- .../debug/crashreport/inproccrashreporter.cpp | 28 +++++++++++++++++-- src/coreclr/pal/inc/pal.h | 7 +++-- src/coreclr/pal/src/CMakeLists.txt | 3 -- src/coreclr/pal/src/thread/process.cpp | 28 ++++++++----------- 4 files changed, 41 insertions(+), 25 deletions(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index 52cbd0d255ab3b..3bb01266900f81 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -331,10 +331,32 @@ InProcCrashReporter::Initialize( CrashReportHelpers::CopyString(m_reportPath, sizeof(m_reportPath), settings.reportPath); m_processName[0] = '\0'; - if (char* exePath = minipal_getexepath()) +#if defined(__ANDROID__) + // On Android every app forks from the Zygote, so /proc/self/exe always + // resolves to /system/bin/app_process64. /proc/self/cmdline holds the + // package name (set by ActivityThread via PR_SET_NAME / setproctitle), + // which is what crash diagnostics actually want. + int cmdlineFd = open("/proc/self/cmdline", O_RDONLY | O_CLOEXEC); + if (cmdlineFd >= 0) + { + char buf[CRASHREPORT_STRING_BUFFER_SIZE]; + ssize_t n = read(cmdlineFd, buf, sizeof(buf) - 1); + close(cmdlineFd); + if (n > 0) + { + buf[n] = '\0'; + CrashReportHelpers::CopyString(m_processName, sizeof(m_processName), + CrashReportHelpers::GetFilename(buf)); + } + } +#endif + if (m_processName[0] == '\0') { - CrashReportHelpers::CopyString(m_processName, sizeof(m_processName), CrashReportHelpers::GetFilename(exePath)); - free(exePath); + if (char* exePath = minipal_getexepath()) + { + CrashReportHelpers::CopyString(m_processName, sizeof(m_processName), CrashReportHelpers::GetFilename(exePath)); + free(exePath); + } } } diff --git a/src/coreclr/pal/inc/pal.h b/src/coreclr/pal/inc/pal.h index 8c9f6c337dd8f7..f23d80bfe729fe 100644 --- a/src/coreclr/pal/inc/pal.h +++ b/src/coreclr/pal/inc/pal.h @@ -264,12 +264,16 @@ PALAPI PAL_SetLogManagedCallstackForSignalCallback( IN PLOGMANAGEDCALLSTACKFORSIGNAL_CALLBACK callback); -#ifdef FEATURE_INPROC_CRASHREPORT /// /// Callback invoked from the fatal-signal path to write an in-proc crash /// report. The callback runs inside the signal handler and must therefore /// be async-signal-safe. siginfo is opaque (siginfo_t*) and context is the /// raw ucontext_t pointer received by the PAL signal handler. +/// +/// Registration is opt-in: if no callback is installed the PAL falls back +/// to its default crash-dump path (createdump where available). The PAL +/// itself has no source-level dependency on the in-proc reporter library; +/// it only knows about this callback ABI. /// typedef VOID (*PINPROCCRASHREPORT_CALLBACK)(int signal, void* siginfo, void* context); @@ -278,7 +282,6 @@ VOID PALAPI PAL_SetInProcCrashReportCallback( IN PINPROCCRASHREPORT_CALLBACK callback); -#endif // FEATURE_INPROC_CRASHREPORT PALIMPORT VOID diff --git a/src/coreclr/pal/src/CMakeLists.txt b/src/coreclr/pal/src/CMakeLists.txt index 0933924a2a7f88..970c9cb7dabef5 100644 --- a/src/coreclr/pal/src/CMakeLists.txt +++ b/src/coreclr/pal/src/CMakeLists.txt @@ -37,9 +37,6 @@ endif(CORECLR_SET_RPATH) # Include directories include_directories(include) -if(FEATURE_INPROC_CRASHREPORT) - add_compile_definitions(FEATURE_INPROC_CRASHREPORT) -endif() # Compile options diff --git a/src/coreclr/pal/src/thread/process.cpp b/src/coreclr/pal/src/thread/process.cpp index a72aafcd972b99..59694faafe4ac6 100644 --- a/src/coreclr/pal/src/thread/process.cpp +++ b/src/coreclr/pal/src/thread/process.cpp @@ -60,6 +60,7 @@ SET_DEFAULT_DEBUG_CHANNEL(PROCESS); // some headers have code with asserts, so d #include #include #include +#include #ifdef __linux__ #include @@ -190,14 +191,15 @@ Volatile g_logManagedCallstackForSignalC #define MAX_ARGV_ENTRIES 32 const char* g_argvCreateDump[MAX_ARGV_ENTRIES] = { nullptr }; -#ifdef FEATURE_INPROC_CRASHREPORT // Read from the fatal-signal path (PROCCreateCrashDumpIfEnabled) and written // once during startup via PAL_SetInProcCrashReportCallback; use Volatile<> // to match the publication ordering of g_logManagedCallstackForSignalCallback. // PAL has no direct dependency on the in-proc crash reporter library; the -// reporter registers itself by installing this signal-safe callback. +// reporter registers itself by installing this signal-safe callback. When +// no callback is registered, the fatal-signal path falls back to the +// out-of-proc createdump utility (where g_argvCreateDump has been populated +// via PAL_InitializeCoreCLR). static Volatile g_inProcCrashReportCallback = nullptr; -#endif // // Key used for associating CPalThread's with the underlying pthread @@ -2795,8 +2797,6 @@ PROCLogManagedCallstackForSignal(int signal) (no return value) --*/ -#ifdef FEATURE_INPROC_CRASHREPORT -#include VOID PALAPI PAL_SetInProcCrashReportCallback( @@ -2809,26 +2809,21 @@ PAL_SetInProcCrashReportCallback( VOID PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, void* context, bool serialize) { - (void)serialize; // Preserve context pointer to prevent optimization DoNotOptimize(&context); - // TODO: Dump stress log into logcat and/or file when enabled? + // If a host registered an in-proc crash report callback, prefer it: the + // host emits its report from this signal frame and the process aborts. PINPROCCRASHREPORT_CALLBACK callback = g_inProcCrashReportCallback; if (callback != nullptr) { callback(signal, siginfo, context); + minipal_log_write_fatal("Aborting process.\n"); + return; } - minipal_log_write_fatal("Aborting process.\n"); -} -#else -VOID -PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, void* context, bool serialize) -{ - // Preserve context pointer to prevent optimization - DoNotOptimize(&context); - // If enabled, launch the create minidump utility and wait until it completes + // Otherwise fall back to launching the out-of-proc createdump utility + // and wait until it completes. if (g_argvCreateDump[0] != nullptr) { const char* argv[MAX_ARGV_ENTRIES]; @@ -2898,7 +2893,6 @@ PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, void* context, bool free(signalAddressArg); } } -#endif /*++ Function: