From 44a9b6b61a367b31cec7417e9f23ee00d8c6380a Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Mon, 17 Aug 2026 19:51:06 -0700 Subject: [PATCH 01/21] Adding test that checks attempt to reinitialize a runtime thread object --- .../ThreadStateDestroyed/CMakeLists.txt | 13 ++ .../ThreadStateDestroyed.cs | 62 +++++++++ .../ThreadStateDestroyed.csproj | 22 ++++ .../ThreadStateDestroyedNative.cpp | 121 ++++++++++++++++++ 4 files changed, 218 insertions(+) create mode 100644 src/tests/baseservices/threading/ThreadStateDestroyed/CMakeLists.txt create mode 100644 src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs create mode 100644 src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.csproj create mode 100644 src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyedNative.cpp diff --git a/src/tests/baseservices/threading/ThreadStateDestroyed/CMakeLists.txt b/src/tests/baseservices/threading/ThreadStateDestroyed/CMakeLists.txt new file mode 100644 index 00000000000000..41866085f2b61b --- /dev/null +++ b/src/tests/baseservices/threading/ThreadStateDestroyed/CMakeLists.txt @@ -0,0 +1,13 @@ +project (ThreadStateDestroyedNative) + +include_directories(${INC_PLATFORM_DIR}) + +set(SOURCES ThreadStateDestroyedNative.cpp) + +if(NOT CLR_CMAKE_HOST_WIN32) + add_compile_options(-pthread) +endif() + +add_library (ThreadStateDestroyedNative SHARED ${SOURCES}) + +install (TARGETS ThreadStateDestroyedNative DESTINATION bin) diff --git a/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs b/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs new file mode 100644 index 00000000000000..1ed80dad9a901c --- /dev/null +++ b/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs @@ -0,0 +1,62 @@ +// 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.InteropServices; +using System.Threading; + +// A native library runs managed code on an OS thread, then runs managed code again from a +// thread-destruction callback that fires after the runtime has already torn down its per-thread +// state. The runtime should fail fast instead of attaching another managed thread. + +public static unsafe class ThreadStateDestroyed +{ + private const string NativeLib = "ThreadStateDestroyedNative"; + + [DllImport(NativeLib)] + private static extern void RunCallbackOnThreadAndDuringItsDestruction(delegate* unmanaged callback); + + private static int s_callbackCount; + private static Thread s_firstThread; + private static bool s_secondCallbackGotNewThread; + + [UnmanagedCallersOnly] + private static void Callback() + { + int count = Interlocked.Increment(ref s_callbackCount); + Thread current = Thread.CurrentThread; + + if (count == 1) + { + s_firstThread = current; + Console.WriteLine("[managed] callback #1 ran; the runtime attached a Thread to the OS thread."); + } + else + { + // Managed thread ids can be recycled. We need to check that it's actually a new Thread object. + s_secondCallbackGotNewThread = !ReferenceEquals(current, s_firstThread); + Console.WriteLine($"[managed] callback #{count} ran; attached to a new Thread: {s_secondCallbackGotNewThread}."); + } + } + + public static int Main() + { + RunCallbackOnThreadAndDuringItsDestruction(&Callback); + + // Only reachable when the runtime did not fail fast. + if (s_callbackCount != 2) + { + Console.WriteLine($"[managed] Expected exactly 2 callbacks but got {s_callbackCount}."); + return 102; + } + + if (!s_secondCallbackGotNewThread) + { + Console.WriteLine("[managed] The second callback reused the existing Thread."); + return 103; + } + + Console.WriteLine("[managed] FAIL: the runtime attached a second Thread instead of failing fast."); + return 101; + } +} diff --git a/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.csproj b/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.csproj new file mode 100644 index 00000000000000..5915823bb67d27 --- /dev/null +++ b/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.csproj @@ -0,0 +1,22 @@ + + + + true + false + true + true + true + + 134 + -1073740286 + true + true + + + + + + + + diff --git a/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyedNative.cpp b/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyedNative.cpp new file mode 100644 index 00000000000000..414c132909186e --- /dev/null +++ b/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyedNative.cpp @@ -0,0 +1,121 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include + +using CallbackFn = void (*)(); + +static CallbackFn s_callback; + +static void RunCallbackDuringThreadDestruction() +{ + printf("[native] invoking managed callback from the thread destruction callback\n"); + fflush(stdout); + + s_callback(); + + printf("[native] managed callback returned; the runtime did not detect the re-initialization\n"); + fflush(stdout); +} + +#ifdef _WIN32 + +// FLS rather than TLS: FlsAlloc takes a destruction callback, which is also the mechanism the +// runtime itself uses to learn that a thread is terminating. + +static DWORD s_flsIndex = FLS_OUT_OF_INDEXES; + +static VOID WINAPI FlsCallback(PVOID) +{ + RunCallbackDuringThreadDestruction(); +} + +static DWORD WINAPI ThreadProc(LPVOID) +{ + s_callback(); + + // Arms FlsCallback, which the OS invokes while this thread is being destroyed. + if (!FlsSetValue(s_flsIndex, reinterpret_cast(1))) + { + abort(); + } + + return 0; +} + +extern "C" DLL_EXPORT void RunCallbackOnThreadAndDuringItsDestruction(CallbackFn callback) +{ + s_callback = callback; + + s_flsIndex = FlsAlloc(FlsCallback); + if (s_flsIndex == FLS_OUT_OF_INDEXES) + { + abort(); + } + + HANDLE thread = CreateThread(nullptr, 0, ThreadProc, nullptr, 0, nullptr); + if (thread == nullptr) + { + abort(); + } + + if (WaitForSingleObject(thread, INFINITE) != WAIT_OBJECT_0) + { + abort(); + } + + CloseHandle(thread); + FlsFree(s_flsIndex); +} + +#else // _WIN32 + +#define AbortIfFail(st) if (st != 0) abort() + +static pthread_key_t s_key; + +static void KeyDestructor(void*) +{ + RunCallbackDuringThreadDestruction(); +} + +static void* ThreadProc(void*) +{ + s_callback(); + + // Arms KeyDestructor. pthread key destructors run after the C++ thread_local destructors + // that the runtime uses to tear down its per-thread state. + // The callback below observes a thread whose runtime thread state is already gone. + int st = pthread_setspecific(s_key, reinterpret_cast(1)); + AbortIfFail(st); + + return nullptr; +} + +extern "C" DLL_EXPORT void RunCallbackOnThreadAndDuringItsDestruction(CallbackFn callback) +{ + s_callback = callback; + + int st = pthread_key_create(&s_key, KeyDestructor); + AbortIfFail(st); + + pthread_attr_t attr; + st = pthread_attr_init(&attr); + AbortIfFail(st); + + // We need to set the stack size due to the very small (80kB) default stack size on MUSL + // based Linux distros. + st = pthread_attr_setstacksize(&attr, 0x180000); // 1.5MB + AbortIfFail(st); + + pthread_t thread; + st = pthread_create(&thread, &attr, ThreadProc, nullptr); + AbortIfFail(st); + + pthread_attr_destroy(&attr); + + st = pthread_join(thread, nullptr); + AbortIfFail(st); +} + +#endif // _WIN32 From a8b74e8e2a6d3d76147f5134d67f2c226f9ccc6e Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Tue, 18 Aug 2026 13:26:13 -0700 Subject: [PATCH 02/21] Update test --- .../ThreadStateDestroyed.cs | 91 ++++++++++++++++--- .../ThreadStateDestroyed.csproj | 10 +- .../ThreadStateDestroyedNative.cpp | 2 +- 3 files changed, 81 insertions(+), 22 deletions(-) diff --git a/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs b/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs index 1ed80dad9a901c..5efab4305a863b 100644 --- a/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs +++ b/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics; using System.Runtime.InteropServices; using System.Threading; @@ -11,14 +12,27 @@ public static unsafe class ThreadStateDestroyed { + private const int Pass = 100; + private const int Fail = -1; + private const string NativeLib = "ThreadStateDestroyedNative"; + private const string RunScenarioArg = "--run-scenario"; + + private const int StatusFailFastException = unchecked((int)0xC0000602); + private const int SigAbrtExitCode = 128 + 6; + private static readonly TimeSpan s_subprocessTimeout = TimeSpan.FromSeconds(60); + + // We need to check both the exit code and the message since the runtime may fail fast + // with the same exit code for other reasons. + private const string ExpectedMessage = + "Attempt to execute managed code after the .NET runtime thread state has been destroyed."; + private const string SecondCallbackMarker = "[managed] callback #2"; [DllImport(NativeLib)] private static extern void RunCallbackOnThreadAndDuringItsDestruction(delegate* unmanaged callback); private static int s_callbackCount; private static Thread s_firstThread; - private static bool s_secondCallbackGotNewThread; [UnmanagedCallersOnly] private static void Callback() @@ -33,30 +47,79 @@ private static void Callback() } else { - // Managed thread ids can be recycled. We need to check that it's actually a new Thread object. - s_secondCallbackGotNewThread = !ReferenceEquals(current, s_firstThread); - Console.WriteLine($"[managed] callback #{count} ran; attached to a new Thread: {s_secondCallbackGotNewThread}."); + // Managed thread ids can be recycled, so compare the Thread objects themselves. + bool newThread = !ReferenceEquals(current, s_firstThread); + Console.WriteLine($"{SecondCallbackMarker} ran; attached to a new Thread: {newThread}."); } } - public static int Main() + public static int Main(string[] args) + { + return args.Length > 0 && args[0] == RunScenarioArg + ? RunScenario() + : RunController(); + } + + private static int RunScenario() { RunCallbackOnThreadAndDuringItsDestruction(&Callback); - // Only reachable when the runtime did not fail fast. - if (s_callbackCount != 2) + Console.WriteLine("[managed] The runtime did not fail fast."); + return Fail; + } + + private static int RunController() + { + string[] arguments = TestLibrary.Utilities.IsNativeAot + ? [RunScenarioArg] + : [typeof(ThreadStateDestroyed).Assembly.Location, RunScenarioArg]; + + ProcessStartInfo psi = new ProcessStartInfo(Environment.ProcessPath, arguments) + { + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + + psi.Environment["DOTNET_DbgEnableMiniDump"] = "0"; + psi.Environment["DOTNET_EnableCrashReport"] = "0"; + + ProcessTextOutput subprocess; + try + { + subprocess = Process.RunAndCaptureText(psi, s_subprocessTimeout); + } + catch (TimeoutException) + { + Console.WriteLine($"Subprocess timed out after {s_subprocessTimeout}."); + return Fail; + } + + string output = subprocess.StandardOutput + subprocess.StandardError; + int exitCode = subprocess.ExitStatus.ExitCode; + + Console.WriteLine($"Subprocess exited with {exitCode}:"); + Console.WriteLine(output); + + if (output.Contains(SecondCallbackMarker)) + { + Console.WriteLine("The runtime ran managed code on a thread whose runtime thread state was already destroyed."); + return Fail; + } + + if (!output.Contains(ExpectedMessage)) { - Console.WriteLine($"[managed] Expected exactly 2 callbacks but got {s_callbackCount}."); - return 102; + Console.WriteLine($"The subprocess terminated for some other reason. Expected to find: {ExpectedMessage}"); + return Fail; } - if (!s_secondCallbackGotNewThread) + // RaiseFailFastException on Windows, abort on Unix, for both CoreCLR and NativeAOT. + int expectedExitCode = OperatingSystem.IsWindows() ? StatusFailFastException : SigAbrtExitCode; + if (exitCode != expectedExitCode) { - Console.WriteLine("[managed] The second callback reused the existing Thread."); - return 103; + Console.WriteLine($"Expected the subprocess to fail fast with {expectedExitCode}."); + return Fail; } - Console.WriteLine("[managed] FAIL: the runtime attached a second Thread instead of failing fast."); - return 101; + return Pass; } } diff --git a/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.csproj b/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.csproj index 5915823bb67d27..ec3399fd1f8d7e 100644 --- a/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.csproj +++ b/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.csproj @@ -1,20 +1,16 @@ - + Exe true false true - true true - - 134 - -1073740286 - true + true true + diff --git a/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyedNative.cpp b/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyedNative.cpp index 414c132909186e..9323af86d902bc 100644 --- a/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyedNative.cpp +++ b/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyedNative.cpp @@ -3,7 +3,7 @@ #include -using CallbackFn = void (*)(); +typedef void (*CallbackFn)(); static CallbackFn s_callback; From 571ed02232f90265970a57347e9b352b05b180b0 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Tue, 18 Aug 2026 17:06:55 -0700 Subject: [PATCH 03/21] Fix --- src/coreclr/vm/ceemain.cpp | 49 +++++++++++++++++++++++++++----------- src/coreclr/vm/ceemain.h | 3 +++ src/coreclr/vm/threads.cpp | 2 ++ 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/coreclr/vm/ceemain.cpp b/src/coreclr/vm/ceemain.cpp index c3258d39098825..9b651c53cb8d7c 100644 --- a/src/coreclr/vm/ceemain.cpp +++ b/src/coreclr/vm/ceemain.cpp @@ -1698,6 +1698,11 @@ static uint32_t g_flsIndex = FLS_OUT_OF_INDEXES; static PLATFORM_THREAD_LOCAL byte t_flsState; +static bool HasThreadStateBeenDestroyed() +{ + return t_flsState == FLS_STATE_INVOKED; +} + // This is called when each *fiber* is destroyed. When the home fiber of a thread is destroyed, // it means that the thread itself is destroyed. // Since we receive that notification outside of the Loader Lock, it allows us to safely acquire @@ -1734,20 +1739,7 @@ static void OsAttachThread(void* thread) { _ASSERTE(g_flsIndex != FLS_OUT_OF_INDEXES); - if (t_flsState == FLS_STATE_INVOKED) - { - // Managed C++ may run managed code in DllMain (e.g. during DLL_PROCESS_DETACH to run global destructors). This is - // not supported and unreliable. Historically, it happened to work most of the time. For backward compatibility, - // suppress this assert in release builds if we have encountered any mixed mode binaries. - if (Module::HasAnyIJWBeenLoaded()) - { - _ASSERTE(!"Attempt to execute managed code after the .NET runtime thread state has been destroyed."); - } - else - { - _ASSERTE_ALL_BUILDS(!"Attempt to execute managed code after the .NET runtime thread state has been destroyed."); - } - } + CheckThreadStateNotDestroyed(); t_flsState = FLS_STATE_ARMED; @@ -1792,6 +1784,13 @@ void EnsureTlsDestructionMonitor() } #else +static PLATFORM_THREAD_LOCAL bool t_threadStateDestroyed; + +static bool HasThreadStateBeenDestroyed() +{ + return t_threadStateDestroyed; +} + struct TlsDestructionMonitor { bool m_activated = false; @@ -1807,6 +1806,8 @@ struct TlsDestructionMonitor { RuntimeThreadShutdown(GetThreadNULLOk()); } + + t_threadStateDestroyed = true; } }; @@ -1821,6 +1822,26 @@ void EnsureTlsDestructionMonitor() #endif +void CheckThreadStateNotDestroyed() +{ + if (!HasThreadStateBeenDestroyed()) + { + return; + } + + // Managed C++ may run managed code in DllMain (e.g. during DLL_PROCESS_DETACH to run global destructors). This is + // not supported and unreliable. Historically, it happened to work most of the time. For backward compatibility, + // suppress this assert in release builds if we have encountered any mixed mode binaries. + if (Module::HasAnyIJWBeenLoaded()) + { + _ASSERTE(!"Attempt to execute managed code after the .NET runtime thread state has been destroyed."); + } + else + { + _ASSERTE_ALL_BUILDS(!"Attempt to execute managed code after the .NET runtime thread state has been destroyed."); + } +} + #ifdef DEBUGGING_SUPPORTED // // InitializeDebugger initialized the Runtime-side CLR Debugging Services diff --git a/src/coreclr/vm/ceemain.h b/src/coreclr/vm/ceemain.h index 120082d30572ca..e518f2889d987b 100644 --- a/src/coreclr/vm/ceemain.h +++ b/src/coreclr/vm/ceemain.h @@ -46,6 +46,9 @@ void ForceEEShutdown(ShutdownCompleteAction sca = SCA_ExitProcessWhenShutdownCom void ThreadDetaching(); void EnsureTlsDestructionMonitor(); + +// Fails fast if the runtime thread state of the current thread has already been destroyed. +void CheckThreadStateNotDestroyed(); #ifdef TARGET_WINDOWS void InitFlsSlot(); void OsDetachThread(void* thread); diff --git a/src/coreclr/vm/threads.cpp b/src/coreclr/vm/threads.cpp index 4519b127bb3250..c856b3786dd937 100644 --- a/src/coreclr/vm/threads.cpp +++ b/src/coreclr/vm/threads.cpp @@ -604,6 +604,8 @@ Thread* SetupThread() if ((pThread = GetThreadNULLOk()) != NULL) return pThread; + CheckThreadStateNotDestroyed(); + // For interop debugging, we must mark that we're in a can't-stop region // b.c we may take Crsts here that may block the helper thread. // We're especially fragile here b/c we don't have a Thread object yet From 166744abafcf5dd65f72617fe39edf3c528a4547 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Wed, 19 Aug 2026 17:35:32 -0700 Subject: [PATCH 04/21] Fix osx --- src/coreclr/vm/ceemain.cpp | 48 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/coreclr/vm/ceemain.cpp b/src/coreclr/vm/ceemain.cpp index 9b651c53cb8d7c..887e8d759c46d4 100644 --- a/src/coreclr/vm/ceemain.cpp +++ b/src/coreclr/vm/ceemain.cpp @@ -223,6 +223,10 @@ HRESULT EEStartup(); static void InitializeGarbageCollector(); +#if defined(TARGET_UNIX) && !defined(TARGET_WASM) +static void InitThreadStateKey(); +#endif + #ifdef DEBUGGING_SUPPORTED static void InitializeDebugger(void); static void TerminateDebugger(void); @@ -705,6 +709,10 @@ void EEStartupHelper() InitThreadManager(); STRESS_LOG0(LF_STARTUP, LL_ALWAYS, "Returned successfully from InitThreadManager"); +#if defined(TARGET_UNIX) && !defined(TARGET_WASM) + InitThreadStateKey(); +#endif + #ifdef FEATURE_PERFTRACING // Initialize the event pipe. EventPipeAdapter::Initialize(); @@ -1783,6 +1791,41 @@ void EnsureTlsDestructionMonitor() OsAttachThread(GetThread()); } +#else +#if defined(TARGET_UNIX) && !defined(TARGET_WASM) +static pthread_key_t g_threadStateKey; +static byte g_threadStateDestroyedMarker; + +static void SetThreadStateDestroyed() +{ + if (pthread_setspecific(g_threadStateKey, &g_threadStateDestroyedMarker) != 0) + { + EEPOLICY_HANDLE_FATAL_ERROR(COR_E_EXECUTIONENGINE); + } +} + +// POSIX clears a key before invoking its destructor, and key destructor order is unspecified. +// Restore the marker so managed re-entry from later destructors can observe the destroyed state. +static void ThreadStateKeyDestructor(void* state) +{ + if (state == &g_threadStateDestroyedMarker) + { + SetThreadStateDestroyed(); + } +} + +static void InitThreadStateKey() +{ + if (pthread_key_create(&g_threadStateKey, ThreadStateKeyDestructor) != 0) + { + COMPlusThrowOM(); + } +} + +static bool HasThreadStateBeenDestroyed() +{ + return pthread_getspecific(g_threadStateKey) == &g_threadStateDestroyedMarker; +} #else static PLATFORM_THREAD_LOCAL bool t_threadStateDestroyed; @@ -1790,6 +1833,7 @@ static bool HasThreadStateBeenDestroyed() { return t_threadStateDestroyed; } +#endif struct TlsDestructionMonitor { @@ -1807,7 +1851,11 @@ struct TlsDestructionMonitor RuntimeThreadShutdown(GetThreadNULLOk()); } +#if defined(TARGET_UNIX) && !defined(TARGET_WASM) + SetThreadStateDestroyed(); +#else t_threadStateDestroyed = true; +#endif } }; From 5d4d049f1f7cc434d5b516e7ac517d70fd8085ea Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Thu, 20 Aug 2026 15:13:49 -0700 Subject: [PATCH 05/21] Abort instead of assert on unixes --- src/coreclr/vm/ceemain.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/coreclr/vm/ceemain.cpp b/src/coreclr/vm/ceemain.cpp index 887e8d759c46d4..295690a0aca4d9 100644 --- a/src/coreclr/vm/ceemain.cpp +++ b/src/coreclr/vm/ceemain.cpp @@ -1877,6 +1877,10 @@ void CheckThreadStateNotDestroyed() return; } +#if defined(TARGET_UNIX) && !defined(TARGET_WASM) + minipal_log_write_error("Attempt to execute managed code after the .NET runtime thread state has been destroyed.\n"); + abort(); +#else // Managed C++ may run managed code in DllMain (e.g. during DLL_PROCESS_DETACH to run global destructors). This is // not supported and unreliable. Historically, it happened to work most of the time. For backward compatibility, // suppress this assert in release builds if we have encountered any mixed mode binaries. @@ -1888,6 +1892,7 @@ void CheckThreadStateNotDestroyed() { _ASSERTE_ALL_BUILDS(!"Attempt to execute managed code after the .NET runtime thread state has been destroyed."); } +#endif } #ifdef DEBUGGING_SUPPORTED From 8ed158279ba47c591fd3d977befa2784fa633697 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Mon, 24 Aug 2026 12:10:01 -0700 Subject: [PATCH 06/21] Directly abort, avoid going through PAL's abort --- .../crashreport/inproccrashreportwatchdog.cpp | 19 +++--------------- src/coreclr/pal/inc/pal.h | 12 +++++++++++ src/coreclr/pal/src/arch/wasm/stubs.cpp | 6 ++++++ src/coreclr/pal/src/exception/signal.cpp | 20 ++++++++++++++++++- src/coreclr/vm/ceemain.cpp | 2 +- 5 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreportwatchdog.cpp b/src/coreclr/debug/crashreport/inproccrashreportwatchdog.cpp index 7d872e18c85d4f..6e41be5c279f10 100644 --- a/src/coreclr/debug/crashreport/inproccrashreportwatchdog.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreportwatchdog.cpp @@ -12,17 +12,13 @@ #include #include #include -#include #include #include #include #include #include -#include "pal/signal.hpp" - static constexpr int CRASH_REPORT_WATCHDOG_SECONDS_TO_MILLISECONDS = 1000; -static constexpr int CRASH_REPORT_WATCHDOG_SIGNAL_EXIT_CODE_OFFSET = 128; pthread_mutex_t CrashReportWatchdog::s_initializationMutex = PTHREAD_MUTEX_INITIALIZER; CrashReportWatchdog* CrashReportWatchdog::s_instance; @@ -350,21 +346,12 @@ CrashReportWatchdog::GetRemainingTimeoutMs(int64_t deadlineMs) return static_cast(remainingMs); } -// Terminate from the watchdog thread after restoring the original SIGABRT -// handler, matching the normal post-crash-report abort path without trying to -// create another crash report from the watchdog thread. +// The watchdog did not observe crash-report completion, so terminate without +// entering crash reporting again. void CrashReportWatchdog::Abort() { - SEHCleanupSignals(false /* isChildProcess */); - - sigset_t signalSet; - sigemptyset(&signalSet); - sigaddset(&signalSet, SIGABRT); - (void)pthread_sigmask(SIG_UNBLOCK, &signalSet, nullptr); - - abort(); - _exit(CRASH_REPORT_WATCHDOG_SIGNAL_EXIT_CODE_OFFSET + SIGABRT); + PAL_Abort(); } CrashReportWatchdogScope::CrashReportWatchdogScope() diff --git a/src/coreclr/pal/inc/pal.h b/src/coreclr/pal/inc/pal.h index 0ff5b7435ecab0..525c64775dd494 100644 --- a/src/coreclr/pal/inc/pal.h +++ b/src/coreclr/pal/inc/pal.h @@ -231,6 +231,18 @@ PALAPI PAL_TerminateEx( int exitCode); +/// +/// This function aborts the process without running PAL shutdown or crash reporting. +/// On signal-based platforms, it restores the pre-PAL SIGABRT disposition and +/// unblocks SIGABRT on the current thread before aborting. +/// +PALIMPORT +PAL_NORETURN +void +PALAPI +PAL_Abort( + void); + typedef VOID (*PSHUTDOWN_CALLBACK)(bool isExecutingOnAltStack); PALIMPORT diff --git a/src/coreclr/pal/src/arch/wasm/stubs.cpp b/src/coreclr/pal/src/arch/wasm/stubs.cpp index 667b5eea786d93..7efaf3caacf4df 100644 --- a/src/coreclr/pal/src/arch/wasm/stubs.cpp +++ b/src/coreclr/pal/src/arch/wasm/stubs.cpp @@ -277,6 +277,12 @@ void SEHCleanupSignals(bool isChildProcess) (void)isChildProcess; } +PAL_NORETURN VOID PALAPI +PAL_Abort(void) +{ + abort(); +} + void UnmaskActivationSignal() { } diff --git a/src/coreclr/pal/src/exception/signal.cpp b/src/coreclr/pal/src/exception/signal.cpp index 74fa88db04e409..c660bf30325381 100644 --- a/src/coreclr/pal/src/exception/signal.cpp +++ b/src/coreclr/pal/src/exception/signal.cpp @@ -28,6 +28,8 @@ SET_DEFAULT_DEBUG_CHANNEL(EXCEPT); // some headers have code with asserts, so do #include #include #include +#include +#include #if !HAVE_MACH_EXCEPTIONS #include "pal/init.h" @@ -37,7 +39,6 @@ SET_DEFAULT_DEBUG_CHANNEL(EXCEPT); // some headers have code with asserts, so do #include #include -#include #include #if HAVE_SYS_UCONTEXT_H @@ -127,6 +128,23 @@ const int StackOverflowFlag = 0x40000000; /* public function definitions ************************************************/ +PAL_NORETURN +VOID +PALAPI +PAL_Abort( + void) +{ + SEHCleanupSignals(false /* isChildProcess */); + + sigset_t signalSet; + sigemptyset(&signalSet); + sigaddset(&signalSet, SIGABRT); + (void)pthread_sigmask(SIG_UNBLOCK, &signalSet, nullptr); + + abort(); + _exit(128 + SIGABRT); +} + /*++ Function: PAL_EnableCrashReportBeforeSignalChaining diff --git a/src/coreclr/vm/ceemain.cpp b/src/coreclr/vm/ceemain.cpp index 818ce0cf740284..68e96fb832202c 100644 --- a/src/coreclr/vm/ceemain.cpp +++ b/src/coreclr/vm/ceemain.cpp @@ -1879,7 +1879,7 @@ void CheckThreadStateNotDestroyed() #if defined(TARGET_UNIX) && !defined(TARGET_WASM) minipal_log_write_error("Attempt to execute managed code after the .NET runtime thread state has been destroyed.\n"); - abort(); + PAL_Abort(); #else // Managed C++ may run managed code in DllMain (e.g. during DLL_PROCESS_DETACH to run global destructors). This is // not supported and unreliable. Historically, it happened to work most of the time. For backward compatibility, From 82ab932bd296db3a99be457525aa174c53f10e2c Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Mon, 24 Aug 2026 12:38:33 -0700 Subject: [PATCH 07/21] Update comment --- src/coreclr/pal/inc/pal.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/coreclr/pal/inc/pal.h b/src/coreclr/pal/inc/pal.h index 525c64775dd494..f0663fcbdc4e6a 100644 --- a/src/coreclr/pal/inc/pal.h +++ b/src/coreclr/pal/inc/pal.h @@ -232,9 +232,9 @@ PAL_TerminateEx( int exitCode); /// -/// This function aborts the process without running PAL shutdown or crash reporting. -/// On signal-based platforms, it restores the pre-PAL SIGABRT disposition and -/// unblocks SIGABRT on the current thread before aborting. +/// Aborts the process without invoking PAL shutdown or PAL crash reporting. +/// On POSIX signal-based platforms, restores the SIGABRT disposition saved +/// by PAL and unblocks SIGABRT on the current thread before calling abort(). /// PALIMPORT PAL_NORETURN From 87e665cd0818dba8e3cb25f805eb6b1b8ec7db72 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Thu, 3 Sep 2026 17:45:27 -0700 Subject: [PATCH 08/21] Revert "Update comment" This reverts commit 82ab932bd296db3a99be457525aa174c53f10e2c. --- src/coreclr/pal/inc/pal.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/coreclr/pal/inc/pal.h b/src/coreclr/pal/inc/pal.h index f0663fcbdc4e6a..525c64775dd494 100644 --- a/src/coreclr/pal/inc/pal.h +++ b/src/coreclr/pal/inc/pal.h @@ -232,9 +232,9 @@ PAL_TerminateEx( int exitCode); /// -/// Aborts the process without invoking PAL shutdown or PAL crash reporting. -/// On POSIX signal-based platforms, restores the SIGABRT disposition saved -/// by PAL and unblocks SIGABRT on the current thread before calling abort(). +/// This function aborts the process without running PAL shutdown or crash reporting. +/// On signal-based platforms, it restores the pre-PAL SIGABRT disposition and +/// unblocks SIGABRT on the current thread before aborting. /// PALIMPORT PAL_NORETURN From e707a2adb8fbd47550d495378c1f5a88779804b5 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Thu, 3 Sep 2026 17:45:35 -0700 Subject: [PATCH 09/21] Revert "Directly abort, avoid going through PAL's abort" This reverts commit 8ed158279ba47c591fd3d977befa2784fa633697. --- .../crashreport/inproccrashreportwatchdog.cpp | 19 +++++++++++++++--- src/coreclr/pal/inc/pal.h | 12 ----------- src/coreclr/pal/src/arch/wasm/stubs.cpp | 6 ------ src/coreclr/pal/src/exception/signal.cpp | 20 +------------------ src/coreclr/vm/ceemain.cpp | 2 +- 5 files changed, 18 insertions(+), 41 deletions(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreportwatchdog.cpp b/src/coreclr/debug/crashreport/inproccrashreportwatchdog.cpp index 6e41be5c279f10..7d872e18c85d4f 100644 --- a/src/coreclr/debug/crashreport/inproccrashreportwatchdog.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreportwatchdog.cpp @@ -12,13 +12,17 @@ #include #include #include +#include #include #include #include #include #include +#include "pal/signal.hpp" + static constexpr int CRASH_REPORT_WATCHDOG_SECONDS_TO_MILLISECONDS = 1000; +static constexpr int CRASH_REPORT_WATCHDOG_SIGNAL_EXIT_CODE_OFFSET = 128; pthread_mutex_t CrashReportWatchdog::s_initializationMutex = PTHREAD_MUTEX_INITIALIZER; CrashReportWatchdog* CrashReportWatchdog::s_instance; @@ -346,12 +350,21 @@ CrashReportWatchdog::GetRemainingTimeoutMs(int64_t deadlineMs) return static_cast(remainingMs); } -// The watchdog did not observe crash-report completion, so terminate without -// entering crash reporting again. +// Terminate from the watchdog thread after restoring the original SIGABRT +// handler, matching the normal post-crash-report abort path without trying to +// create another crash report from the watchdog thread. void CrashReportWatchdog::Abort() { - PAL_Abort(); + SEHCleanupSignals(false /* isChildProcess */); + + sigset_t signalSet; + sigemptyset(&signalSet); + sigaddset(&signalSet, SIGABRT); + (void)pthread_sigmask(SIG_UNBLOCK, &signalSet, nullptr); + + abort(); + _exit(CRASH_REPORT_WATCHDOG_SIGNAL_EXIT_CODE_OFFSET + SIGABRT); } CrashReportWatchdogScope::CrashReportWatchdogScope() diff --git a/src/coreclr/pal/inc/pal.h b/src/coreclr/pal/inc/pal.h index 525c64775dd494..0ff5b7435ecab0 100644 --- a/src/coreclr/pal/inc/pal.h +++ b/src/coreclr/pal/inc/pal.h @@ -231,18 +231,6 @@ PALAPI PAL_TerminateEx( int exitCode); -/// -/// This function aborts the process without running PAL shutdown or crash reporting. -/// On signal-based platforms, it restores the pre-PAL SIGABRT disposition and -/// unblocks SIGABRT on the current thread before aborting. -/// -PALIMPORT -PAL_NORETURN -void -PALAPI -PAL_Abort( - void); - typedef VOID (*PSHUTDOWN_CALLBACK)(bool isExecutingOnAltStack); PALIMPORT diff --git a/src/coreclr/pal/src/arch/wasm/stubs.cpp b/src/coreclr/pal/src/arch/wasm/stubs.cpp index 7efaf3caacf4df..667b5eea786d93 100644 --- a/src/coreclr/pal/src/arch/wasm/stubs.cpp +++ b/src/coreclr/pal/src/arch/wasm/stubs.cpp @@ -277,12 +277,6 @@ void SEHCleanupSignals(bool isChildProcess) (void)isChildProcess; } -PAL_NORETURN VOID PALAPI -PAL_Abort(void) -{ - abort(); -} - void UnmaskActivationSignal() { } diff --git a/src/coreclr/pal/src/exception/signal.cpp b/src/coreclr/pal/src/exception/signal.cpp index c660bf30325381..74fa88db04e409 100644 --- a/src/coreclr/pal/src/exception/signal.cpp +++ b/src/coreclr/pal/src/exception/signal.cpp @@ -28,8 +28,6 @@ SET_DEFAULT_DEBUG_CHANNEL(EXCEPT); // some headers have code with asserts, so do #include #include #include -#include -#include #if !HAVE_MACH_EXCEPTIONS #include "pal/init.h" @@ -39,6 +37,7 @@ SET_DEFAULT_DEBUG_CHANNEL(EXCEPT); // some headers have code with asserts, so do #include #include +#include #include #if HAVE_SYS_UCONTEXT_H @@ -128,23 +127,6 @@ const int StackOverflowFlag = 0x40000000; /* public function definitions ************************************************/ -PAL_NORETURN -VOID -PALAPI -PAL_Abort( - void) -{ - SEHCleanupSignals(false /* isChildProcess */); - - sigset_t signalSet; - sigemptyset(&signalSet); - sigaddset(&signalSet, SIGABRT); - (void)pthread_sigmask(SIG_UNBLOCK, &signalSet, nullptr); - - abort(); - _exit(128 + SIGABRT); -} - /*++ Function: PAL_EnableCrashReportBeforeSignalChaining diff --git a/src/coreclr/vm/ceemain.cpp b/src/coreclr/vm/ceemain.cpp index 68e96fb832202c..818ce0cf740284 100644 --- a/src/coreclr/vm/ceemain.cpp +++ b/src/coreclr/vm/ceemain.cpp @@ -1879,7 +1879,7 @@ void CheckThreadStateNotDestroyed() #if defined(TARGET_UNIX) && !defined(TARGET_WASM) minipal_log_write_error("Attempt to execute managed code after the .NET runtime thread state has been destroyed.\n"); - PAL_Abort(); + abort(); #else // Managed C++ may run managed code in DllMain (e.g. during DLL_PROCESS_DETACH to run global destructors). This is // not supported and unreliable. Historically, it happened to work most of the time. For backward compatibility, From 647a2cb680cad1c2ef05fe999c56760455c33c96 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Fri, 4 Sep 2026 16:55:26 -0700 Subject: [PATCH 10/21] Avoid abort, use kill/_exit --- src/coreclr/pal/inc/pal.h | 15 +++++++++++++++ src/coreclr/pal/src/exception/signal.cpp | 23 ++++++++++++++++++++++- src/coreclr/vm/ceemain.cpp | 2 +- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/coreclr/pal/inc/pal.h b/src/coreclr/pal/inc/pal.h index 0ff5b7435ecab0..9f72adab5000e2 100644 --- a/src/coreclr/pal/inc/pal.h +++ b/src/coreclr/pal/inc/pal.h @@ -231,6 +231,21 @@ PALAPI PAL_TerminateEx( int exitCode); +#if !defined(TARGET_WASM) +/// +/// Aborts the process without invoking PAL shutdown or PAL crash reporting. +/// Restores the SIGABRT disposition saved by PAL, unblocks SIGABRT on the +/// current thread, and sends SIGABRT to the process, falling back to an +/// immediate exit if the signal does not terminate it. +/// +PALIMPORT +PAL_NORETURN +void +PALAPI +PAL_Abort( + void); +#endif // !TARGET_WASM + typedef VOID (*PSHUTDOWN_CALLBACK)(bool isExecutingOnAltStack); PALIMPORT diff --git a/src/coreclr/pal/src/exception/signal.cpp b/src/coreclr/pal/src/exception/signal.cpp index 74fa88db04e409..6d942a323144ba 100644 --- a/src/coreclr/pal/src/exception/signal.cpp +++ b/src/coreclr/pal/src/exception/signal.cpp @@ -28,6 +28,7 @@ SET_DEFAULT_DEBUG_CHANNEL(EXCEPT); // some headers have code with asserts, so do #include #include #include +#include #if !HAVE_MACH_EXCEPTIONS #include "pal/init.h" @@ -37,7 +38,6 @@ SET_DEFAULT_DEBUG_CHANNEL(EXCEPT); // some headers have code with asserts, so do #include #include -#include #include #if HAVE_SYS_UCONTEXT_H @@ -127,6 +127,27 @@ const int StackOverflowFlag = 0x40000000; /* public function definitions ************************************************/ +#if !defined(TARGET_WASM) +PAL_NORETURN +void +PALAPI +PAL_Abort( + void) +{ + SEHCleanupSignals(false /* isChildProcess */); + + sigset_t signalSet; + sigemptyset(&signalSet); + sigaddset(&signalSet, SIGABRT); + (void)pthread_sigmask(SIG_UNBLOCK, &signalSet, nullptr); + + // Avoid abort(), which may run cleanup on darwin's libc implementation. + // https://github.com/apple-oss-distributions/Libc/blob/af11da5ca9d527ea2f48bb7efbd0f0f2a4ea4812/stdlib/FreeBSD/abort.c#L64-L131 + (void)kill(getpid(), SIGABRT); + _exit(128 + SIGABRT); +} +#endif // !TARGET_WASM + /*++ Function: PAL_EnableCrashReportBeforeSignalChaining diff --git a/src/coreclr/vm/ceemain.cpp b/src/coreclr/vm/ceemain.cpp index 818ce0cf740284..68e96fb832202c 100644 --- a/src/coreclr/vm/ceemain.cpp +++ b/src/coreclr/vm/ceemain.cpp @@ -1879,7 +1879,7 @@ void CheckThreadStateNotDestroyed() #if defined(TARGET_UNIX) && !defined(TARGET_WASM) minipal_log_write_error("Attempt to execute managed code after the .NET runtime thread state has been destroyed.\n"); - abort(); + PAL_Abort(); #else // Managed C++ may run managed code in DllMain (e.g. during DLL_PROCESS_DETACH to run global destructors). This is // not supported and unreliable. Historically, it happened to work most of the time. For backward compatibility, From 5dcbf4d51a0f60207bd72f8b2bf7935fd8e8153e Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Tue, 15 Sep 2026 06:11:12 -0700 Subject: [PATCH 11/21] Update test --- .../threading/ThreadStateDestroyed/ThreadStateDestroyed.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs b/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs index 5efab4305a863b..4ee732a1ca596d 100644 --- a/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs +++ b/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs @@ -22,8 +22,7 @@ public static unsafe class ThreadStateDestroyed private const int SigAbrtExitCode = 128 + 6; private static readonly TimeSpan s_subprocessTimeout = TimeSpan.FromSeconds(60); - // We need to check both the exit code and the message since the runtime may fail fast - // with the same exit code for other reasons. + // In non-Release builds, also check the message to distinguish this failure from other crashes. private const string ExpectedMessage = "Attempt to execute managed code after the .NET runtime thread state has been destroyed."; private const string SecondCallbackMarker = "[managed] callback #2"; @@ -106,13 +105,13 @@ private static int RunController() return Fail; } - if (!output.Contains(ExpectedMessage)) + if (!TestLibrary.CoreClrConfigurationDetection.IsReleaseRuntime && !output.Contains(ExpectedMessage)) { Console.WriteLine($"The subprocess terminated for some other reason. Expected to find: {ExpectedMessage}"); return Fail; } - // RaiseFailFastException on Windows, abort on Unix, for both CoreCLR and NativeAOT. + // RaiseFailFastException on Windows, abort on Unix. int expectedExitCode = OperatingSystem.IsWindows() ? StatusFailFastException : SigAbrtExitCode; if (exitCode != expectedExitCode) { From 6b4c146cae37d59ef84b8f5c31ea0215b625cbdb Mon Sep 17 00:00:00 2001 From: Eduardo Velarde <32459232+eduardo-vp@users.noreply.github.com> Date: Tue, 15 Sep 2026 06:40:29 -0700 Subject: [PATCH 12/21] Update CMakeLists and ThreadStateDestroyed logic Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../baseservices/threading/ThreadStateDestroyed/CMakeLists.txt | 2 +- .../threading/ThreadStateDestroyed/ThreadStateDestroyed.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/baseservices/threading/ThreadStateDestroyed/CMakeLists.txt b/src/tests/baseservices/threading/ThreadStateDestroyed/CMakeLists.txt index 41866085f2b61b..d64070b3f9bbe7 100644 --- a/src/tests/baseservices/threading/ThreadStateDestroyed/CMakeLists.txt +++ b/src/tests/baseservices/threading/ThreadStateDestroyed/CMakeLists.txt @@ -8,6 +8,6 @@ if(NOT CLR_CMAKE_HOST_WIN32) add_compile_options(-pthread) endif() -add_library (ThreadStateDestroyedNative SHARED ${SOURCES}) +add_library (ThreadStateDestroyedNative ${TEST_LIB_TYPE} ${SOURCES}) install (TARGETS ThreadStateDestroyedNative DESTINATION bin) diff --git a/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs b/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs index 4ee732a1ca596d..dac4f2e282f8ef 100644 --- a/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs +++ b/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs @@ -105,7 +105,7 @@ private static int RunController() return Fail; } - if (!TestLibrary.CoreClrConfigurationDetection.IsReleaseRuntime && !output.Contains(ExpectedMessage)) + if ((!OperatingSystem.IsWindows() || !TestLibrary.CoreClrConfigurationDetection.IsReleaseRuntime) && !output.Contains(ExpectedMessage)) { Console.WriteLine($"The subprocess terminated for some other reason. Expected to find: {ExpectedMessage}"); return Fail; From 46414b14ede6d043076a231881a2c61a7b22cef6 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde <32459232+eduardo-vp@users.noreply.github.com> Date: Tue, 15 Sep 2026 12:42:00 -0700 Subject: [PATCH 13/21] Make CallbackFn's calling convention explicit Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../ThreadStateDestroyed/ThreadStateDestroyedNative.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyedNative.cpp b/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyedNative.cpp index 9323af86d902bc..813a6710217a46 100644 --- a/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyedNative.cpp +++ b/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyedNative.cpp @@ -3,7 +3,7 @@ #include -typedef void (*CallbackFn)(); +typedef void (STDMETHODCALLTYPE *CallbackFn)(); static CallbackFn s_callback; From fc6c557839993373e3ecb9dd871ced8f66bbc0de Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Tue, 15 Sep 2026 13:28:41 -0700 Subject: [PATCH 14/21] Make it NAOT incompatible, enable it in a separate PR --- .../threading/ThreadStateDestroyed/ThreadStateDestroyed.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.csproj b/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.csproj index ec3399fd1f8d7e..bd6b31acc80d27 100644 --- a/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.csproj +++ b/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.csproj @@ -5,6 +5,7 @@ false true true + true true true From 151f475044538e643cd004b0d99c17e500bf31c8 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Tue, 15 Sep 2026 17:05:07 -0700 Subject: [PATCH 15/21] Use a pthread key on apple only --- src/coreclr/vm/ceemain.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/coreclr/vm/ceemain.cpp b/src/coreclr/vm/ceemain.cpp index 222f60eed78fe7..952d302e401b98 100644 --- a/src/coreclr/vm/ceemain.cpp +++ b/src/coreclr/vm/ceemain.cpp @@ -223,7 +223,7 @@ HRESULT EEStartup(); static void InitializeGarbageCollector(); -#if defined(TARGET_UNIX) && !defined(TARGET_WASM) +#ifdef TARGET_APPLE static void InitThreadStateKey(); #endif @@ -713,7 +713,7 @@ void EEStartupHelper() InitThreadManager(); STRESS_LOG0(LF_STARTUP, LL_ALWAYS, "Returned successfully from InitThreadManager"); -#if defined(TARGET_UNIX) && !defined(TARGET_WASM) +#ifdef TARGET_APPLE InitThreadStateKey(); #endif @@ -1794,7 +1794,7 @@ void EnsureTlsDestructionMonitor() } #else -#if defined(TARGET_UNIX) && !defined(TARGET_WASM) +#ifdef TARGET_APPLE static pthread_key_t g_threadStateKey; static byte g_threadStateDestroyedMarker; @@ -1853,7 +1853,7 @@ struct TlsDestructionMonitor RuntimeThreadShutdown(GetThreadNULLOk()); } -#if defined(TARGET_UNIX) && !defined(TARGET_WASM) +#ifdef TARGET_APPLE SetThreadStateDestroyed(); #else t_threadStateDestroyed = true; From 4d5272a7b9c295cf20dd1320322c00607eaff164 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Tue, 15 Sep 2026 22:07:43 -0700 Subject: [PATCH 16/21] Use _ASSERTE_ALL_BUILDS instead of aborting. It did work on my arm64 machine, need to check CI --- src/coreclr/pal/inc/pal.h | 15 ------------ src/coreclr/pal/src/exception/signal.cpp | 23 +------------------ src/coreclr/vm/ceemain.cpp | 5 ---- .../ThreadStateDestroyed.cs | 10 -------- 4 files changed, 1 insertion(+), 52 deletions(-) diff --git a/src/coreclr/pal/inc/pal.h b/src/coreclr/pal/inc/pal.h index ecb51c408ffbf7..a4f4e64848bd47 100644 --- a/src/coreclr/pal/inc/pal.h +++ b/src/coreclr/pal/inc/pal.h @@ -231,21 +231,6 @@ PALAPI PAL_TerminateEx( int exitCode); -#if !defined(TARGET_WASM) -/// -/// Aborts the process without invoking PAL shutdown or PAL crash reporting. -/// Restores the SIGABRT disposition saved by PAL, unblocks SIGABRT on the -/// current thread, and sends SIGABRT to the process, falling back to an -/// immediate exit if the signal does not terminate it. -/// -PALIMPORT -PAL_NORETURN -void -PALAPI -PAL_Abort( - void); -#endif // !TARGET_WASM - typedef VOID (*PSHUTDOWN_CALLBACK)(bool isExecutingOnAltStack); PALIMPORT diff --git a/src/coreclr/pal/src/exception/signal.cpp b/src/coreclr/pal/src/exception/signal.cpp index e17badb1285316..ed2b72d69c6ce1 100644 --- a/src/coreclr/pal/src/exception/signal.cpp +++ b/src/coreclr/pal/src/exception/signal.cpp @@ -28,7 +28,6 @@ SET_DEFAULT_DEBUG_CHANNEL(EXCEPT); // some headers have code with asserts, so do #include #include #include -#include #if !HAVE_MACH_EXCEPTIONS #include "pal/init.h" @@ -38,6 +37,7 @@ SET_DEFAULT_DEBUG_CHANNEL(EXCEPT); // some headers have code with asserts, so do #include #include +#include #include #if HAVE_SYS_UCONTEXT_H @@ -127,27 +127,6 @@ const int StackOverflowFlag = 0x40000000; /* public function definitions ************************************************/ -#if !defined(TARGET_WASM) -PAL_NORETURN -void -PALAPI -PAL_Abort( - void) -{ - SEHCleanupSignals(false /* isChildProcess */); - - sigset_t signalSet; - sigemptyset(&signalSet); - sigaddset(&signalSet, SIGABRT); - (void)pthread_sigmask(SIG_UNBLOCK, &signalSet, nullptr); - - // Avoid abort(), which may run cleanup on darwin's libc implementation. - // https://github.com/apple-oss-distributions/Libc/blob/af11da5ca9d527ea2f48bb7efbd0f0f2a4ea4812/stdlib/FreeBSD/abort.c#L64-L131 - (void)kill(getpid(), SIGABRT); - _exit(128 + SIGABRT); -} -#endif // !TARGET_WASM - /*++ Function: PAL_EnableCrashReportBeforeSignalChaining diff --git a/src/coreclr/vm/ceemain.cpp b/src/coreclr/vm/ceemain.cpp index 952d302e401b98..1d03a7457f07d5 100644 --- a/src/coreclr/vm/ceemain.cpp +++ b/src/coreclr/vm/ceemain.cpp @@ -1879,10 +1879,6 @@ void CheckThreadStateNotDestroyed() return; } -#if defined(TARGET_UNIX) && !defined(TARGET_WASM) - minipal_log_write_error("Attempt to execute managed code after the .NET runtime thread state has been destroyed.\n"); - PAL_Abort(); -#else // Managed C++ may run managed code in DllMain (e.g. during DLL_PROCESS_DETACH to run global destructors). This is // not supported and unreliable. Historically, it happened to work most of the time. For backward compatibility, // suppress this assert in release builds if we have encountered any mixed mode binaries. @@ -1894,7 +1890,6 @@ void CheckThreadStateNotDestroyed() { _ASSERTE_ALL_BUILDS(!"Attempt to execute managed code after the .NET runtime thread state has been destroyed."); } -#endif } #ifdef DEBUGGING_SUPPORTED diff --git a/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs b/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs index dac4f2e282f8ef..1ec9298d5fb54c 100644 --- a/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs +++ b/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs @@ -18,8 +18,6 @@ public static unsafe class ThreadStateDestroyed private const string NativeLib = "ThreadStateDestroyedNative"; private const string RunScenarioArg = "--run-scenario"; - private const int StatusFailFastException = unchecked((int)0xC0000602); - private const int SigAbrtExitCode = 128 + 6; private static readonly TimeSpan s_subprocessTimeout = TimeSpan.FromSeconds(60); // In non-Release builds, also check the message to distinguish this failure from other crashes. @@ -111,14 +109,6 @@ private static int RunController() return Fail; } - // RaiseFailFastException on Windows, abort on Unix. - int expectedExitCode = OperatingSystem.IsWindows() ? StatusFailFastException : SigAbrtExitCode; - if (exitCode != expectedExitCode) - { - Console.WriteLine($"Expected the subprocess to fail fast with {expectedExitCode}."); - return Fail; - } - return Pass; } } From a2e339ddd32d880d9e74f9d9c2a7bd01eb1aab4d Mon Sep 17 00:00:00 2001 From: Eduardo Velarde <32459232+eduardo-vp@users.noreply.github.com> Date: Tue, 15 Sep 2026 22:25:57 -0700 Subject: [PATCH 17/21] Enhance thread destruction test validation Add additional checks for thread destruction test scenarios. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../threading/ThreadStateDestroyed/ThreadStateDestroyed.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs b/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs index 1ec9298d5fb54c..6d060189c7ce10 100644 --- a/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs +++ b/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs @@ -103,6 +103,13 @@ private static int RunController() return Fail; } + if (output.Contains("[managed] The runtime did not fail fast.") || + !output.Contains("[native] invoking managed callback from the thread destruction callback")) + { + Console.WriteLine("The test scenario did not reach the expected thread-destruction failure."); + return Fail; + } + if ((!OperatingSystem.IsWindows() || !TestLibrary.CoreClrConfigurationDetection.IsReleaseRuntime) && !output.Contains(ExpectedMessage)) { Console.WriteLine($"The subprocess terminated for some other reason. Expected to find: {ExpectedMessage}"); From e5a4a22fe02b419ffc8f64de364c9771206240f4 Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Wed, 16 Sep 2026 01:37:04 -0700 Subject: [PATCH 18/21] Disable OS core dump, looks like it was causing the test to time out --- .../threading/ThreadStateDestroyed/ThreadStateDestroyed.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs b/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs index 6d060189c7ce10..e44643809bc726 100644 --- a/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs +++ b/src/tests/baseservices/threading/ThreadStateDestroyed/ThreadStateDestroyed.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Runtime.InteropServices; using System.Threading; +using TestLibrary; // A native library runs managed code on an OS thread, then runs managed code again from a // thread-destruction callback that fires after the runtime has already torn down its per-thread @@ -59,6 +60,9 @@ public static int Main(string[] args) private static int RunScenario() { + // Ensure that the OS doesn't generate core dump for this intentionally crashing process + Utilities.DisableOSCoreDump(); + RunCallbackOnThreadAndDuringItsDestruction(&Callback); Console.WriteLine("[managed] The runtime did not fail fast."); From e124be901b23105abdfa6ad43bdf84ed41139f9b Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Wed, 16 Sep 2026 11:50:14 -0700 Subject: [PATCH 19/21] Use _ASSERTE_ALL_BUILDS in SetThreadStateDestroyed --- src/coreclr/vm/ceemain.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/vm/ceemain.cpp b/src/coreclr/vm/ceemain.cpp index 3e80a8d6d54cab..7569ad21a85229 100644 --- a/src/coreclr/vm/ceemain.cpp +++ b/src/coreclr/vm/ceemain.cpp @@ -1806,7 +1806,7 @@ static void SetThreadStateDestroyed() { if (pthread_setspecific(g_threadStateKey, &g_threadStateDestroyedMarker) != 0) { - EEPOLICY_HANDLE_FATAL_ERROR(COR_E_EXECUTIONENGINE); + _ASSERTE_ALL_BUILDS(!"Failed to preserve the destroyed runtime thread state."); } } From b4751fbbbd2ab421e01235c31f4df60dcbf2894e Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Wed, 16 Sep 2026 18:02:18 -0700 Subject: [PATCH 20/21] Remove one ifdef --- src/coreclr/vm/ceemain.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/coreclr/vm/ceemain.cpp b/src/coreclr/vm/ceemain.cpp index 7569ad21a85229..856d23d62bc188 100644 --- a/src/coreclr/vm/ceemain.cpp +++ b/src/coreclr/vm/ceemain.cpp @@ -1839,6 +1839,11 @@ static bool HasThreadStateBeenDestroyed() { return t_threadStateDestroyed; } + +static void SetThreadStateDestroyed() +{ + t_threadStateDestroyed = true; +} #endif struct TlsDestructionMonitor @@ -1857,11 +1862,7 @@ struct TlsDestructionMonitor RuntimeThreadShutdown(GetThreadNULLOk()); } -#ifdef TARGET_APPLE SetThreadStateDestroyed(); -#else - t_threadStateDestroyed = true; -#endif } }; From 440db39e029bc971631d013a6867886e89fe634d Mon Sep 17 00:00:00 2001 From: Eduardo Velarde Date: Wed, 16 Sep 2026 18:15:03 -0700 Subject: [PATCH 21/21] Remove second check in OsAttachThread, add an assertion --- src/coreclr/vm/ceemain.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/coreclr/vm/ceemain.cpp b/src/coreclr/vm/ceemain.cpp index 856d23d62bc188..dbf5d2602faa06 100644 --- a/src/coreclr/vm/ceemain.cpp +++ b/src/coreclr/vm/ceemain.cpp @@ -1746,14 +1746,12 @@ void InitFlsSlot() } // Register the thread with OS to be notified when thread is about to be destroyed -// It fails fast if a different thread was already registered with the current fiber. // Parameters: // thread - thread to attach static void OsAttachThread(void* thread) { _ASSERTE(g_flsIndex != FLS_OUT_OF_INDEXES); - - CheckThreadStateNotDestroyed(); + _ASSERTE(t_flsState == FLS_STATE_CLEAR); t_flsState = FLS_STATE_ARMED;