Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
180c0b1
Implement SetFatalErrorHandler for NativeAOT
AaronRobinsonMSFT Jun 16, 2026
fc2fd84
Implement SetFatalErrorHandler for CoreCLR
AaronRobinsonMSFT Jun 17, 2026
9e102dd
Apply suggestions from code review
AaronRobinsonMSFT Jun 17, 2026
0da0fa6
Use _exit() for SkipDefaultHandler on Unix
AaronRobinsonMSFT Jun 17, 2026
01fe291
Add using declaration for Process in test
AaronRobinsonMSFT Jun 17, 2026
fd85bf0
Simplify test process launch with Process.RunAndCaptureText
AaronRobinsonMSFT Jun 17, 2026
1c4d60c
Address PR feedback: use _Exit, fix IsCrashExitCode, suppress warning
AaronRobinsonMSFT Jun 18, 2026
e27a586
Fix handler invocation on Windows and exclude Mono
AaronRobinsonMSFT Jun 18, 2026
e6eec0a
Address PR feedback: UTF-8 crash log, exit code checks, truncation fix
AaronRobinsonMSFT Jun 18, 2026
6965928
Merge remote-tracking branch 'upstream/main' into set-fatal-error-han…
AaronRobinsonMSFT Jun 19, 2026
6249ab4
Replace managed test handlers with native C++ library
AaronRobinsonMSFT Jun 19, 2026
05e0539
Potential fix for pull request finding
AaronRobinsonMSFT Jun 19, 2026
d48a54e
Move crash log writer abstraction into eepolicy.cpp
AaronRobinsonMSFT Jun 23, 2026
88d4857
Fix HandleFatalStackOverflow: invoke handler after stack trace
AaronRobinsonMSFT Jun 23, 2026
6b1848b
Remove unused SystemNative__Exit entrypoint
AaronRobinsonMSFT Jun 24, 2026
58f269d
Address PR review feedback
AaronRobinsonMSFT Jun 24, 2026
8a47f90
Populate FatalErrorInfo with native exception pointers on Unix
AaronRobinsonMSFT Jun 25, 2026
9f69d40
Move fatal error handler to C++ static with QCall registration
AaronRobinsonMSFT Jun 25, 2026
a9d2d1c
Simplify InvokeFatalErrorHandler to direct call
AaronRobinsonMSFT Jun 25, 2026
3b40c8c
Address PR feedback: fix QCall return type, PAL decorators, remove de…
AaronRobinsonMSFT Jun 25, 2026
d798770
Route stack trace through CrashInfoWriter for callback support
AaronRobinsonMSFT Jun 26, 2026
83a87c4
Surface native exception info/context to the fatal error handler on m…
AaronRobinsonMSFT Jun 30, 2026
56ce69a
Refactor fatal error property storage into common core + platform module
AaronRobinsonMSFT Jul 6, 2026
790b319
Fix duplicate crash log and stack-overflow header in fatal error path
AaronRobinsonMSFT Jul 7, 2026
a65444a
Store hardware-exception records per-fault to survive nested faults
AaronRobinsonMSFT Jul 7, 2026
ad36176
Align hardware-exception records within the per-fault buffer
AaronRobinsonMSFT Jul 7, 2026
9ab5194
Use InlineArray16 for crash log fragment storage
AaronRobinsonMSFT Jul 7, 2026
5ec609f
Address PR review feedback
AaronRobinsonMSFT Jul 8, 2026
baa5e41
Clarify comment on FatalErrorPropertyGetter regarding ownership of data
AaronRobinsonMSFT Jul 8, 2026
9a074eb
Address PR review feedback
AaronRobinsonMSFT Jul 8, 2026
2abb2df
Merge remote-tracking branch 'upstream/main' into set-fatal-error-han…
AaronRobinsonMSFT Jul 9, 2026
a3ce5f0
Address PR review feedback
AaronRobinsonMSFT Jul 9, 2026
01e4dc1
Stage 1: surface fault IP to fatal error handler (managed path)
AaronRobinsonMSFT Jul 10, 2026
323a39f
Stage 2: route genuinely-unmanaged fatal faults to the fatal error ha…
AaronRobinsonMSFT Jul 10, 2026
9e1739e
Stage 2: route genuinely-unmanaged fatal faults to the fatal error ha…
AaronRobinsonMSFT Jul 10, 2026
401175a
WIP
AaronRobinsonMSFT Jul 10, 2026
f28cd9f
Merge branch 'main' into set-fatal-error-handler
AaronRobinsonMSFT Jul 12, 2026
e011455
Address review feedback: synchronize native fatal handler pointer, po…
AaronRobinsonMSFT Jul 13, 2026
3b16843
Enhance comments in fatal error handling to clarify encoder behavior …
AaronRobinsonMSFT Jul 17, 2026
1a608d4
Refactor crash log handling to use a single managed string, improving…
AaronRobinsonMSFT Jul 20, 2026
f72811e
Serialize concurrent fatal error handling to ensure only the first fa…
AaronRobinsonMSFT Jul 24, 2026
7960de5
Merge remote-tracking branch 'upstream/main' into set-fatal-error-han…
AaronRobinsonMSFT Jul 24, 2026
2fa3051
Install fatal error handling header
AaronRobinsonMSFT Jul 27, 2026
cd46bf2
Make C compliant
AaronRobinsonMSFT Jul 27, 2026
5871c68
Handle native fatal exceptions in CoreCLR
AaronRobinsonMSFT Jul 29, 2026
46a4925
Invoke fatal error handler for SIGABRT
AaronRobinsonMSFT Jul 30, 2026
0b0e3ad
Merge upstream/main into set-fatal-error-handler
AaronRobinsonMSFT Jul 31, 2026
2fba4fa
Split TerminateProcess interop declarations
AaronRobinsonMSFT Jul 31, 2026
df78676
Refactor fatal error handling: remove previous unhandled exception fi…
AaronRobinsonMSFT Jul 31, 2026
b67696a
Reserve fatal error handler return values
AaronRobinsonMSFT Jul 31, 2026
5bd6d69
Invoke NativeAOT fatal handlers from native code
AaronRobinsonMSFT Aug 3, 2026
72669ae
Cover CoreCLR native utility threads in the fatal error handler
AaronRobinsonMSFT Aug 4, 2026
b6190a6
Ensure process termination in InvokeFatalErrorHandler on spurious wakeup
AaronRobinsonMSFT Aug 4, 2026
ec3ae8e
Update README.md for clarity and formatting improvements
AaronRobinsonMSFT Aug 6, 2026
0a99673
Merge remote-tracking branch 'upstream/main' into set-fatal-error-han…
AaronRobinsonMSFT Aug 6, 2026
cbd3c88
Bad merge.
AaronRobinsonMSFT Aug 6, 2026
173e007
Merge branch 'main' into set-fatal-error-handler
AaronRobinsonMSFT Aug 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;

