Implement ExceptionHandling.SetFatalErrorHandler#129543
Implement ExceptionHandling.SetFatalErrorHandler#129543AaronRobinsonMSFT wants to merge 42 commits into
ExceptionHandling.SetFatalErrorHandler#129543Conversation
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>
ExceptionHandling.SetFatalErrorHandler
There was a problem hiding this comment.
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 inSystem.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
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
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>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- 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>
…and output emission during stack overflow
There was a problem hiding this comment.
Copilot's findings
Comments suppressed due to low confidence (2)
src/libraries/System.Private.CoreLib/src/System/Runtime/ExceptionServices/ExceptionHandling.cs:82
- On Mono, the method throws PlatformNotSupportedException before validating
handlerfor null. The approved API comment on #101560 documentsArgumentNullExceptionfor a null handler; argument validation should generally happen before platform checks so callers get consistent exception behavior.
public static unsafe void SetFatalErrorHandler(delegate* unmanaged<int, void*, int> handler)
{
#if MONO
throw new PlatformNotSupportedException();
#else
ArgumentNullException.ThrowIfNull((void*)handler, nameof(handler));
if (!TrySetFatalErrorHandler((IntPtr)handler))
{
throw new InvalidOperationException(SR.InvalidOperation_CannotRegisterSecondFatalErrorHandler);
}
#endif
src/native/public/FatalErrorHandling.h:7
- The PR description / linked issue text describes a
FatalErrorInfostruct-based contract, but this header actually defines a property-getter callback model (noFatalErrorInfostruct). Please either update the PR description/docs to match the implemented contract, or adjust the header/managed docs if the struct-based design is still the intended API.
// This header defines the native types used by the
// ExceptionHandling.SetFatalErrorHandler API. A native fatal error handler
// receives an HRESULT and a property-getter callback through which it can
// request additional crash information on demand.
- Files reviewed: 29/29 changed files
- Comments generated: 0 new
|
Workflow state for the Holistic Review Orchestrator. {
"version": 5,
"last_dispatched_commit": "1a608d4054e854663de18298a4dd61e6e098ea16",
"last_dispatched_base_ref": "main",
"last_dispatched_base_sha": "c9265d78e03e96c00b7ec062f2dbf2465bbb86d6",
"last_reviewed_commit": "1a608d4054e854663de18298a4dd61e6e098ea16",
"last_reviewed_base_ref": "main",
"last_reviewed_base_sha": "c9265d78e03e96c00b7ec062f2dbf2465bbb86d6",
"last_recorded_worker_run_id": "29772964837",
"review_attempt_commit": "",
"review_attempt_base_ref": "",
"review_attempt_count": 0,
"max_review_attempts": 5,
"review_history_format": "holistic-review-disclosure-v1",
"review_history": [
{
"commit": "3b168438f67eb0478f255d4a21a55bb4b41186fe",
"review_id": 4730782393
},
{
"commit": "1a608d4054e854663de18298a4dd61e6e098ea16",
"review_id": 4738486867
}
]
} |
There was a problem hiding this comment.
Holistic Review
Motivation: Strong and well-established. This implements the api-approved ExceptionHandling.SetFatalErrorHandler (issue #101560), a long-requested capability (crash reporters like breakpad/Sentry behind the .NET runtime) with clear demand from the linked issue's discussion. The problem is real and the direction was reviewed by API review.
Approach: Reasonable and consistent with the codebase. The public managed surface exactly matches the approved shape (namespace, SetFatalErrorHandler(delegate* unmanaged<int, void*, int>), [CLSCompliant(false)], ANE/IOE/PNSE exceptions). The shared native contract lives in a new public header (FatalErrorHandling.h), registration uses a lock-free InterlockedCompareExchange single-registration model, and crash output is refactored behind a CrashInfoWriter abstraction so the same text can go to stderr or the user's log callback. CoreCLR hooks HandleFatalError/HandleFatalStackOverflow; NativeAOT hooks the classlib FailFast plus the Unix signal handlers and a new Windows last-chance unhandled-exception filter. The GetRuntimeException classlib export signature change (adding faultingIP) is threaded consistently through all call sites and Test.CoreLib.
Summary:
Detailed Findings
✅ API Approval — Public surface matches the approved proposal
The ref/System.Runtime.cs addition and the src implementation match the api-approved comment on issue #101560 exactly: System.Runtime.ExceptionServices.ExceptionHandling.SetFatalErrorHandler(delegate* unmanaged<int, void*, int> handler), [CLSCompliant(false)], with ArgumentNullException/InvalidOperationException/PlatformNotSupportedException. No extra or missing public managed surface was found. The new src/native/public/FatalErrorHandling.h is a native (non-managed-ref) contract and is appropriately not part of the managed ref assembly.
⚠️ Fatal-path robustness / re-entrancy — see inline on eepolicy.cpp (InvokeFatalErrorHandler)
User code now runs on the crashing thread before the s_pCrashingThreadID latch (which moved into LogInfoForFatalError, only on the RunDefaultHandler path). This exposes concurrent and re-entrant handler invocation that the FatalErrorHandling.h contract does not document. Detailed inline comment provided; the same invocation pattern exists at the NativeAOT sites.
💡 Skip-path control flow — see inline on eepolicy.cpp (skip SafeExitProcess)
Minor defense-in-depth / consistency: the SkipDefaultHandler SafeExitProcess has no trailing UNREACHABLE() unlike the stack-overflow site, so a hypothetical return would fall through and emit the suppressed crash log.
✅ Cross-platform consistency — Windows filter vs. Unix signal handlers
The genuinely-unmanaged fatal path is handled symmetrically: Unix SIGSEGVHandler/SIGFPEHandler gate on ShouldSkipDefaultHandlingForNativeException and restore the previous disposition on skip; Windows adds a chained last-chance SetUnhandledExceptionFilter that preserves any previously-installed filter. IsFatalHardwareExceptionForFatalErrorHandler mirrors the same fault-code set on both CoreCLR/Windows and NativeAOT/Windows, and stack overflow is consistently and deliberately excluded (documented) because too little stack remains. One observation for the author/human reviewer: NativeAOT/Unix installs SIGSEGV and SIGFPE handlers but not SIGILL, while the fault-code lists include illegal/privileged-instruction codes — worth confirming that illegal-instruction native faults are intentionally out of scope on Unix.
✅ Test quality — comprehensive subprocess matrix
The test spawns child processes per scenario and asserts on stderr markers and exit codes: SkipHandler (crash log suppressed), RunHandler (default log emitted exactly once — with an explicit regression guard against the double-emission "Fatal error while logging another fatal error." path), LogHandler (pfnGetFatalErrorLog round-trip), native/native-code/nested-hardware-fault address+platform-record propagation, and the SetNull/SetTwice argument-validation paths. Platform/runtime applicability is gated correctly (e.g. native-code path skipped on CoreCLR/Unix, nested-fault path NativeAOT-only). RequiresProcessIsolation and the Mono CLRTestTargetUnsupported gate are set appropriately. This is a good, behavior-focused test suite.
💡 Minor naming/consistency observations (non-blocking)
- The XML doc on
SetFatalErrorHandlerand the header comment referencepfnGetFatalErrorLog, but the managed doc refers toFatalErrorPropertyGetter; the naming across the doc comment, header, and code is mostly consistent but a human may want to confirm the doc-comment terminology (RunDefaultHandler/SkipDefaultHandlervalues, property getter) reads cleanly for the public API. - The NativeAOT
FatalErrorPropertyenum is duplicated in managed code with a "must be kept in sync" comment against the native header — acceptable, but a drift risk worth a human's awareness.
Note
This review was generated by this repository's Holistic Review agentic workflow to complement the built-in Copilot review.
Generated by Holistic Review · 412.1 AIC · ⌖ 12 AIC · ⊞ 10K
… memory efficiency and eliminating truncation risks
There was a problem hiding this comment.
Copilot's findings
Comments suppressed due to low confidence (2)
src/libraries/System.Private.CoreLib/src/System/Runtime/ExceptionServices/ExceptionHandling.cs:83
- On Mono this method always throws PlatformNotSupportedException before validating the handler pointer. That means SetFatalErrorHandler(null) throws PNSE (not ArgumentNullException) and the documented exception behavior differs by runtime. In dotnet/runtime APIs, argument validation is typically performed before platform checks so exception behavior is consistent.
public static unsafe void SetFatalErrorHandler(delegate* unmanaged<int, void*, int> handler)
{
#if MONO
throw new PlatformNotSupportedException();
#else
ArgumentNullException.ThrowIfNull((void*)handler, nameof(handler));
if (!TrySetFatalErrorHandler((IntPtr)handler))
{
throw new InvalidOperationException(SR.InvalidOperation_CannotRegisterSecondFatalErrorHandler);
}
#endif
}
src/native/public/FatalErrorHandling.h:79
- Same C-compatibility concern as FatalErrorHandlerResult:
enum FatalErrorProperty : int32_tis C++-only and makes this public header unusable from C. If C support is intended, prefer atypedef enumform here too.
enum FatalErrorProperty : int32_t
{
// Value: FatalErrorLogFunc. Entry point for retrieving the crash log.
FEP_FatalErrorLogFunc = 0x1,
// Value: void*. Code location correlated with the failure (for example,
// the address where FailFast was called). May be unavailable.
FEP_Address = 0x2,
// Value: PEXCEPTION_RECORD. Windows exception record for the failure.
FEP_WindowsExceptionRecord = 0x3,
// Value: PCONTEXT. Windows thread context at the point of failure.
FEP_WindowsContextRecord = 0x4,
// Value: ucontext_t*. Thread context on signal-based Unix platforms.
FEP_UContext = 0x5,
// Value: siginfo_t*. Signal information on signal-based Unix platforms.
FEP_PosixSigInfo = 0x6,
// Value: Mach thread state for the current architecture
// (arm_thread_state64_t* on arm64, x86_thread_state64_t* on x64).
FEP_MachExceptionInfo = 0x7,
};
- Files reviewed: 29/29 changed files
- Comments generated: 1
There was a problem hiding this comment.
Holistic Review
Motivation: Unchanged from the prior review. This implements the api-approved ExceptionHandling.SetFatalErrorHandler (issue #101560), a long-requested capability for hooking crash reporters (breakpad/Sentry-style) beneath the .NET runtime. The problem is real and API-review-approved.
Approach: Unchanged in shape and risk. The single new commit since the last review (1a608d4 — "Refactor crash log handling to use a single managed string") is a localized simplification of the NativeAOT crash-log storage in RuntimeExceptionHelpers.cs. It replaces the fixed-capacity InlineArray16<string?> fragment buffer (and its StoreCrashLogFragment/MaxCrashLogFragments machinery) with a single [ThreadStatic] string? t_crashLog composed via a StringBuilder on the crashing thread in FailFast, then replayed on demand by GetFatalErrorLog (handler callback) and WriteCrashLogToStdErr (default path). This eliminates the 16-fragment capacity limit and the associated Debug.Fail truncation risk that earlier reviews flagged, and it removes per-fragment iteration in favor of a single UTF-8 chunked encode. The overall design (public FatalErrorHandling.h contract, lock-free single registration, CoreCLR + NativeAOT + platform EH hooks) is otherwise identical.
Summary: t_crashLog (the non-crashing threads block in Thread.Sleep(int.MaxValue)), so making the storage [ThreadStatic] is safe and, if anything, more correct than the previous plain-static buffer. The minimal OOM path still avoids StringBuilder allocation by assigning a string literal directly. I found no new actionable defects in this commit. My prior reservations about fatal-path robustness / re-entrancy of invoking arbitrary user code and the documented concurrency contract still stand and remain the province of the exceptions/EH maintainers; they are unaffected by this commit. As a new public runtime API with platform-specific EH surgery, this continues to warrant careful human review, and I could not run builds or tests in this environment.
Assessment History
- review 4730782393 reviewed commit
3b168438, verdict⚠️ Needs Human Review. Current verdict⚠️ Needs Human Review — unchanged. The only new commit is a localized crash-log storage refactor that resolves a previously-noted truncation risk without altering the API surface, registration model, or fatal-path invocation semantics; motivation, approach, and risk are all unchanged.
Detailed Findings
No new actionable findings in the incremental scope (3b168438..1a608d4). Observations on the refactor:
✅ Truncation risk resolved
The previous fragment-array design capped crash-log content at 16 fragments and would silently drop overflow (guarded by a Debug.Fail). The single t_crashLog string removes both the cap and the truncation path. GetFatalErrorLog still chunks the UTF-8 encode with the correct deferred flush handling to avoid splitting surrogate pairs across ChunkSize boundaries.
✅ ThreadStatic storage is thread-affinity-safe
Making the crash text [ThreadStatic] is correct here: it is written on the crashing thread in FailFast, and both readers (GetFatalErrorLog invoked synchronously through the user handler, and WriteCrashLogToStdErr on the default path) execute on that same thread. Other threads that enter FailFast block indefinitely, so there is no cross-thread read of t_crashLog.
Note
This review was generated by this repository's Holistic Review agentic workflow to complement the built-in Copilot review.
Generated by Holistic Review · 75 AIC · ⌖ 14.9 AIC · ⊞ 10K
## Summary Adds an on-demand entry point to the in-proc crash reporter so a report can be generated programmatically while the runtime is still running, independent of the fatal-signal path. This is the reporting mechanism that a user-registered native fatal-error handler (proposed in #129543) can call to produce a crash report. ## Motivation The in-proc crash reporter can currently only run from the PAL fatal-signal dispatcher. The fatal-error-handler proposal (#129543) needs a way to invoke just the reporting mechanism on demand, formatting the report to a caller-supplied sink, without the watchdog/lifecycle/file-management that the signal path performs. ## What's in this PR - **On-demand report API** — `InProcCrashReportCreateReport(outputFormat, signal, context, outputCallback, callbackContext)`. Emits the same report as the signal path but streams the selected format (`Json` or `Log`) to a caller-supplied callback, with no watchdog or file output. Re-runnable; yields to any report already in flight. - **Shared in-flight guard** — the signal-path and on-demand `CreateReport` paths are made mutually exclusive over the shared signal-safe writers, module table, and process-global thread-suspension machinery via a single `m_reportInFlight` guard. The on-demand path releases the guard on completion; the signal path never does (the process is terminating). - **Init / services split** — reporter bring-up is split into `CrashReportInitialize` (VM callbacks only, so on-demand reports are possible) and `InProcCrashReportInitializeServices` (env-gated watchdog + lifecycle + PAL dispatcher registration). This lets the reporter be initialized from either the runtime startup path or a future fatal-error-handler setup path, without arming the signal-path crash-dump behavior. - **Output sink refactor** — `SignalSafeConsoleWriter` and `SignalSafeJsonWriter` gain a small copyable sink abstraction so a single caller-supplied callback shape can drive either writer (platform console / drop-all / caller callback). - **Stack-overflow classification fix** — the in-proc SO stack trace is now captured only on the real stack-overflow path. Previously a `FailFast` (which funnels through the same fatal-error callstack logger) populated the SO-trace latch, causing an on-demand report to misclassify a `FailFast` as a `StackOverflow`. Gating capture to the real-SO path makes the latch authoritative for classification. ## Behavior notes - No behavior changes when crash reporting is disabled: startup allocates nothing. - No behavior changes when crash reporting is enabled: allocates and configure signal-based in-proc crash reporter. ## Testing - In-proc crash reporter lacks automatic tests, on-demand can be tested through #129543 tests when integrated. - Manually ran FailFast and StackOverflow paths on Android arm64 emulator. - All tests in https://github.com/mdh1418/runtime/blob/inproc_crashreport_tests/src/tests/FunctionalTests/Android/Device_Emulator/InProcCrashReport/OVERVIEW.md pass on Android arm64 emulator. ## Follow-ups - Wiring the on-demand entry point into the fatal-error handler once #129543 lands.
…iling thread invokes the handler, preventing reentrancy and blocking other threads during process termination.
…dler # Conflicts: # src/coreclr/vm/eepolicy.cpp
There was a problem hiding this comment.
Copilot's findings
Comments suppressed due to low confidence (2)
src/coreclr/vm/eepolicy.cpp:458
#include <public/FatalErrorHandling.h>is currently placed mid-file, after code, which is unusual for CoreCLR sources and makes it easy to miss compile dependencies (and can interact poorly with precompiled-header/include-order assumptions). Please move this include to the main include block at the top of the file, and keep this section focused on the fatal-error logging implementation.
//
// Uses the public FatalErrorHandling.h header for shared type definitions.
//
#include <public/FatalErrorHandling.h>
src/native/public/FatalErrorHandling.h:8
- The PR description says
FatalErrorHandling.hdefinesFatalErrorInfo/FatalErrorHandlerResult/ callback typedefs. The header added here definesFatalErrorHandlerResultbut does not define aFatalErrorInfostruct; it instead exposes a property-getter based model (FatalErrorProperty+FatalErrorPropertyGetter). Please align the PR description (and any related docs) with the actual header/API shape to avoid misleading native consumers.
// This header defines the native types used by the
// ExceptionHandling.SetFatalErrorHandler API. A native fatal error handler
// receives an HRESULT and a property-getter callback through which it can
// request additional crash information on demand.
//
- Files reviewed: 29/29 changed files
- Comments generated: 0 new
There was a problem hiding this comment.
Isn't this directory meant for mono embedded API? I think minipal is the better place for it.
There was a problem hiding this comment.
This is a public header file. It is there by design. See the new public native description at #101560.
There was a problem hiding this comment.
This looks odd because:
-
Docs:
runtime/src/native/public/README.md
Lines 1 to 11 in 68fdb23
-
First C++ header in this dir
There was a problem hiding this comment.
minipal is the better place for it.
I do not think minipal would be a better place for it. minipal is meant as an abstraction of the underlying platform.
We do not have a central location for CoreCLR/NAOT public headers. Some examples of where public headers live currently:
src\native\corehost\hostfxr.h
src\coreclr\inc\corprof.idl
src\coreclr\src\coreclr\pal\prebuilt\inc\corprof.h
We place some of the public headers into published packages, and some public headers we just tell people to copy out manually.
If we do not like src\native\public, src\native\inc or src\coreclr\inc are potential alternatives.
How are people expected to get FatalErrorHandling.h?
| /// A pointer to an unmanaged callback invoked when the runtime encounters a fatal | ||
| /// error. The callback receives the HRESULT associated with the failure and a | ||
| /// property-getter callback (<c>FatalErrorPropertyGetter</c>, declared in | ||
| /// <c>FatalErrorHandling.h</c>) through which it can request additional crash |
There was a problem hiding this comment.
Consider somebody reading this doc at https://learn.microsoft.com/dotnet/api/system.runtime.exceptionservices.exceptionhandling - how are they going to figure out where to acquire FatalErrorHandling.h?
Implements the
ExceptionHandling.SetFatalErrorHandlerAPI (#101560) for both NativeAOT and CoreCLR. Mono throwsPlatformNotSupportedException.Changes
Managed API (
ExceptionHandling.cs)NativeAOT (
RuntimeExceptionHelpers.cs)CoreCLR (
eepolicy.cpp)Public native header (
src/native/public/FatalErrorHandling.h)FatalErrorHandlerResultenum,FatalErrorInfostruct, callback typedefsTests (
src/tests/baseservices/exceptions/FatalErrorHandler/)Fixes #101560