Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 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
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 @@ -219,6 +219,7 @@
<Compile Include="$(MSBuildThisFileDirectory)..\nativeaot\Runtime.Base\src\System\Runtime\ExceptionIDs.cs" />
<Compile Include="$(MSBuildThisFileDirectory)..\nativeaot\Runtime.Base\src\System\Runtime\StackFrameIterator.cs" />
<Compile Include="$(BclSourcesRoot)\System\Runtime\ExceptionServices\AsmOffsets.cs" />
<Compile Include="$(BclSourcesRoot)\System\Runtime\ExceptionServices\ExceptionHandling.CoreCLR.cs" />
<Compile Include="$(BclSourcesRoot)\System\Runtime\ExceptionServices\InternalCalls.cs" />
<Compile Include="$(BclSourcesRoot)\System\Runtime\GCSettings.CoreCLR.cs" />
<Compile Include="$(BclSourcesRoot)\System\Runtime\JitInfo.CoreCLR.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace System.Runtime.ExceptionServices
{
public static partial class ExceptionHandling
{
[LibraryImport(RuntimeHelpers.QCall, EntryPoint = "ExceptionHandling_TrySetFatalErrorHandler")]
[SuppressGCTransition]
[return: MarshalAs(UnmanagedType.U1)]
private static partial bool TrySetFatalErrorHandler(IntPtr handler);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ internal static Exception GetClasslibException(ExceptionIDs id, IntPtr address)
Exception? e = null;
try
{
e = ((delegate*<ExceptionIDs, Exception>)pGetRuntimeExceptionFunction)(id);
e = ((delegate*<ExceptionIDs, IntPtr, Exception>)pGetRuntimeExceptionFunction)(id, address);
}
catch when (true)
{
Expand Down Expand Up @@ -362,7 +362,8 @@ internal static Exception GetClasslibExceptionFromEEType(ExceptionIDs id, Method
Exception? e = null;
try
{
e = ((delegate*<ExceptionIDs, Exception>)pGetRuntimeExceptionFunction)(id);
// The MethodTable-based lookup path has no faulting instruction pointer to surface.
e = ((delegate*<ExceptionIDs, IntPtr, Exception>)pGetRuntimeExceptionFunction)(id, IntPtr.Zero);
}
catch when (true)
{
Expand Down Expand Up @@ -424,8 +425,9 @@ public static void FailedAllocation(MethodTable* pEEType, bool fIsOverflow)
// There are only a few cases where this happens now (the fast allocation helpers), so we limit the
// exception types that MRT will return.
[RuntimeExport("GetRuntimeException")]
public static Exception GetRuntimeException(ExceptionIDs id)
public static Exception GetRuntimeException(ExceptionIDs id, IntPtr faultingIP)
{
_ = faultingIP;
switch (id)
{
case ExceptionIDs.OutOfMemory:
Expand Down
190 changes: 190 additions & 0 deletions src/coreclr/nativeaot/Runtime/EHHelpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "TypeManager.h"
#include "PalLimitedContext.h"
#include "Pal.h"
#include "volatile.h"
#include "holder.h"
#include "Crst.h"
#include "RuntimeInstance.h"
Expand All @@ -30,6 +31,7 @@
#include "NativeContext.h"
#include <minipal/debugger.h>
#include "corexcep.h"
#include <public/fatal_error_handling.h>

struct MethodRegionInfo
{
Expand All @@ -39,6 +41,140 @@ struct MethodRegionInfo
size_t coldSize;
};

using FatalErrorHandlerFunc = int (DOTNET_CALLCONV *)(int hresult, FatalErrorPropertyGetter getProperty);

struct NativeFatalErrorContext
{
void* Address;
void* PlatformData0;
void* PlatformData1;
};

static void* g_fatalErrorHandler = NULL;
static volatile int64_t g_fatalErrorHandlerThreadId;
static NativeFatalErrorContext g_nativeFatalErrorContext;

static uint64_t TryAcquireFatalErrorHandler()
{
uint64_t currentThreadId = PalGetCurrentOSThreadId();
return static_cast<uint64_t>(PalInterlockedCompareExchange64(
&g_fatalErrorHandlerThreadId, static_cast<int64_t>(currentThreadId), 0));
}

static void WaitForFatalErrorHandlerIfGcSafe(uint64_t previousThreadId)
{
if (previousThreadId == PalGetCurrentOSThreadId())
{
return;
}

#if defined(TARGET_UNIX) && !defined(TARGET_WASM)
Thread* pThread = ThreadStore::GetCurrentThreadIfAvailableAsyncSafe();
#else
Thread* pThread = ThreadStore::GetCurrentThreadIfAvailable();
#endif
if (pThread == nullptr || !pThread->IsCurrentThreadInCooperativeMode())
{
PalSleep(INFINITE);
}
}

static int32_t DOTNET_CALLCONV GetNativeFatalErrorProperty(int32_t prop, const void** value)
{
if (value == nullptr)
{
return 0;
}

switch (static_cast<FatalErrorProperty>(prop))
{
case FEP_Address:
if (g_nativeFatalErrorContext.Address == nullptr)
{
return 0;
}
*value = g_nativeFatalErrorContext.Address;
return 1;

#ifdef TARGET_WINDOWS
case FEP_WindowsExceptionRecord:
if (g_nativeFatalErrorContext.PlatformData0 == nullptr)
{
return 0;
}
*value = g_nativeFatalErrorContext.PlatformData0;
return 1;

case FEP_WindowsContextRecord:
if (g_nativeFatalErrorContext.PlatformData1 == nullptr)
{
return 0;
}
*value = g_nativeFatalErrorContext.PlatformData1;
return 1;
#else
case FEP_PosixSigInfo:
if (g_nativeFatalErrorContext.PlatformData0 == nullptr)
{
return 0;
}
*value = g_nativeFatalErrorContext.PlatformData0;
return 1;

case FEP_UContext:
if (g_nativeFatalErrorContext.PlatformData1 == nullptr)
{
return 0;
}
*value = g_nativeFatalErrorContext.PlatformData1;
return 1;
#endif

default:
return 0;
}
}

void RhpInvokeFatalErrorHandlerForNativeException(
int32_t errorCode,
void* faultAddress,
void* platformData0,
void* platformData1)
{
void* pHandler = VolatileLoad(&g_fatalErrorHandler);
if (pHandler == nullptr)
{
return;
}

uint64_t previousThreadId = TryAcquireFatalErrorHandler();
if (previousThreadId != 0)
{
// A cooperative thread cannot block here because the managed fatal owner may
// need a GC while composing its crash report. Let default fatal handling proceed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the default fatal handling going to do in this case - is the process just going to exit?

It means that the user callback on the first thread may be terminated by the process exit before it gets a chance to do much.

(I am not sure what to do about this.)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jkotas Yes. In this case, the loser drives process exit. After RhpInvokeFatalErrorHandlerForNativeException returns on the losing thread:

Windows - RhpVectoredExceptionHandler returns EXCEPTION_CONTINUE_SEARCH or RaiseFailFastException if the fault IP is inside the runtime module -> unhandled termination.

Non-Windows - SIGSEGVHandler chains to the previous/default signal action -> termination.

So if that races the owner thread still running the user callback, the callback can be truncated by process exit.

But this only happens in one case and I'm not sure how common it is. A concurrent exception where the losing thread is in cooperative mode. Non-cooperative / unattached threads already PalSleep(INFINITE) in WaitForFatalErrorHandlerIfGcSafe and never reach the terminating path, so the owner would complete normally.

I think we've got two options right now.

  • Declare handler execution best-effort but under concurrent faults it may be truncated.
  • Park the cooperative loser in a GC-suspendable loop. I'm not sure the best way to make this happen though.

WaitForFatalErrorHandlerIfGcSafe(previousThreadId);
return;
}

g_nativeFatalErrorContext = { faultAddress, platformData0, platformData1 };

FatalErrorHandlerFunc pfnHandler = reinterpret_cast<FatalErrorHandlerFunc>(pHandler);
int result = pfnHandler(errorCode, GetNativeFatalErrorProperty);
ASSERT(result == RunDefaultHandler);
}

FCIMPL1(void, RhpRegisterFatalErrorHandler, void* pHandler)
{
PalInterlockedExchangePointer(&g_fatalErrorHandler, pHandler);
}
FCIMPLEND

FCIMPL0(uint64_t, RhpTryAcquireFatalErrorHandler)
{
return TryAcquireFatalErrorHandler();
}
FCIMPLEND

FCIMPL3(FC_BOOL_RET, RhpEHEnumInitFromStackFrameIterator,
StackFrameIterator* pFrameIter, MethodRegionInfo* pMethodRegionInfoOut, EHEnum* pEHEnum)
{
Expand Down Expand Up @@ -447,6 +583,53 @@ EXTERN_C void RhpContinueOnFatalErrors()
g_ContinueOnFatalErrors = true;
}

// Returns true for the genuinely-fatal hardware fault codes that mirror the Unix signal
// choke point (SIGSEGV/SIGFPE/SIGILL). Stack overflow is deliberately excluded: the guard
// region leaves too little stack to invoke a user callback safely.
static bool IsFatalHardwareExceptionForFatalErrorHandler(uintptr_t faultCode)
{
switch (faultCode)
{
case STATUS_ACCESS_VIOLATION:
case STATUS_IN_PAGE_ERROR:
case STATUS_DATATYPE_MISALIGNMENT:
case STATUS_ILLEGAL_INSTRUCTION:
case STATUS_PRIVILEGED_INSTRUCTION:
case STATUS_ARRAY_BOUNDS_EXCEEDED:
case STATUS_INTEGER_DIVIDE_BY_ZERO:
case STATUS_INTEGER_OVERFLOW:
case STATUS_FLOAT_DIVIDE_BY_ZERO:
case STATUS_FLOAT_INVALID_OPERATION:
case STATUS_FLOAT_OVERFLOW:
case STATUS_FLOAT_UNDERFLOW:
case STATUS_FLOAT_INEXACT_RESULT:
case STATUS_FLOAT_DENORMAL_OPERAND:
case STATUS_FLOAT_STACK_CHECK:
return true;
default:
return false;
}
}

// For a genuinely-unmanaged fatal fault (one that the vectored exception handler did not
// translate to a managed exception), forward the live exception records to a user-installed
// fatal error handler, if one is registered.
static void InvokeFatalErrorHandlerForNativeException(PEXCEPTION_POINTERS pExPtrs)
{
uintptr_t faultCode = pExPtrs->ExceptionRecord->ExceptionCode;
if (!IsFatalHardwareExceptionForFatalErrorHandler(faultCode))
{
return;
}

// The fault address surfaced to the handler is the faulting instruction pointer,
// matching the managed fatal path. The accessed memory address (for a memory fault)
// remains available to the handler through the forwarded PEXCEPTION_RECORD.
void* faultAddress = (void*)((NATIVE_CONTEXT*)pExPtrs->ContextRecord)->GetIp();
RhpInvokeFatalErrorHandlerForNativeException(
static_cast<int32_t>(faultCode), faultAddress, pExPtrs->ExceptionRecord, pExPtrs->ContextRecord);
}

LONG WINAPI RhpVectoredExceptionHandler(PEXCEPTION_POINTERS pExPtrs)
{
uintptr_t faultCode = pExPtrs->ExceptionRecord->ExceptionCode;
Expand Down Expand Up @@ -570,6 +753,13 @@ LONG WINAPI RhpVectoredExceptionHandler(PEXCEPTION_POINTERS pExPtrs)
return EXCEPTION_CONTINUE_EXECUTION;
}

// RhpContinueOnFatalErrors leaves hardware faults for client handlers, so they are
// not known to be fatal at this point.
if (!g_ContinueOnFatalErrors)
{
InvokeFatalErrorHandlerForNativeException(pExPtrs);
}

// The client may have told us to continue to search for custom handlers,
// but in general we consider any form of hardware exception within the runtime itself a fatal error.
// Note this includes the managed code within the runtime.
Expand Down
29 changes: 29 additions & 0 deletions src/coreclr/nativeaot/Runtime/unix/HardwareExceptions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "common.h"
#include "CommonTypes.h"
#include "Pal.h"
#include "volatile.h"
#include "PalLimitedContext.h"
#include "CommonMacros.h"
#include "config.h"
Expand Down Expand Up @@ -541,6 +542,30 @@ bool HardwareExceptionHandler(int code, siginfo_t *siginfo, void *context, void*
return false;
}

// Defined in EHHelpers.cpp. Invokes the user callback directly from native code and
// forwards the live signal structures through the native property getter.
extern void RhpInvokeFatalErrorHandlerForNativeException(
int32_t errorCode,
void* faultAddress,
void* platformData0,
void* platformData1);

// For a genuinely-unmanaged fatal fault (one that HardwareExceptionHandler did not translate
// to a managed exception), forward the live siginfo_t/ucontext_t to a user-installed fatal
// error handler, if one is registered.
static void InvokeFatalErrorHandlerForNativeException(siginfo_t *siginfo, void *context)
{
// The fault address surfaced to the handler is the faulting instruction pointer,
// matching the managed fatal path. The accessed memory address (for a memory fault)
// remains available to the handler through the forwarded siginfo_t (si_addr).
PAL_LIMITED_CONTEXT palContext;
NativeContextToPalContext(context, &palContext);
void* faultAddress = (void*)palContext.IP;
uint32_t faultCode = GetExceptionCodeForSignal(siginfo, context);
RhpInvokeFatalErrorHandlerForNativeException(
static_cast<int32_t>(faultCode), faultAddress, siginfo, context);
}

// Handler for the SIGSEGV signal
void SIGSEGVHandler(int code, siginfo_t *siginfo, void *context)
{
Expand All @@ -550,6 +575,8 @@ void SIGSEGVHandler(int code, siginfo_t *siginfo, void *context)
return;
}

InvokeFatalErrorHandlerForNativeException(siginfo, context);

if (g_previousSIGSEGV.sa_sigaction != NULL)
{
g_previousSIGSEGV.sa_sigaction(code, siginfo, context);
Expand All @@ -572,6 +599,8 @@ void SIGFPEHandler(int code, siginfo_t *siginfo, void *context)
return;
}

InvokeFatalErrorHandlerForNativeException(siginfo, context);

if (g_previousSIGFPE.sa_sigaction != NULL)
{
g_previousSIGFPE.sa_sigaction(code, siginfo, context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ internal unsafe partial struct MethodTable

internal Exception GetClasslibException(ExceptionIDs id)
{
return RuntimeExceptionHelpers.GetRuntimeException(id);
return RuntimeExceptionHelpers.GetRuntimeException(id, IntPtr.Zero);
}
#pragma warning restore CA1822

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@
<Compile Include="System\RuntimeTypeHandle.cs" />
<Compile Include="System\Exception.NativeAot.cs" />
<Compile Include="System\RuntimeExceptionHelpers.cs" />
<Compile Include="System\Runtime\ExceptionServices\ExceptionHandling.NativeAot.cs" />
<Compile Include="System\EETypePtr.cs" />
<Compile Include="System\Runtime\RuntimeImports.cs" />
<Compile Include="System\ModuleHandle.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Threading;

namespace System.Runtime.ExceptionServices
{
public static partial class ExceptionHandling
{
internal static IntPtr s_fatalErrorHandler;

private static unsafe bool TrySetFatalErrorHandler(IntPtr handler)
{
if (Interlocked.CompareExchange(ref s_fatalErrorHandler, handler, IntPtr.Zero) != IntPtr.Zero)
return false;

// Register the user callback with the native runtime so genuinely-unmanaged
// fatal exceptions can invoke it without transitioning into managed code.
RuntimeImports.RhpRegisterFatalErrorHandler((void*)handler);

return true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ internal static partial class RuntimeImports
[RuntimeImport(RuntimeLibrary, "RhGetCrashInfoBuffer")]
internal static extern unsafe byte* RhGetCrashInfoBuffer(out int cbMaxSize);

// Registers the user callback for genuinely-unmanaged fatal exceptions.
[MethodImplAttribute(MethodImplOptions.InternalCall)]
[RuntimeImport(RuntimeLibrary, "RhpRegisterFatalErrorHandler")]
internal static extern unsafe void RhpRegisterFatalErrorHandler(void* pHandler);

[MethodImplAttribute(MethodImplOptions.InternalCall)]
[RuntimeImport(RuntimeLibrary, "RhpTryAcquireFatalErrorHandler")]
internal static extern ulong RhpTryAcquireFatalErrorHandler();

#if TARGET_UNIX
[MethodImplAttribute(MethodImplOptions.InternalCall)]
[RuntimeImport(RuntimeLibrary, "RhCreateCrashDumpIfEnabled")]
Expand Down
Loading
Loading