Skip to content

Implement ExceptionHandling.SetFatalErrorHandler - #129543

Open
AaronRobinsonMSFT wants to merge 56 commits into
dotnet:mainfrom
AaronRobinsonMSFT:set-fatal-error-handler
Open

Implement ExceptionHandling.SetFatalErrorHandler#129543
AaronRobinsonMSFT wants to merge 56 commits into
dotnet:mainfrom
AaronRobinsonMSFT:set-fatal-error-handler

Conversation

@AaronRobinsonMSFT

@AaronRobinsonMSFT AaronRobinsonMSFT commented Jun 17, 2026

Copy link
Copy Markdown
Member

Implements the ExceptionHandling.SetFatalErrorHandler API (#101560) for both NativeAOT and CoreCLR. Mono throws PlatformNotSupportedException.

Changes

Managed API (ExceptionHandling.cs)

  • Mono throws PNSE

NativeAOT (RuntimeExceptionHelpers.cs)

  • Supported.

CoreCLR (eepolicy.cpp)

  • Supported.

Public native header (src/native/public/FatalErrorHandling.h)

  • Defines FatalErrorHandlerResult enum, FatalErrorInfo struct, callback typedefs

Tests (src/tests/baseservices/exceptions/FatalErrorHandler/)

  • Subprocess-based tests validating: SkipHandler, RunHandler, LogHandler, SetNull, SetTwice
  • Works on both CoreCLR and NativeAOT

Fixes #101560
Fixes #95461

AaronRobinsonMSFT and others added 2 commits June 17, 2026 15:05
Implement the ExceptionHandling.SetFatalErrorHandler API for NativeAOT.
The handler is invoked from RuntimeExceptionHelpers.FailFast before the
runtime performs its default crash handling (crash dump + abort).

- Add src/native/public/FatalErrorHandling.h defining the native
  FatalErrorInfo struct and FatalErrorHandlerResult enum
- Wire RegisterFatalErrorHandler as a no-op for NativeAOT (handler
  pointer stored in managed s_fatalErrorHandler field)
- Add crash log capture in FailFast alongside existing stderr output
- Implement pfnGetFatalErrorLog callback via UnmanagedCallersOnly
- SkipDefaultHandler exits via _Exit/ExitProcess instead of crash dump
- Consolidate ExceptionHandling partials: MONO||CORECLR throws PNSE
  inline, eliminating per-runtime partial files
- Add subprocess-based smoke tests validating handler invocation,
  SkipDefaultHandler/RunDefaultHandler, pfnGetFatalErrorLog callback,
  and API contract (null/double-set)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wire up the user-registered fatal error handler in the CoreCLR runtime.
Read the managed ExceptionHandling.s_fatalErrorHandler static field via
CoreLibBinder and invoke the handler after LogFatalError completes in
both HandleFatalError and HandleFatalStackOverflow. If the handler
returns SkipDefaultHandler, exit without crash dump.

- Add ExceptionHandling class/field bindings to corelib.h
- Enable s_fatalErrorHandler field and SetFatalErrorHandler for CoreCLR
- Add crash log capture in PrintToStdErrA for pfnGetFatalErrorLog
- Include public/FatalErrorHandling.h for shared type definitions
- Fix test subprocess launch for CoreCLR (pass DLL path to corerun)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 17, 2026 22:24
@github-actions github-actions Bot added the area-ExceptionHandling-coreclr only use for closed issues label Jun 17, 2026
@AaronRobinsonMSFT AaronRobinsonMSFT changed the title Implement ExceptionHandling.SetFatalErrorHandler Implement ExceptionHandling.SetFatalErrorHandler Jun 17, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces the public System.Runtime.ExceptionServices.ExceptionHandling.SetFatalErrorHandler API and wires it up so CoreCLR and NativeAOT invoke a user-provided unmanaged callback during fatal-error paths, with a mechanism to retrieve the fatal-error log text.

Changes:

  • Adds ExceptionHandling.SetFatalErrorHandler(delegate* unmanaged<int, void*, int>) to the public surface and implements registration in System.Private.CoreLib.
  • Implements fatal-error handler invocation + crash-log capture in both CoreCLR (VM) and NativeAOT fail-fast paths.
  • Adds a new native public header (FatalErrorHandling.h) and a new subprocess-based test covering handler behaviors.
Show a summary per file
File Description
src/tests/baseservices/exceptions/FatalErrorHandler/FatalErrorHandlerTest.csproj Adds new standalone test project for fatal error handler scenarios.
src/tests/baseservices/exceptions/FatalErrorHandler/FatalErrorHandlerTest.cs Subprocess-based validation of handler invocation, skip/run default behavior, and log retrieval.
src/native/public/FatalErrorHandling.h Defines native ABI structs/enums/callback types for fatal error handling and log retrieval.
src/libraries/System.Runtime/ref/System.Runtime.cs Adds the new public ref-assembly API for SetFatalErrorHandler.
src/libraries/System.Private.CoreLib/src/System/Runtime/ExceptionServices/ExceptionHandling.cs Implements handler registration and stores function pointer for runtimes to read.
src/libraries/System.Private.CoreLib/src/Resources/Strings.resx Adds resource string for duplicate fatal handler registration.
src/coreclr/vm/util.hpp Declares crash-log capture helpers used by fatal-error handler plumbing.
src/coreclr/vm/util.cpp Implements stderr “tee” into a fixed crash-log buffer.
src/coreclr/vm/eepolicy.cpp Invokes the fatal handler after logging fatal errors / stack overflow and provides log callback.
src/coreclr/vm/corelib.h Adds CoreLibBinder field binding for ExceptionHandling.s_fatalErrorHandler.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs Captures crash output into a buffer and invokes the fatal handler before default crash processing.

Copilot's findings

  • Files reviewed: 11/11 changed files
  • Comments generated: 5

Comment thread src/coreclr/vm/eepolicy.cpp Outdated
Comment thread src/tests/baseservices/exceptions/FatalErrorHandler/FatalErrorHandlerTest.cs Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 17, 2026 22:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 11/11 changed files
  • Comments generated: 6

Comment thread src/coreclr/vm/util.cpp Outdated
Comment thread src/coreclr/vm/eepolicy.cpp Outdated
Comment thread src/tests/baseservices/exceptions/FatalErrorHandler/FatalErrorHandlerTest.cs Outdated
Comment thread src/libraries/System.Runtime/ref/System.Runtime.cs
AaronRobinsonMSFT and others added 2 commits June 17, 2026 15:58
The SkipDefaultHandler path should terminate immediately without running
atexit handlers, which can deadlock in a corrupted process. Replace the
call to exit() (via Interop.Sys.Exit) with _exit() (via a new
Interop.Sys._Exit P/Invoke) in the NativeAOT FailFast path, matching
CoreCLR's native _exit() semantics.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread src/tests/baseservices/exceptions/FatalErrorHandler/FatalErrorHandlerTest.cs Outdated
Comment thread src/tests/baseservices/exceptions/FatalErrorHandler/FatalErrorHandlerTest.cs Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 17, 2026 23:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 17/17 changed files
  • Comments generated: 4

Comment thread src/tests/baseservices/exceptions/FatalErrorHandler/FatalErrorHandlerTest.cs Outdated
Comment thread src/coreclr/vm/eepolicy.cpp Outdated
Comment thread src/native/libs/System.Native/pal_threading.c Outdated
Comment thread src/native/libs/System.Native/pal_threading_wasi.c Outdated
- Use C99 _Exit() instead of _exit() to avoid unistd.h dependency
- Add COR_E_FAILFAST to IsCrashExitCode for Windows CoreCLR
- Suppress unused parameter warning in GetFatalErrorLogCallback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
On Windows, WatsonLastChance calls RaiseFailFastException which
terminates the process before InvokeFatalErrorHandler is reached.
Move the handler invocation before the Watson/debugger code path
in both HandleFatalError and HandleFatalStackOverflow.

In HandleFatalError, call LogInfoForFatalError directly first to
populate the crash log buffer for the handler, then invoke the
handler, then proceed with LogFatalError for ETW and Watson.

Exclude FatalErrorHandlerTest from Mono runs since
SetFatalErrorHandler throws PlatformNotSupportedException on Mono.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 18, 2026 19:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

Comments suppressed due to low confidence (3)

src/libraries/System.Runtime/ref/System.Runtime.cs:15474

  • This PR adds a new public API (ExceptionHandling.SetFatalErrorHandler in System.Runtime ref), but none of the linked issues (#101560, #95461) currently have the api-approved label. New public APIs require an approved proposal; please link an api-approved issue (or get the proposal issue labeled), otherwise this should stay internal until approved.
    src/native/public/fatal_error_handling.h:53
  • The FatalErrorLogFunc contract says the handler may call it "at most once", but both CoreCLR and NativeAOT implementations appear to allow multiple calls (they just replay the stored crash log). This comment is part of the public API contract and should match the actual supported behavior.
    src/native/public/fatal_error_handling.h:96
  • The header defines the property getter type but doesn’t provide a typedef for the fatal error handler callback itself, so consumers must restate the signature (risking mismatched calling convention / parameter types). Adding a FatalErrorHandler typedef would make the public contract self-contained.
  • Files reviewed: 38/38 changed files
  • Comments generated: 0 new

Comment thread src/coreclr/nativeaot/Runtime/EHHelpers.cpp Outdated
Comment thread src/libraries/Common/src/Interop/Windows/Kernel32/Interop.TerminateProcess.cs Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 299682bb-b7ba-48bb-839f-4ebf3fb4cab3
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 299682bb-b7ba-48bb-839f-4ebf3fb4cab3
…lter and streamline exception handling logic

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

Suppressed comments (2)

src/coreclr/vm/eepolicy.cpp:453

  • The new <public/fatal_error_handling.h> include is placed mid-file (right before the fatal crash-output helpers). This makes include ordering harder to follow and is inconsistent with the rest of the file’s include block at the top; it also risks subtle dependency issues if other code above starts relying on types from this header later. Consider moving this include up with the other headers near the top of eepolicy.cpp.
#include <public/fatal_error_handling.h>

src/tests/baseservices/exceptions/FatalErrorHandler/FatalErrorHandlerNative.cpp:161

  • This C++ test helper uses NULL in new code (e.g., initializing pointers). The runtime codebase’s C++ conventions prefer nullptr for pointer literals to avoid accidental integer conversions and improve type safety.
  • Files reviewed: 38/38 changed files
  • Comments generated: 0 new

Comment thread src/coreclr/nativeaot/Runtime/unix/HardwareExceptions.cpp Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 299682bb-b7ba-48bb-839f-4ebf3fb4cab3

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

Suppressed comments (2)

src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs:117

  • The flush heuristic here (remaining.Length <= ChunkSize - 1) doesn’t actually indicate the final iteration, because UTF-8 output size depends on content (up to 4 bytes/char). This can set flush: true while there are still chars remaining, which risks prematurely flushing encoder state (notably surrogate-pair handling) and producing incorrect UTF-8 output. A safer pattern is to always flush: false while consuming input, then do a final Convert with empty input and flush: true to flush any buffered state (mirrors other encoder usage in the repo).
                    // the entire input is consumed, so a prematurely-true flush is a
                    // no-op (Convert reports completed: false and the remaining chars
                    // are handled on the next iteration).
                    bool flush = remaining.Length <= ChunkSize - 1;
                    encoder.Convert(remaining, buffer[..^1], flush: flush, out int charsUsed, out int bytesUsed, out _);

src/tests/baseservices/exceptions/FatalErrorHandler/FatalErrorHandlerTest.cs:445

  • The PR description mentions a SkipDefaultHandler/“SkipHandler” test scenario, but this test entrypoint list doesn’t include any skip-default-handling case. Either add coverage for that scenario or update the PR description so it matches what’s actually tested.
  • Files reviewed: 37/37 changed files
  • Comments generated: 0 new

AaronRobinsonMSFT and others added 2 commits August 3, 2026 16:12
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 299682bb-b7ba-48bb-839f-4ebf3fb4cab3
Windows server GC worker threads (and other native utility threads) have no
managed Thread object, so a genuinely-unhandled native fault on them never
reaches the fatal error handler through the vectored-exception path, which
gates on a managed Thread. Hook the fatal handler at the top-level unhandled
filter for NativeThreadUnhandledException so these threads are covered.

- Add EEPolicy::HandleFatalErrorForNativeException(PEXCEPTION_POINTERS) and
  invoke it from InternalUnhandledExceptionFilter_Worker on Windows.
- Make the losing-thread wait in InvokeFatalErrorHandler safe for threads
  with no managed Thread object.
- Add a cross-platform regression test that faults on a raw OS thread with
  no managed Thread object, gated to the platforms where it is meaningful.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 299682bb-b7ba-48bb-839f-4ebf3fb4cab3

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

Suppressed comments (3)

src/native/public/fatal_error_handling.h:36

  • The public native header here defines a callback/property-getter model and reserves all FatalErrorHandlerResult values except RunDefaultHandler. However, the linked api-approved proposal issue (#101560) describes a different public native contract (a FatalErrorInfo struct and a SkipDefaultHandler return value). Since this header is installed into the corehost/nethost deliverable, it should match the approved proposal (or the proposal should be updated/clarified) so native consumers get the expected shape and capabilities.
    src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs:388
  • FailFast now builds the crash log using new StringBuilder() and captures stack traces without a surrounding try/catch. On fatal-error paths (especially low-memory / type-initialization-failed scenarios), these allocations can throw and potentially cause re-entrancy or lose the crash log entirely. The crash-log composition should be guarded so that failures fall back to a minimal message (similar to the existing minimal-OOM path) rather than throwing during FailFast.
                {
                    // Compose the crash-log text on this (the crashing) thread. The stack trace must be
                    // captured eagerly here because it reflects the live stack at the point of failure;
                    // it cannot be regenerated later from the handler callback.
                    var crashLog = new StringBuilder();

src/tests/baseservices/exceptions/FatalErrorHandler/FatalErrorHandlerTest.cs:240

  • These stderr validations use string.Contains(string) which is culture-sensitive. For deterministic test behavior (and to avoid oddities under custom cultures), use the StringComparison.Ordinal overload for all marker searches.
  • Files reviewed: 38/38 changed files
  • Comments generated: 1

Comment thread src/coreclr/vm/eepolicy.cpp

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

Suppressed comments (3)

src/native/public/fatal_error_handling.h:7

  • PR description mentions FatalErrorInfo/FatalErrorHandlerResult (including SkipDefaultHandler), but this header exposes a different shape (property-getter model and only RunDefaultHandler). Please update the PR description (and/or linked design notes) to match the actual public header/API shape so reviewers and external consumers aren’t misled.
    src/libraries/System.Private.CoreLib/src/System/Runtime/ExceptionServices/ExceptionHandling.cs:56
  • The XML docs say the handler receives an "HRESULT", but both CoreCLR and NativeAOT can invoke the fatal-error handler for native hardware faults (e.g. STATUS_ACCESS_VIOLATION / SIGSEGV-derived codes), which are not HRESULTs. Please adjust the docs to describe this as an error code that may be an HRESULT or a platform-native exception/signal code, so callers don't misinterpret the value.
    src/native/public/fatal_error_handling.h:95
  • The public header defines the property getter and related callback types, but it doesn't define a typedef for the fatal error handler itself. Adding a FatalErrorHandler typedef would reduce duplication in native consumers and make the intended signature explicit in the header.
  • Files reviewed: 38/38 changed files
  • Comments generated: 0 new

@AaronRobinsonMSFT

Copy link
Copy Markdown
Member Author

@lateralusX or @mdh1418 Any chance one of you could look? @lateralusX you mentioned work that is parented on top of this. Can you run through a few scenarios and see if anything falls out?

@am11

am11 commented Aug 5, 2026

Copy link
Copy Markdown
Member

Public C API looking great! :)
@AaronRobinsonMSFT, could you please apply readme update while at it, something like AaronRobinsonMSFT#3.

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.

@lateralusX

Copy link
Copy Markdown
Member

@lateralusX or @mdh1418 Any chance one of you could look? @lateralusX you mentioned work that is parented on top of this. Can you run through a few scenarios and see if anything falls out?

I rebase my WIP PR and clean it up, then we can use that to run through some in-proc crash reporter scenarios on Android/iOS.

@lateralusX

Copy link
Copy Markdown
Member

I previously raised idea around getting access to previous sigaction (held by runtime) in the fatal error handler and would like to re-iterate on it one more time as an effect of this comment, #129543 (comment) and the change to return value. When the fatal error handler gets called from a signal handler (not from runtimes fatal error path), it has option to either return back and let the invoke_previous_action do its work or terminate/abort the process inside the fatal error handler. If we offered the previous sigaction to the fatal error handler (through property API and only on the signal path) it would be possible to replace invoke_previous_action and have capabilities to chain signal handlers, use in-proc crash reporter API and terminate the process. That would make it possible for fatal error handler to get full control of what happens to signal handling once runtime decided it's not a signal it will/can handle. Right now, it can almost do all that, but since it doesn't have access to the previous signal handler in the chain, there is no way for it to correctly chain signal handlers. Getting access to previous sigaction will open up that capability.

@AaronRobinsonMSFT

Copy link
Copy Markdown
Member Author

If we offered the previous sigaction to the fatal error handler (through property API and only on the signal path) it would be possible to replace invoke_previous_action and have capabilities to chain signal handlers
Getting access to previous sigaction will open up that capability.

I'm fine having this conversation in a follow-up PR. With the property API it becomes trivial to expose new properties. We would need to think about the guarantees for that though and how to message it to users of the API. I'd also like to hear more from @janvorli about the suggestion. I think he's make his opinion clear for this PR but a follow-up feature for integrating the in-proc system seems more appropriate for discussing the best way to add that property.

@lateralusX

lateralusX commented Aug 6, 2026

Copy link
Copy Markdown
Member

#131959, draft PR including the in-proc on demand crash reporter API extension + tests. Only the last two commits are new, the rest is just commits included in this PR, so once this lands, that PR should be rebased on main and all except the last two commits will be dropped.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Note

This error may be related to your runner configuration. You can now configure runners for Copilot code review separately from Copilot cloud agent by creating a copilot-code-review.yml file with your setup steps. Read the docs for details.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[API Proposal]: Overriding the default behavior in case of unhandled exceptions and fatal errors. Register a custom crash handler

8 participants