diff --git a/src/coreclr/clrdefinitions.cmake b/src/coreclr/clrdefinitions.cmake index 60b79850f53ffd..f273fd4378985c 100644 --- a/src/coreclr/clrdefinitions.cmake +++ b/src/coreclr/clrdefinitions.cmake @@ -156,6 +156,10 @@ if(FEATURE_WEBCIL) add_compile_definitions(FEATURE_WEBCIL) endif() +if(FEATURE_INPROC_CRASHREPORT) + add_compile_definitions(FEATURE_INPROC_CRASHREPORT) +endif() + add_compile_definitions($<${FEATURE_JAVAMARSHAL}:FEATURE_JAVAMARSHAL>) add_definitions(-DFEATURE_READYTORUN) diff --git a/src/coreclr/clrfeatures.cmake b/src/coreclr/clrfeatures.cmake index f5c84aaaad2947..62bb946177b9a7 100644 --- a/src/coreclr/clrfeatures.cmake +++ b/src/coreclr/clrfeatures.cmake @@ -92,6 +92,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 5a0a420346882f..acfd2fa7fe7c23 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(FEATURE_INPROC_CRASHREPORT) + 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..f88699a4c6a464 --- /dev/null +++ b/src/coreclr/debug/crashreport/CMakeLists.txt @@ -0,0 +1,10 @@ +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CRASHREPORT_SOURCES + signalsafejsonwriter.cpp + inproccrashreporter.cpp +) + +add_library(inproccrashreport OBJECT ${CRASHREPORT_SOURCES}) + +target_sources(coreclrpal PRIVATE $) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp new file mode 100644 index 00000000000000..3bb01266900f81 --- /dev/null +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -0,0 +1,996 @@ +// 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. +// +// Streams a createdump-shaped JSON skeleton to a crashreport.json file. + +#include "inproccrashreporter.h" +#include "signalsafejsonwriter.h" + +#include "pal.h" + +#include +#include +#include +#include +#include +#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 + +class ThreadEnumerationContext +{ +public: + ThreadEnumerationContext( + SignalSafeJsonWriter* 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) + { + } + + ThreadEnumerationContext(const ThreadEnumerationContext&) = delete; + ThreadEnumerationContext& operator=(const ThreadEnumerationContext&) = delete; + + size_t ThreadCount() const { return m_threadCount; } + 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, + 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: + SignalSafeJsonWriter* m_writer; + void* m_signalContext; + size_t m_threadCount; + bool m_sawCrashThread; + bool m_hasCrashException; + const char* m_crashExceptionType; + uint32_t m_crashExceptionHResult; +}; + +class CrashReportOutputContext +{ +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; +}; + +class CrashReportHelpers +{ +public: + static void GetVersionString( + char* buffer, + size_t bufferSize); + + static bool AppendString( + char* buffer, + size_t bufferSize, + size_t* pos, + const char* value); + + static void WriteRegistersToJson( + SignalSafeJsonWriter* writer, + void* context); + + static uint64_t GetInstructionPointer( + void* context); + + static uint64_t GetStackPointer( + void* context); + + static uint64_t GetFramePointer( + void* context); + + static void WriteCrashSiteFrameToJson( + SignalSafeJsonWriter* writer, + void* context); + + static void BuildMethodName( + char* buffer, + size_t bufferSize, + const char* className, + const char* methodName); + + static const char* GetFilename( + const char* path); + + static void CopyString( + char* buffer, + size_t bufferSize, + const char* value); + + 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); + + 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( + int signal, + siginfo_t* siginfo, + void* context) +{ + static LONG s_generating = 0; + if (InterlockedCompareExchange(&s_generating, 1, 0) != 0) + { + return; + } + + char reportPath[CRASHREPORT_STRING_BUFFER_SIZE]; + reportPath[0] = '\0'; + + if (m_reportPath[0] == '\0' || !CrashReportHelpers::BuildReportPath(reportPath, sizeof(reportPath), m_reportPath)) + { + return; + } + + int fd = open(reportPath, O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (fd == -1) + { + return; + } + + (void)siginfo; + + char exTypeBuf[CRASHREPORT_STRING_BUFFER_SIZE]; + uint32_t exHresult = 0; + exTypeBuf[0] = '\0'; + + bool hasException = false; + if (m_getExceptionCallback != nullptr && signal != SIGSEGV && signal != SIGBUS) + { + hasException = m_getExceptionCallback(exTypeBuf, sizeof(exTypeBuf), &exHresult); + } + + CrashReportOutputContext outputContext(fd); + + m_jsonWriter.Init(&CrashReportOutputContext::ChunkCallback, &outputContext); + + m_jsonWriter.OpenObject(); + m_jsonWriter.OpenObject("payload"); + m_jsonWriter.WriteString("protocol_version", "1.0.0"); + + m_jsonWriter.OpenObject("configuration"); +#if defined(__x86_64__) + m_jsonWriter.WriteString("architecture", "amd64"); +#elif defined(__aarch64__) + m_jsonWriter.WriteString("architecture", "arm64"); +#elif defined(__arm__) + m_jsonWriter.WriteString("architecture", "arm"); +#endif + char version[sizeof(sccsid)]; + CrashReportHelpers::GetVersionString(version, sizeof(version)); + m_jsonWriter.WriteString("version", version); + m_jsonWriter.CloseObject(); // configuration + + if (m_processName[0] != '\0') + { + m_jsonWriter.WriteString("process_name", m_processName); + } + + m_jsonWriter.WriteDecimalAsString("pid", static_cast(GetCurrentProcessId())); + + m_jsonWriter.OpenArray("threads"); + if (m_enumerateThreadsCallback != nullptr) + { + ThreadEnumerationContext threadContext(&m_jsonWriter, context, hasException, exTypeBuf, exHresult); + uint64_t crashingTid = static_cast(minipal_get_current_thread_id()); + + m_enumerateThreadsCallback(crashingTid, &ThreadEnumerationContext::ThreadCallback, &ThreadEnumerationContext::FrameCallback, &threadContext); + + threadContext.EndEnumeration(); + + if (threadContext.ThreadCount() == 0 || !threadContext.SawCrashThread()) + { + EmitSynthesizedCrashThread(context, hasException, exTypeBuf, exHresult, /*walkStack*/ false); + } + } + else + { + EmitSynthesizedCrashThread(context, hasException, exTypeBuf, exHresult, /*walkStack*/ true); + } + m_jsonWriter.CloseArray(); // threads + + m_jsonWriter.CloseObject(); // payload + + m_jsonWriter.OpenObject("parameters"); + m_jsonWriter.WriteSignedDecimalAsString("signal", static_cast(signal)); + m_jsonWriter.CloseObject(); // parameters + + m_jsonWriter.CloseObject(); // root + + if (fd != -1) + { + bool writeSucceeded = m_jsonWriter.Finish() && + !outputContext.WriteFailed() && + CrashReportHelpers::WriteToFile(fd, "\n", 1); + + if (close(fd) != 0 || !writeSucceeded) + { + unlink(reportPath); + } + } +} + +InProcCrashReporter& +InProcCrashReporter::GetInstance() +{ + static InProcCrashReporter s_instance; + return s_instance; +} + +void +InProcCrashReporter::Initialize( + const InProcCrashReporterSettings& settings) +{ + m_isManagedThreadCallback = settings.isManagedThreadCallback; + m_walkStackCallback = settings.walkStackCallback; + m_getExceptionCallback = settings.getExceptionCallback; + m_enumerateThreadsCallback = settings.enumerateThreadsCallback; + CrashReportHelpers::CopyString(m_reportPath, sizeof(m_reportPath), settings.reportPath); + + m_processName[0] = '\0'; +#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') + { + if (char* exePath = minipal_getexepath()) + { + CrashReportHelpers::CopyString(m_processName, sizeof(m_processName), CrashReportHelpers::GetFilename(exePath)); + free(exePath); + } + } +} + +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 +CrashReportHelpers::WriteToFile( + int fd, + const char* buffer, + size_t len) +{ + if (fd < 0 || buffer == nullptr) + { + return false; + } + + size_t 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 false; + } + + return true; +} + +bool +CrashReportOutputContext::HandleChunk( + const char* buffer, + size_t len) +{ + if (m_fd == -1) + { + return false; + } + + if (!CrashReportHelpers::WriteToFile(m_fd, buffer, len)) + { + 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 outputContext->HandleChunk(buffer, len); +} + +// 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. +size_t +CrashReportHelpers::ExpandDumpTemplate( + char* buffer, + size_t bufferSize, + const char* pattern) +{ + if (buffer == nullptr || bufferSize == 0 || pattern == nullptr) + { + return 0; + } + + size_t pos = 0; + unsigned pid = static_cast(GetCurrentProcessId()); + + while (*pattern != '\0' && pos + 1 < bufferSize) + { + if (*pattern == '%') + { + pattern++; + if (*pattern == '%') + { + buffer[pos++] = '%'; + } + else if (*pattern == 'p' || *pattern == 'd') + { + char pidBuf[CRASHREPORT_NUMBER_BUFFER_SIZE]; + size_t pidLen = SignalSafeJsonWriter::FormatUnsignedDecimal(pidBuf, sizeof(pidBuf), pid); + if (pidLen == 0 || pos + pidLen >= bufferSize) + { + // 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 + { + // Unknown specifier — pass through literally. + if (pos + 1 < bufferSize) + { + buffer[pos++] = '%'; + } + if (*pattern != '\0' && pos + 1 < bufferSize) + { + buffer[pos++] = *pattern; + } + } + + if (*pattern != '\0') + { + pattern++; + } + } + else + { + buffer[pos++] = *pattern++; + } + } + + buffer[pos] = '\0'; + return pos; +} + +bool +CrashReportHelpers::BuildReportPath( + char* buffer, + size_t bufferSize, + const char* dumpPath) +{ + if (buffer == nullptr || bufferSize == 0 || dumpPath == nullptr || dumpPath[0] == '\0') + { + return false; + } + + char expanded[CRASHREPORT_STRING_BUFFER_SIZE]; + size_t expandedLen = ExpandDumpTemplate(expanded, sizeof(expanded), dumpPath); + if (expandedLen == 0) + { + return false; + } + + size_t pos = 0; + if (!AppendString(buffer, bufferSize, &pos, expanded)) + { + return false; + } + if (!AppendString(buffer, bufferSize, &pos, ".crashreport.json")) + { + return false; + } + return true; +} + +void +CrashReportHelpers::GetVersionString( + char* buffer, + size_t bufferSize) +{ + if (buffer == nullptr || 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; + + size_t copied = strnlen(version, bufferSize - 1); + if (copied != 0) + { + memcpy(buffer, version, copied); + } + + buffer[copied] = '\0'; +} + +// 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 +CrashReportHelpers::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 +CrashReportHelpers::WriteRegistersToJson( + SignalSafeJsonWriter* writer, + void* context) +{ + uint64_t ipValue = GetInstructionPointer(context); + uint64_t spValue = GetStackPointer(context); + uint64_t bpValue = GetFramePointer(context); + + writer->OpenObject("ctx"); + writer->WriteHexAsString("IP", ipValue); + writer->WriteHexAsString("SP", spValue); + writer->WriteHexAsString("BP", bpValue); + writer->CloseObject(); // ctx +} + +uint64_t +CrashReportHelpers::GetInstructionPointer( + 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_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 +CrashReportHelpers::GetStackPointer( + 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_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 +} + +uint64_t +CrashReportHelpers::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 +CrashReportHelpers::WriteCrashSiteFrameToJson( + SignalSafeJsonWriter* writer, + void* context) +{ + uint64_t ipValue = GetInstructionPointer(context); + uint64_t spValue = GetStackPointer(context); + + 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->WriteHexAsString("stack_pointer", spValue); + writer->WriteHexAsString("native_address", ipValue); + writer->CloseObject(); // frame +} + +void +CrashReportHelpers::BuildMethodName( + char* buffer, + size_t bufferSize, + const char* className, + const char* methodName) +{ + if (buffer == nullptr || bufferSize == 0) + { + return; + } + + if (className != nullptr && methodName != nullptr) + { + size_t pos = 0; + AppendString(buffer, bufferSize, &pos, className); + AppendString(buffer, bufferSize, &pos, "."); + AppendString(buffer, bufferSize, &pos, methodName); + } + else if (className != nullptr) + { + CopyString(buffer, bufferSize, className); + } + else if (methodName != nullptr) + { + CopyString(buffer, bufferSize, methodName); + } + else + { + buffer[0] = '\0'; + } +} + +// 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) +{ + const char* last = path; + for (const char* p = path; *p != '\0'; p++) + { + if (*p == CRASHREPORT_DIRECTORY_SEPARATOR) + { + last = p + 1; + } + } + + return last; +} + +void +CrashReportHelpers::CopyString( + char* buffer, + size_t bufferSize, + const char* value) +{ + if (buffer == nullptr || bufferSize == 0) + { + return; + } + + if (value == nullptr) + { + buffer[0] = '\0'; + return; + } + + size_t copied = strnlen(value, bufferSize - 1); + if (copied != 0) + { + memcpy(buffer, value, copied); + } + + buffer[copied] = '\0'; +} + +void +CrashReportHelpers::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) +{ + SignalSafeJsonWriter* writer = reinterpret_cast(ctx); + + writer->OpenObject(); + writer->WriteHexAsString("stack_pointer", stackPointer); + writer->WriteHexAsString("native_address", ip); + writer->WriteHexAsString("native_offset", nativeOffset); + + if (methodName != nullptr) + { + char fullName[CRASHREPORT_STRING_BUFFER_SIZE]; + BuildMethodName(fullName, sizeof(fullName), className, methodName); + writer->WriteString("method_name", fullName); + writer->WriteString("is_managed", "true"); + writer->WriteHexAsString("token", token); + writer->WriteHexAsString("il_offset", ilOffset); + if (moduleName != nullptr) + { + writer->WriteString("filename", moduleName); + } + if (moduleTimestamp != 0) + { + writer->WriteHexAsString("timestamp", moduleTimestamp); + } + if (moduleSize != 0) + { + writer->WriteHexAsString("sizeofimage", moduleSize); + } + if (moduleGuid != nullptr && moduleGuid[0] != '\0') + { + writer->WriteString("guid", moduleGuid); + } + } + else + { + writer->WriteString("is_managed", "false"); + if (moduleName != nullptr) + { + writer->WriteString("native_module", moduleName); + } + } + + writer->CloseObject(); // frame +} + +void +ThreadEnumerationContext::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) +{ + CrashReportHelpers::JsonFrameCallback(ip, stackPointer, methodName, className, moduleName, nativeOffset, token, ilOffset, moduleTimestamp, moduleSize, moduleGuid, m_writer); +} + +void +ThreadEnumerationContext::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) +{ + reinterpret_cast(ctx)->OnFrame(ip, stackPointer, methodName, className, moduleName, nativeOffset, token, ilOffset, moduleTimestamp, moduleSize, moduleGuid); +} + +void +ThreadEnumerationContext::OnThread( + uint64_t osThreadId, + bool isCrashThread, + const char* exceptionType, + uint32_t exceptionHResult) +{ + if (m_threadCount > 0) + { + m_writer->CloseArray(); // stack_frames + m_writer->CloseObject(); // thread + + (void)m_writer->Flush(); + } + + if (isCrashThread) + { + m_sawCrashThread = true; + } + m_threadCount++; + + m_writer->OpenObject(); + m_writer->WriteString("is_managed", "true"); + m_writer->WriteString("crashed", isCrashThread ? "true" : "false"); + m_writer->WriteHexAsString("native_thread_id", osThreadId); + + if (isCrashThread && m_hasCrashException) + { + m_writer->WriteString("managed_exception_type", m_crashExceptionType); + m_writer->WriteHexAsString("managed_exception_hresult", m_crashExceptionHResult); + } + else if (exceptionType != nullptr && exceptionType[0] != '\0') + { + m_writer->WriteString("managed_exception_type", exceptionType); + m_writer->WriteHexAsString("managed_exception_hresult", exceptionHResult); + } + + if (isCrashThread) + { + CrashReportHelpers::WriteRegistersToJson(m_writer, m_signalContext); + } + + m_writer->OpenArray("stack_frames"); + if (isCrashThread) + { + CrashReportHelpers::WriteCrashSiteFrameToJson(m_writer, m_signalContext); + } +} + +void +ThreadEnumerationContext::ThreadCallback( + uint64_t osThreadId, + bool isCrashThread, + const char* exceptionType, + uint32_t exceptionHResult, + void* ctx) +{ + 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(); +} + +void +InProcCrashReporter::EmitSynthesizedCrashThread( + void* context, + bool hasException, + const char* crashExceptionType, + uint32_t crashExceptionHResult, + bool walkStack) +{ + uint64_t crashingTid = static_cast(minipal_get_current_thread_id()); + + m_jsonWriter.OpenObject(); + m_jsonWriter.WriteString("is_managed", + m_isManagedThreadCallback != nullptr && m_isManagedThreadCallback() ? "true" : "false"); + m_jsonWriter.WriteString("crashed", "true"); + m_jsonWriter.WriteHexAsString("native_thread_id", crashingTid); + + if (hasException) + { + m_jsonWriter.WriteString("managed_exception_type", crashExceptionType); + m_jsonWriter.WriteHexAsString("managed_exception_hresult", crashExceptionHResult); + } + + CrashReportHelpers::WriteRegistersToJson(&m_jsonWriter, context); + m_jsonWriter.OpenArray("stack_frames"); + CrashReportHelpers::WriteCrashSiteFrameToJson(&m_jsonWriter, context); + if (walkStack && m_walkStackCallback != nullptr) + { + 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 new file mode 100644 index 00000000000000..351c43c34a7576 --- /dev/null +++ b/src/coreclr/debug/crashreport/inproccrashreporter.h @@ -0,0 +1,109 @@ +// 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 a *.crashreport.json file +// on disk. + +#pragma once + +#include +#include + +#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 (*)( + 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); + +using InProcCrashReportWalkStackCallback = void (*)( + InProcCrashReportFrameCallback frameCallback, + void* ctx); + +using InProcCrashReportGetExceptionCallback = bool (*)( + char* exceptionTypeBuf, + size_t exceptionTypeBufSize, + uint32_t* hresult); + +using InProcCrashReportThreadCallback = void (*)( + uint64_t osThreadId, + bool isCrashThread, + const char* exceptionType, + uint32_t exceptionHResult, + void* ctx); + +using InProcCrashReportEnumerateThreadsCallback = void (*)( + uint64_t crashingTid, + InProcCrashReportThreadCallback threadCallback, + InProcCrashReportFrameCallback frameCallback, + void* ctx); + +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[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 new file mode 100644 index 00000000000000..4bed326d7cbcda --- /dev/null +++ b/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp @@ -0,0 +1,394 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include "signalsafejsonwriter.h" + +#include +#include + +static +char +ToHexChar(unsigned value) +{ + return (value < 10) ? static_cast('0' + value) : static_cast('a' + (value - 10)); +} + +void +SignalSafeJsonWriter::Init( + SignalSafeJsonOutputCallback outputCallback, + void* outputContext) +{ + m_pos = 0; + m_commaNeeded = false; + m_writeFailed = false; + m_outputCallback = outputCallback; + m_outputContext = outputContext; +} + +bool +SignalSafeJsonWriter::OpenObject( + const char* key) +{ + WriteSeparator(); + if (key != nullptr) + { + WriteEscapedString(key); + AppendStr(": "); + } + AppendChar('{'); + m_commaNeeded = false; + return !m_writeFailed; +} + +bool +SignalSafeJsonWriter::OpenObject() +{ + return OpenObject(nullptr); +} + +bool +SignalSafeJsonWriter::CloseObject() +{ + AppendChar('}'); + m_commaNeeded = true; + return !m_writeFailed; +} + +bool +SignalSafeJsonWriter::OpenArray( + const char* key) +{ + WriteSeparator(); + if (key != nullptr) + { + WriteEscapedString(key); + AppendStr(": "); + } + AppendChar('['); + m_commaNeeded = false; + return !m_writeFailed; +} + +bool +SignalSafeJsonWriter::OpenArray() +{ + return OpenArray(nullptr); +} + +bool +SignalSafeJsonWriter::CloseArray() +{ + AppendChar(']'); + m_commaNeeded = true; + return !m_writeFailed; +} + +bool +SignalSafeJsonWriter::WriteString( + const char* key, + const char* value) +{ + WriteSeparator(); + WriteEscapedString(key); + AppendStr(": "); + WriteEscapedString(value); + return !m_writeFailed; +} + +bool +SignalSafeJsonWriter::Finish() +{ + return Flush(); +} + +bool +SignalSafeJsonWriter::Flush() +{ + if (m_writeFailed) + { + return false; + } + + if (m_pos == 0) + { + return true; + } + + if (m_outputCallback != nullptr && !m_outputCallback(m_buffer, m_pos, m_outputContext)) + { + m_writeFailed = true; + return false; + } + + m_pos = 0; + return true; +} + +bool +SignalSafeJsonWriter::Append( + const char* str, + size_t len) +{ + if (m_writeFailed) + { + return false; + } + + 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. + m_writeFailed = true; + return false; + } + + if (len == 0) + { + return true; + } + + size_t offset = 0; + size_t remaining = SIGNAL_SAFE_JSON_BUFFER_SIZE - m_pos; + while (offset < len) + { + if (remaining == 0) + { + if (!Flush()) + { + return false; + } + remaining = SIGNAL_SAFE_JSON_BUFFER_SIZE; + } + + size_t chunk = len - offset; + if (chunk > remaining) + { + chunk = remaining; + } + + memcpy(m_buffer + m_pos, str + offset, chunk); + m_pos += chunk; + offset += chunk; + remaining -= chunk; + } + + return true; +} + +bool +SignalSafeJsonWriter::AppendChar(char c) +{ + if (m_writeFailed) + { + return false; + } + + if (m_pos == SIGNAL_SAFE_JSON_BUFFER_SIZE && !Flush()) + { + return false; + } + + m_buffer[m_pos++] = c; + return true; +} + +bool +SignalSafeJsonWriter::AppendStr( + const char* str) +{ + if (str == nullptr) + { + m_writeFailed = true; + return false; + } + + return Append(str, strlen(str)); +} + +void +SignalSafeJsonWriter::WriteSeparator() +{ + if (m_commaNeeded) + AppendChar(','); + + m_commaNeeded = true; +} + +// Escape a string value for JSON. Handles \, ", and control characters. +void +SignalSafeJsonWriter::WriteEscapedString( + const char* str) +{ + AppendChar('"'); + if (str != nullptr) + { + for (size_t i = 0; str[i]; i++) + { + char c = str[i]; + if (c == '"') + AppendStr("\\\""); + else if (c == '\\') + AppendStr("\\\\"); + else if (c == '\n') + AppendStr("\\n"); + else if (c == '\r') + AppendStr("\\r"); + else if (c == '\t') + AppendStr("\\t"); + else if (static_cast(c) < 0x20) + { + 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); + Append(esc, sizeof(esc)); + } + else + { + AppendChar(c); + } + } + } + + 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::WriteHexAsString( + const char* key, + uint64_t value) +{ + char scratch[MAX_HEX_FORMAT_BUFFER_SIZE]; + FormatHexValue(scratch, sizeof(scratch), value); + return WriteString(key, scratch); +} + +bool +SignalSafeJsonWriter::WriteDecimalAsString( + 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::WriteSignedDecimalAsString( + 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 new file mode 100644 index 00000000000000..54eac5dbf6d30d --- /dev/null +++ b/src/coreclr/debug/crashreport/signalsafejsonwriter.h @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Bounded, signal-safe 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 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 SignalSafeJsonOutputCallback = bool (*)(const char* buffer, size_t len, void* ctx); + +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), + m_writeFailed(false), + m_outputCallback(nullptr), + m_outputContext(nullptr) + { + } + + SignalSafeJsonWriter(const SignalSafeJsonWriter&) = delete; + SignalSafeJsonWriter& operator=(const SignalSafeJsonWriter&) = delete; + + 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 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(); + + // 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); + bool AppendStr(const char* str); + void WriteSeparator(); + void WriteEscapedString(const char* str); + + char m_buffer[SIGNAL_SAFE_JSON_BUFFER_SIZE]; + size_t m_pos; + bool m_commaNeeded; + bool m_writeFailed; + SignalSafeJsonOutputCallback m_outputCallback; + void* m_outputContext; +}; diff --git a/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt b/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt index c0cf0a1ff4176b..c2494a0ec246c8 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(FEATURE_INPROC_CRASHREPORT) + target_sources(coreclr_static PRIVATE $) +endif() + if (CLR_CMAKE_HOST_ANDROID) target_link_libraries(coreclr PUBLIC log) endif() diff --git a/src/coreclr/pal/inc/pal.h b/src/coreclr/pal/inc/pal.h index 9fb95fffdd5385..97cf2780696243 100644 --- a/src/coreclr/pal/inc/pal.h +++ b/src/coreclr/pal/inc/pal.h @@ -264,6 +264,25 @@ PALAPI PAL_SetLogManagedCallstackForSignalCallback( IN PLOGMANAGEDCALLSTACKFORSIGNAL_CALLBACK callback); +/// +/// 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); + +PALIMPORT +VOID +PALAPI +PAL_SetInProcCrashReportCallback( + IN PINPROCCRASHREPORT_CALLBACK callback); + PALIMPORT VOID PALAPI diff --git a/src/coreclr/pal/src/thread/process.cpp b/src/coreclr/pal/src/thread/process.cpp index 31996bb4c3e78c..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,6 +191,16 @@ Volatile g_logManagedCallstackForSignalC #define MAX_ARGV_ENTRIES 32 const char* g_argvCreateDump[MAX_ARGV_ENTRIES] = { nullptr }; +// 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. 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; + // // Key used for associating CPalThread's with the underlying pthread // (through pthread_setspecific) @@ -2786,25 +2797,33 @@ PROCLogManagedCallstackForSignal(int signal) (no return value) --*/ -#ifdef HOST_ANDROID -#include VOID -PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, void* context, bool serialize) +PALAPI +PAL_SetInProcCrashReportCallback( + IN PINPROCCRASHREPORT_CALLBACK callback) { - // Preserve context pointer to prevent optimization - DoNotOptimize(&context); - - // TODO: Dump stress log into logcat and/or file when enabled? - minipal_log_write_fatal("Aborting process.\n"); + _ASSERTE(g_inProcCrashReportCallback == nullptr); + g_inProcCrashReportCallback = callback; } -#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 + // 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; + } + + // 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]; @@ -2874,7 +2893,6 @@ PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, void* context, bool free(signalAddressArg); } } -#endif /*++ Function: diff --git a/src/coreclr/vm/CMakeLists.txt b/src/coreclr/vm/CMakeLists.txt index a2dbcb4c89c429..8a0be0d02830f7 100644 --- a/src/coreclr/vm/CMakeLists.txt +++ b/src/coreclr/vm/CMakeLists.txt @@ -4,6 +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(FEATURE_INPROC_CRASHREPORT) + include_directories(${CLR_DIR}/debug/crashreport) +endif(FEATURE_INPROC_CRASHREPORT) include_directories(${CLR_SRC_NATIVE_DIR}) include_directories(${RUNTIME_DIR}) @@ -560,6 +563,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 2d0680d41c6d19..df87b6bdfb9221 100644 --- a/src/coreclr/vm/ceemain.cpp +++ b/src/coreclr/vm/ceemain.cpp @@ -210,6 +210,10 @@ #include "gdbjit.h" #endif // FEATURE_GDBJIT +#ifdef FEATURE_INPROC_CRASHREPORT +#include "crashreportstackwalker.h" +#endif // FEATURE_INPROC_CRASHREPORT + #include "genanalysis.h" #ifdef HAVE_GCCOVER @@ -710,6 +714,10 @@ void EEStartupHelper() PAL_SetLogManagedCallstackForSignalCallback(EEPolicy::LogManagedCallstackForSignal); #endif // HOST_ANDROID +#ifdef FEATURE_INPROC_CRASHREPORT + CrashReportConfigure(); +#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 new file mode 100644 index 00000000000000..22391429424048 --- /dev/null +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -0,0 +1,446 @@ +// 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 "dbginterface.h" +#include "method.hpp" +#include "peassembly.h" +#include +#include + +#ifdef FEATURE_INPROC_CRASHREPORT + +#include "inproccrashreporter.h" +#include "threadsuspend.h" +#include "gcenv.h" + +struct WalkContext +{ + InProcCrashReportFrameCallback callback; + void* userCtx; +}; + +static void BuildTypeName(LPUTF8 buffer, size_t bufferSize, LPCUTF8 namespaceName, LPCUTF8 className); + +static +StackWalkAction +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) + { + return SWA_CONTINUE; + } + + LPCUTF8 methodName = pMD->GetName(); + mdMethodDef token = pMD->GetMemberDef(); + + LPCUTF8 className = nullptr; + LPCUTF8 namespaceName = nullptr; + MethodTable* pMT = pMD->GetMethodTable(); + if (pMT != nullptr) + { + mdTypeDef cl = pMT->GetCl(); + IMDInternalImport* pImport = pMD->GetMDImport(); + if (pImport != nullptr && cl != mdTypeDefNil) + { + pImport->GetNameOfTypeDef(cl, &className, &namespaceName); + } + } + + char classNameBuf[CRASHREPORT_STRING_BUFFER_SIZE]; + BuildTypeName(classNameBuf, sizeof(classNameBuf), namespaceName, className); + + LPCUTF8 moduleName = nullptr; + Module* pModule = pMD->GetModule(); + if (pModule != nullptr) + { + Assembly* pAssembly = pModule->GetAssembly(); + if (pAssembly != nullptr) + { + moduleName = pAssembly->GetSimpleName(); + } + } + + uint32_t nativeOffset = pCF->HasFaulted() ? 0 : pCF->GetRelOffset(); + uint32_t ilOffset = 0; + PCODE ip = (PCODE)0; + TADDR stackPointer = (TADDR)0; + PREGDISPLAY pRD = pCF->GetRegisterSet(); + if (pRD != nullptr) + { + ip = GetControlPC(pRD); + stackPointer = GetRegdisplaySP(pRD); + } + + if (ip == (PCODE)0 && stackPointer == (TADDR)0) + { + return SWA_CONTINUE; + } + + if (g_pDebugInterface != nullptr && pMD != nullptr) + { + DWORD resolvedILOffset = 0; + 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; + } + } + + uint32_t moduleTimestamp = 0; + uint32_t moduleSize = 0; + char moduleGuid[MINIPAL_GUID_BUFFER_LEN]; + moduleGuid[0] = '\0'; + + if (pModule != nullptr) + { + PEAssembly* pPEAssembly = pModule->GetPEAssembly(); + if (pPEAssembly != nullptr && pPEAssembly->HasLoadedPEImage()) + { + moduleTimestamp = pPEAssembly->GetLoadedLayout()->GetTimeDateStamp(); + moduleSize = static_cast(pPEAssembly->GetLoadedLayout()->GetSize()); + } + + IMDInternalImport* pImport = pModule->GetMDImport(); + if (pImport != nullptr) + { + GUID mvid; + if (SUCCEEDED(pImport->GetScopeProps(nullptr, &mvid))) + { + minipal_guid_as_string(mvid, moduleGuid, MINIPAL_GUID_BUFFER_LEN); + } + } + } + + 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; +} + +static +void +CrashReportWalkThread( + Thread* pThread, + InProcCrashReportFrameCallback frameCallback, + void* ctx) +{ + if (pThread == nullptr || frameCallback == nullptr) + { + return; + } + + WalkContext walkContext = { frameCallback, ctx }; + pThread->StackWalkFrames(FrameCallbackAdapter, &walkContext, + QUICKUNWIND | FUNCTIONSONLY | ALLOW_ASYNC_STACK_WALK); +} + +static +void +CrashReportWalkStack( + InProcCrashReportFrameCallback frameCallback, + void* ctx) +{ + CrashReportWalkThread(GetThreadAsyncSafe(), frameCallback, ctx); +} + +static +bool +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( + LPUTF8 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( + Thread* pThread, + char* exceptionTypeBuf, + size_t exceptionTypeBufSize, + uint32_t* hresult) +{ + CONTRACTL + { + NOTHROW; + GC_NOTRIGGER; + CANNOT_TAKE_LOCK; + MODE_ANY; + } + CONTRACTL_END; + + if (exceptionTypeBufSize > 0) + { + exceptionTypeBuf[0] = '\0'; + } + + if (hresult != nullptr) + { + *hresult = 0; + } + + // Only inspect the managed throwable when the thread is already in cooperative mode. + if (!pThread->PreemptiveGCDisabled()) + { + return false; + } + + bool result = false; + + GCX_COOP(); + + OBJECTREF throwable = pThread->GetThrowable(); + GCPROTECT_BEGIN(throwable); + + if (throwable != nullptr) + { + MethodTable* pMT = throwable->GetMethodTable(); + if (pMT != nullptr) + { + mdTypeDef cl = pMT->GetCl(); + Module* pModule = pMT->GetModule(); + if (pModule != nullptr) + { + IMDInternalImport* pImport = pModule->GetMDImport(); + if (pImport != nullptr && cl != mdTypeDefNil) + { + LPCUTF8 className = nullptr; + LPCUTF8 namespaceName = nullptr; + pImport->GetNameOfTypeDef(cl, &className, &namespaceName); + + BuildTypeName(exceptionTypeBuf, exceptionTypeBufSize, namespaceName, className); + } + } + } + + if (hresult != nullptr) + { + *hresult = static_cast(((EXCEPTIONREF)throwable)->GetHResult()); + } + + result = true; + } + + GCPROTECT_END(); + + return result; +} + +static +bool +CrashReportGetException( + char* exceptionTypeBuf, + size_t exceptionTypeBufSize, + uint32_t* hresult) +{ + Thread* pThread = GetThreadAsyncSafe(); + if (pThread == nullptr) + { + return false; + } + + 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 +CrashReportSuspendThreads(Thread* pCrashThread) +{ + if (g_fFatalErrorOccurredOnGCThread + || GCHeapUtilities::IsGCInProgress() + || IsGCSpecialThread() + || ThreadStore::HoldingThreadStore(pCrashThread)) + { + return false; + } + + ThreadSuspend::SuspendEE(ThreadSuspend::SUSPEND_OTHER); + return true; +} + +static +void +CrashReportResumeThreads() +{ + ThreadSuspend::RestartEE(FALSE /* bFinishedGC */, TRUE /* SuspendSucceeded */); +} + +static +void +CrashReportEnumerateThreads( + uint64_t crashingTid, + InProcCrashReportThreadCallback threadCallback, + InProcCrashReportFrameCallback frameCallback, + void* ctx) +{ + Thread* pCrashThread = GetThreadAsyncSafe(); + + bool runtimeSuspended = CrashReportSuspendThreads(pCrashThread); + + // Emit the crashing thread first so the report keeps the most important + // thread even if later enumeration is incomplete. + if (pCrashThread != nullptr) + { + uint64_t crashOsId = static_cast(pCrashThread->GetOSThreadId()); + if (crashOsId == crashingTid) + { + char exceptionType[CRASHREPORT_STRING_BUFFER_SIZE]; + uint32_t hresult = 0; + bool hasException = CrashReportGetExceptionForThread(pCrashThread, exceptionType, sizeof(exceptionType), &hresult); + + threadCallback(crashOsId, true, hasException ? exceptionType : "", hresult, ctx); + + CrashReportWalkThread(pCrashThread, frameCallback, ctx); + } + } + + // 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 (runtimeSuspended) + { + Thread* pThread = nullptr; + while ((pThread = ThreadStore::GetThreadList(pThread)) != nullptr) + { + if (pThread == pCrashThread) + continue; + + uint64_t osThreadId = static_cast(pThread->GetOSThreadId()); + if (osThreadId == 0 || osThreadId == crashingTid) + continue; + + threadCallback(osThreadId, false, "", 0, ctx); + CrashReportWalkThread(pThread, frameCallback, ctx); + } + + CrashReportResumeThreads(); + } +} + +void +CrashReportConfigure() +{ + // 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; + if (dumpName == nullptr || dumpName[0] == '\0') + { + return; + } + + InProcCrashReporterSettings settings = {}; + settings.reportPath = dumpName; + settings.isManagedThreadCallback = CrashReportIsCurrentThreadManaged; + settings.walkStackCallback = CrashReportWalkStack; + settings.getExceptionCallback = CrashReportGetException; + settings.enumerateThreadsCallback = CrashReportEnumerateThreads; + + // 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 new file mode 100644 index 00000000000000..7afa32eeb71f2f --- /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 FEATURE_INPROC_CRASHREPORT + +void CrashReportConfigure(); + +#endif // FEATURE_INPROC_CRASHREPORT + +#endif // CRASHREPORTSTACKWALKER_H