diff --git a/src/libraries/System.Private.CoreLib/src/System/AppContext.cs b/src/libraries/System.Private.CoreLib/src/System/AppContext.cs index 6c86be6901e2bb..c48d93688fbd3f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/AppContext.cs +++ b/src/libraries/System.Private.CoreLib/src/System/AppContext.cs @@ -82,28 +82,50 @@ public static void SetData(string name, object? data) internal static event EventHandler? FirstChanceException; #pragma warning restore CS0067 + private static ulong s_crashingThreadId; + #if NATIVEAOT [System.Runtime.RuntimeExport("OnUnhandledException")] #endif internal static void OnUnhandledException(object e) { + ulong currentThreadId = Thread.CurrentOSThreadId; + ulong previousCrashingThreadId = Interlocked.CompareExchange(ref s_crashingThreadId, currentThreadId, 0); + if (previousCrashingThreadId == 0) + { #if NATIVEAOT - RuntimeExceptionHelpers.SerializeCrashInfo(System.Runtime.RhFailFastReason.UnhandledException, (e as Exception)?.Message, e as Exception); + RuntimeExceptionHelpers.SerializeCrashInfo(System.Runtime.RhFailFastReason.UnhandledException, (e as Exception)?.Message, e as Exception); #endif - if (UnhandledException is UnhandledExceptionEventHandler handlers) - { - UnhandledExceptionEventArgs args = new(e, isTerminating: true); - foreach (UnhandledExceptionEventHandler handler in Delegate.EnumerateInvocationList(handlers)) + if (UnhandledException is UnhandledExceptionEventHandler handlers) { - try - { - handler(/* AppDomain */ null!, args); - } - catch + UnhandledExceptionEventArgs args = new(e, isTerminating: true); + foreach (UnhandledExceptionEventHandler handler in Delegate.EnumerateInvocationList(handlers)) { + try + { + handler(/* AppDomain */ null!, args); + } + catch + { + } } } } + else + { + if (s_crashingThreadId == previousCrashingThreadId) + { + Environment.FailFast("OnUnhandledException called recursively"); + } + + // If we are already in the process of handling an unhandled + // exception, we do not want to raise the event again. We wait + // here while the other thread raises the unhandled exception. + // Waiting is important because it is possible upon returning, this thread + // could call some rude abort method that would terminate the process + // before the other thread finishes raising the unhandled exception. + Thread.Sleep(-1); + } } internal static void OnProcessExit() diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/ExceptionServices/ExceptionHandling.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/ExceptionServices/ExceptionHandling.cs index 116c94982f03d1..da598532381607 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/ExceptionServices/ExceptionHandling.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/ExceptionServices/ExceptionHandling.cs @@ -48,6 +48,10 @@ public static void SetUnhandledExceptionHandler(Func handler) /// event and then return. /// /// It will not raise the the handler registered with . + /// + /// This API is thread safe and can be called from multiple threads. However, only one thread + /// will trigger the event handlers, while other threads will wait indefinitely without raising + /// the event. /// public static void RaiseAppDomainUnhandledExceptionEvent(object exception) { diff --git a/src/tests/baseservices/exceptions/RaiseAppDomainUnhandledExceptionEvent/RaiseEvent.cs b/src/tests/baseservices/exceptions/RaiseAppDomainUnhandledExceptionEvent/RaiseEvent.cs index 94f5e4f7422ddc..aeff4dbd63771a 100644 --- a/src/tests/baseservices/exceptions/RaiseAppDomainUnhandledExceptionEvent/RaiseEvent.cs +++ b/src/tests/baseservices/exceptions/RaiseAppDomainUnhandledExceptionEvent/RaiseEvent.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; using TestLibrary; using Xunit; @@ -19,7 +20,14 @@ public HandlerRegistration(UnhandledExceptionEventHandler handler) public void Dispose() { AppDomain.CurrentDomain.UnhandledException -= _handler; + + // See usage of s_crashingThreadId in the ExceptionHandling class. + // This is to ensure that the static field is reset after the test. + GetCrashingThreadId(null) = 0; } + + [UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = "s_crashingThreadId")] + private static extern ref ulong GetCrashingThreadId([UnsafeAccessorType("System.AppContext")]object? obj); } [ThreadStatic]