using Internal.Reflection.Augments;
Expand All @@ -26,6 +28,80 @@ internal static void Initialize()

internal static class RuntimeExceptionHelpers
{
// Matches the FatalErrorInfo struct defined in src/native/public/FatalErrorHandling.h.
// The layout must be kept in sync with the native header.
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct FatalErrorInfo
{
internal nuint Size;
internal void* Address;
internal void* Info;
internal void* Context;
internal delegate* unmanaged<FatalErrorInfo*, delegate* unmanaged<byte*, void*, void>, void*, void> PfnGetFatalErrorLog;
}

// Pre-allocated buffer for crash log text (UTF-8). Only one thread should
// be in the crash path at a time (guarded by s_crashingThreadId).
private const int CrashLogBufferSize = 8192;
private static readonly byte[] s_crashLogBuffer = new byte[CrashLogBufferSize];
private static int s_crashLogLength;

/// <summary>
/// Appends a string to the crash log buffer as UTF-8. If the buffer is full,
/// the remaining text is silently truncated.
/// </summary>
private static void AppendToCrashLog(string? text)
{
if (string.IsNullOrEmpty(text))
return;

try
{
int remaining = CrashLogBufferSize - s_crashLogLength;
if (remaining <= 0)
return;

ReadOnlySpan<char> source = text.AsSpan();
Span<byte> destination = s_crashLogBuffer.AsSpan(s_crashLogLength, remaining);

int bytesUsed = Encoding.UTF8.GetBytes(source, destination);
s_crashLogLength += bytesUsed;
}
catch
{
// Never fail on the crash path.
}
}
Comment thread
AaronRobinsonMSFT marked this conversation as resolved.
Outdated

private static void AppendNewlineToCrashLog()
{
if (s_crashLogLength < CrashLogBufferSize)
{
s_crashLogBuffer[s_crashLogLength++] = (byte)'\n';
}
}

[UnmanagedCallersOnly]
private static unsafe void GetFatalErrorLog(
FatalErrorInfo* errorData,
delegate* unmanaged<byte*, void*, void> pfnLogAction,
void* userContext)
{
if (s_crashLogLength == 0 || pfnLogAction == null)
return;

// Null-terminate the buffer for the callback.
if (s_crashLogLength < CrashLogBufferSize)
s_crashLogBuffer[s_crashLogLength] = 0;
else
s_crashLogBuffer[CrashLogBufferSize - 1] = 0;

fixed (byte* pBuffer = s_crashLogBuffer)
{
pfnLogAction(pBuffer, userContext);
}
}

//------------------------------------------------------------------------------------------------------------
// @TODO: this function is related to throwing exceptions out of Rtm. If we did not have to throw
// out of Rtm, then we would note have to have the code below to get a classlib exception object given
Expand Down Expand Up @@ -249,55 +325,72 @@ internal static unsafe void FailFast(string? message = null, Exception? exceptio
// report that OOM is the reason for the crash.
try
{
// Try to print the same short message CoreCLR prints.
Internal.Console.Error.Write("Out of memory.");
Internal.Console.Error.WriteLine();
AppendToCrashLog("Out of memory.\n");
}
catch { }
}
else
{
Internal.Console.Error.Write(((exception == null) || (reason is RhFailFastReason.EnvironmentFailFast or RhFailFastReason.AssertionFailure)) ?
"Process terminated. " : "Unhandled exception. ");
string header = ((exception == null) || (reason is RhFailFastReason.EnvironmentFailFast or RhFailFastReason.AssertionFailure)) ?
"Process terminated. " : "Unhandled exception. ";
Internal.Console.Error.Write(header);
AppendToCrashLog(header);

if (errorSource != null)
{
Internal.Console.Error.Write(errorSource);
Internal.Console.Error.WriteLine();
AppendToCrashLog(errorSource);
AppendNewlineToCrashLog();
}

if (message != null)
{
Internal.Console.Error.Write(message);
Internal.Console.Error.WriteLine();
AppendToCrashLog(message);
AppendNewlineToCrashLog();
}

if (errorSource == null && message == null && (exception == null || reason is RhFailFastReason.EnvironmentFailFast))
{
Internal.Console.Error.Write(GetStringForFailFastReason(reason));
string reasonText = GetStringForFailFastReason(reason);
Internal.Console.Error.Write(reasonText);
Internal.Console.Error.WriteLine();
AppendToCrashLog(reasonText);
AppendNewlineToCrashLog();
}

if (reason is RhFailFastReason.EnvironmentFailFast)
{
Internal.Console.Error.Write(new StackTrace().ToString());
string stackTrace = new StackTrace().ToString();
Internal.Console.Error.Write(stackTrace);
AppendToCrashLog(stackTrace);
}

if ((exception != null) && (reason is not RhFailFastReason.AssertionFailure))
{
try
{
Internal.Console.Error.Write(exception.ToString());
string exText = exception.ToString();
Internal.Console.Error.Write(exText);
Internal.Console.Error.WriteLine();
AppendToCrashLog(exText);
AppendNewlineToCrashLog();
}
catch
{
// If ToString() fails (for example, due to OOM), fall back to a simpler message.
try
{
// Use an allocation-free MethodTable comparison.
Internal.Console.Error.Write(exception.GetMethodTable() == Internal.Runtime.MethodTable.Of<OutOfMemoryException>() ? "Out of memory." : exception.GetType().FullName);
string? typeName = exception.GetMethodTable() == Internal.Runtime.MethodTable.Of<OutOfMemoryException>() ? "Out of memory." : exception.GetType().FullName;
Internal.Console.Error.Write(typeName);
Internal.Console.Error.WriteLine();
AppendToCrashLog(typeName);
AppendNewlineToCrashLog();
}
catch { }
}
Expand Down Expand Up @@ -352,6 +445,29 @@ internal static unsafe void FailFast(string? message = null, Exception? exceptio
}
}

// Invoke the user's fatal error handler, if one was registered.
IntPtr fatalHandler = ExceptionHandling.s_fatalErrorHandler;
Comment thread
AaronRobinsonMSFT marked this conversation as resolved.
Outdated
if (fatalHandler != IntPtr.Zero)
{
Comment thread
AaronRobinsonMSFT marked this conversation as resolved.
FatalErrorInfo errorInfo;
errorInfo.Size = (nuint)sizeof(FatalErrorInfo);
errorInfo.Address = (void*)pExAddress;
errorInfo.Info = null;
errorInfo.Context = (void*)pExContext;
errorInfo.PfnGetFatalErrorLog = &GetFatalErrorLog;

int handlerResult = ((delegate* unmanaged<int, void*, int>)fatalHandler)(errorCode, &errorInfo);
if (handlerResult == 1)
{
// SkipDefaultHandler — terminate without crash dump.
#if TARGET_WINDOWS
Interop.Kernel32.ExitProcess(errorCode);
Comment thread
AaronRobinsonMSFT marked this conversation as resolved.
Outdated
#else
Interop.Sys.Exit(errorCode);
#endif
Comment thread
AaronRobinsonMSFT marked this conversation as resolved.
Outdated
}
}

EXCEPTION_RECORD exceptionRecord;
// STATUS_STACK_BUFFER_OVERRUN is a "transport" exception code required by Watson to trigger the proper analyzer/provider for bucketing
exceptionRecord.ExceptionCode = STATUS_STACK_BUFFER_OVERRUN;
Expand Down
3 changes: 3 additions & 0 deletions src/coreclr/vm/corelib.h
Original file line number Diff line number Diff line change
Expand Up @@ -1008,6 +1008,9 @@ DEFINE_CLASS(EXCEPTION_DISPATCH_INFO, ExceptionServices, ExceptionDispatchI
DEFINE_METHOD(EXCEPTION_DISPATCH_INFO, CAPTURE, Capture, NoSig)
DEFINE_METHOD(EXCEPTION_DISPATCH_INFO, THROW, Throw, IM_RetVoid)

DEFINE_CLASS(EXCEPTION_HANDLING, ExceptionServices, ExceptionHandling)
DEFINE_FIELD(EXCEPTION_HANDLING, FATAL_ERROR_HANDLER, s_fatalErrorHandler)

DEFINE_CLASS_U(Loader, AssemblyLoadContext, AssemblyLoadContextBaseObject)
DEFINE_FIELD_U(_unloadLock, AssemblyLoadContextBaseObject, _unloadLock)
DEFINE_FIELD_U(_resolvingUnmanagedDll, AssemblyLoadContextBaseObject, _resolvingUnmanagedDll)
Expand Down
83 changes: 83 additions & 0 deletions src/coreclr/vm/eepolicy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,72 @@ void EEPolicy::LogFatalError(UINT exitCode, UINT_PTR address, LPCWSTR pszMessage
}
}

//
// Fatal error handler invocation support.
// Uses the public FatalErrorHandling.h header for shared type definitions.
//

#include <public/FatalErrorHandling.h>

using FatalErrorHandlerFunc = int (DOTNET_CALLCONV *)(int hresult, void* errorData);

static void DOTNET_CALLCONV GetFatalErrorLogCallback(FatalErrorInfo* errorData, FatalErrorLogAction pfnLogAction, void* userContext)
{
LIMITED_METHOD_CONTRACT;

size_t length = 0;
char* buffer = GetCrashLogBuffer(&length);
if (length > 0 && pfnLogAction != nullptr)
pfnLogAction(buffer, userContext);
}
Comment thread
AaronRobinsonMSFT marked this conversation as resolved.
Outdated

// Invokes the user-registered fatal error handler if one has been set.
// Returns true if the handler indicated that default handling should be skipped.
static bool InvokeFatalErrorHandler(UINT exitCode, UINT_PTR address, PEXCEPTION_POINTERS pExceptionInfo)
{
WRAPPER_NO_CONTRACT;

// We are in a fatal error path — suppress all contract enforcement.
CONTRACT_VIOLATION(GCViolation | ModeViolation | FaultNotFatal | TakesLockViolation);

bool skipDefault = false;

EX_TRY
{
FatalErrorHandlerFunc pfnHandler;
{
// CoreLibBinder::GetField requires GC_TRIGGERS and
// GetCurrentStaticAddress requires MODE_COOPERATIVE.
GCX_COOP();
FieldDesc* pFD = CoreLibBinder::GetField(FIELD__EXCEPTION_HANDLING__FATAL_ERROR_HANDLER);
Comment thread
AaronRobinsonMSFT marked this conversation as resolved.
Outdated
pfnHandler = reinterpret_cast<FatalErrorHandlerFunc>(pFD->GetStaticValuePtr());
}
Comment thread
AaronRobinsonMSFT marked this conversation as resolved.
Outdated
if (pfnHandler != NULL)
{
GCX_PREEMP();
Comment thread
AaronRobinsonMSFT marked this conversation as resolved.
FatalErrorInfo errorInfo{};
errorInfo.size = sizeof(FatalErrorInfo);
errorInfo.address = reinterpret_cast<void*>(address);
if (pExceptionInfo != NULL)
{
errorInfo.info = pExceptionInfo->ExceptionRecord;
errorInfo.context = pExceptionInfo->ContextRecord;
}
Comment thread
AaronRobinsonMSFT marked this conversation as resolved.
Outdated
errorInfo.pfnGetFatalErrorLog = GetFatalErrorLogCallback;

// Call user-defined fatal error handler.
int result = pfnHandler(static_cast<int>(exitCode), &errorInfo);
skipDefault = (result == SkipDefaultHandler);
}
}
EX_CATCH
Comment thread
AaronRobinsonMSFT marked this conversation as resolved.
Outdated
{
}
EX_END_CATCH

return skipDefault;
}

void DisplayStackOverflowException()
{
LIMITED_METHOD_CONTRACT;
Expand Down Expand Up @@ -698,6 +764,8 @@ void DECLSPEC_NORETURN EEPolicy::HandleFatalStackOverflow(EXCEPTION_POINTERS *pE

STRESS_LOG0(LF_EH, LL_INFO100, "In EEPolicy::HandleFatalStackOverflow\n");

EnableCrashLogCapture();

FaultingExceptionFrame fef;
if (pExceptionInfo->ContextRecord)
{
Expand Down Expand Up @@ -845,6 +913,13 @@ void DECLSPEC_NORETURN EEPolicy::HandleFatalStackOverflow(EXCEPTION_POINTERS *pE
if (g_LogStackOverflowExit)
PrintToStdErrA("@Terminating the process.\n");
#endif

UINT_PTR soAddress = pExceptionInfo->ContextRecord ? GetIP(pExceptionInfo->ContextRecord) : 0;
if (InvokeFatalErrorHandler(COR_E_STACKOVERFLOW, soAddress, pExceptionInfo))
{
_exit(COR_E_STACKOVERFLOW);
}

CrashDumpAndTerminateProcess(COR_E_STACKOVERFLOW);
UNREACHABLE();
}
Expand Down Expand Up @@ -927,8 +1002,16 @@ int NOINLINE EEPolicy::HandleFatalError(UINT exitCode, UINT_PTR address, LPCWSTR

g_fFastExitProcess = 2;

EnableCrashLogCapture();

STRESS_LOG0(LF_CORDB,LL_INFO100, "D::HFE: About to call LogFatalError\n");
LogFatalError(exitCode, address, pszMessage, pExceptionInfo, errorSource, argExceptionString);

if (InvokeFatalErrorHandler(exitCode, address, pExceptionInfo))
{
_exit(exitCode);
}
Comment thread
AaronRobinsonMSFT marked this conversation as resolved.

SafeExitProcess(exitCode, SCA_TerminateProcessWhenShutdownComplete);
}

Expand Down
37 changes: 37 additions & 0 deletions src/coreclr/vm/util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,30 @@ LPVOID CQuickHeap::Alloc(UINT sz)
// Output functions that avoid the crt's.
//----------------------------------------------------------------------------

static char s_crashLogBuffer[8192];
static size_t s_crashLogPos = 0;
static bool s_crashLogActive = false;

void EnableCrashLogCapture()
{
LIMITED_METHOD_CONTRACT;
s_crashLogActive = true;
}

char* GetCrashLogBuffer(size_t* pLength)
{
LIMITED_METHOD_CONTRACT;

// Ensure null termination when requested.
size_t nullTerminatorPos = (s_crashLogPos < ARRAY_SIZE(s_crashLogBuffer))
? s_crashLogPos
: ARRAY_SIZE(s_crashLogBuffer) - 1;
s_crashLogBuffer[nullTerminatorPos] = '\0';

*pLength = s_crashLogPos;
return s_crashLogBuffer;
}
Comment thread
AaronRobinsonMSFT marked this conversation as resolved.
Outdated
Comment thread
AaronRobinsonMSFT marked this conversation as resolved.
Outdated

void PrintToStdErrA(const char *pszString)
{
CONTRACTL
Expand All @@ -131,6 +155,19 @@ void PrintToStdErrA(const char *pszString)
CONTRACTL_END

minipal_log_write_error(pszString);

if (s_crashLogActive)
{
size_t len = strlen(pszString);
size_t remaining = ARRAY_SIZE(s_crashLogBuffer) - s_crashLogPos;
if (len > remaining)
len = remaining;
if (len > 0)
{
memcpy(s_crashLogBuffer + s_crashLogPos, pszString, len);
s_crashLogPos += len;
}
}
}

void PrintToStdErrW(const WCHAR *pwzString)
Expand Down
3 changes: 3 additions & 0 deletions src/coreclr/vm/util.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@ class CQuickHeap
void PrintToStdErrA(const char *pszString);
void PrintToStdErrW(const WCHAR *pwzString);

void EnableCrashLogCapture();
char* GetCrashLogBuffer(size_t* pLength);

#include "nativevaraccessors.h"

// --------------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2611,6 +2611,9 @@
<data name="InvalidOperation_CannotRegisterSecondHandler" xml:space="preserve">
<value>A handler for unhandled exceptions is already set.</value>
</data>
<data name="InvalidOperation_CannotRegisterSecondFatalErrorHandler" xml:space="preserve">
<value>A handler for fatal errors is already set.</value>
</data>
Comment thread
AaronRobinsonMSFT marked this conversation as resolved.
Outdated
<data name="InvalidOperation_CannotRestoreUnsuppressedFlow" xml:space="preserve">
<value>Cannot restore context flow when it is not suppressed.</value>
</data>
Expand Down
Loading