From faa1887cc682657e5b1a92f485d2b904bc80e078 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:19:37 -0400 Subject: [PATCH 1/7] Lazy LTO sync: set m_LastThrownObjectHandle in PopExInfos instead of eagerly Stop eagerly synchronizing m_LastThrownObjectHandle with ExInfo::m_exception during active exception dispatch. Instead, set LTO lazily when the ExInfo is destroyed in PopExInfos. This simplifies the code and makes m_exception the sole source of truth during dispatch. Removed: SafeSetThrowables, SafeUpdateLastThrownObject, SyncManagedExceptionState, and the InternalUnhandledExceptionFilter re-sync block. Made SetLastThrownObject private; all external callers now use SafeSetLastThrownObject which handles OOM gracefully. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/comutilnative.cpp | 2 +- src/coreclr/vm/eepolicy.cpp | 5 +- src/coreclr/vm/excep.cpp | 16 ---- src/coreclr/vm/exceptionhandling.cpp | 4 - src/coreclr/vm/exinfo.cpp | 8 ++ src/coreclr/vm/threads.cpp | 107 +-------------------------- src/coreclr/vm/threads.h | 35 ++++++--- 7 files changed, 38 insertions(+), 139 deletions(-) diff --git a/src/coreclr/vm/comutilnative.cpp b/src/coreclr/vm/comutilnative.cpp index bcbb0c6d8bfd9c..53a9be085ba76e 100644 --- a/src/coreclr/vm/comutilnative.cpp +++ b/src/coreclr/vm/comutilnative.cpp @@ -1555,7 +1555,7 @@ extern "C" void QCALLTYPE Environment_FailFast(QCall::StackCrawlMarkHandle mark, // stash the user-provided exception object. this will be used as // the inner exception object to the FatalExecutionEngineException. if (exception.Get() != NULL) - pThread->SetLastThrownObject(exception.Get()); + pThread->SafeSetLastThrownObject(exception.Get()); EEPolicy::HandleFatalError(COR_E_FAILFAST, findCallerData.retAddress, message, NULL, errorSource, argExceptionString); diff --git a/src/coreclr/vm/eepolicy.cpp b/src/coreclr/vm/eepolicy.cpp index 6fcbaec5aaf61a..237e4bbfb0704e 100644 --- a/src/coreclr/vm/eepolicy.cpp +++ b/src/coreclr/vm/eepolicy.cpp @@ -623,7 +623,7 @@ void EEPolicy::LogFatalError(UINT exitCode, UINT_PTR address, LPCWSTR pszMessage EXCEPTIONREF curEx = (EXCEPTIONREF)ObjectFromHandle(ohException); curEx->SetInnerException(lto); } - pThread->SetLastThrownObject(ObjectFromHandle(ohException), TRUE); + pThread->SafeSetLastThrownObject(ObjectFromHandle(ohException), TRUE); } // If a managed debugger is already attached, and if that debugger is thinking it might be inclined to @@ -789,8 +789,7 @@ void DECLSPEC_NORETURN EEPolicy::HandleFatalStackOverflow(EXCEPTION_POINTERS *pE OBJECTHANDLE ohSO = CLRException::GetPreallocatedStackOverflowExceptionHandle(); if (ohSO != NULL) { - pThread->SafeSetThrowables(ObjectFromHandle(ohSO), - TRUE); + pThread->SafeSetLastThrownObject(ObjectFromHandle(ohSO), TRUE); } else { diff --git a/src/coreclr/vm/excep.cpp b/src/coreclr/vm/excep.cpp index a841feafc3aca4..92eb04e8160074 100644 --- a/src/coreclr/vm/excep.cpp +++ b/src/coreclr/vm/excep.cpp @@ -3995,22 +3995,6 @@ LONG InternalUnhandledExceptionFilter_Worker( LOG((LF_EH, LL_INFO100, "InternalUnhandledExceptionFilter_Worker: Not collecting bucket information as thread object does not exist\n")); } - // AppDomain.UnhandledException event could have thrown an exception that would have gone unhandled in managed code. - // The runtime swallows all such exceptions. Hence, if we are not using LastThrownObject and the current LastThrownObject - // is not the same as the one in active exception tracker (if available), then update the last thrown object. - if ((pParam->pThread != NULL) && (!useLastThrownObject)) - { - GCX_COOP_NO_DTOR(); - - OBJECTREF oThrowable = pParam->pThread->GetThrowable(); - if ((oThrowable != NULL) && (pParam->pThread->LastThrownObject() != oThrowable)) - { - pParam->pThread->SafeSetLastThrownObject(oThrowable); - LOG((LF_EH, LL_INFO100, "InternalUnhandledExceptionFilter_Worker: Resetting the LastThrownObject as it appears to have changed.\n")); - } - - GCX_COOP_NO_DTOR_END(); - } // Launch Watson and see if we want to debug the process // diff --git a/src/coreclr/vm/exceptionhandling.cpp b/src/coreclr/vm/exceptionhandling.cpp index 6473f4d069958c..0c8a51f29fce47 100644 --- a/src/coreclr/vm/exceptionhandling.cpp +++ b/src/coreclr/vm/exceptionhandling.cpp @@ -3189,9 +3189,6 @@ void CallCatchFunclet(OBJECTREF throwable, BYTE* pHandlerIP, REGDISPLAY* pvRegDi pThread->SafeSetLastThrownObject(NULL); } - // Sync managed exception state, for the managed thread, based upon any active exception tracker - pThread->SyncManagedExceptionState(false); - ExInfo::UpdateNonvolatileRegisters(pvRegDisplay->pCurrentContext, pvRegDisplay, FALSE); if (pHandlerIP != NULL) { @@ -3596,7 +3593,6 @@ static void NotifyExceptionPassStarted(StackFrameIterator *pThis, Thread *pThrea if (pExInfo->m_passNumber == 1) { GCX_COOP(); - pThread->SafeSetThrowables(pExInfo->m_exception); FirstChanceExceptionNotification(); EEToProfilerExceptionInterfaceWrapper::ExceptionThrown(pThread); } diff --git a/src/coreclr/vm/exinfo.cpp b/src/coreclr/vm/exinfo.cpp index 76d02424215de6..a3f1ba4e88605a 100644 --- a/src/coreclr/vm/exinfo.cpp +++ b/src/coreclr/vm/exinfo.cpp @@ -131,6 +131,14 @@ void ExInfo::PopExInfos(Thread *pThread, void *targetSp) } #endif // DEBUGGING_SUPPORTED + // Set LTO from the exception being destroyed so that post-ExInfo consumers + // (EX_CATCH via CLRLastThrownObjectException, ProcessCLRException bridging) + // can find the exception object after the ExInfo is gone. + if (pExInfo->m_exception != NULL) + { + pThread->SafeSetLastThrownObject(pExInfo->m_exception); + } + pExInfo->ReleaseResources(); pExInfo = (PTR_ExInfo)pExInfo->m_pPrevNestedInfo; } diff --git a/src/coreclr/vm/threads.cpp b/src/coreclr/vm/threads.cpp index 5e1f114f56d5a4..b1de36febcc491 100644 --- a/src/coreclr/vm/threads.cpp +++ b/src/coreclr/vm/threads.cpp @@ -2271,7 +2271,7 @@ Thread::~Thread() if (!IsAtProcessExit()) { // Destroy any handles that we're using to hold onto exception objects - SafeSetThrowables(NULL); + SafeSetLastThrownObject(NULL); DestroyShortWeakHandle(m_ExposedObject); DestroyStrongHandle(m_StrongHndToExposedObject); @@ -2635,7 +2635,7 @@ void Thread::OnThreadTerminate(BOOL holdingLock) GCX_COOP(); // Destroy the LastThrown handle (and anything that violates the above assert). - SafeSetThrowables(NULL); + SafeSetLastThrownObject(NULL); // Free loader allocator structures related to this thread FreeLoaderAllocatorHandlesForTLSData(this); @@ -3190,7 +3190,7 @@ void Thread::SetSOForLastThrownObject() // the handle for the throwable, and setting the last thrown object to the preallocated out of memory exception // instead. // -OBJECTREF Thread::SafeSetLastThrownObject(OBJECTREF throwable) +OBJECTREF Thread::SafeSetLastThrownObject(OBJECTREF throwable, BOOL isUnhandled) { CONTRACTL { @@ -3205,88 +3205,20 @@ OBJECTREF Thread::SafeSetLastThrownObject(OBJECTREF throwable) EX_TRY { - // Try to set the throwable. - SetLastThrownObject(throwable); + SetLastThrownObject(throwable, isUnhandled); } EX_CATCH { // If it didn't work, then set the last thrown object to the preallocated OOM exception, and return that // object instead of the original throwable. ret = CLRException::GetPreallocatedOutOfMemoryException(); - SetLastThrownObject(ret); - } - EX_END_CATCH - - return ret; -} - -// -// This is a nice wrapper for updating the last thrown object handle, which catches any exceptions caused by not -// being able to create the handle for the throwable, and falls back to the preallocated out of memory exception -// for the last thrown object instead. The throwable itself is stored directly in ExInfo::m_exception by managed -// EH code, so this helper only updates the last thrown object state. -// -OBJECTREF Thread::SafeSetThrowables(OBJECTREF throwable, - BOOL isUnhandled) -{ - CONTRACTL - { - NOTHROW; - GC_NOTRIGGER; - if (throwable == NULL) MODE_ANY; else MODE_COOPERATIVE; - } - CONTRACTL_END; - - // We return the original throwable if nothing goes wrong. - OBJECTREF ret = throwable; - - EX_TRY - { - // The exception object is stored directly in ExInfo::m_exception by managed EH code, - // so we only need to update the last thrown object handle here. - if (LastThrownObject() != throwable) - { - SetLastThrownObject(throwable); - } - - if (isUnhandled) - { - MarkLastThrownObjectUnhandled(); - } - } - EX_CATCH - { - // If we can't create a handle, set the last thrown object to the preallocated OOM exception. - ret = CLRException::GetPreallocatedOutOfMemoryException(); - SetLastThrownObject(ret, isUnhandled); } EX_END_CATCH - return ret; } -// This method will sync the managed exception state to be in sync with the topmost active exception -// for a given thread -void Thread::SyncManagedExceptionState(bool fIsDebuggerThread) -{ - CONTRACTL - { - NOTHROW; - GC_NOTRIGGER; - MODE_ANY; - } - CONTRACTL_END; - - { - GCX_COOP(); - - // Syncup the LastThrownObject on the managed thread - SafeUpdateLastThrownObject(); - } -} - void Thread::SetLastThrownObjectHandle(OBJECTHANDLE h) { CONTRACTL @@ -3306,37 +3238,6 @@ void Thread::SetLastThrownObjectHandle(OBJECTHANDLE h) m_LastThrownObjectHandle = h; } -// -// Create a duplicate handle of the current throwable and set the last thrown object to that. This ensures that the -// last thrown object and the current throwable have handles that are in the same app domain. -// -void Thread::SafeUpdateLastThrownObject(void) -{ - CONTRACTL - { - NOTHROW; - GC_NOTRIGGER; - MODE_COOPERATIVE; - } - CONTRACTL_END; - - OBJECTREF throwable = GetExceptionState()->GetThrowable(); - - if (throwable != NULL) - { - EX_TRY - { - SetLastThrownObject(throwable); - } - EX_CATCH - { - // If we can't create a handle, set the last thrown object to the preallocated OOM exception. - SafeSetThrowables(CLRException::GetPreallocatedOutOfMemoryException()); - } - EX_END_CATCH - } -} - // Background threads must be counted, because the EE should shut down when the // last non-background thread terminates. But we only count running ones. void Thread::SetBackground(BOOL isBack) diff --git a/src/coreclr/vm/threads.h b/src/coreclr/vm/threads.h index 195a01434d8ed3..a23233865d5d8a 100644 --- a/src/coreclr/vm/threads.h +++ b/src/coreclr/vm/threads.h @@ -1470,8 +1470,6 @@ class Thread } - void SyncManagedExceptionState(bool fIsDebuggerThread); - //--------------------------------------------------------------- // Per-thread information used by handler //--------------------------------------------------------------- @@ -2613,10 +2611,26 @@ class Thread friend class EEDbgInterfaceImpl; private: - // Stores the most recently thrown exception. We need to have a handle in case a GC occurs before - // we catch so we don't lose the object. Having a static allows others to catch outside of CLR w/o leaking - // a handler and allows rethrow outside of CLR too. - // Differs from m_pThrowable in that it doesn't stack on nested exceptions. + // m_LastThrownObjectHandle (LTO) holds the most recently thrown exception as + // an OBJECTHANDLE. It serves two purposes: + // + // 1. Bridging: When a managed exception propagates through native frames via + // SEH (RaiseTheExceptionInternalOnly -> ProcessCLRException), LTO carries + // the exception object across the gap where no ExInfo exists yet. + // + // 2. Post-ExInfo access: After an ExInfo is popped (e.g. in EX_CATCH blocks), + // LTO is the only way to retrieve the exception object. This is used by + // CLRLastThrownObjectException::CreateThrowable and other EX_CATCH consumers. + // + // LTO is NOT kept in sync with ExInfo::m_exception during active exception + // dispatch. While an ExInfo is alive, m_exception is the source of truth - + // use GetThrowable() or GetThrowableAsPseudoHandle() to read it. LTO is set + // lazily by PopExInfos just before each ExInfo is destroyed, and by + // RaiseTheExceptionInternalOnly before the ExInfo is created. + // + // LTO may be stale during active dispatch. Readers that need the current + // exception should call GetThrowable() first and fall back to LastThrownObject() + // only when GetThrowable() returns NULL. OBJECTHANDLE m_LastThrownObjectHandle; // Unsafe to use directly. Use accessors instead. // Indicates that the throwable in m_lastThrownObjectHandle should be treated as @@ -2626,6 +2640,8 @@ class Thread friend void DECLSPEC_NORETURN EEPolicy::HandleFatalStackOverflow(EXCEPTION_POINTERS *pExceptionInfo, BOOL fSkipDebugger); + void SetLastThrownObject(OBJECTREF throwable, BOOL isUnhandled = FALSE); + public: BOOL IsLastThrownObjectNull() { WRAPPER_NO_CONTRACT; return (m_LastThrownObjectHandle == (OBJECTHANDLE)0); } @@ -2653,9 +2669,8 @@ class Thread return m_LastThrownObjectHandle; } - void SetLastThrownObject(OBJECTREF throwable, BOOL isUnhandled = FALSE); void SetSOForLastThrownObject(); - OBJECTREF SafeSetLastThrownObject(OBJECTREF throwable); + OBJECTREF SafeSetLastThrownObject(OBJECTREF throwable, BOOL isUnhandled = FALSE); // Inidcates that the last thrown object is now treated as unhandled void MarkLastThrownObjectUnhandled() @@ -2671,10 +2686,6 @@ class Thread return m_ltoIsUnhandled; } - void SafeUpdateLastThrownObject(void); - OBJECTREF SafeSetThrowables(OBJECTREF pThrowable, - BOOL isUnhandled = FALSE); - bool IsLastThrownObjectStackOverflowException() { LIMITED_METHOD_CONTRACT; From 54ea628c94c4a0f5961e6a5c7507e69b7c62ccbf Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Fri, 1 May 2026 11:05:45 -0400 Subject: [PATCH 2/7] Add MODE_COOPERATIVE contract to ExInfo::PopExInfos The reviewer noted that PopExInfos now reads m_exception (an OBJECTREF) and calls Thread::SafeSetLastThrownObject, which requires MODE_COOPERATIVE for non-NULL throwables, without an explicit GC mode contract. All six callers were already cooperative: - exceptionhandling.cpp:601 - explicit GCX_COOP() three lines above - CleanUpForSecondPass callers (lines 2084/2139/2148) - the asm stub puts the thread in COOP before entering, and the SO path uses GCX_COOP_NO_DTOR - CallCatchFunclet (line 3185) - has MODE_COOPERATIVE CONTRACTL itself - ResumeAtInterceptionLocation (line 3300) - catch-dispatch path also calls PopExplicitFrames, which already requires cooperative mode Add the contract explicitly so that future callers cannot violate it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/exinfo.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/coreclr/vm/exinfo.cpp b/src/coreclr/vm/exinfo.cpp index a3f1ba4e88605a..2772068ba3648b 100644 --- a/src/coreclr/vm/exinfo.cpp +++ b/src/coreclr/vm/exinfo.cpp @@ -98,6 +98,14 @@ void ExInfo::ReleaseResources() // static void ExInfo::PopExInfos(Thread *pThread, void *targetSp) { + CONTRACTL + { + NOTHROW; + GC_NOTRIGGER; + MODE_COOPERATIVE; + } + CONTRACTL_END; + STRESS_LOG1(LF_EH, LL_INFO100, "Popping ExInfos below SP=%p\n", targetSp); ExInfo *pExInfo = (PTR_ExInfo)pThread->GetExceptionState()->GetCurrentExceptionTracker(); From e0fbfd5514251ce8dfc792f7a36a0d139d74a201 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Fri, 1 May 2026 16:08:35 -0400 Subject: [PATCH 3/7] Remove dead exception-handling helpers per review feedback Cleans up dead code identified by @jkotas in PR #127649 review: - Delete Thread::SetLastThrownObjectHandle: zero callers post-PR. - Delete SetManagedUnhandledExceptionBit forward declaration: had no definition anywhere in the codebase. - Remove the always-NULL pThrowableIn parameter from NotifyAppDomainsOfUnhandledException. - Replace UpdateCurrentThrowable (whose name was misleading: it never updated anything, only returned a boolean) with an inline check at the single call site using the GC-mode-agnostic IsThrowableNull / IsLastThrownObjectNull helpers, removing the need for a GCX_COOP inside the surrounding PAL_TRY block. - Merge SafeSetLastThrownObject into SetLastThrownObject: a single public method whose contract reflects the safe NOTHROW behavior. The EX_TRY/EX_CATCH wraps only the throwing CreateHandle call; observable behavior on the OOM fallback path is unchanged. - Drop a stale `similar to UpdateCurrentThrowable()` comment in eedbginterfaceimpl.cpp. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/comutilnative.cpp | 2 +- src/coreclr/vm/eedbginterfaceimpl.cpp | 1 - src/coreclr/vm/eepolicy.cpp | 4 +- src/coreclr/vm/excep.cpp | 51 ++---------- src/coreclr/vm/excep.h | 1 - src/coreclr/vm/exceptionhandling.cpp | 2 +- src/coreclr/vm/exinfo.cpp | 2 +- src/coreclr/vm/jitinterface.cpp | 2 +- src/coreclr/vm/threads.cpp | 110 ++++++++------------------ src/coreclr/vm/threads.h | 10 +-- 10 files changed, 51 insertions(+), 134 deletions(-) diff --git a/src/coreclr/vm/comutilnative.cpp b/src/coreclr/vm/comutilnative.cpp index 53a9be085ba76e..bcbb0c6d8bfd9c 100644 --- a/src/coreclr/vm/comutilnative.cpp +++ b/src/coreclr/vm/comutilnative.cpp @@ -1555,7 +1555,7 @@ extern "C" void QCALLTYPE Environment_FailFast(QCall::StackCrawlMarkHandle mark, // stash the user-provided exception object. this will be used as // the inner exception object to the FatalExecutionEngineException. if (exception.Get() != NULL) - pThread->SafeSetLastThrownObject(exception.Get()); + pThread->SetLastThrownObject(exception.Get()); EEPolicy::HandleFatalError(COR_E_FAILFAST, findCallerData.retAddress, message, NULL, errorSource, argExceptionString); diff --git a/src/coreclr/vm/eedbginterfaceimpl.cpp b/src/coreclr/vm/eedbginterfaceimpl.cpp index 2c33b9c94a7fc7..d3a2b06c1cdd5d 100644 --- a/src/coreclr/vm/eedbginterfaceimpl.cpp +++ b/src/coreclr/vm/eedbginterfaceimpl.cpp @@ -247,7 +247,6 @@ OBJECTHANDLE EEDbgInterfaceImpl::GetThreadException(Thread *pThread) } // Return the last thrown object if there's no current throwable. - // This logic is similar to UpdateCurrentThrowable(). return pThread->m_LastThrownObjectHandle; } diff --git a/src/coreclr/vm/eepolicy.cpp b/src/coreclr/vm/eepolicy.cpp index 237e4bbfb0704e..036c984a2ed7af 100644 --- a/src/coreclr/vm/eepolicy.cpp +++ b/src/coreclr/vm/eepolicy.cpp @@ -623,7 +623,7 @@ void EEPolicy::LogFatalError(UINT exitCode, UINT_PTR address, LPCWSTR pszMessage EXCEPTIONREF curEx = (EXCEPTIONREF)ObjectFromHandle(ohException); curEx->SetInnerException(lto); } - pThread->SafeSetLastThrownObject(ObjectFromHandle(ohException), TRUE); + pThread->SetLastThrownObject(ObjectFromHandle(ohException), TRUE); } // If a managed debugger is already attached, and if that debugger is thinking it might be inclined to @@ -789,7 +789,7 @@ void DECLSPEC_NORETURN EEPolicy::HandleFatalStackOverflow(EXCEPTION_POINTERS *pE OBJECTHANDLE ohSO = CLRException::GetPreallocatedStackOverflowExceptionHandle(); if (ohSO != NULL) { - pThread->SafeSetLastThrownObject(ObjectFromHandle(ohSO), TRUE); + pThread->SetLastThrownObject(ObjectFromHandle(ohSO), TRUE); } else { diff --git a/src/coreclr/vm/excep.cpp b/src/coreclr/vm/excep.cpp index 92eb04e8160074..4a280202c56850 100644 --- a/src/coreclr/vm/excep.cpp +++ b/src/coreclr/vm/excep.cpp @@ -116,10 +116,6 @@ BOOL ShouldOurUEFDisplayUI(PEXCEPTION_POINTERS pExceptionInfo) void NotifyAppDomainsOfUnhandledException( PEXCEPTION_POINTERS pExceptionPointers, - OBJECTREF *pThrowableIn, - BOOL useLastThrownObject); - -VOID SetManagedUnhandledExceptionBit( BOOL useLastThrownObject); //------------------------------------------------------------------------------- @@ -2021,10 +2017,10 @@ VOID DECLSPEC_NORETURN RaiseTheExceptionInternalOnly(OBJECTREF throwable) // Always save the current object in the handle so on rethrow we can reuse it. This is important as it // contains stack trace info. // - // Note: we use SafeSetLastThrownObject, which will try to set the throwable and if there are any problems, + // Note: we use SetLastThrownObject, which will try to set the throwable and if there are any problems, // it will set the throwable to something appropriate (like OOM exception) and return the new // exception. Thus, the user's exception object can be replaced here. - throwable = pThread->SafeSetLastThrownObject(throwable); + throwable = pThread->SetLastThrownObject(throwable); ULONG_PTR hr = GetHRFromThrowable(throwable); @@ -3750,36 +3746,6 @@ BOOL InstallUnhandledExceptionFilter() { return TRUE; } -// -// Update the current throwable on the thread if necessary. If we're looking at one of our exceptions, and if the -// current throwable on the thread is NULL, then we'll set it to something more useful based on the -// LastThrownObject. -// -BOOL UpdateCurrentThrowable(PEXCEPTION_RECORD pExceptionRecord) -{ - STATIC_CONTRACT_THROWS; - STATIC_CONTRACT_MODE_ANY; - STATIC_CONTRACT_GC_TRIGGERS; - - BOOL useLastThrownObject = FALSE; - - Thread* pThread = GetThread(); - - // GetThrowable needs cooperative. - GCX_COOP(); - - if ((pThread->GetThrowable() == NULL) && (pThread->LastThrownObject() != NULL)) - { - // If GetThrowable is NULL and LastThrownObject is not, use lastThrownObject. - // In current (June 05) implementation, this is only used to pass to - // NotifyAppDomainsOfUnhandledException, which needs to get a throwable - // from somewhere, with which to notify the AppDomains. - useLastThrownObject = TRUE; - } - - return useLastThrownObject; -} - // // COMUnhandledExceptionFilter is used to catch all unhandled exceptions. // The debugger will either handle the exception, attach a debugger, or @@ -3953,7 +3919,7 @@ LONG InternalUnhandledExceptionFilter_Worker( BOOL useLastThrownObject = FALSE; if (!pParam->fIgnore && (pParam->pThread != NULL)) { - useLastThrownObject = UpdateCurrentThrowable(pParam->pExceptionInfo->ExceptionRecord); + useLastThrownObject = pParam->pThread->IsThrowableNull() && !pParam->pThread->IsLastThrownObjectNull(); } #ifdef DEBUGGING_SUPPORTED @@ -3988,7 +3954,7 @@ LONG InternalUnhandledExceptionFilter_Worker( #endif // !TARGET_UNIX // Send notifications to the AppDomains. - NotifyAppDomainsOfUnhandledException(pParam->pExceptionInfo, NULL, useLastThrownObject); + NotifyAppDomainsOfUnhandledException(pParam->pExceptionInfo, useLastThrownObject); } else { @@ -4530,7 +4496,6 @@ DefaultCatchHandler(PEXCEPTION_POINTERS pExceptionPointers, //****************************************************************************** void NotifyAppDomainsOfUnhandledException( PEXCEPTION_POINTERS pExceptionPointers, - OBJECTREF *pThrowableIn, BOOL useLastThrownObject) { CONTRACTL @@ -4564,11 +4529,7 @@ void NotifyAppDomainsOfUnhandledException( OBJECTREF throwable; - if (pThrowableIn != NULL) - { - throwable = *pThrowableIn; - } - else if (useLastThrownObject) + if (useLastThrownObject) { throwable = pThread->LastThrownObject(); } @@ -8178,7 +8139,7 @@ void SetupInitialThrowBucketDetails(UINT_PTR adjustedIp) #ifdef _DEBUG // Under OOM scenarios, its possible that when we are raising a threadabort, // the throwable may get converted to preallocated OOM object when RaiseTheExceptionInternalOnly - // invokes Thread::SafeSetLastThrownObject. We check if this is the current case and use it in + // invokes Thread::SetLastThrownObject. We check if this is the current case and use it in // our validation below. BOOL fIsPreallocatedOOMExceptionForTA = FALSE; if ((!fIsThreadAbortException) && pUEWatsonBucketTracker->CapturedForThreadAbort()) diff --git a/src/coreclr/vm/excep.h b/src/coreclr/vm/excep.h index 33f7444ae220a7..f355c8fd1b18d4 100644 --- a/src/coreclr/vm/excep.h +++ b/src/coreclr/vm/excep.h @@ -113,7 +113,6 @@ BOOL IsExceptionOfType(RuntimeExceptionKind reKind, OBJECTREF *pThrowable); BOOL IsExceptionOfType(RuntimeExceptionKind reKind, Exception *pException); BOOL IsUncatchable(OBJECTREF *pThrowable); VOID FixupOnRethrow(Thread *pCurThread, EXCEPTION_POINTERS *pExceptionPointers); -BOOL UpdateCurrentThrowable(PEXCEPTION_RECORD pExceptionRecord); BOOL IsStackOverflowException(Thread* pThread, EXCEPTION_RECORD* pExceptionRecord); void WrapNonCompliantException(OBJECTREF *ppThrowable); OBJECTREF PossiblyUnwrapThrowable(OBJECTREF throwable, Assembly *pAssembly); diff --git a/src/coreclr/vm/exceptionhandling.cpp b/src/coreclr/vm/exceptionhandling.cpp index 0c8a51f29fce47..0f0f8b46f2c6a3 100644 --- a/src/coreclr/vm/exceptionhandling.cpp +++ b/src/coreclr/vm/exceptionhandling.cpp @@ -3186,7 +3186,7 @@ void CallCatchFunclet(OBJECTREF throwable, BYTE* pHandlerIP, REGDISPLAY* pvRegDi if (!pThread->GetExceptionState()->IsExceptionInProgress()) { - pThread->SafeSetLastThrownObject(NULL); + pThread->SetLastThrownObject(NULL); } ExInfo::UpdateNonvolatileRegisters(pvRegDisplay->pCurrentContext, pvRegDisplay, FALSE); diff --git a/src/coreclr/vm/exinfo.cpp b/src/coreclr/vm/exinfo.cpp index 2772068ba3648b..f7663c2a33cd8a 100644 --- a/src/coreclr/vm/exinfo.cpp +++ b/src/coreclr/vm/exinfo.cpp @@ -144,7 +144,7 @@ void ExInfo::PopExInfos(Thread *pThread, void *targetSp) // can find the exception object after the ExInfo is gone. if (pExInfo->m_exception != NULL) { - pThread->SafeSetLastThrownObject(pExInfo->m_exception); + pThread->SetLastThrownObject(pExInfo->m_exception); } pExInfo->ReleaseResources(); diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index ebf0533da70904..6a594ed0ceab86 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -10546,7 +10546,7 @@ void CEEInfo::HandleException(struct _EXCEPTION_POINTERS *pExceptionPointers) // Update the LTO. // // Note: Incase of OOM, this will get set to OOM instance. - pCurThread->SafeSetLastThrownObject(_gc.oCurrentThrowable); + pCurThread->SetLastThrownObject(_gc.oCurrentThrowable); } GCPROTECT_END(); diff --git a/src/coreclr/vm/threads.cpp b/src/coreclr/vm/threads.cpp index b1de36febcc491..9d9cf0ff092889 100644 --- a/src/coreclr/vm/threads.cpp +++ b/src/coreclr/vm/threads.cpp @@ -2271,7 +2271,7 @@ Thread::~Thread() if (!IsAtProcessExit()) { // Destroy any handles that we're using to hold onto exception objects - SafeSetLastThrownObject(NULL); + SetLastThrownObject(NULL); DestroyShortWeakHandle(m_ExposedObject); DestroyStrongHandle(m_StrongHndToExposedObject); @@ -2635,7 +2635,7 @@ void Thread::OnThreadTerminate(BOOL holdingLock) GCX_COOP(); // Destroy the LastThrown handle (and anything that violates the above assert). - SafeSetLastThrownObject(NULL); + SetLastThrownObject(NULL); // Free loader allocator structures related to this thread FreeLoaderAllocatorHandlesForTLSData(this); @@ -3107,11 +3107,11 @@ void Thread::SetExposedObject(OBJECTREF exposed) // IncExternalCount(); } -void Thread::SetLastThrownObject(OBJECTREF throwable, BOOL isUnhandled) +OBJECTREF Thread::SetLastThrownObject(OBJECTREF throwable, BOOL isUnhandled) { CONTRACTL { - if ((throwable == NULL) || CLRException::IsPreallocatedExceptionObject(throwable)) NOTHROW; else THROWS; // From CreateHandle + NOTHROW; GC_NOTRIGGER; if (throwable == NULL) MODE_ANY; else MODE_COOPERATIVE; } @@ -3138,33 +3138,44 @@ void Thread::SetLastThrownObject(OBJECTREF throwable, BOOL isUnhandled) // a new handle below. } - if (throwable != NULL) + if (throwable == NULL) { - _ASSERTE(this == GetThread()); + m_ltoIsUnhandled = FALSE; + return NULL; + } - // Non-compliant exceptions are always wrapped. - // The use of the ExceptionNative:: helper here (rather than the global ::IsException helper) - // is hokey, but we need a GC_NOTRIGGER version and it's only for an ASSERT. - _ASSERTE(IsException(throwable->GetMethodTable())); + _ASSERTE(this == GetThread()); - // If we're tracking one of the preallocated exception objects, then just use the global handle that - // matches it rather than creating a new one. - if (CLRException::IsPreallocatedExceptionObject(throwable)) - { - m_LastThrownObjectHandle = CLRException::GetPreallocatedHandleForObject(throwable); - } - else - { - m_LastThrownObjectHandle = AppDomain::GetCurrentDomain()->CreateHandle(throwable); - } + // Non-compliant exceptions are always wrapped. + // The use of the ExceptionNative:: helper here (rather than the global ::IsException helper) + // is hokey, but we need a GC_NOTRIGGER version and it's only for an ASSERT. + _ASSERTE(IsException(throwable->GetMethodTable())); - _ASSERTE(m_LastThrownObjectHandle != NULL); - m_ltoIsUnhandled = isUnhandled; + // If we're tracking one of the preallocated exception objects, then just use the global handle that + // matches it rather than creating a new one. + if (CLRException::IsPreallocatedExceptionObject(throwable)) + { + m_LastThrownObjectHandle = CLRException::GetPreallocatedHandleForObject(throwable); } else { - m_ltoIsUnhandled = FALSE; + EX_TRY + { + m_LastThrownObjectHandle = AppDomain::GetCurrentDomain()->CreateHandle(throwable); + } + EX_CATCH + { + // If we can't allocate a handle for the throwable, fall back to the preallocated OOM exception + // and return it so the caller can use it in place of the original throwable. + throwable = CLRException::GetPreallocatedOutOfMemoryException(); + m_LastThrownObjectHandle = CLRException::GetPreallocatedHandleForObject(throwable); + } + EX_END_CATCH } + + _ASSERTE(m_LastThrownObjectHandle != NULL); + m_ltoIsUnhandled = isUnhandled; + return throwable; } void Thread::SetSOForLastThrownObject() @@ -3185,59 +3196,6 @@ void Thread::SetSOForLastThrownObject() m_LastThrownObjectHandle = CLRException::GetPreallocatedStackOverflowExceptionHandle(); } -// -// This is a nice wrapper for SetLastThrownObject which catches any exceptions caused by not being able to create -// the handle for the throwable, and setting the last thrown object to the preallocated out of memory exception -// instead. -// -OBJECTREF Thread::SafeSetLastThrownObject(OBJECTREF throwable, BOOL isUnhandled) -{ - CONTRACTL - { - NOTHROW; - GC_NOTRIGGER; - if (throwable == NULL) MODE_ANY; else MODE_COOPERATIVE; - } - CONTRACTL_END; - - // We return the original throwable if nothing goes wrong. - OBJECTREF ret = throwable; - - EX_TRY - { - SetLastThrownObject(throwable, isUnhandled); - } - EX_CATCH - { - // If it didn't work, then set the last thrown object to the preallocated OOM exception, and return that - // object instead of the original throwable. - ret = CLRException::GetPreallocatedOutOfMemoryException(); - SetLastThrownObject(ret, isUnhandled); - } - EX_END_CATCH - - return ret; -} - -void Thread::SetLastThrownObjectHandle(OBJECTHANDLE h) -{ - CONTRACTL - { - NOTHROW; - GC_NOTRIGGER; - MODE_COOPERATIVE; - } - CONTRACTL_END; - - if (m_LastThrownObjectHandle != NULL && - !CLRException::IsPreallocatedExceptionHandle(m_LastThrownObjectHandle)) - { - DestroyHandle(m_LastThrownObjectHandle); - } - - m_LastThrownObjectHandle = h; -} - // Background threads must be counted, because the EE should shut down when the // last non-background thread terminates. But we only count running ones. void Thread::SetBackground(BOOL isBack) diff --git a/src/coreclr/vm/threads.h b/src/coreclr/vm/threads.h index a23233865d5d8a..a6f1d878f6a4f0 100644 --- a/src/coreclr/vm/threads.h +++ b/src/coreclr/vm/threads.h @@ -2640,8 +2640,6 @@ class Thread friend void DECLSPEC_NORETURN EEPolicy::HandleFatalStackOverflow(EXCEPTION_POINTERS *pExceptionInfo, BOOL fSkipDebugger); - void SetLastThrownObject(OBJECTREF throwable, BOOL isUnhandled = FALSE); - public: BOOL IsLastThrownObjectNull() { WRAPPER_NO_CONTRACT; return (m_LastThrownObjectHandle == (OBJECTHANDLE)0); } @@ -2670,7 +2668,11 @@ class Thread } void SetSOForLastThrownObject(); - OBJECTREF SafeSetLastThrownObject(OBJECTREF throwable, BOOL isUnhandled = FALSE); + + // Sets the last thrown object. If the throwable cannot be tracked due to OOM, sets the + // last thrown object to the preallocated OOM exception and returns it instead of the + // original throwable. + OBJECTREF SetLastThrownObject(OBJECTREF throwable, BOOL isUnhandled = FALSE); // Inidcates that the last thrown object is now treated as unhandled void MarkLastThrownObjectUnhandled() @@ -2704,8 +2706,6 @@ class Thread void ClearThreadCurrNotification(); private: - void SetLastThrownObjectHandle(OBJECTHANDLE h); - ThreadExceptionState m_ExceptionState; private: From 70188bbaa13a5bc223b862312c654548595419f9 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Fri, 1 May 2026 17:19:59 -0400 Subject: [PATCH 4/7] Address review feedback: dead-code removal and declaration order Two changes responding to PR review feedback: 1. Remove dead CEEInfo::HandleException. The function has been unreachable since 2016 (commit 4d9f4b8 `Remove SEH interactions between the JIT and the EE'') which replaced the old ICorJitInfo::FilterException/HandleException pair with runWithErrorTrap. The function is private, non-virtual, not part of the ICorJitInfo interface, and has zero callers in coreclr, the JIT, the AOT thunks, or SuperPMI (the SuperPMI Packet_HandleException slot is commented out). Removing it also retires the long-stale comment about `sync between the LTO and the exception tracker'' that pre-dates the ExInfo redesign and the lazy-LTO model from #127300/#127649. 2. Reorder declarations in threads.h so SetLastThrownObject precedes SetSOForLastThrownObject, matching the order of the definitions in threads.cpp. Also update an unrelated stale comment in ExInfo::PopExInfos: the `unmanaged thread'' rationale is incorrect because both UMThunkUnwindFrameChainHandler and CallDescrWorkerUnwindFrameChainHandler short-circuit unmanaged threads before reaching PopExInfos, and the function carries a MODE_COOPERATIVE contract. No behavior change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/exinfo.cpp | 2 +- src/coreclr/vm/jitinterface.cpp | 77 --------------------------------- src/coreclr/vm/jitinterface.h | 1 - src/coreclr/vm/threads.h | 4 +- 4 files changed, 3 insertions(+), 81 deletions(-) diff --git a/src/coreclr/vm/exinfo.cpp b/src/coreclr/vm/exinfo.cpp index f7663c2a33cd8a..7c9596a6ea5874 100644 --- a/src/coreclr/vm/exinfo.cpp +++ b/src/coreclr/vm/exinfo.cpp @@ -112,7 +112,7 @@ void ExInfo::PopExInfos(Thread *pThread, void *targetSp) #if defined(DEBUGGING_SUPPORTED) DWORD_PTR dwInterceptStackFrame = 0; - // This method may be called on an unmanaged thread, in which case no interception can be done. + // If there is no current ExInfo, there is nothing to inspect for interception. if (pExInfo) { ThreadExceptionState* pExState = pThread->GetExceptionState(); diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 6a594ed0ceab86..9a04ef77307db1 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -10479,83 +10479,6 @@ int32_t * CEEInfo::getAddrOfCaptureThreadGlobal(void **ppIndirection) return result; } -// This code is called if FilterException chose to handle the exception. -void CEEInfo::HandleException(struct _EXCEPTION_POINTERS *pExceptionPointers) -{ - CONTRACTL { - NOTHROW; - GC_NOTRIGGER; - } CONTRACTL_END; - - JIT_TO_EE_TRANSITION_LEAF(); - - if (IsComPlusException(pExceptionPointers->ExceptionRecord)) - { - GCX_COOP(); - - // This is actually the LastThrown exception object. - OBJECTREF throwable = CLRException::GetThrowableFromExceptionRecord(pExceptionPointers->ExceptionRecord); - - if (throwable != NULL) - { - struct - { - OBJECTREF oLastThrownObject; - OBJECTREF oCurrentThrowable; - } _gc; - - ZeroMemory(&_gc, sizeof(_gc)); - - PTR_Thread pCurThread = GetThread(); - - // Setup the throwables - _gc.oLastThrownObject = throwable; - - // This will be NULL if no managed exception is active. Otherwise, - // it will reference the active throwable. - _gc.oCurrentThrowable = pCurThread->GetThrowable(); - - GCPROTECT_BEGIN(_gc); - - // JIT does not use or reference managed exceptions at all and simply swallows them, - // or lets them fly through so that they will either get caught in managed code, the VM - // or will go unhandled. - // - // Blind swallowing of managed exceptions can break the semantic of "which exception handler" - // gets to process the managed exception first. The expected handler is managed code exception - // handler (e.g. COMPlusFrameHandler on x86 and ProcessCLRException on 64bit) which will setup - // the exception tracker for the exception that will enable the expected sync between the - // LastThrownObject (LTO), setup in RaiseTheExceptionInternalOnly, and the exception tracker. - // - // However, JIT can break this by swallowing the managed exception before managed code exception - // handler gets a chance to setup an exception tracker for it. Since there is no cleanup - // done for the swallowed exception as part of the unwind (because no exception tracker may have been setup), - // we need to reset the LTO, if it is out of sync from the active throwable. - // - // Hence, check if the LastThrownObject and active-exception throwable are in sync or not. - // If not, bring them in sync. - // - // Example - // ------- - // It is possible that an exception was already in progress and while processing it (e.g. - // invoking finally block), we invoked JIT that had another managed exception @ JIT-EE transition boundary - // that is swallowed by the JIT before managed code exception handler sees it. This breaks the sync between - // LTO and the active exception in the exception tracker. - if (_gc.oCurrentThrowable != _gc.oLastThrownObject) - { - // Update the LTO. - // - // Note: Incase of OOM, this will get set to OOM instance. - pCurThread->SetLastThrownObject(_gc.oCurrentThrowable); - } - - GCPROTECT_END(); - } - } - - EE_TO_JIT_TRANSITION_LEAF(); -} - CORINFO_MODULE_HANDLE CEEInfo::embedModuleHandle(CORINFO_MODULE_HANDLE handle, void **ppIndirection) { diff --git a/src/coreclr/vm/jitinterface.h b/src/coreclr/vm/jitinterface.h index 955923c747fe4b..c761d6e18514a5 100644 --- a/src/coreclr/vm/jitinterface.h +++ b/src/coreclr/vm/jitinterface.h @@ -303,7 +303,6 @@ class CEEInfo : public ICorJitInfo TypeHandle GetTypeFromContext(CORINFO_CONTEXT_HANDLE context); void GetTypeContext(CORINFO_CONTEXT_HANDLE context, SigTypeContext* pTypeContext); - void HandleException(struct _EXCEPTION_POINTERS* pExceptionPointers); public: #include "icorjitinfoimpl_generated.h" uint32_t getClassAttribsInternal (CORINFO_CLASS_HANDLE cls); diff --git a/src/coreclr/vm/threads.h b/src/coreclr/vm/threads.h index a6f1d878f6a4f0..c98b1404058c2d 100644 --- a/src/coreclr/vm/threads.h +++ b/src/coreclr/vm/threads.h @@ -2667,13 +2667,13 @@ class Thread return m_LastThrownObjectHandle; } - void SetSOForLastThrownObject(); - // Sets the last thrown object. If the throwable cannot be tracked due to OOM, sets the // last thrown object to the preallocated OOM exception and returns it instead of the // original throwable. OBJECTREF SetLastThrownObject(OBJECTREF throwable, BOOL isUnhandled = FALSE); + void SetSOForLastThrownObject(); + // Inidcates that the last thrown object is now treated as unhandled void MarkLastThrownObjectUnhandled() { From 3aa98b593e2af88aa75d131da3c5513f72b89d5f Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Mon, 4 May 2026 12:51:07 -0400 Subject: [PATCH 5/7] Apply suggestions from code review Co-authored-by: Jan Kotas --- src/coreclr/vm/excep.cpp | 1 - src/coreclr/vm/threads.h | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/coreclr/vm/excep.cpp b/src/coreclr/vm/excep.cpp index 4a280202c56850..38db79c780e16d 100644 --- a/src/coreclr/vm/excep.cpp +++ b/src/coreclr/vm/excep.cpp @@ -3961,7 +3961,6 @@ LONG InternalUnhandledExceptionFilter_Worker( LOG((LF_EH, LL_INFO100, "InternalUnhandledExceptionFilter_Worker: Not collecting bucket information as thread object does not exist\n")); } - // Launch Watson and see if we want to debug the process // // Note that we need to do this before "ignoring" exceptions like diff --git a/src/coreclr/vm/threads.h b/src/coreclr/vm/threads.h index c98b1404058c2d..c0b9beede422bd 100644 --- a/src/coreclr/vm/threads.h +++ b/src/coreclr/vm/threads.h @@ -1469,7 +1469,6 @@ class Thread return m_ExceptionState.IsExceptionInProgress(); } - //--------------------------------------------------------------- // Per-thread information used by handler //--------------------------------------------------------------- @@ -2631,7 +2630,7 @@ class Thread // LTO may be stale during active dispatch. Readers that need the current // exception should call GetThrowable() first and fall back to LastThrownObject() // only when GetThrowable() returns NULL. - OBJECTHANDLE m_LastThrownObjectHandle; // Unsafe to use directly. Use accessors instead. + OBJECTHANDLE m_LastThrownObjectHandle; // Indicates that the throwable in m_lastThrownObjectHandle should be treated as // unhandled. This occurs during fatal error and a few other early error conditions From 7ac958a83000d4382b54dd3a50c25634c4dab8c6 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Mon, 4 May 2026 14:15:28 -0400 Subject: [PATCH 6/7] optimization: don't create object handle in CallCatchFunclets case --- src/coreclr/vm/exceptionhandling.cpp | 10 +++++----- src/coreclr/vm/exinfo.cpp | 16 +++++++++++----- src/coreclr/vm/exinfo.h | 2 +- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/coreclr/vm/exceptionhandling.cpp b/src/coreclr/vm/exceptionhandling.cpp index 0f0f8b46f2c6a3..a48ab5013b17cc 100644 --- a/src/coreclr/vm/exceptionhandling.cpp +++ b/src/coreclr/vm/exceptionhandling.cpp @@ -3182,12 +3182,12 @@ void CallCatchFunclet(OBJECTREF throwable, BYTE* pHandlerIP, REGDISPLAY* pvRegDi } #endif // DEBUGGING_SUPPORTED - ExInfo::PopExInfos(pThread, (void*)targetSp); + ExInfo::PopExInfos(pThread, (void*)targetSp, /* setLtoToPoppedException */ false); - if (!pThread->GetExceptionState()->IsExceptionInProgress()) - { - pThread->SetLastThrownObject(NULL); - } + // Unconditionally clear the LastThrownObject after popping the ExInfos so that + // post-exception consumers do not see a stale exception object. + // If there is still ExInfos on the chain, the throwables can be read from there. + pThread->SetLastThrownObject(NULL); ExInfo::UpdateNonvolatileRegisters(pvRegDisplay->pCurrentContext, pvRegDisplay, FALSE); if (pHandlerIP != NULL) diff --git a/src/coreclr/vm/exinfo.cpp b/src/coreclr/vm/exinfo.cpp index 7c9596a6ea5874..92c1ef82616284 100644 --- a/src/coreclr/vm/exinfo.cpp +++ b/src/coreclr/vm/exinfo.cpp @@ -96,7 +96,7 @@ void ExInfo::ReleaseResources() } // static -void ExInfo::PopExInfos(Thread *pThread, void *targetSp) +void ExInfo::PopExInfos(Thread *pThread, void *targetSp, bool setLtoToPoppedException) { CONTRACTL { @@ -127,6 +127,7 @@ void ExInfo::PopExInfos(Thread *pThread, void *targetSp) } #endif // DEBUGGING_SUPPORTED + OBJECTREF lastPoppedException = NULL; while (pExInfo && pExInfo < (void*)targetSp) { #if defined(DEBUGGING_SUPPORTED) @@ -139,18 +140,23 @@ void ExInfo::PopExInfos(Thread *pThread, void *targetSp) } #endif // DEBUGGING_SUPPORTED - // Set LTO from the exception being destroyed so that post-ExInfo consumers - // (EX_CATCH via CLRLastThrownObjectException, ProcessCLRException bridging) - // can find the exception object after the ExInfo is gone. if (pExInfo->m_exception != NULL) { - pThread->SetLastThrownObject(pExInfo->m_exception); + lastPoppedException = pExInfo->m_exception; } pExInfo->ReleaseResources(); pExInfo = (PTR_ExInfo)pExInfo->m_pPrevNestedInfo; } pThread->GetExceptionState()->m_pCurrentTracker = pExInfo; + + if (lastPoppedException != NULL && setLtoToPoppedException) + { + // Set LTO from the exception being destroyed so that post-ExInfo consumers + // (EX_CATCH via CLRLastThrownObjectException, ProcessCLRException bridging) + // can find the exception object after the ExInfo is gone. + pThread->SetLastThrownObject(lastPoppedException); + } } static bool IsFilterStartOffset(EE_ILEXCEPTION_CLAUSE* pEHClause, DWORD_PTR dwHandlerStartPC) diff --git a/src/coreclr/vm/exinfo.h b/src/coreclr/vm/exinfo.h index 1b1db4c1b9d122..d6f3355e6a9d59 100644 --- a/src/coreclr/vm/exinfo.h +++ b/src/coreclr/vm/exinfo.h @@ -88,7 +88,7 @@ struct ExInfo DWORD_PTR dwHandlerStartPC, StackFrame sf); - static void PopExInfos(Thread *pThread, void *targetSp); + static void PopExInfos(Thread *pThread, void *targetSp, bool setLtoToPoppedException = true); // Previous ExInfo in the chain of exceptions rethrown from their catch / finally handlers PTR_ExInfo m_pPrevNestedInfo; From baf072e7e05ac0494263fa1be350a0c44ab698ea Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Wed, 6 May 2026 11:15:18 -0400 Subject: [PATCH 7/7] WIP: Source-tagged throwable accessors Replace implicit `Thread::LastThrownObject` / `GetThrowable` / `IsThrowableNull` family with source-explicit accessors that name the view the caller wants: - `Thread::GetThrowableHandle(ThrowableSource)` - `Thread::GetThrowableRef(ThrowableSource)` - `Thread::IsThrowableNull(ThrowableSource)` `ThrowableSource` enum values: `ExInfoOnly`, `LTOOnly`, `ExInfoOrLTO`, `LTOIfUnhandled`, `ExInfoOrLTOIfUnhandled`. Callers explicitly select the view they want instead of relying on the (now lazy) LTO field being coherent with the active ExInfo. Migrated all 36 reader sites in CoreCLR (VM, EE, profiling, ETW, prestub/interp, runtime EH, fatal/Watson, debugger DBI, DAC). Removed seven legacy wrappers from `threads.h`: `GetThrowable`, `HasException`, `GetThrowableAsPseudoHandle`, `IsThrowableNull()` no-arg, `IsLastThrownObjectNull`, `LastThrownObject`, `LastThrownObjectHandle`, plus `IsLastThrownObjectStackOverflowException` (one caller inlined). Also fixes a Reflection.Invoke crash on the lazy branch: `CallDescrWorkerUnwindFrameChainHandler`'s non-SO unwind path called `CleanUpForSecondPass` -> `PopExInfos` from PREEMP, but the lazy `PopExInfos` reads `OBJECTREF` and requires COOP. Wrapped with `GCX_COOP()` in exceptionhandling.cpp. --- src/coreclr/debug/daccess/dacdbiimpl.cpp | 12 +- src/coreclr/debug/daccess/enummem.cpp | 4 +- src/coreclr/debug/daccess/request.cpp | 2 +- src/coreclr/debug/daccess/task.cpp | 6 +- src/coreclr/debug/ee/debugger.cpp | 4 +- src/coreclr/vm/clrex.cpp | 16 +- src/coreclr/vm/dwbucketmanager.hpp | 6 +- src/coreclr/vm/dwreport.cpp | 9 +- src/coreclr/vm/eedbginterfaceimpl.cpp | 11 +- src/coreclr/vm/eepolicy.cpp | 2 +- src/coreclr/vm/eetoprofinterfacewrapper.inl | 4 +- src/coreclr/vm/eventtrace.cpp | 4 +- src/coreclr/vm/excep.cpp | 45 +++-- src/coreclr/vm/exceptionhandling.cpp | 3 +- src/coreclr/vm/interpexec.cpp | 4 +- src/coreclr/vm/prestub.cpp | 4 +- src/coreclr/vm/threads.cpp | 183 +++++++++++++++++++- src/coreclr/vm/threads.h | 116 ++++++------- src/coreclr/vm/threadsuspend.cpp | 2 +- 19 files changed, 290 insertions(+), 147 deletions(-) diff --git a/src/coreclr/debug/daccess/dacdbiimpl.cpp b/src/coreclr/debug/daccess/dacdbiimpl.cpp index dfb417762d77d8..395efec617909c 100644 --- a/src/coreclr/debug/daccess/dacdbiimpl.cpp +++ b/src/coreclr/debug/daccess/dacdbiimpl.cpp @@ -4795,7 +4795,7 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::HasUnhandledException(VMPTR_Threa { // most managed exceptions are just a throwable bound to a // native exception. In that case this handle will be non-null - OBJECTHANDLE ohException = pThread->GetThrowableAsPseudoHandle(); + OBJECTHANDLE ohException = pThread->GetThrowableHandle(ThrowableSource::ExInfoOnly); if (ohException != (OBJECTHANDLE)NULL) { // during the UEF we set the unhandled bit, if it is set the exception @@ -4932,15 +4932,7 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::GetCurrentException(VMPTR_Thread Thread * pThread = vmThread.GetDacPtr(); - OBJECTHANDLE ohException = pThread->GetThrowableAsPseudoHandle(); - - if (ohException == (OBJECTHANDLE)NULL) - { - if (pThread->IsLastThrownObjectUnhandled()) - { - ohException = pThread->LastThrownObjectHandle(); - } - } + OBJECTHANDLE ohException = pThread->GetThrowableHandle(ThrowableSource::ExInfoOrLTOIfUnhandled); VMPTR_OBJECTHANDLE vmObjHandle; vmObjHandle.SetDacTargetPtr(ohException); diff --git a/src/coreclr/debug/daccess/enummem.cpp b/src/coreclr/debug/daccess/enummem.cpp index 23d30c832c4658..2dda079416f470 100644 --- a/src/coreclr/debug/daccess/enummem.cpp +++ b/src/coreclr/debug/daccess/enummem.cpp @@ -1147,7 +1147,7 @@ HRESULT ClrDataAccess::EnumMemDumpAllThreadsStack(CLRDataEnumMemoryFlags flags) pThread = ((ClrDataTask *)pIXCLRDataTask.GetValue())->GetThread(); // dump the exception object - DumpManagedExcepObject(flags, pThread->LastThrownObject()); + DumpManagedExcepObject(flags, pThread->GetThrowableRef(ThrowableSource::ExInfoOrLTO)); // Now probe into the exception info status = pIXCLRDataTask->GetCurrentExceptionState(&pExcepState); @@ -1235,7 +1235,7 @@ HRESULT ClrDataAccess::EnumMemDumpAllThreadsStack(CLRDataEnumMemoryFlags flags) #ifndef FEATURE_MINIMETADATA_IN_TRIAGEDUMPS // dump the exception object - DumpManagedExcepObject(flags, pThread->LastThrownObject()); + DumpManagedExcepObject(flags, pThread->GetThrowableRef(ThrowableSource::ExInfoOrLTO)); #endif // FEATURE_MINIMETADATA_IN_TRIAGEDUMPS // Stack Walking diff --git a/src/coreclr/debug/daccess/request.cpp b/src/coreclr/debug/daccess/request.cpp index c68ee4c8530fd4..9eb2c53a223e82 100644 --- a/src/coreclr/debug/daccess/request.cpp +++ b/src/coreclr/debug/daccess/request.cpp @@ -894,7 +894,7 @@ HRESULT ClrDataAccess::GetThreadData(CLRDATA_ADDRESS threadAddr, struct DacpThre // from the OS thread ID via the debugger's native API (e.g., IDebuggerServices::GetThreadTeb). threadData->teb = (CLRDATA_ADDRESS)NULL; threadData->lastThrownObjectHandle = - TO_CDADDR(thread->m_LastThrownObjectHandle); + TO_CDADDR(thread->GetThrowableHandle(ThrowableSource::ExInfoOrLTO)); threadData->nextThread = HOST_CDADDR(ThreadStore::s_pThreadStore->m_ThreadList.GetNext(thread)); if (thread->m_ExceptionState.m_pCurrentTracker) diff --git a/src/coreclr/debug/daccess/task.cpp b/src/coreclr/debug/daccess/task.cpp index 4fc7604440aca0..2243a4fd726fb6 100644 --- a/src/coreclr/debug/daccess/task.cpp +++ b/src/coreclr/debug/daccess/task.cpp @@ -521,7 +521,7 @@ ClrDataTask::GetLastExceptionState( EX_TRY { - if (m_thread->m_LastThrownObjectHandle) + if (!m_thread->IsThrowableNull(ThrowableSource::ExInfoOrLTO)) { *exception = new (nothrow) ClrDataExceptionState(m_dac, @@ -529,7 +529,7 @@ ClrDataTask::GetLastExceptionState( m_thread, CLRDATA_EXCEPTION_PARTIAL, NULL, - m_thread->m_LastThrownObjectHandle, + m_thread->GetThrowableHandle(ThrowableSource::ExInfoOrLTO), NULL); status = *exception ? S_OK : E_OUTOFMEMORY; } @@ -4910,7 +4910,7 @@ ClrDataExceptionState::NewFromThread(ClrDataAccess* dac, ClrDataExceptionState** exception, IXCLRDataExceptionState** pubException) { - if (!thread->HasException()) + if (thread->IsThrowableNull(ThrowableSource::ExInfoOrLTO)) { return E_NOINTERFACE; } diff --git a/src/coreclr/debug/ee/debugger.cpp b/src/coreclr/debug/ee/debugger.cpp index 1e853b149fbc7c..383a809e2244f0 100644 --- a/src/coreclr/debug/ee/debugger.cpp +++ b/src/coreclr/debug/ee/debugger.cpp @@ -7449,7 +7449,7 @@ HRESULT Debugger::SendException(Thread *pThread, (fFirstChance && (!pExState->GetFlags()->SentDebugFirstChance() || !pExState->GetFlags()->SentDebugUserFirstChance()))); // There must be a managed exception object to send a managed exception event - if (g_pEEInterface->IsThreadExceptionNull(pThread) && (pThread->LastThrownObjectHandle() == NULL)) + if (pThread->IsThrowableNull(ThrowableSource::ExInfoOrLTO)) { managedEventNeeded = FALSE; } @@ -7878,7 +7878,7 @@ BOOL Debugger::ShouldSendCatchHandlerFound(Thread* pThread) else { BOOL forceSendCatchHandlerFound = FALSE; - OBJECTHANDLE objHandle = pThread->GetThrowableAsPseudoHandle(); + OBJECTHANDLE objHandle = pThread->GetThrowableHandle(ThrowableSource::ExInfoOnly); OBJECTHANDLE retrievedHandle = m_pForceCatchHandlerFoundEventsTable->Lookup(objHandle); if (retrievedHandle != NULL) { diff --git a/src/coreclr/vm/clrex.cpp b/src/coreclr/vm/clrex.cpp index 565f8ec633b68e..f2a81d2d685677 100644 --- a/src/coreclr/vm/clrex.cpp +++ b/src/coreclr/vm/clrex.cpp @@ -74,7 +74,7 @@ OBJECTREF CLRException::GetThrowable() } if ((IsType(CLRLastThrownObjectException::GetType()) && - pThread->LastThrownObject() == GetPreallocatedStackOverflowException())) + pThread->GetThrowableRef(ThrowableSource::LTOOnly) == GetPreallocatedStackOverflowException())) { return GetPreallocatedStackOverflowException(); } @@ -596,7 +596,7 @@ OBJECTREF CLRException::GetThrowableFromException(Exception *pException) if (NULL == pException) { - return pThread->LastThrownObject(); + return pThread->GetThrowableRef(ThrowableSource::LTOOnly); } if (pException->IsType(CLRException::GetType())) @@ -624,7 +624,7 @@ OBJECTREF CLRException::GetThrowableFromException(Exception *pException) // as an unrelated unmanaged exception. if (IsComPlusException(&(pSEHException->m_exception))) { - return pThread->LastThrownObject(); + return pThread->GetThrowableRef(ThrowableSource::LTOOnly); } else { @@ -725,11 +725,11 @@ OBJECTREF CLRException::GetThrowableFromException(Exception *pException) } else if (pNewException->IsType(CLRLastThrownObjectException::GetType()) && - (pThread->LastThrownObject() != NULL)) + !pThread->IsThrowableNull(ThrowableSource::LTOOnly)) { STRESS_LOG0(LF_EH, LL_INFO100, "CLRException::GetThrowableFromException: LTO Exception creating throwable; getting LastThrownObject.\n"); if (oRetVal == NULL) - oRetVal = pThread->LastThrownObject(); + oRetVal = pThread->GetThrowableRef(ThrowableSource::LTOOnly); } else { @@ -770,7 +770,7 @@ OBJECTREF CLRException::GetThrowableFromExceptionRecord(EXCEPTION_RECORD *pExcep if (IsComPlusException(pExceptionRecord)) { - return GetThread()->LastThrownObject(); + return GetThread()->GetThrowableRef(ThrowableSource::LTOOnly); } return NULL; @@ -2004,7 +2004,7 @@ OBJECTREF CLRLastThrownObjectException::CreateThrowable() DEBUG_STMT(Validate()); - return GetThread()->LastThrownObject(); + return GetThread()->GetThrowableRef(ThrowableSource::LTOOnly); } // OBJECTREF CLRLastThrownObjectException::CreateThrowable() #if defined(_DEBUG) @@ -2025,7 +2025,7 @@ CLRLastThrownObjectException* CLRLastThrownObjectException::Validate() GCPROTECT_BEGIN(throwable); Thread * pThread = GetThread(); - throwable = pThread->LastThrownObject(); + throwable = pThread->GetThrowableRef(ThrowableSource::LTOOnly); DWORD dwCurrentExceptionCode = GetCurrentExceptionCode(); diff --git a/src/coreclr/vm/dwbucketmanager.hpp b/src/coreclr/vm/dwbucketmanager.hpp index c51c6f0f3ac411..3b3672aa533ddc 100644 --- a/src/coreclr/vm/dwbucketmanager.hpp +++ b/src/coreclr/vm/dwbucketmanager.hpp @@ -942,11 +942,7 @@ OBJECTREF BaseBucketParamsManager::GetRealExceptionObject() // If it is an exception, see if there is a Throwable object. if (m_pThread != NULL) { - throwable = m_pThread->GetThrowable(); - - // If the "Throwable" is null, try the "LastThrownObject" - if (throwable == NULL) - throwable = m_pThread->LastThrownObject(); + throwable = m_pThread->GetThrowableRef(ThrowableSource::ExInfoOrLTO); } } diff --git a/src/coreclr/vm/dwreport.cpp b/src/coreclr/vm/dwreport.cpp index c7de8cfb59dff3..dd699cd83f1b04 100644 --- a/src/coreclr/vm/dwreport.cpp +++ b/src/coreclr/vm/dwreport.cpp @@ -453,12 +453,9 @@ UINT_PTR GetIPOfThrowSite( // trace, it will start with the topmost (lowest address, newest) managed // code, which is what we want. GCX_COOP(); - OBJECTREF throwable = pThread->GetThrowable(); - - // If there was no managed code on the stack and we are on 64-bit, then we won't have propagated - // the LastThrownObject into the Throwable yet. - if (throwable == NULL) - throwable = pThread->LastThrownObject(); + // ExInfo first; fall back to LTO when no managed code was on the stack + // (e.g., 64-bit native-only callstack didn't propagate LTO into the Throwable). + OBJECTREF throwable = pThread->GetThrowableRef(ThrowableSource::ExInfoOrLTO); _ASSERTE(throwable != NULL); _ASSERTE(IsException(throwable->GetMethodTable())); diff --git a/src/coreclr/vm/eedbginterfaceimpl.cpp b/src/coreclr/vm/eedbginterfaceimpl.cpp index d3a2b06c1cdd5d..379f90ce770e78 100644 --- a/src/coreclr/vm/eedbginterfaceimpl.cpp +++ b/src/coreclr/vm/eedbginterfaceimpl.cpp @@ -239,15 +239,8 @@ OBJECTHANDLE EEDbgInterfaceImpl::GetThreadException(Thread *pThread) } CONTRACTL_END; - OBJECTHANDLE oh = pThread->GetThrowableAsPseudoHandle(); - - if (oh != NULL) - { - return oh; - } - // Return the last thrown object if there's no current throwable. - return pThread->m_LastThrownObjectHandle; + return pThread->GetThrowableHandle(ThrowableSource::ExInfoOrLTO); } bool EEDbgInterfaceImpl::IsThreadExceptionNull(Thread *pThread) @@ -260,7 +253,7 @@ bool EEDbgInterfaceImpl::IsThreadExceptionNull(Thread *pThread) } CONTRACTL_END; - return pThread->IsThrowableNull(); + return pThread->IsThrowableNull(ThrowableSource::ExInfoOnly); } void EEDbgInterfaceImpl::ClearThreadException(Thread *pThread) diff --git a/src/coreclr/vm/eepolicy.cpp b/src/coreclr/vm/eepolicy.cpp index 036c984a2ed7af..b967940e7698fa 100644 --- a/src/coreclr/vm/eepolicy.cpp +++ b/src/coreclr/vm/eepolicy.cpp @@ -616,7 +616,7 @@ void EEPolicy::LogFatalError(UINT exitCode, UINT_PTR address, LPCWSTR pszMessage // for fail-fast, if there's a LTO available then use that as the inner exception object // for the FEEE we'll be reporting. this can help the Watson back-end to generate better // buckets for apps that call Environment.FailFast() and supply an exception object. - OBJECTREF lto = pThread->LastThrownObject(); + OBJECTREF lto = pThread->GetThrowableRef(ThrowableSource::LTOOnly); if (exitCode == static_cast(COR_E_FAILFAST) && lto != NULL) { diff --git a/src/coreclr/vm/eetoprofinterfacewrapper.inl b/src/coreclr/vm/eetoprofinterfacewrapper.inl index afa00001b17b8a..3f292ea3b4cea2 100644 --- a/src/coreclr/vm/eetoprofinterfacewrapper.inl +++ b/src/coreclr/vm/eetoprofinterfacewrapper.inl @@ -36,7 +36,7 @@ class EEToProfilerExceptionInterfaceWrapper _ASSERTE(pThread->PreemptiveGCDisabled()); // Get a reference to the object that won't move - OBJECTREF thrown = pThread->GetThrowable(); + OBJECTREF thrown = pThread->GetThrowableRef(ThrowableSource::ExInfoOnly); (&g_profControlBlock)->ExceptionThrown( reinterpret_cast((*(BYTE **)&thrown))); @@ -195,7 +195,7 @@ class EEToProfilerExceptionInterfaceWrapper // passed CAN change when gc happens. OBJECTREF thrown = NULL; GCPROTECT_BEGIN(thrown); - thrown = pThread->GetThrowable(); + thrown = pThread->GetThrowableRef(ThrowableSource::ExInfoOnly); { (&g_profControlBlock)->ExceptionCatcherEnter( (FunctionID) pFunc, diff --git a/src/coreclr/vm/eventtrace.cpp b/src/coreclr/vm/eventtrace.cpp index cd4cff43f3c88d..364bbc43ac7387 100644 --- a/src/coreclr/vm/eventtrace.cpp +++ b/src/coreclr/vm/eventtrace.cpp @@ -2782,7 +2782,7 @@ VOID ETW::ExceptionLog::ExceptionThrown(CrawlFrame *pCf, BOOL bIsReThrownExcept NOTHROW; GC_TRIGGERS; PRECONDITION(GetThreadNULLOk() != NULL); - PRECONDITION(GetThread()->GetThrowable() != NULL); + PRECONDITION(!GetThread()->IsThrowableNull(ThrowableSource::ExInfoOnly)); } CONTRACTL_END; if(!(bIsReThrownException || bIsNewException)) @@ -2814,7 +2814,7 @@ VOID ETW::ExceptionLog::ExceptionThrown(CrawlFrame *pCf, BOOL bIsReThrownExcept gc.exceptionMessageRef = NULL; GCPROTECT_BEGIN(gc); - gc.exceptionObj = pThread->GetThrowable(); + gc.exceptionObj = pThread->GetThrowableRef(ThrowableSource::ExInfoOnly); gc.innerExceptionObj = ((EXCEPTIONREF)gc.exceptionObj)->GetInnerException(); ThreadExceptionState *pExState = pThread->GetExceptionState(); diff --git a/src/coreclr/vm/excep.cpp b/src/coreclr/vm/excep.cpp index 38db79c780e16d..0067a81cd0d28d 100644 --- a/src/coreclr/vm/excep.cpp +++ b/src/coreclr/vm/excep.cpp @@ -1845,7 +1845,7 @@ BOOL IsInFirstFrameOfHandler(Thread *pThread, IJitManager *pJitManager, const ME CONTRACTL_END; // if don't have a throwable the aren't processing an exception - if (pThread->IsThrowableNull()) + if (pThread->IsThrowableNull(ThrowableSource::ExInfoOnly)) return FALSE; EH_CLAUSE_ENUMERATOR pEnumState; @@ -2807,7 +2807,7 @@ BOOL IsStackOverflowException(Thread* pThread, EXCEPTION_RECORD* pExceptionRecor } if (IsComPlusException(pExceptionRecord) && - pThread->IsLastThrownObjectStackOverflowException()) + pThread->GetThrowableHandle(ThrowableSource::LTOOnly) == g_pPreallocatedStackOverflowException) { return true; } @@ -3545,10 +3545,7 @@ BOOL ExceptionIsAlwaysSwallowed(EXCEPTION_POINTERS *pExceptionInfo) OBJECTREF throwable; GCX_COOP(); - if ((throwable = pThread->GetThrowable()) == NULL) - { - throwable = pThread->LastThrownObject(); - } + throwable = pThread->GetThrowableRef(ThrowableSource::ExInfoOrLTO); //@todo: could throwable be NULL here? isSwallowed = IsExceptionOfType(kThreadAbortException, &throwable); } @@ -3908,7 +3905,7 @@ LONG InternalUnhandledExceptionFilter_Worker( { // Possibly interesting exception. Is there no Thread at all? Or, is there a Thread, // but with no exception at all on it? if ((pParam->pThread == NULL) || - (pParam->pThread->IsThrowableNull() && pParam->pThread->IsLastThrownObjectNull()) ) + pParam->pThread->IsThrowableNull(ThrowableSource::ExInfoOrLTO) ) { // Whatever this exception is, we don't know about it. Treat as Native. tore = TypeOfReportedError::NativeThreadUnhandledException; } @@ -3919,7 +3916,8 @@ LONG InternalUnhandledExceptionFilter_Worker( BOOL useLastThrownObject = FALSE; if (!pParam->fIgnore && (pParam->pThread != NULL)) { - useLastThrownObject = pParam->pThread->IsThrowableNull() && !pParam->pThread->IsLastThrownObjectNull(); + useLastThrownObject = pParam->pThread->IsThrowableNull(ThrowableSource::ExInfoOnly) && + !pParam->pThread->IsThrowableNull(ThrowableSource::LTOOnly); } #ifdef DEBUGGING_SUPPORTED @@ -4377,11 +4375,11 @@ DefaultCatchHandler(PEXCEPTION_POINTERS pExceptionPointers, } else if (useLastThrownObject) { - throwable = pThread->LastThrownObject(); + throwable = pThread->GetThrowableRef(ThrowableSource::LTOOnly); } else { - throwable = pThread->GetThrowable(); + throwable = pThread->GetThrowableRef(ThrowableSource::ExInfoOnly); } // If we've got no managed object, then we can't send an event or print a message, so we just return. @@ -4530,11 +4528,11 @@ void NotifyAppDomainsOfUnhandledException( if (useLastThrownObject) { - throwable = pThread->LastThrownObject(); + throwable = pThread->GetThrowableRef(ThrowableSource::LTOOnly); } else { - throwable = pThread->GetThrowable(); + throwable = pThread->GetThrowableRef(ThrowableSource::ExInfoOnly); } // If we've got no managed object, then we can't send an event, so we just return. @@ -4759,7 +4757,7 @@ LPVOID COMPlusCheckForAbort(UINT_PTR uTryCatchResumeAddress) if ((!pThread->IsAbortRequested()) || // if no abort has been requested (!pThread->IsRudeAbort() && - (pThread->GetThrowable() != NULL)) ) // or if there is a pending exception + !pThread->IsThrowableNull(ThrowableSource::ExInfoOnly)) ) // or if there is a pending exception { goto exit; } @@ -4774,7 +4772,7 @@ LPVOID COMPlusCheckForAbort(UINT_PTR uTryCatchResumeAddress) } // else we must produce an abort - if ((pThread->GetThrowable() == NULL) && + if (pThread->IsThrowableNull(ThrowableSource::ExInfoOnly) && (pThread->IsAbortInitiated())) { // Oops, we just swallowed an abort, must restart the process @@ -5033,7 +5031,7 @@ bool IsInterceptableException(Thread *pThread) return ((pThread != NULL) && (!pThread->IsAbortRequested()) && (pThread->IsExceptionInProgress()) && - (!pThread->IsThrowableNull()) + (!pThread->IsThrowableNull(ThrowableSource::ExInfoOnly)) #ifdef DEBUGGING_SUPPORTED && @@ -6854,7 +6852,7 @@ LONG ReflectionInvocationExceptionFilter( // Attempt to capture buckets for non-preallocated exceptions just before the ReflectionInvocation boundary { GCX_COOP(); - OBJECTREF oThrowable = GetThread()->GetThrowable(); + OBJECTREF oThrowable = GetThread()->GetThrowableRef(ThrowableSource::ExInfoOnly); if ((oThrowable != NULL) && (CLRException::IsPreallocatedExceptionObject(oThrowable) == FALSE)) { SetupWatsonBucketsForNonPreallocatedExceptions(); @@ -7168,7 +7166,7 @@ BOOL SetupWatsonBucketsForNonPreallocatedExceptions(OBJECTREF oThrowable /* = NU GCPROTECT_BEGIN(gc); // Get the throwable to be used - gc.oThrowable = (oThrowable != NULL) ? oThrowable : pThread->GetThrowable(); + gc.oThrowable = (oThrowable != NULL) ? oThrowable : pThread->GetThrowableRef(ThrowableSource::ExInfoOnly); if (gc.oThrowable == NULL) { // If we have no throwable, then simply return back. @@ -7292,7 +7290,7 @@ BOOL SetupWatsonBucketsForEscapingPreallocatedExceptions() GCPROTECT_BEGIN(gc); // Get the throwable corresponding to the escaping exception - gc.oThrowable = pThread->GetThrowable(); + gc.oThrowable = pThread->GetThrowableRef(ThrowableSource::ExInfoOnly); if (gc.oThrowable == NULL) { // If we have no throwable, then simply return back. @@ -7444,7 +7442,8 @@ void SetupWatsonBucketsForUEF(BOOL fUseLastThrownObject) gc.oBuckets = NULL; GCPROTECT_BEGIN(gc); - gc.oThrowable = fUseLastThrownObject ? pThread->LastThrownObject() : pThread->GetThrowable(); + gc.oThrowable = fUseLastThrownObject ? pThread->GetThrowableRef(ThrowableSource::LTOOnly) + : pThread->GetThrowableRef(ThrowableSource::ExInfoOnly); BOOL fThrowableExists = (gc.oThrowable != NULL); BOOL fIsThrowablePreallocated = !fThrowableExists ? FALSE : CLRException::IsPreallocatedExceptionObject(gc.oThrowable); @@ -8523,7 +8522,7 @@ void CopyWatsonBucketsBetweenThrowables(U1ARRAYREF oManagedWatsonBuckets, OBJECT GCPROTECT_BEGIN(_gc); _gc.oSourceWatsonBuckets = oManagedWatsonBuckets; - _gc.oTo = (oThrowableTo == NULL)?GetThread()->GetThrowable():oThrowableTo; + _gc.oTo = (oThrowableTo == NULL) ? GetThread()->GetThrowableRef(ThrowableSource::ExInfoOnly) : oThrowableTo; _ASSERTE(_gc.oTo != NULL); // The target throwable to which Watson buckets are going to be copied @@ -8596,7 +8595,7 @@ BOOL CopyWatsonBucketsToThrowable(PTR_VOID pUnmanagedBuckets, OBJECTREF oTargetT THROWS; PRECONDITION(GetThreadNULLOk() != NULL); PRECONDITION(pUnmanagedBuckets != NULL); - PRECONDITION(!CLRException::IsPreallocatedExceptionObject((oTargetThrowable == NULL)?GetThread()->GetThrowable():oTargetThrowable)); + PRECONDITION(!CLRException::IsPreallocatedExceptionObject((oTargetThrowable == NULL) ? GetThread()->GetThrowableRef(ThrowableSource::ExInfoOnly) : oTargetThrowable)); PRECONDITION(IsWatsonEnabled()); } CONTRACTL_END; @@ -8610,7 +8609,7 @@ BOOL CopyWatsonBucketsToThrowable(PTR_VOID pUnmanagedBuckets, OBJECTREF oTargetT ZeroMemory(&_gc, sizeof(_gc)); GCPROTECT_BEGIN(_gc); - _gc.oThrowable = (oTargetThrowable == NULL)?GetThread()->GetThrowable():oTargetThrowable; + _gc.oThrowable = (oTargetThrowable == NULL) ? GetThread()->GetThrowableRef(ThrowableSource::ExInfoOnly) : oTargetThrowable; // Throwable to which buckets should be copied to, must exist. _ASSERTE(_gc.oThrowable != NULL); @@ -8712,7 +8711,7 @@ void SetStateForWatsonBucketing(BOOL fIsRethrownException, OBJECTHANDLE ohOrigin _ASSERTE(pCurExState->GetCurrentExceptionTracker() != NULL); // Get the current throwable - gc.oCurrentThrowable = pThread->GetThrowable(); + gc.oCurrentThrowable = pThread->GetThrowableRef(ThrowableSource::ExInfoOnly); _ASSERTE(gc.oCurrentThrowable != NULL); // Is the throwable a preallocated exception object? diff --git a/src/coreclr/vm/exceptionhandling.cpp b/src/coreclr/vm/exceptionhandling.cpp index a48ab5013b17cc..e1e27774d01347 100644 --- a/src/coreclr/vm/exceptionhandling.cpp +++ b/src/coreclr/vm/exceptionhandling.cpp @@ -778,7 +778,7 @@ OBJECTREF ExInfo::CreateThrowable( if ((!bAsynchronousThreadStop) && IsComPlusException(pExceptionRecord)) { - oThrowable = pThread->LastThrownObject(); + oThrowable = pThread->GetThrowableRef(ThrowableSource::LTOOnly); } else { @@ -2145,6 +2145,7 @@ CallDescrWorkerUnwindFrameChainHandler(IN PEXCEPTION_RECORD pExceptionReco } else if (IS_UNWINDING(pExceptionRecord->ExceptionFlags)) { + GCX_COOP(); CleanUpForSecondPass(pThread, false, pEstablisherFrame, pEstablisherFrame); } diff --git a/src/coreclr/vm/interpexec.cpp b/src/coreclr/vm/interpexec.cpp index f6e8947984a19d..b36cdeb68592e0 100644 --- a/src/coreclr/vm/interpexec.cpp +++ b/src/coreclr/vm/interpexec.cpp @@ -241,7 +241,7 @@ std::invoke_result_t CallWithSEHWrapper(Function function) // The managed ones are represented by SEH exception, which cannot be handled there // because it is not possible to handle both SEH and C++ exceptions in the same frame. GCX_COOP_NO_DTOR(); - OBJECTREF ohThrowable = GetThread()->LastThrownObject(); + OBJECTREF ohThrowable = GetThread()->GetThrowableRef(ThrowableSource::ExInfoOrLTO); DispatchManagedException(ohThrowable); } PAL_ENDTRY @@ -278,7 +278,7 @@ void InvokeUnmanagedMethodWithTransition(UnmanagedMethodWithTransitionParam *pPa // The managed ones are represented by SEH exception, which cannot be handled there // because it is not possible to handle both SEH and C++ exceptions in the same frame. GCX_COOP_NO_DTOR(); - OBJECTREF ohThrowable = GetThread()->LastThrownObject(); + OBJECTREF ohThrowable = GetThread()->GetThrowableRef(ThrowableSource::LTOOnly); DispatchManagedException(ohThrowable); } PAL_ENDTRY diff --git a/src/coreclr/vm/prestub.cpp b/src/coreclr/vm/prestub.cpp index d5e7ee2d2893b9..72e0a01712a8e9 100644 --- a/src/coreclr/vm/prestub.cpp +++ b/src/coreclr/vm/prestub.cpp @@ -1933,7 +1933,7 @@ extern "C" PCODE STDCALL PreStubWorker(TransitionBlock* pTransitionBlock, Method } EX_CATCH { - OBJECTHANDLE ohThrowable = CURRENT_THREAD->LastThrownObjectHandle(); + OBJECTHANDLE ohThrowable = CURRENT_THREAD->GetThrowableHandle(ThrowableSource::LTOOnly); _ASSERTE(ohThrowable); StackTraceInfo::AppendElement(ObjectFromHandle(ohThrowable), 0, (UINT_PTR)pTransitionBlock, pMD, NULL); EX_RETHROW; @@ -2136,7 +2136,7 @@ void ExecuteInterpretedMethodWithArgs_PortableEntryPoint_Complex(PCODE portableE } EX_CATCH { - OBJECTHANDLE ohThrowable = CURRENT_THREAD->LastThrownObjectHandle(); + OBJECTHANDLE ohThrowable = CURRENT_THREAD->GetThrowableHandle(ThrowableSource::LTOOnly); _ASSERTE(ohThrowable); if (finishedPrestubPortion) { diff --git a/src/coreclr/vm/threads.cpp b/src/coreclr/vm/threads.cpp index 9d9cf0ff092889..daf945b08efb4c 100644 --- a/src/coreclr/vm/threads.cpp +++ b/src/coreclr/vm/threads.cpp @@ -3196,6 +3196,187 @@ void Thread::SetSOForLastThrownObject() m_LastThrownObjectHandle = CLRException::GetPreallocatedStackOverflowExceptionHandle(); } +#endif // !DACCESS_COMPILE + +// Source-explicit handle accessor. Returns whichever handle the caller asked +// for, or (OBJECTHANDLE)NULL if no source matched. See ThrowableSource for +// the meaning of each value. +OBJECTHANDLE Thread::GetThrowableHandle(ThrowableSource source) +{ + LIMITED_METHOD_CONTRACT; + SUPPORTS_DAC; + + OBJECTHANDLE exInfoHandle = (OBJECTHANDLE)NULL; + bool considerExInfo = false; + bool considerLTO = false; + bool ltoRequiresUnhandled = false; + + switch (source) + { + case ThrowableSource::ExInfoOnly: + considerExInfo = true; + break; + case ThrowableSource::LTOOnly: + considerLTO = true; + break; + case ThrowableSource::ExInfoOrLTO: + considerExInfo = true; + considerLTO = true; + break; + case ThrowableSource::LTOIfUnhandled: + considerLTO = true; + ltoRequiresUnhandled = true; + break; + case ThrowableSource::ExInfoOrLTOIfUnhandled: + considerExInfo = true; + considerLTO = true; + ltoRequiresUnhandled = true; + break; + } + + if (considerExInfo) + { + exInfoHandle = m_ExceptionState.GetThrowableAsPseudoHandle(); + if (exInfoHandle != (OBJECTHANDLE)NULL) + { + return exInfoHandle; + } + } + + if (considerLTO) + { + if (ltoRequiresUnhandled && !m_ltoIsUnhandled) + { + return (OBJECTHANDLE)NULL; + } + return m_LastThrownObjectHandle; + } + + return (OBJECTHANDLE)NULL; +} + +// Source-explicit OBJECTREF accessor. Materializes the chosen source as an +// OBJECTREF; requires MODE_COOPERATIVE because non-NULL OBJECTREFs are only +// safe to construct under cooperative GC mode. Use GetThrowableHandle when +// MODE_ANY is required. +OBJECTREF Thread::GetThrowableRef(ThrowableSource source){ + CONTRACTL + { + NOTHROW; + GC_NOTRIGGER; + MODE_COOPERATIVE; + } + CONTRACTL_END; + + bool considerExInfo = false; + bool considerLTO = false; + bool ltoRequiresUnhandled = false; + + switch (source) + { + case ThrowableSource::ExInfoOnly: + considerExInfo = true; + break; + case ThrowableSource::LTOOnly: + considerLTO = true; + break; + case ThrowableSource::ExInfoOrLTO: + considerExInfo = true; + considerLTO = true; + break; + case ThrowableSource::LTOIfUnhandled: + considerLTO = true; + ltoRequiresUnhandled = true; + break; + case ThrowableSource::ExInfoOrLTOIfUnhandled: + considerExInfo = true; + considerLTO = true; + ltoRequiresUnhandled = true; + break; + } + + if (considerExInfo) + { + OBJECTREF exInfoThrowable = m_ExceptionState.GetThrowable(); + if (exInfoThrowable != NULL) + { + return exInfoThrowable; + } + } + + if (considerLTO) + { + if (ltoRequiresUnhandled && !m_ltoIsUnhandled) + { + return NULL; + } + if (m_LastThrownObjectHandle == (OBJECTHANDLE)NULL) + { + return NULL; + } + // We only have a handle if we have an object to keep in it. + _ASSERTE(ObjectFromHandle(m_LastThrownObjectHandle) != NULL); + return ObjectFromHandle(m_LastThrownObjectHandle); + } + + return NULL; +} + +// Source-explicit null predicate. Returns TRUE iff the chosen source has no +// throwable to return. Safe to call in MODE_ANY (including preemptive GC +// mode) and from DAC: the underlying checks are pointer-value comparisons +// (handle == NULL, m_pCurrentTracker == NULL, m_exception == NULL) and a +// flag read; no OBJECTREF is materialized. +BOOL Thread::IsThrowableNull(ThrowableSource source) +{ + LIMITED_METHOD_DAC_CONTRACT; + + bool considerExInfo = false; + bool considerLTO = false; + bool ltoRequiresUnhandled = false; + + switch (source) + { + case ThrowableSource::ExInfoOnly: + considerExInfo = true; + break; + case ThrowableSource::LTOOnly: + considerLTO = true; + break; + case ThrowableSource::ExInfoOrLTO: + considerExInfo = true; + considerLTO = true; + break; + case ThrowableSource::LTOIfUnhandled: + considerLTO = true; + ltoRequiresUnhandled = true; + break; + case ThrowableSource::ExInfoOrLTOIfUnhandled: + considerExInfo = true; + considerLTO = true; + ltoRequiresUnhandled = true; + break; + } + + if (considerExInfo && !m_ExceptionState.IsThrowableNull()) + { + return FALSE; + } + + if (considerLTO) + { + if (ltoRequiresUnhandled && !m_ltoIsUnhandled) + { + return TRUE; + } + return m_LastThrownObjectHandle == (OBJECTHANDLE)NULL; + } + + return TRUE; +} + +#ifndef DACCESS_COMPILE + // Background threads must be counted, because the EE should shut down when the // last non-background thread terminates. But we only count running ones. void Thread::SetBackground(BOOL isBack) @@ -6549,7 +6730,7 @@ extern "C" InterpThreadContext* STDCALL GetInterpThreadContextWithPossiblyMissin } EX_CATCH { - OBJECTHANDLE ohThrowable = CURRENT_THREAD->LastThrownObjectHandle(); + OBJECTHANDLE ohThrowable = CURRENT_THREAD->GetThrowableHandle(ThrowableSource::LTOOnly); _ASSERTE(ohThrowable); StackTraceInfo::AppendElement(ObjectFromHandle(ohThrowable), 0, (UINT_PTR)pTransitionBlock, pByteCodeStart->Method->methodHnd, NULL); EX_RETHROW; diff --git a/src/coreclr/vm/threads.h b/src/coreclr/vm/threads.h index c0b9beede422bd..857dbecc845cbf 100644 --- a/src/coreclr/vm/threads.h +++ b/src/coreclr/vm/threads.h @@ -429,6 +429,40 @@ typedef DPTR(struct gc_alloc_context) PTR_gc_alloc_context; // // A code:Thread contains all the per-thread information needed by the runtime. We can get this // structure through the OS TLS slot see code:#RuntimeThreadLocals for more information. + +// Names the source from which Thread::GetThrowableRef / GetThrowableHandle should +// retrieve the current exception object. After the lazy-LTO change, the active +// ExInfo is the source of truth during dispatch and the LastThrownObject (LTO) +// field may lag or hold a synthetic/fatal value. Each reader must say which +// view it wants instead of relying on an implicit invariant. +enum class ThrowableSource +{ + // Only consult the active ExInfo (m_ExceptionState). NULL if none. + // Use when the in-flight dispatch is the only acceptable answer + // (e.g. GC stack scan, dispatch-time consumers). + ExInfoOnly, + + // Only consult the LTO field. Returns whatever LTO holds, including stale + // or synthetic values. Use for Watson / fatal-error reporters that want the + // LTO state literally. + LTOOnly, + + // ExInfo first; fall back to LTO if no active ExInfo. Replicates the + // pre-lazy "LTO was always coherent" reader behavior. Default replacement + // for callers that previously read LTO assuming eager sync. + ExInfoOrLTO, + + // LTO only when m_ltoIsUnhandled is set; otherwise NULL. Use for + // "did the process die with an unhandled exception" probes (DBI's + // HasUnhandledException / GetCurrentException flavors). + LTOIfUnhandled, + + // ExInfo first; then LTO only if m_ltoIsUnhandled. Avoids the mid-dispatch + // stale-LTO trap when the in-flight ExInfo is the canonical source but a + // pre-EH-init fatal may have populated LTO. + ExInfoOrLTOIfUnhandled, +}; + class Thread { friend class ThreadStore; @@ -1434,34 +1468,6 @@ class Thread // Last exception to be thrown //--------------------------------------------------------------- - OBJECTREF GetThrowable() - { - WRAPPER_NO_CONTRACT; - - return m_ExceptionState.GetThrowable(); - } - - BOOL HasException() - { - LIMITED_METHOD_CONTRACT; - return !IsThrowableNull(); - } - - // See ExInfo::GetThrowableAsPseudoHandle for details on the pseudo-handle. - OBJECTHANDLE GetThrowableAsPseudoHandle() - { - LIMITED_METHOD_DAC_CONTRACT; - - return m_ExceptionState.GetThrowableAsPseudoHandle(); - } - - // special null test (for use when we're in the wrong GC mode) - BOOL IsThrowableNull() - { - WRAPPER_NO_CONTRACT; - return m_ExceptionState.IsThrowableNull(); - } - BOOL IsExceptionInProgress() { SUPPORTS_DAC; @@ -2622,14 +2628,14 @@ class Thread // CLRLastThrownObjectException::CreateThrowable and other EX_CATCH consumers. // // LTO is NOT kept in sync with ExInfo::m_exception during active exception - // dispatch. While an ExInfo is alive, m_exception is the source of truth - - // use GetThrowable() or GetThrowableAsPseudoHandle() to read it. LTO is set - // lazily by PopExInfos just before each ExInfo is destroyed, and by - // RaiseTheExceptionInternalOnly before the ExInfo is created. + // dispatch. While an ExInfo is alive, m_exception is the source of truth. + // LTO is set lazily by PopExInfos just before each ExInfo is destroyed, and + // by RaiseTheExceptionInternalOnly before the ExInfo is created. // // LTO may be stale during active dispatch. Readers that need the current - // exception should call GetThrowable() first and fall back to LastThrownObject() - // only when GetThrowable() returns NULL. + // exception should use Thread::GetThrowableHandle / GetThrowableRef / + // IsThrowableNull with a ThrowableSource that captures their intent + // (typically ExInfoOrLTO for "current exception"). OBJECTHANDLE m_LastThrownObjectHandle; // Indicates that the throwable in m_lastThrownObjectHandle should be treated as @@ -2641,30 +2647,16 @@ class Thread public: - BOOL IsLastThrownObjectNull() { WRAPPER_NO_CONTRACT; return (m_LastThrownObjectHandle == (OBJECTHANDLE)0); } - - OBJECTREF LastThrownObject() - { - WRAPPER_NO_CONTRACT; - - if (m_LastThrownObjectHandle == (OBJECTHANDLE)0) - { - return NULL; - } - else - { - // We only have a handle if we have an object to keep in it. - _ASSERTE(ObjectFromHandle(m_LastThrownObjectHandle) != NULL); - return ObjectFromHandle(m_LastThrownObjectHandle); - } - } - - OBJECTHANDLE LastThrownObjectHandle() - { - LIMITED_METHOD_DAC_CONTRACT; - - return m_LastThrownObjectHandle; - } + // Source-explicit accessors. See the ThrowableSource enum for the meaning + // of each value. These are the canonical way to retrieve the "current" + // exception object: the caller names which view it wants instead of + // relying on the (lazy) LTO field being coherent with the active ExInfo. + // GetThrowableRef requires MODE_COOPERATIVE for a non-NULL result; + // GetThrowableHandle and IsThrowableNull are mode-agnostic and safe in + // preemptive contexts and from DAC. + OBJECTHANDLE GetThrowableHandle(ThrowableSource source); + OBJECTREF GetThrowableRef (ThrowableSource source); + BOOL IsThrowableNull (ThrowableSource source); // Sets the last thrown object. If the throwable cannot be tracked due to OOM, sets the // last thrown object to the preallocated OOM exception and returns it instead of the @@ -2687,14 +2679,6 @@ class Thread return m_ltoIsUnhandled; } - bool IsLastThrownObjectStackOverflowException() - { - LIMITED_METHOD_CONTRACT; - CONSISTENCY_CHECK(NULL != g_pPreallocatedStackOverflowException); - - return (m_LastThrownObjectHandle == g_pPreallocatedStackOverflowException); - } - // get the current notification (if any) from this thread OBJECTHANDLE GetThreadCurrNotification(); diff --git a/src/coreclr/vm/threadsuspend.cpp b/src/coreclr/vm/threadsuspend.cpp index f265094ce9876a..d70ec6c34e50bd 100644 --- a/src/coreclr/vm/threadsuspend.cpp +++ b/src/coreclr/vm/threadsuspend.cpp @@ -1526,7 +1526,7 @@ Thread::UserAbort(EEPolicy::ThreadAbortTypes abortType, DWORD timeout) // we're leaving managed code anyway. The top-most handler is responsible for resetting // the bit. // - if (HasException() && + if (!IsThrowableNull(ThrowableSource::ExInfoOnly) && // For rude abort, we will initiated abort !IsRudeAbort()) {