diff --git a/src/coreclr/debug/crashreport/CMakeLists.txt b/src/coreclr/debug/crashreport/CMakeLists.txt index c9e2a62540ddb3..c9827e1713a6ba 100644 --- a/src/coreclr/debug/crashreport/CMakeLists.txt +++ b/src/coreclr/debug/crashreport/CMakeLists.txt @@ -1,9 +1,11 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CRASHREPORT_SOURCES + crashreportstringutils.cpp signalsafeformatter.cpp signalsafejsonwriter.cpp signalsafeconsolewriter.cpp + inproccrashreportlifecycle.cpp inproccrashreporter.cpp inproccrashreportwatchdog.cpp ) diff --git a/src/coreclr/debug/crashreport/crashreportstringutils.cpp b/src/coreclr/debug/crashreport/crashreportstringutils.cpp new file mode 100644 index 00000000000000..ee8872ac1a72ee --- /dev/null +++ b/src/coreclr/debug/crashreport/crashreportstringutils.cpp @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include "crashreportstringutils.h" + +#include + +void CrashReportStringUtils::CopyString(char* buffer, size_t bufferSize, const char* value) +{ + if (buffer == nullptr || bufferSize == 0) + { + return; + } + + if (value == nullptr) + { + buffer[0] = '\0'; + return; + } + + size_t toCopy = strnlen(value, bufferSize - 1); + if (toCopy != 0) + { + memcpy(buffer, value, toCopy); + } + + buffer[toCopy] = '\0'; +} + +bool CrashReportStringUtils::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'; +} diff --git a/src/coreclr/debug/crashreport/crashreportstringutils.h b/src/coreclr/debug/crashreport/crashreportstringutils.h new file mode 100644 index 00000000000000..a3499908dd6de7 --- /dev/null +++ b/src/coreclr/debug/crashreport/crashreportstringutils.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. + +// Shared, allocation-free, bounds-safe string helpers used by both the in-proc +// crash reporter and the crash report lifecycle. These are safe to call from the +// signal/crash path: they perform no heap allocation and never call into the +// runtime. + +#pragma once + +#include + +namespace CrashReportStringUtils +{ + // Copies value into buffer, truncating if necessary, and always + // null-terminates. A null value yields an empty string. No-op if buffer is + // null or bufferSize is 0. + void CopyString( + char* buffer, + size_t bufferSize, + const char* value); + + // Appends value to buffer starting at *pos, copying as much as fits, advancing + // *pos past the characters actually written, and always null-terminating. + // Returns true if the entire value fit; returns false if the value was + // truncated, the arguments are invalid, or bufferSize is 0. + bool AppendString( + char* buffer, + size_t bufferSize, + size_t* pos, + const char* value); +} diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index eb3560c6306a1c..09ec9ddfc12607 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -6,6 +6,8 @@ // Streams a createdump-shaped JSON skeleton to a crashreport.json file. #include "inproccrashreporter.h" +#include "inproccrashreportlifecycle.h" +#include "crashreportstringutils.h" #include "inproccrashreportwatchdog.h" #include "signalsafeconsolewriter.h" #include "signalsafejsonwriter.h" @@ -98,33 +100,10 @@ struct StackOverflowTraceSnapshot static char sccsid[] = "@(#)Version N/A"; #endif -static void CopyStringToBuffer(char* buffer, size_t bufferSize, const char* value) -{ - if (buffer == nullptr || bufferSize == 0) - { - return; - } - - if (value == nullptr) - { - buffer[0] = '\0'; - return; - } - - size_t toCopy = strnlen(value, bufferSize - 1); - if (toCopy != 0) - { - memcpy(buffer, value, toCopy); - } - - buffer[toCopy] = '\0'; -} - #if defined(TARGET_IOS) || defined(TARGET_TVOS) || defined(TARGET_MACCATALYST) // Query a sysctl by name into a caller-supplied buffer. Called from Initialize, NOT from the // signal handler -- sysctl/sysctlbyname is not on POSIX's async-signal-safe list, so the -// queried values are cached for use during crash reporting (mirrors the hostName / -// gethostname pattern). +// queried values are cached for use during crash reporting. static void CacheSysctlString(const char* sysctlName, char* buffer, size_t bufferSize) { buffer[0] = '\0'; @@ -409,12 +388,6 @@ class InProcCrashReporter bool jsonEnabled, int fd); - bool BuildReportPath(); - size_t ExpandDumpTemplate( - char* buffer, - size_t bufferSize, - const char* pattern); - static const char* GetSignalNameAscii(int signal); SignalSafeJsonWriter m_jsonWriter; @@ -430,10 +403,9 @@ class InProcCrashReporter InProcCrashReportModuleInfoCallback m_moduleInfoCallback = nullptr; volatile LONG m_crashKind = static_cast(InProcCrashReportCrashKind::Unknown); uint32_t m_frameLimitPerThread = 0; - char m_reportPath[CRASHREPORT_PATH_BUFFER_SIZE]; + InProcCrashReportLifecycle m_lifecycle; char m_reportFilePath[CRASHREPORT_PATH_BUFFER_SIZE]; char m_processName[CRASHREPORT_STRING_BUFFER_SIZE]; - char m_hostName[CRASHREPORT_STRING_BUFFER_SIZE]; char m_stringScratch[CRASHREPORT_STRING_BUFFER_SIZE]; #if defined(TARGET_IOS) || defined(TARGET_TVOS) || defined(TARGET_MACCATALYST) char m_osVersion[CRASHREPORT_STRING_BUFFER_SIZE]; @@ -482,11 +454,6 @@ class CrashReportHelpers static const char* GetFilename( const char* path); - static void CopyString( - char* buffer, - size_t bufferSize, - const char* value); - static void WriteFrameToJson( SignalSafeJsonWriter* writer, SignalSafeFormatter* formatter, @@ -593,9 +560,9 @@ class CrashReportHelpers size_t len); // SignalSafeJsonWriter callback that drops everything: used when the - // crash report is running in compact-log-only mode (no DbgMiniDumpName) - // so the JSON formatter still keeps its bookkeeping consistent without - // emitting bytes anywhere. + // crash report is running in compact-log-only mode (no managed report + // directory configured) so the JSON formatter still keeps its bookkeeping + // consistent without emitting bytes anywhere. static bool DiscardOutputCallback(const char* buffer, size_t len, void* ctx); }; @@ -613,21 +580,17 @@ InProcCrashReporter::CreateReport( CrashReportWatchdogScope watchdogScope; m_reportFilePath[0] = '\0'; - // The JSON file sink is only enabled when DbgMiniDumpName supplied a - // template AND the template expanded to a valid path. Otherwise the - // crash report runs in compact-log-only mode: the JSON emitter still - // executes (so it can keep its bookkeeping consistent) but writes go - // to a no-op DiscardOutputCallback instead of an open fd. - bool jsonEnabled = m_reportPath[0] != '\0' && BuildReportPath(); - + // The JSON file sink is enabled only by lifecycle-managed output. Otherwise + // the crash report runs in compact-log-only mode: the JSON emitter still + // executes (so it can keep its bookkeeping consistent) but writes go to a + // no-op DiscardOutputCallback instead of an open fd. int fd = -1; - if (jsonEnabled) + bool jsonEnabled = m_lifecycle.IsReportFileOutputEnabled() && + m_lifecycle.PrepareReportFile(&m_formatter, m_reportFilePath, sizeof(m_reportFilePath), &fd); + + if (jsonEnabled && fd == -1) { - fd = open(m_reportFilePath, O_WRONLY | O_CREAT | O_TRUNC, 0600); - if (fd == -1) - { - jsonEnabled = false; - } + jsonEnabled = false; } InProcCrashReportCrashKind crashKind = static_cast( @@ -747,7 +710,7 @@ InProcCrashReporter::Initialize( m_frameLimitPerThread = settings.frameLimitPerThread; m_crashKind = static_cast(InProcCrashReportCrashKind::Unknown); m_stackOverflowTrace.available = 0; - CrashReportHelpers::CopyString(m_reportPath, sizeof(m_reportPath), settings.reportPath); + m_reportFilePath[0] = '\0'; (void)CrashReportWatchdog::TryInitialize(settings.timeoutSeconds); @@ -765,7 +728,7 @@ InProcCrashReporter::Initialize( if (n > 0) { m_stringScratch[n] = '\0'; - CrashReportHelpers::CopyString(m_processName, sizeof(m_processName), CrashReportHelpers::GetFilename(m_stringScratch)); + CrashReportStringUtils::CopyString(m_processName, sizeof(m_processName), CrashReportHelpers::GetFilename(m_stringScratch)); } } #endif @@ -773,22 +736,17 @@ InProcCrashReporter::Initialize( { if (char* exePath = minipal_getexepath()) { - CrashReportHelpers::CopyString(m_processName, sizeof(m_processName), CrashReportHelpers::GetFilename(exePath)); + CrashReportStringUtils::CopyString(m_processName, sizeof(m_processName), CrashReportHelpers::GetFilename(exePath)); free(exePath); } } - // Cache hostname here because gethostname is not on the POSIX - // async-signal-safe list; the dump-template expander needs it for %h - // expansion at crash time. - m_hostName[0] = '\0'; - if (gethostname(m_hostName, sizeof(m_hostName) - 1) == 0) - { - m_hostName[sizeof(m_hostName) - 1] = '\0'; - } - else + // File output is produced only through the lifecycle-managed report + // directory. When no root is configured the reporter still runs, emitting + // compact console logs without writing a JSON report file. + if (settings.reportRootPath != nullptr && settings.reportRootPath[0] != '\0') { - m_hostName[0] = '\0'; + m_lifecycle.Initialize(settings.reportRootPath, settings.maxFileCount); } #if defined(TARGET_IOS) || defined(TARGET_TVOS) || defined(TARGET_MACCATALYST) @@ -832,7 +790,7 @@ InProcCrashReporter::AddStackOverflowTraceFrame( } StackOverflowTraceFrame& frame = trace.frames[trace.frameCount++]; - CopyStringToBuffer(frame.methodName, sizeof(frame.methodName), methodName); + CrashReportStringUtils::CopyString(frame.methodName, sizeof(frame.methodName), methodName); frame.repeatCount = repeatCount; frame.repeatSequenceLength = repeatSequenceLength; } @@ -854,7 +812,10 @@ InProcCrashReportSignalDispatcher(int signal, void* siginfo, void* context) return; } + // Preserve the interrupted context's errno before the crash reporter uses syscalls. + int savedErrno = errno; reporter->CreateReport(signal, context); + errno = savedErrno; } void @@ -1015,131 +976,6 @@ CrashReportOutputContext::ChunkCallback( return outputContext->HandleChunk(buffer, len); } -// Expand the coredump template patterns supported by createdump's -// FormatDumpName for DOTNET_DbgMiniDumpName: %% %p %d (PID), %e (process -// name, cached at Initialize), %h (hostname, cached at Initialize), and %t -// (current epoch seconds via time(2), POSIX async-signal-safe). Unknown -// specifiers are rejected (return 0) to match createdump and to avoid -// silently producing diverging file names from the same template. -size_t -InProcCrashReporter::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 != '%') - { - buffer[pos++] = *pattern++; - continue; - } - - pattern++; - char specifier = *pattern; - - const char* substitution = nullptr; - - switch (specifier) - { - case '%': - if (pos + 1 < bufferSize) - { - buffer[pos++] = '%'; - } - pattern++; - continue; - - case 'p': - case 'd': - substitution = m_formatter.FormatUnsignedDecimal(pid); - break; - - case 'e': - substitution = (m_processName[0] != '\0') ? m_processName : nullptr; - break; - - case 'h': - substitution = (m_hostName[0] != '\0') ? m_hostName : nullptr; - break; - - case 't': - substitution = m_formatter.FormatUnsignedDecimal(static_cast(time(nullptr))); - break; - - default: - // Unknown / unsupported specifier; fail rather than emit a - // path with a literal '%X' that would diverge from the file - // name createdump would produce for the same template. - return 0; - } - - if (substitution == nullptr) - { - // Required substitution unavailable (e.g. hostname capture failed - // at Initialize). Fail rather than emit a path missing this - // component, which could collide with the dump file on disk. - return 0; - } - - size_t subLen = strlen(substitution); - if (pos + subLen >= bufferSize) - { - return 0; - } - memcpy(buffer + pos, substitution, subLen); - pos += subLen; - - if (*pattern != '\0') - { - pattern++; - } - } - - buffer[pos] = '\0'; - if (*pattern != '\0') - { - // The output buffer filled before the full template was consumed. - // Fail rather than returning a truncated path that could collide or - // unexpectedly change the report location. - return 0; - } - return pos; -} - -bool -InProcCrashReporter::BuildReportPath() -{ - if (m_reportPath[0] == '\0') - { - return false; - } - - size_t pos = ExpandDumpTemplate( - m_reportFilePath, - sizeof(m_reportFilePath), - m_reportPath); - if (pos == 0) - { - return false; - } - - if (!CrashReportHelpers::AppendString(m_reportFilePath, sizeof(m_reportFilePath), &pos, ".crashreport.json")) - { - return false; - } - return true; -} - void CrashReportHelpers::GetVersionString( char* buffer, @@ -1187,19 +1023,7 @@ CrashReportHelpers::AppendString( 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'; + return CrashReportStringUtils::AppendString(buffer, bufferSize, pos, value); } void @@ -1336,11 +1160,11 @@ CrashReportHelpers::BuildMethodName( } else if (className != nullptr) { - CopyString(buffer, bufferSize, className); + CrashReportStringUtils::CopyString(buffer, bufferSize, className); } else if (methodName != nullptr) { - CopyString(buffer, bufferSize, methodName); + CrashReportStringUtils::CopyString(buffer, bufferSize, methodName); } else { @@ -1397,15 +1221,6 @@ HasManagedIdentity( (token != 0 && HasModuleName(moduleName)); } -void -CrashReportHelpers::CopyString( - char* buffer, - size_t bufferSize, - const char* value) -{ - CopyStringToBuffer(buffer, bufferSize, value); -} - void CrashReportHelpers::WriteFrameToJson( SignalSafeJsonWriter* writer, @@ -2312,10 +2127,9 @@ InProcCrashReporter::EndJsonReport( writeFailed = true; } - if (close(fd) != 0 || !finishSucceeded || writeFailed) - { - unlink(m_reportFilePath); - } + bool closeSucceeded = close(fd) == 0; + bool reportSucceeded = finishSucceeded && !writeFailed && closeSucceeded; + m_lifecycle.FinishReportFile(reportSucceeded, m_reportFilePath); } else { diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.h b/src/coreclr/debug/crashreport/inproccrashreporter.h index ad69426b479d1c..90f54a1e4ad367 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.h +++ b/src/coreclr/debug/crashreport/inproccrashreporter.h @@ -15,12 +15,11 @@ #include // Scratch-buffer sizes used throughout the in-proc crash reporter: -// - 1024 (matching createdump's MAX_LONGPATH) for paths (report paths and -// expanded dump templates), so DOTNET_DbgMiniDumpName values that work -// with createdump also work here. +// - 1024 (matching createdump's MAX_LONGPATH) for report paths. // - 256 for identifiers (process name, type/class/exception names). static constexpr size_t CRASHREPORT_PATH_BUFFER_SIZE = 1024; static constexpr size_t CRASHREPORT_STRING_BUFFER_SIZE = 256; +static constexpr int32_t CRASHREPORT_DEFAULT_MAX_FILE_COUNT = 32; #if defined(__ANDROID__) static const char CRASHREPORT_LOG_TAG[] = "DOTNET_CRASH"; @@ -73,13 +72,14 @@ using InProcCrashReportModuleInfoCallback = bool (*)( struct InProcCrashReporterSettings { - const char* reportPath; + const char* reportRootPath; int timeoutSeconds; InProcCrashReportIsManagedThreadCallback isManagedThreadCallback; InProcCrashReportWalkStackCallback walkStackCallback; InProcCrashReportEnumerateThreadsCallback enumerateThreadsCallback; InProcCrashReportModuleInfoCallback moduleInfoCallback; uint32_t frameLimitPerThread; + int32_t maxFileCount; }; // Free-function entry point used by the runtime to wire the in-proc crash diff --git a/src/coreclr/debug/crashreport/inproccrashreportlifecycle.cpp b/src/coreclr/debug/crashreport/inproccrashreportlifecycle.cpp new file mode 100644 index 00000000000000..31159e05f66062 --- /dev/null +++ b/src/coreclr/debug/crashreport/inproccrashreportlifecycle.cpp @@ -0,0 +1,569 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include "inproccrashreportlifecycle.h" + +#include "crashreportstringutils.h" +#include "pal.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const char CrashReportManagedRootDirectory[] = ".dotnet"; +static const char CrashReportManagedReportDirectory[] = "crash-reports"; +static const char CrashReportFilePrefix[] = "report-"; +static const char CrashReportFileExtension[] = ".crashreport.json"; +static const char CrashReportTempExtension[] = ".tmp"; + +static const uint64_t NanosecondsPerSecond = 1000000000ull; + +bool +InProcCrashReportLifecycle::Initialize( + const char* rootPath, + int32_t maxFileCount) +{ + m_reportFileOutputEnabled = false; + m_reportDirectory[0] = '\0'; + m_tempReportFilePath[0] = '\0'; + m_cachedOldestReport.value[0] = '\0'; + + if (!EstablishReportDirectory(rootPath)) + { + return false; + } + + if (!PruneExistingReports(maxFileCount)) + { + return false; + } + + m_reportFileOutputEnabled = true; + return true; +} + +bool +InProcCrashReportLifecycle::EstablishReportDirectory( + const char* rootPath) +{ + if (rootPath == nullptr || rootPath[0] == '\0') + { + return false; + } + + char root[CRASHREPORT_PATH_BUFFER_SIZE]; + if (!ResolveRootPath(root, sizeof(root), rootPath)) + { + InProcCrashReportLogInitializationFailure(".NET crash report file output disabled: invalid CrashReportRootPath"); + return false; + } + + struct stat rootStat; + if (stat(root, &rootStat) != 0 || !S_ISDIR(rootStat.st_mode)) + { + InProcCrashReportLogInitializationFailure(".NET crash report file output disabled: CrashReportRootPath is not an existing directory"); + return false; + } + + size_t pos = 0; + if (!CrashReportStringUtils::AppendString(m_reportDirectory, sizeof(m_reportDirectory), &pos, root) || + !AppendPathComponent(m_reportDirectory, sizeof(m_reportDirectory), &pos, CrashReportManagedRootDirectory) || + !EnsureDirectory(m_reportDirectory) || + !AppendPathComponent(m_reportDirectory, sizeof(m_reportDirectory), &pos, CrashReportManagedReportDirectory) || + !EnsureDirectory(m_reportDirectory) || + !ProbeDirectoryWritable(m_reportDirectory)) + { + InProcCrashReportLogInitializationFailure(".NET crash report file output disabled: failed to initialize crash report directory"); + m_reportDirectory[0] = '\0'; + return false; + } + + return true; +} + +size_t +InProcCrashReportLifecycle::FindOldestReportIndex( + const FileInfo* reports, + size_t reportCount) +{ + size_t oldest = 0; + for (size_t i = 1; i < reportCount; i++) + { + if (CompareFileInfo(&reports[i], &reports[oldest]) < 0) + { + oldest = i; + } + } + + return oldest; +} + +bool +InProcCrashReportLifecycle::PruneExistingReports(int32_t maxFileCount) +{ + DIR* dir = opendir(m_reportDirectory); + if (dir == nullptr) + { + InProcCrashReportLogInitializationFailure(".NET crash report file output disabled: failed to scan crash report directory"); + return false; + } + + // Retain at most the newest maxFileCount completed reports. The kept set is + // held in a fixed array sized to the bound, so a directory with an + // unexpectedly large number of reports cannot drive an unbounded allocation; + // overflow reports are unlinked inline as they are encountered. maxFileCount + // is guaranteed positive by the configuration layer. + size_t capacity = static_cast(maxFileCount); + FileInfo* kept = new (std::nothrow) FileInfo[capacity](); + if (kept == nullptr) + { + closedir(dir); + InProcCrashReportLogInitializationFailure(".NET crash report file output disabled: failed to allocate retention scan storage"); + return false; + } + + size_t keptCount = 0; + while (dirent* entry = readdir(dir)) + { + FileInfo info = {}; + bool hasTempExtension = false; + bool parsedOwnedName = TryParseReportName(entry->d_name, &info, &hasTempExtension); + bool isTemp = parsedOwnedName && hasTempExtension; + bool isCompleted = parsedOwnedName && !hasTempExtension; + + if (!isTemp && !isCompleted) + { + continue; + } + + char fullPath[CRASHREPORT_PATH_BUFFER_SIZE]; + fullPath[0] = '\0'; + size_t fullPathPos = 0; + if (!CrashReportStringUtils::AppendString(fullPath, sizeof(fullPath), &fullPathPos, m_reportDirectory) || + !AppendPathComponent(fullPath, sizeof(fullPath), &fullPathPos, entry->d_name)) + { + continue; + } + + if (isTemp) + { + // Any leftover temp file is from a previous, now-defunct run of this + // app (each app has its own report directory under its private + // storage, and the writer renames its temp to the final name before + // returning), so it can be removed unconditionally. + unlink(fullPath); + continue; + } + + if (keptCount < capacity) + { + kept[keptCount].timestamp = info.timestamp; + kept[keptCount].pid = info.pid; + CrashReportStringUtils::CopyString(kept[keptCount].path.value, sizeof(kept[keptCount].path.value), fullPath); + keptCount++; + continue; + } + + // The kept set is full, so this entry competes with the current oldest + // kept report: unlink the older of the two and keep the newer. A linear + // FindOldestReportIndex scan is used rather than a timestamp-ordered heap: + // the array holds at most maxFileCount entries (a small bound), this runs + // once at init, and the directory is pruned to the bound on every init so + // the scanned count stays near the bound in steady state. A min-heap would + // perform better but adds an dependency and heap bookkeeping + // for no measurable gain here. + size_t oldestIndex = FindOldestReportIndex(kept, keptCount); + + if (kept[oldestIndex].timestamp < info.timestamp || + (kept[oldestIndex].timestamp == info.timestamp && strcmp(kept[oldestIndex].path.value, fullPath) < 0)) + { + unlink(kept[oldestIndex].path.value); + kept[oldestIndex].timestamp = info.timestamp; + kept[oldestIndex].pid = info.pid; + CrashReportStringUtils::CopyString(kept[oldestIndex].path.value, sizeof(kept[oldestIndex].path.value), fullPath); + } + else + { + unlink(fullPath); + } + } + + closedir(dir); + + // A full kept set means the directory already holds maxFileCount reports, so + // the next crash report would exceed the bound: cache the oldest, and the + // crash path unlinks it before publishing the new report. Below the bound + // nothing is cached and the crash path deletes nothing. + if (keptCount == capacity) + { + size_t oldestIndex = FindOldestReportIndex(kept, keptCount); + CrashReportStringUtils::CopyString(m_cachedOldestReport.value, sizeof(m_cachedOldestReport.value), kept[oldestIndex].path.value); + } + + delete[] kept; + return true; +} + +bool +InProcCrashReportLifecycle::PrepareReportFile( + SignalSafeFormatter* formatter, + char* reportFilePath, + size_t reportFilePathSize, + int* fd) +{ + if (formatter == nullptr || reportFilePath == nullptr || reportFilePathSize == 0 || + fd == nullptr || m_reportDirectory[0] == '\0') + { + return false; + } + + reportFilePath[0] = '\0'; + *fd = -1; + + // Nanosecond-resolution timestamp keeps report names unique without a retry + // loop: even back-to-back crashes in the same process get distinct names. + // clock_gettime(CLOCK_REALTIME) is POSIX async-signal-safe, so it is valid + // on the crash path. A failed read degrades to a zero timestamp rather than + // aborting the report write. + struct timespec now = {}; + clock_gettime(CLOCK_REALTIME, &now); + uint64_t timestampNs = static_cast(now.tv_sec) * NanosecondsPerSecond + + static_cast(now.tv_nsec); + uint32_t pid = static_cast(GetCurrentProcessId()); + + // Delete the cached over-retention report (if any) before opening the temp + // file, freeing a slot so the completed set stays at the bound. A later write + // failure intentionally does not restore it. + if (m_cachedOldestReport.value[0] != '\0') + { + unlink(m_cachedOldestReport.value); + } + + if (!BuildReportPaths(formatter, reportFilePath, reportFilePathSize, m_tempReportFilePath, sizeof(m_tempReportFilePath), timestampNs, pid)) + { + return false; + } + + int tempFd = open(m_tempReportFilePath, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600); + if (tempFd == -1) + { + reportFilePath[0] = '\0'; + m_tempReportFilePath[0] = '\0'; + return false; + } + + *fd = tempFd; + return true; +} + +void +InProcCrashReportLifecycle::FinishReportFile( + bool succeeded, + const char* reportFilePath) +{ + if (m_tempReportFilePath[0] == '\0') + { + return; + } + + // Publish the completed report by renaming the temp file to its final name. + // rename is async-signal-safe and, unlike link, is permitted in the Android + // and Apple app-private storage sandboxes (where link fails with EPERM); temp + // and final share m_reportDirectory, so this is an atomic same-directory op. + // Only publish when the destination is absent (access fails with ENOENT) to + // preserve the "never overwrite a completed report" invariant; any other + // errno leaves the destination state unknown, so decline rather than risk a + // replace. The collision-resistant final name (nanosecond timestamp plus pid) + // keeps the residual TOCTOU window benign. On any failure the temp is removed. + if (succeeded && reportFilePath != nullptr && reportFilePath[0] != '\0' && + access(reportFilePath, F_OK) != 0 && errno == ENOENT && + rename(m_tempReportFilePath, reportFilePath) == 0) + { + m_tempReportFilePath[0] = '\0'; + return; + } + + unlink(m_tempReportFilePath); + m_tempReportFilePath[0] = '\0'; +} + +bool +InProcCrashReportLifecycle::BuildReportPaths( + SignalSafeFormatter* formatter, + char* reportFilePath, + size_t reportFilePathSize, + char* tempReportFilePath, + size_t tempReportFilePathSize, + uint64_t timestamp, + uint32_t pid) +{ + reportFilePath[0] = '\0'; + tempReportFilePath[0] = '\0'; + + size_t pos = 0; + if (!CrashReportStringUtils::AppendString(reportFilePath, reportFilePathSize, &pos, m_reportDirectory) || + !AppendPathComponent(reportFilePath, reportFilePathSize, &pos, CrashReportFilePrefix) || + !CrashReportStringUtils::AppendString(reportFilePath, reportFilePathSize, &pos, formatter->FormatUnsignedDecimal(timestamp)) || + !CrashReportStringUtils::AppendString(reportFilePath, reportFilePathSize, &pos, "-") || + !CrashReportStringUtils::AppendString(reportFilePath, reportFilePathSize, &pos, formatter->FormatUnsignedDecimal(pid))) + { + return false; + } + + if (!CrashReportStringUtils::AppendString(reportFilePath, reportFilePathSize, &pos, CrashReportFileExtension)) + { + return false; + } + + size_t tempPos = 0; + return CrashReportStringUtils::AppendString(tempReportFilePath, tempReportFilePathSize, &tempPos, reportFilePath) && + CrashReportStringUtils::AppendString(tempReportFilePath, tempReportFilePathSize, &tempPos, CrashReportTempExtension); +} + +bool +InProcCrashReportLifecycle::AppendPathComponent( + char* buffer, + size_t bufferSize, + size_t* pos, + const char* component) +{ + if (buffer == nullptr || pos == nullptr || component == nullptr || component[0] == '\0') + { + return false; + } + + if (*pos != 0 && buffer[*pos - 1] != '/') + { + if (!CrashReportStringUtils::AppendString(buffer, bufferSize, pos, "/")) + { + return false; + } + } + + while (*component == '/') + { + component++; + } + + return component[0] != '\0' && CrashReportStringUtils::AppendString(buffer, bufferSize, pos, component); +} + +bool +InProcCrashReportLifecycle::IsAbsolutePath(const char* path) +{ + return path != nullptr && path[0] == '/'; +} + +bool +InProcCrashReportLifecycle::ResolveRootPath( + char* buffer, + size_t bufferSize, + const char* rootPath) +{ + if (buffer == nullptr || bufferSize == 0 || rootPath == nullptr || rootPath[0] == '\0') + { + return false; + } + + // The configuring host is responsible for supplying a fully-resolved + // absolute path; the runtime does not expand a leading '~' or environment + // variables and rejects anything that is not already absolute. + if (!IsAbsolutePath(rootPath)) + { + return false; + } + + buffer[0] = '\0'; + size_t pos = 0; + return CrashReportStringUtils::AppendString(buffer, bufferSize, &pos, rootPath); +} + +bool +InProcCrashReportLifecycle::EnsureDirectory(const char* path) +{ + if (path == nullptr || path[0] == '\0') + { + return false; + } + + struct stat st; + if (stat(path, &st) == 0) + { + return S_ISDIR(st.st_mode); + } + + if (errno != ENOENT) + { + return false; + } + + if (mkdir(path, 0700) != 0) + { + return errno == EEXIST && stat(path, &st) == 0 && S_ISDIR(st.st_mode); + } + + return true; +} + +bool +InProcCrashReportLifecycle::ProbeDirectoryWritable(const char* path) +{ + // This runs only on the initialization path, so the probe paths are kept in + // local stack buffers rather than borrowing a member buffer; that keeps the + // probe self-contained and off both the heap and the signal path. + char probePath[CRASHREPORT_PATH_BUFFER_SIZE]; + probePath[0] = '\0'; + size_t pos = 0; + + // Use a hidden throwaway file to verify the directory allows create, rename, + // and delete (rename is the operation FinishReportFile uses to publish). + SignalSafeFormatter formatter; + bool built = + CrashReportStringUtils::AppendString(probePath, sizeof(probePath), &pos, path) && + CrashReportStringUtils::AppendString(probePath, sizeof(probePath), &pos, "/.probe-") && + CrashReportStringUtils::AppendString(probePath, sizeof(probePath), &pos, formatter.FormatUnsignedDecimal(static_cast(GetCurrentProcessId()))); + + bool writable = false; + if (built) + { + int fd = open(probePath, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600); + if (fd != -1) + { + bool closeSucceeded = close(fd) == 0; + + // Also verify the publish primitive the crash path depends on: a + // same-directory rename. Some sandboxes (notably Android and Apple + // app-private storage) permit create/delete yet reject other + // link/rename operations. Probing it here disables file output up + // front with a clear diagnostic instead of silently losing every + // report when FinishReportFile cannot publish on the signal path. + char committedPath[CRASHREPORT_PATH_BUFFER_SIZE]; + size_t committedPos = 0; + bool committedBuilt = + CrashReportStringUtils::AppendString(committedPath, sizeof(committedPath), &committedPos, probePath) && + CrashReportStringUtils::AppendString(committedPath, sizeof(committedPath), &committedPos, ".committed"); + + if (committedBuilt) + { + // Clear any stale committed artifact so the rename targets a fresh name instead of replacing it. + unlink(committedPath); + } + + if (committedBuilt && rename(probePath, committedPath) == 0) + { + bool unlinkSucceeded = unlink(committedPath) == 0; + writable = closeSucceeded && unlinkSucceeded; + } + else + { + // The rename probe failed (or the target path did not fit); remove the probe file so no stray artifact remains. + unlink(probePath); + } + } + } + + return writable; +} + +bool +InProcCrashReportLifecycle::TryParseReportName( + const char* name, + FileInfo* info, + bool* isTempExtension) +{ + if (name == nullptr || info == nullptr || isTempExtension == nullptr) + { + return false; + } + + *isTempExtension = false; + + size_t prefixLength = sizeof(CrashReportFilePrefix) - 1; + size_t extensionLength = sizeof(CrashReportFileExtension) - 1; + + // The shortest name this function can accept is the prefix, a single + // timestamp digit, the '-' separator, a single pid digit, and the extension. + // "0-0" encodes that minimal timestamp-separator-pid core. Reject anything + // shorter up front so we never walk the per-part parse for a name that cannot + // possibly match. + size_t minimumLength = prefixLength + (sizeof("0-0") - 1) + extensionLength; + if (strlen(name) < minimumLength) + { + return false; + } + + if (strncmp(name, CrashReportFilePrefix, prefixLength) != 0) + { + return false; + } + + const char* current = name + prefixLength; + + // The timestamp and pid are written by this process as plain decimal digits, + // so parse them directly with strtoull/strtoul. end == current means no digits + // were consumed, which rejects an empty timestamp or pid component. + char* end = nullptr; + uint64_t timestamp = strtoull(current, &end, 10); + if (end == current || *end != '-') + { + return false; + } + current = end + 1; + + uint64_t pid = strtoul(current, &end, 10); + if (end == current) + { + return false; + } + current = end; + + if (strncmp(current, CrashReportFileExtension, extensionLength) != 0) + { + return false; + } + current += extensionLength; + + if (*current != '\0') + { + size_t tempExtensionLength = sizeof(CrashReportTempExtension) - 1; + if (strncmp(current, CrashReportTempExtension, tempExtensionLength) != 0 || + current[tempExtensionLength] != '\0') + { + return false; + } + + *isTempExtension = true; + } + + info->timestamp = timestamp; + info->pid = pid; + return true; +} + +// Comparator ordering reports oldest-first (timestamp, then path). +int +InProcCrashReportLifecycle::CompareFileInfo( + const void* left, + const void* right) +{ + const FileInfo* leftInfo = reinterpret_cast(left); + const FileInfo* rightInfo = reinterpret_cast(right); + + if (leftInfo->timestamp < rightInfo->timestamp) + { + return -1; + } + if (leftInfo->timestamp > rightInfo->timestamp) + { + return 1; + } + return strcmp(leftInfo->path.value, rightInfo->path.value); +} diff --git a/src/coreclr/debug/crashreport/inproccrashreportlifecycle.h b/src/coreclr/debug/crashreport/inproccrashreportlifecycle.h new file mode 100644 index 00000000000000..84d92c8b5dba54 --- /dev/null +++ b/src/coreclr/debug/crashreport/inproccrashreportlifecycle.h @@ -0,0 +1,149 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma once + +#include "inproccrashreporter.h" +#include "signalsafeformatter.h" + +#include +#include + +// Manages the on-disk lifecycle of in-proc crash reports: at startup it +// establishes the managed report directory and prunes stale and over-retention +// reports; on a crash it hands out a uniquely named temp report file and +// finalizes it. Composed alongside (not derived from) the signal-safe writer +// family that emits the report contents. +// +// Members run in one of two execution contexts: +// * Initialization path -- runs once at process startup. May allocate and call +// libc/filesystem APIs; NOT async-signal-safe. This is the default contract +// for members of this class. +// * Crash/signal path -- invoked from the crash signal handler. Must be +// async-signal-safe and allocation-free. Only these members run there: +// IsReportFileOutputEnabled, PrepareReportFile, FinishReportFile, +// BuildReportPaths, and the shared AppendPathComponent. +// Each is marked accordingly below. +class InProcCrashReportLifecycle +{ +public: + InProcCrashReportLifecycle() = default; + InProcCrashReportLifecycle(const InProcCrashReportLifecycle&) = delete; + InProcCrashReportLifecycle& operator=(const InProcCrashReportLifecycle&) = delete; + + // Prepares lifecycle-managed storage for crash reports under rootPath, + // keeping at most maxFileCount completed reports. Runs at startup (not the + // crash path) and may allocate. Returns false if storage could not be + // initialized. + bool Initialize( + const char* rootPath, + int32_t maxFileCount); + + // Returns whether lifecycle-managed report files should be written. False when + // initialization failed. Crash/signal-path safe: reads one bool. + bool IsReportFileOutputEnabled() const { return m_reportFileOutputEnabled; } + + // Opens a uniquely named temporary report file under the managed directory, + // returning its path and an open fd. Deletes the cached over-retention report + // first. Runs on the crash/signal path: allocation-free and signal-safe. + // Returns false if no file could be opened. + bool PrepareReportFile( + SignalSafeFormatter* formatter, + char* reportFilePath, + size_t reportFilePathSize, + int* fd); + + // Finalizes the report opened by PrepareReportFile: on success renames the temp + // file to its final reportFilePath, otherwise removes the temp file. Runs on the + // crash/signal path: allocation-free and signal-safe. + void FinishReportFile( + bool succeeded, + const char* reportFilePath); + +private: + struct ReportPath + { + char value[CRASHREPORT_PATH_BUFFER_SIZE]; + }; + + struct FileInfo + { + uint64_t timestamp; + uint64_t pid; + ReportPath path; + }; + + // Resolves rootPath into m_reportDirectory, creating the managed directory + // tree and verifying it is writable. Initialization path; logs and + // returns false on failure. + bool EstablishReportDirectory( + const char* rootPath); + + // Scans m_reportDirectory, removing stale temp files and retaining only the + // newest maxFileCount completed reports (unlinking older ones inline). When + // the directory is already at the bound, caches the oldest retained report so + // the crash path can unlink it before publishing a new one. Initialization + // path; may allocate. Logs and returns false on failure. + bool PruneExistingReports(int32_t maxFileCount); + + // Returns the index of the oldest report in reports per CompareFileInfo. + static size_t FindOldestReportIndex( + const FileInfo* reports, + size_t reportCount); + + // Builds the final and temporary report paths from the timestamp/pid + // into the caller-provided buffers. Crash-path, allocation-free. + bool BuildReportPaths( + SignalSafeFormatter* formatter, + char* reportFilePath, + size_t reportFilePathSize, + char* tempReportFilePath, + size_t tempReportFilePathSize, + uint64_t timestamp, + uint32_t pid); + + // Appends component as a new path segment (inserting a single '/' separator as + // needed). Allocation-free; used by both the init and crash paths. + static bool AppendPathComponent( + char* buffer, + size_t bufferSize, + size_t* pos, + const char* component); + + // Returns whether path is absolute (begins with '/'). Initialization path. + static bool IsAbsolutePath(const char* path); + + // Copies rootPath into buffer, requiring it to already be an absolute path; + // the runtime does not expand a leading '~' or environment variables. + static bool ResolveRootPath( + char* buffer, + size_t bufferSize, + const char* rootPath); + + // Creates the directory if missing; succeeds if it already exists as a directory. + static bool EnsureDirectory(const char* path); + + // Verifies the directory permits create, rename, and delete by exercising a + // hidden probe file (rename is the primitive FinishReportFile uses to publish + // a completed report). Runs only at initialization, so the probe paths live in + // local stack buffers. + bool ProbeDirectoryWritable(const char* path); + + // Parses a managed report file name (report--), + // accepting either a completed report or the in-progress .tmp form, into info. + // Reports which form matched through isTempExtension. + static bool TryParseReportName( + const char* name, + FileInfo* info, + bool* isTempExtension); + + // Comparator ordering reports oldest-first (timestamp, then path). + static int CompareFileInfo( + const void* left, + const void* right); + + char m_reportDirectory[CRASHREPORT_PATH_BUFFER_SIZE] = {}; + char m_tempReportFilePath[CRASHREPORT_PATH_BUFFER_SIZE] = {}; + ReportPath m_cachedOldestReport = {}; + bool m_reportFileOutputEnabled = false; +}; diff --git a/src/coreclr/inc/clrconfigvalues.h b/src/coreclr/inc/clrconfigvalues.h index 324e2b7241bed5..ccc0b586789e68 100644 --- a/src/coreclr/inc/clrconfigvalues.h +++ b/src/coreclr/inc/clrconfigvalues.h @@ -578,6 +578,8 @@ RETAIL_CONFIG_DWORD_INFO(INTERNAL_DbgMiniDumpType, W("DbgMiniDumpType"), 0, "Cra RETAIL_CONFIG_DWORD_INFO(INTERNAL_CreateDumpDiagnostics, W("CreateDumpDiagnostics"), 0, "Enable crash dump generation diagnostic logging") RETAIL_CONFIG_DWORD_INFO(INTERNAL_CrashReportBeforeSignalChaining, W("CrashReportBeforeSignalChaining"), 0, "Enable crash report generation before chaining to previous signal handler") RETAIL_CONFIG_DWORD_INFO_EX(INTERNAL_CrashReportFrameLimitPerThread, W("CrashReportFrameLimitPerThread"), 32, "Maximum number of managed stack frames per thread to emit in the in-proc crash report's compact log; 0 disables the limit; remaining frames are summarized as '... +N more frames'", CLRConfig::LookupOptions::ParseIntegerAsBase10) +RETAIL_CONFIG_STRING_INFO(INTERNAL_CrashReportRootPath, W("CrashReportRootPath"), "Root path for lifecycle-managed in-proc crash report JSON files") +RETAIL_CONFIG_DWORD_INFO_EX(INTERNAL_CrashReportMaxFileCount, W("CrashReportMaxFileCount"), 32, "Maximum number of lifecycle-managed in-proc crash report JSON files to retain (positive integer); default 32", CLRConfig::LookupOptions::ParseIntegerAsBase10) /// /// R2R diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp index 70ebeb4bb201f1..48aa551f572a10 100644 --- a/src/coreclr/vm/crashreportstackwalker.cpp +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -594,48 +595,6 @@ CrashReportEnumerateThreads( } } -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; - } - - if (!EnsureCrashReportStackWalkerState()) - { - InProcCrashReportLogInitializationFailure(".NET crash report disabled: failed to allocate stack walker storage"); - return; - } - - CLRConfigNoCache dmpNameCfg = CLRConfigNoCache::Get("DbgMiniDumpName", /*noprefix*/ false, &getenv); - const char* dumpName = dmpNameCfg.IsSet() ? dmpNameCfg.AsString() : nullptr; - - InProcCrashReporterSettings settings = {}; - settings.reportPath = dumpName; - settings.timeoutSeconds = GetCrashReportTimeoutSeconds(); - settings.isManagedThreadCallback = CrashReportIsCurrentThreadManaged; - settings.walkStackCallback = CrashReportWalkStack; - settings.enumerateThreadsCallback = CrashReportEnumerateThreads; - settings.moduleInfoCallback = CrashReportGetModuleInfo; - settings.frameLimitPerThread = GetCrashReportFrameLimitPerThread(); - - // 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); -} - static bool TryParseCrashReportConfigurationInteger(CLRConfigNoCache config, int minValue, int maxValue, int* value) @@ -670,6 +629,34 @@ TryParseCrashReportConfigurationInteger(CLRConfigNoCache config, int minValue, i return true; } +// This runs during configuration, not from the crash signal path. maxFileCount +// is a positive retention bound; values outside [1, INT32_MAX] fall back to the +// default. +static +int32_t +GetCrashReportMaxFileCount() +{ + int32_t maxFileCount = CRASHREPORT_DEFAULT_MAX_FILE_COUNT; + + CLRConfigNoCache maxFileCountCfg = CLRConfigNoCache::Get("CrashReportMaxFileCount", /*noprefix*/ false, &getenv); + if (!maxFileCountCfg.IsSet()) + { + return maxFileCount; + } + + int configuredMaxFileCount; + if (TryParseCrashReportConfigurationInteger(maxFileCountCfg, 1, INT32_MAX, &configuredMaxFileCount)) + { + maxFileCount = configuredMaxFileCount; + } + else + { + InProcCrashReportLogInitializationFailure(".NET crash report using default CrashReportMaxFileCount: invalid configured value"); + } + + return maxFileCount; +} + // Parses configuration during CrashReportConfigure initialization. This is not // async-signal-safe and must not be called from the crash-reporting path. // DOTNET_CrashReportTimeoutSeconds is a seconds-based watchdog knob: unset, @@ -696,4 +683,52 @@ GetCrashReportTimeoutSeconds() return timeoutSeconds; } +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; + } + + if (!EnsureCrashReportStackWalkerState()) + { + InProcCrashReportLogInitializationFailure(".NET crash report disabled: failed to allocate stack walker storage"); + return; + } + + CLRConfigNoCache crashReportRootPathCfg = CLRConfigNoCache::Get("CrashReportRootPath", /*noprefix*/ false, &getenv); + const char* crashReportRootPath = crashReportRootPathCfg.IsSet() ? crashReportRootPathCfg.AsString() : nullptr; + bool rootConfigured = crashReportRootPath != nullptr && crashReportRootPath[0] != '\0'; + + InProcCrashReporterSettings settings = {}; + if (rootConfigured) + { + settings.reportRootPath = crashReportRootPath; + settings.maxFileCount = GetCrashReportMaxFileCount(); + } + + settings.timeoutSeconds = GetCrashReportTimeoutSeconds(); + settings.isManagedThreadCallback = CrashReportIsCurrentThreadManaged; + settings.walkStackCallback = CrashReportWalkStack; + settings.enumerateThreadsCallback = CrashReportEnumerateThreads; + settings.moduleInfoCallback = CrashReportGetModuleInfo; + settings.frameLimitPerThread = GetCrashReportFrameLimitPerThread(); + + // 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