Implement ExceptionHandling.SetFatalErrorHandler - #129543
Implement ExceptionHandling.SetFatalErrorHandler#129543AaronRobinsonMSFT wants to merge 56 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>
There was a problem hiding this comment.
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-approvedlabel. New public APIs require an approved proposal; please link anapi-approvedissue (or get the proposal issue labeled), otherwise this should stayinternaluntil 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
FatalErrorHandlertypedef would make the public contract self-contained.
- Files reviewed: 38/38 changed files
- Comments generated: 0 new
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
There was a problem hiding this comment.
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
NULLin new code (e.g., initializing pointers). The runtime codebase’s C++ conventions prefernullptrfor pointer literals to avoid accidental integer conversions and improve type safety.
- Files reviewed: 38/38 changed files
- Comments generated: 0 new
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 299682bb-b7ba-48bb-839f-4ebf3fb4cab3
There was a problem hiding this comment.
Copilot's findings
Suppressed comments (2)
src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeExceptionHelpers.cs:117
- The
flushheuristic 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 setflush: truewhile 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 alwaysflush: falsewhile consuming input, then do a finalConvertwith empty input andflush: trueto 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
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
There was a problem hiding this comment.
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
FatalErrorHandlerResultvalues exceptRunDefaultHandler. However, the linkedapi-approvedproposal issue (#101560) describes a different public native contract (aFatalErrorInfostruct and aSkipDefaultHandlerreturn value). Since this header is installed into thecorehost/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 FailFastnow builds the crash log usingnew 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 theStringComparison.Ordinaloverload for all marker searches.
- Files reviewed: 38/38 changed files
- Comments generated: 1
There was a problem hiding this comment.
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 onlyRunDefaultHandler). 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
FatalErrorHandlertypedef 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
|
@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? |
|
Public C API looking great! :) |
| 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. |
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
@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.
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. |
|
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. |
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. |
|
#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. |
There was a problem hiding this comment.
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.
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
Fixes #95461