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; }