From f5db4e5cc50b9836d022a087aca4317334a7f57e Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Wed, 3 Jun 2026 01:18:04 -0400 Subject: [PATCH] Emit generic instantiation args in the crash report Capture each generic frame's instantiation on-device and emit it as a suffix on the managed frame line (and an additive generic_args field in the JSON). The instantiation is a runtime construct not encoded in the token/PDB, so it is recoverable only on-device; emitting it into the console report keeps the log self-sufficient. Value-type args resolve exactly (they map to distinct generated native code); reference-type args collapse to System.__Canon under shared generics. The walker builds the string into the existing fixed scratch buffer via BuildGenericArgs / AppendTypeHandleName / AppendUtf8 using only no-allocation, signal-safe metadata APIs, with bounds-guarded writes, a recursion depth cap, and a '?' sentinel for malformed shapes. A const char* genericArgs is threaded through the frame-callback typedef and the report sinks. This is the standalone variant of the generics change, applied directly on the verbose reporter so it can ship independently of the compaction work. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../debug/crashreport/inproccrashreporter.cpp | 58 +++-- .../debug/crashreport/inproccrashreporter.h | 7 + src/coreclr/vm/crashreportstackwalker.cpp | 199 ++++++++++++++++++ 3 files changed, 251 insertions(+), 13 deletions(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index 95d62e95a38651..0147d1d0d0e670 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -291,6 +291,7 @@ class ThreadEnumerationContext uint32_t nativeOffset, uint32_t token, uint32_t ilOffset, + const char* genericArgs, void* ctx); private: @@ -312,7 +313,8 @@ class ThreadEnumerationContext const GUID* moduleGuid, uint32_t nativeOffset, uint32_t token, - uint32_t ilOffset); + uint32_t ilOffset, + const char* genericArgs); void EndCurrentConsoleThreadBlock(); void EndCurrentJsonThreadBlock(); @@ -500,7 +502,8 @@ class CrashReportHelpers const GUID* moduleGuid, uint32_t nativeOffset, uint32_t token, - uint32_t ilOffset); + uint32_t ilOffset, + const char* genericArgs); static void WriteFrameToConsole( SignalSafeConsoleWriter* consoleWriter, @@ -514,7 +517,8 @@ class CrashReportHelpers const char* fallbackModuleName, uint32_t nativeOffset, uint32_t token, - uint32_t ilOffset); + uint32_t ilOffset, + const char* genericArgs); static void WriteStackOverflowFrameToJson( SignalSafeJsonWriter* writer, @@ -568,6 +572,7 @@ class CrashReportHelpers uint32_t nativeOffset, uint32_t token, uint32_t ilOffset, + const char* genericArgs, void* ctx); static void WriteFrameToReport( @@ -583,7 +588,8 @@ class CrashReportHelpers const GUID* moduleGuid, uint32_t nativeOffset, uint32_t token, - uint32_t ilOffset); + uint32_t ilOffset, + const char* genericArgs); static bool WriteToFile( int fd, @@ -1417,7 +1423,8 @@ CrashReportHelpers::WriteFrameToJson( const GUID* moduleGuid, uint32_t nativeOffset, uint32_t token, - uint32_t ilOffset) + uint32_t ilOffset, + const char* genericArgs) { if (writer == nullptr) { @@ -1447,6 +1454,14 @@ CrashReportHelpers::WriteFrameToJson( writer->WriteHexAsString("token", token); writer->WriteHexAsString("il_offset", ilOffset); } + // Generic instantiation arguments cannot be recovered off-device from the + // token + PDB (instantiations are a runtime construct, not metadata), so + // record them here alongside the deferred keys to keep the JSON report a + // complete, self-describing store. + if (genericArgs != nullptr && genericArgs[0] != '\0') + { + writer->WriteString("generic_args", genericArgs); + } if (HasModuleName(moduleName)) { writer->WriteString("filename", moduleName); @@ -1492,7 +1507,8 @@ CrashReportHelpers::WriteFrameToConsole( const char* fallbackModuleName, uint32_t nativeOffset, uint32_t token, - uint32_t ilOffset) + uint32_t ilOffset, + const char* genericArgs) { if (consoleWriter == nullptr) { @@ -1534,6 +1550,14 @@ CrashReportHelpers::WriteFrameToConsole( consoleWriter->AppendStr(" (token=0x"); consoleWriter->AppendHex(static_cast(token)); consoleWriter->AppendChar(')'); + // Generic instantiation arguments travel inline because they cannot be + // recovered off-device from the token + PDB. Format: "" + // (value-type args exact; reference-type args show as System.__Canon). + if (genericArgs != nullptr && genericArgs[0] != '\0') + { + consoleWriter->AppendChar(' '); + consoleWriter->AppendStr(genericArgs); + } } else if (token != 0 && HasModuleName(fallbackModuleName)) { @@ -1730,6 +1754,7 @@ CrashReportHelpers::WriteFrame( uint32_t nativeOffset, uint32_t token, uint32_t ilOffset, + const char* genericArgs, void* ctx) { FrameContext* frameContext = reinterpret_cast(ctx); @@ -1751,7 +1776,8 @@ CrashReportHelpers::WriteFrame( moduleGuid, nativeOffset, token, - ilOffset); + ilOffset, + genericArgs); } void @@ -1768,7 +1794,8 @@ CrashReportHelpers::WriteFrameToReport( const GUID* moduleGuid, uint32_t nativeOffset, uint32_t token, - uint32_t ilOffset) + uint32_t ilOffset, + const char* genericArgs) { if (frameContext == nullptr) { @@ -1796,7 +1823,8 @@ CrashReportHelpers::WriteFrameToReport( methodNameBuffer, methodNameBufferSize, ip, stackPointer, methodName, className, moduleName, - moduleTimestamp, moduleSize, moduleGuid, nativeOffset, token, ilOffset); + moduleTimestamp, moduleSize, moduleGuid, nativeOffset, token, ilOffset, + genericArgs); bool consoleCapped = frameLimitPerThread != 0 && frameIndex >= frameLimitPerThread; @@ -1817,7 +1845,7 @@ CrashReportHelpers::WriteFrameToReport( methodNameBuffer, methodNameBufferSize, frameIndex, moduleIndex, ip, methodName, className, moduleName, - nativeOffset, token, ilOffset); + nativeOffset, token, ilOffset, genericArgs); } else if (currentThreadDroppedCount != nullptr) { @@ -1843,7 +1871,8 @@ ThreadEnumerationContext::OnFrame( const GUID* moduleGuid, uint32_t nativeOffset, uint32_t token, - uint32_t ilOffset) + uint32_t ilOffset, + const char* genericArgs) { CrashReportHelpers::WriteFrameToReport( &m_frameContext, @@ -1858,7 +1887,8 @@ ThreadEnumerationContext::OnFrame( moduleGuid, nativeOffset, token, - ilOffset); + ilOffset, + genericArgs); } void @@ -1875,6 +1905,7 @@ ThreadEnumerationContext::FrameCallback( uint32_t nativeOffset, uint32_t token, uint32_t ilOffset, + const char* genericArgs, void* ctx) { if (ctx == nullptr) @@ -1893,7 +1924,8 @@ ThreadEnumerationContext::FrameCallback( moduleGuid, nativeOffset, token, - ilOffset); + ilOffset, + genericArgs); } void diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.h b/src/coreclr/debug/crashreport/inproccrashreporter.h index 71e9148bec8a4d..b552fdf31cf3dc 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.h +++ b/src/coreclr/debug/crashreport/inproccrashreporter.h @@ -47,6 +47,13 @@ using InProcCrashReportFrameCallback = void (*)( uint32_t nativeOffset, uint32_t token, uint32_t ilOffset, + // Generic instantiation arguments for this frame's method, pre-formatted by + // the VM-side walker as "" (empty/null when the method + // is non-generic). Generic instantiations are a runtime construct absent from + // metadata, so they cannot be recovered off-device from the token + PDB and + // must travel with the frame. Value-type args are exact; reference-type args + // collapse to System.__Canon under shared generics. + const char* genericArgs, void* ctx); using InProcCrashReportWalkStackCallback = void (*)( diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index 1a259cb2a23cec..ae6a44ab2dfee9 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -28,6 +28,7 @@ struct CrashReportStackWalkerScratch { char crashExceptionType[CRASHREPORT_STRING_BUFFER_SIZE]; char className[CRASHREPORT_STRING_BUFFER_SIZE]; + char genericArgs[CRASHREPORT_STRING_BUFFER_SIZE]; GUID moduleGuid; bool hasModuleGuid; }; @@ -89,6 +90,199 @@ GetCrashReportFrameLimitPerThread() static void BuildTypeName(LPUTF8 buffer, size_t bufferSize, LPCUTF8 namespaceName, LPCUTF8 className); +// Cap on nested-generic expansion depth while building a frame's instantiation +// string in the signal handler. Keeps the recursion (and output) bounded for +// pathological types without risking the fixed scratch buffer. +static constexpr int CRASHREPORT_GENERIC_MAX_DEPTH = 4; + +// Append a NUL-terminated string into a fixed buffer, advancing *index and +// always leaving room for the terminator. No allocation; signal-safe. +static void AppendUtf8(LPUTF8 buffer, size_t bufferSize, size_t& index, const char* text) +{ + LIMITED_METHOD_CONTRACT; + + if (text == nullptr) + { + return; + } + while (*text != '\0' && index + 1 < bufferSize) + { + buffer[index++] = *text++; + } +} + +// Render a single instantiation argument TypeHandle into the buffer. Reads only +// already-loaded metadata (NOTHROW/GC_NOTRIGGER/no lock/no alloc) so it is safe +// from the crash signal handler. Unsupported or malformed shapes degrade to "?" +// rather than risking an unsafe traversal. +static void AppendTypeHandleName(LPUTF8 buffer, size_t bufferSize, size_t& index, TypeHandle th, int depth) +{ + CONTRACTL + { + NOTHROW; + GC_NOTRIGGER; + CANNOT_TAKE_LOCK; + MODE_ANY; + } + CONTRACTL_END; + + if (index + 1 >= bufferSize) + { + return; + } + if (th.IsNull() || depth > CRASHREPORT_GENERIC_MAX_DEPTH) + { + AppendUtf8(buffer, bufferSize, index, "?"); + return; + } + + // byref / pointer / generic-var / function-pointer: not expected for the + // concrete instantiation args of a JITted frame; emit a sentinel rather than + // traverse TypeDesc state from the signal handler. + if (th.IsTypeDesc()) + { + AppendUtf8(buffer, bufferSize, index, "?"); + return; + } + + if (th.IsArray()) + { + AppendTypeHandleName(buffer, bufferSize, index, th.GetArrayElementTypeHandle(), depth + 1); + AppendUtf8(buffer, bufferSize, index, "[]"); + return; + } + + MethodTable* pMT = th.GetMethodTable(); + if (pMT == nullptr) + { + AppendUtf8(buffer, bufferSize, index, "?"); + return; + } + + LPCUTF8 name = nullptr; + LPCUTF8 namespaceName = nullptr; + mdTypeDef cl = pMT->GetCl(); + IMDInternalImport* pImport = pMT->GetMDImport(); + if (pImport == nullptr || cl == mdTypeDefNil || + FAILED(pImport->GetNameOfTypeDef(cl, &name, &namespaceName))) + { + AppendUtf8(buffer, bufferSize, index, "?"); + return; + } + + if (namespaceName != nullptr && *namespaceName != '\0') + { + AppendUtf8(buffer, bufferSize, index, namespaceName); + if (index + 1 < bufferSize) + { + buffer[index++] = '.'; + } + } + AppendUtf8(buffer, bufferSize, index, name); + + // Expand a nested generic instantiation (e.g. List`1 -> List`1) + // using balanced angle brackets so an offline parser can recover nested args. + if (th.HasInstantiation()) + { + Instantiation inst = th.GetInstantiation(); + if (index + 1 < bufferSize) + { + buffer[index++] = '<'; + } + for (DWORD i = 0; i < inst.GetNumArgs(); i++) + { + if (i != 0 && index + 1 < bufferSize) + { + buffer[index++] = ','; + } + AppendTypeHandleName(buffer, bufferSize, index, inst[i], depth + 1); + } + if (index + 1 < bufferSize) + { + buffer[index++] = '>'; + } + } +} + +// Build the frame's generic instantiation string as "" +// (empty when the method is non-generic). Value-type args resolve exactly; +// reference-type args collapse to System.__Canon under shared generics. The +// runtime instantiation is not encoded in metadata, so this is the only place +// it can be captured for the deferred-symbolication report. +// +// Async-signal-safe by construction, which is required because this runs from +// the crash signal handler with other threads suspended: +// - NOTHROW/GC_NOTRIGGER/CANNOT_TAKE_LOCK: no exceptions, no GC, no locks, so it +// cannot deadlock against state the suspended threads may hold. +// - No heap allocation: output goes into the caller's fixed buffer and every +// write is bounds-guarded (index + 1 < bufferSize), so a small buffer simply +// truncates instead of overrunning. +// - Reads only already-loaded metadata via the LIMITED_METHOD(_DAC)_CONTRACT +// accessors on MethodDesc/TypeHandle/MethodTable (instantiation arrays, the +// TypeDef name); it never forces type loading or touches the live generic +// dictionary. +// - Bounded recursion (CRASHREPORT_GENERIC_MAX_DEPTH) and "?" degradation for +// TypeDesc/null/malformed shapes keep traversal away from unsafe state. +static void BuildGenericArgs(LPUTF8 buffer, size_t bufferSize, MethodDesc* pMD) +{ + CONTRACTL + { + NOTHROW; + GC_NOTRIGGER; + CANNOT_TAKE_LOCK; + MODE_ANY; + } + CONTRACTL_END; + + if (bufferSize == 0) + { + return; + } + buffer[0] = '\0'; + if (pMD == nullptr) + { + return; + } + + Instantiation classInst = pMD->GetClassInstantiation(); + Instantiation methodInst = pMD->GetMethodInstantiation(); + if (classInst.GetNumArgs() == 0 && methodInst.GetNumArgs() == 0) + { + return; + } + + size_t index = 0; + if (index + 1 < bufferSize) + { + buffer[index++] = '<'; + } + for (DWORD i = 0; i < classInst.GetNumArgs(); i++) + { + if (i != 0 && index + 1 < bufferSize) + { + buffer[index++] = ','; + } + AppendTypeHandleName(buffer, bufferSize, index, classInst[i], 0); + } + if (index + 1 < bufferSize) + { + buffer[index++] = ';'; + } + for (DWORD i = 0; i < methodInst.GetNumArgs(); i++) + { + if (i != 0 && index + 1 < bufferSize) + { + buffer[index++] = ','; + } + AppendTypeHandleName(buffer, bufferSize, index, methodInst[i], 0); + } + if (index + 1 < bufferSize) + { + buffer[index++] = '>'; + } + buffer[index] = '\0'; +} + static void CrashReportGetModuleDetails( @@ -250,6 +444,9 @@ FrameCallbackAdapter( scratch.className[0] = '\0'; BuildTypeName(scratch.className, sizeof(scratch.className), namespaceName, className); + scratch.genericArgs[0] = '\0'; + BuildGenericArgs(scratch.genericArgs, sizeof(scratch.genericArgs), pMD); + Module* pModule = pMD->GetModule(); uint32_t nativeOffset = pCF->HasFaulted() ? 0 : pCF->GetRelOffset(); @@ -305,6 +502,7 @@ FrameCallbackAdapter( &moduleSize); className = scratch.className[0] == '\0' ? nullptr : scratch.className; + const char* genericArgs = scratch.genericArgs[0] == '\0' ? nullptr : scratch.genericArgs; ctx->callback( static_cast(ip), static_cast(stackPointer), @@ -318,6 +516,7 @@ FrameCallbackAdapter( nativeOffset, static_cast(token), ilOffset, + genericArgs, ctx->userCtx); return SWA_CONTINUE; }