From a8f273932a7fdaea61913d3cbae883ef3811308f Mon Sep 17 00:00:00 2001 From: Tom McDonald Date: Thu, 19 Mar 2026 11:32:00 -0400 Subject: [PATCH 1/4] Move FuncEvalFrame/DebuggerU2MCatchHandlerFrame detection to SfiNextWorker The FuncEvalFrame and DebuggerU2MCatchHandlerFrame detection in NotifyExceptionPassStarted relied on stale StackFrameIterator state from the end of pass 1. The iterator is no longer guaranteed to be in SFITER_FRAME_FUNCTION state at that point, causing the debugger to never receive the CATCH_HANDLER_FOUND notification during func eval. Move the check to SfiNextWorker where it runs during pass 1 at the native transition boundary with live iterator state and direct access to the frame chain. This matches the original design intent from PR #102470. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/exceptionhandling.cpp | 43 +++++++++++++++++----------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/src/coreclr/vm/exceptionhandling.cpp b/src/coreclr/vm/exceptionhandling.cpp index 6473f4d069958c..d63d8ab1ede9e8 100644 --- a/src/coreclr/vm/exceptionhandling.cpp +++ b/src/coreclr/vm/exceptionhandling.cpp @@ -3653,23 +3653,9 @@ static void NotifyExceptionPassStarted(StackFrameIterator *pThis, Thread *pThrea } else { - // The debugger explicitly checks that the notification refers to a FuncEvalFrame in case an exception becomes unhandled in a func eval. - // We need to do the notification here before we start propagating the exception through native frames, since that will remove - // all managed frames from the stack and the debugger would not see the failure location. - if (pThis->GetFrameState() == StackFrameIterator::SFITER_FRAME_FUNCTION) - { - Frame* pFrame = pThis->m_crawl.GetFrame(); - // If the frame is ProtectValueClassFrame, move to the next one as we want to report the FuncEvalFrame - if (pFrame->GetFrameIdentifier() == FrameIdentifier::ProtectValueClassFrame) - { - pFrame = pFrame->PtrNextFrame(); - _ASSERTE(pFrame != FRAME_TOP); - } - if ((pFrame->GetFrameIdentifier() == FrameIdentifier::FuncEvalFrame) || IsTopmostDebuggerU2MCatchHandlerFrame(pFrame)) - { - EEToDebuggerExceptionInterfaceWrapper::NotifyOfCHFFilter((EXCEPTION_POINTERS *)&pExInfo->m_ptrs, pFrame); - } - } + // FuncEvalFrame / DebuggerU2MCatchHandlerFrame detection has been moved to SfiNextWorker + // where it runs during pass 1 at the native transition boundary with live iterator state. + // See the DEBUGGING_SUPPORTED block in SfiNextWorker's isPropagatingToNativeCode path. } } } @@ -4035,6 +4021,29 @@ CLR_BOOL SfiNextWorker(StackFrameIterator* pThis, uint* uExCollideClauseIdx, CLR } } + // When the exception is propagating to native code during pass 1, notify the debugger if the next + // explicit frame is a FuncEvalFrame or DebuggerU2MCatchHandlerFrame. The debugger needs this notification + // while the managed stack frames are still present so it can inspect the failure location. + // This was previously done in NotifyExceptionPassStarted using stale StackFrameIterator state, but the + // iterator is no longer guaranteed to be in SFITER_FRAME_FUNCTION state at that point. Here, we have + // live iterator state and direct access to the frame chain. +#if defined(DEBUGGING_SUPPORTED) + if (pTopExInfo->m_passNumber == 1 && pFrame != FRAME_TOP) + { + Frame* pNotifyFrame = pFrame; + // Skip ProtectValueClassFrame — we want to report the FuncEvalFrame behind it + if (pNotifyFrame->GetFrameIdentifier() == FrameIdentifier::ProtectValueClassFrame) + { + pNotifyFrame = pNotifyFrame->PtrNextFrame(); + _ASSERTE(pNotifyFrame != FRAME_TOP); + } + if ((pNotifyFrame->GetFrameIdentifier() == FrameIdentifier::FuncEvalFrame) || IsTopmostDebuggerU2MCatchHandlerFrame(pNotifyFrame)) + { + EEToDebuggerExceptionInterfaceWrapper::NotifyOfCHFFilter((EXCEPTION_POINTERS *)&pTopExInfo->m_ptrs, pNotifyFrame); + } + } +#endif // DEBUGGING_SUPPORTED + *pfIsExceptionIntercepted = FALSE; if (fUnwoundReversePInvoke) From 3dfca14d2dee6304dfa1f6029ef752ca87b7e3ea Mon Sep 17 00:00:00 2001 From: Tom McDonald Date: Sun, 3 May 2026 19:34:13 -0400 Subject: [PATCH 2/4] Update src/coreclr/vm/exceptionhandling.cpp Co-authored-by: Jan Kotas --- src/coreclr/vm/exceptionhandling.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/coreclr/vm/exceptionhandling.cpp b/src/coreclr/vm/exceptionhandling.cpp index d63d8ab1ede9e8..3ebdc2fff428ba 100644 --- a/src/coreclr/vm/exceptionhandling.cpp +++ b/src/coreclr/vm/exceptionhandling.cpp @@ -3653,10 +3653,6 @@ static void NotifyExceptionPassStarted(StackFrameIterator *pThis, Thread *pThrea } else { - // FuncEvalFrame / DebuggerU2MCatchHandlerFrame detection has been moved to SfiNextWorker - // where it runs during pass 1 at the native transition boundary with live iterator state. - // See the DEBUGGING_SUPPORTED block in SfiNextWorker's isPropagatingToNativeCode path. - } } } From 5e376f19562b3f0fc8ff80ee0930913fec49a9c4 Mon Sep 17 00:00:00 2001 From: Tom McDonald Date: Sun, 3 May 2026 19:40:51 -0400 Subject: [PATCH 3/4] fix compilation issue --- src/coreclr/vm/exceptionhandling.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/coreclr/vm/exceptionhandling.cpp b/src/coreclr/vm/exceptionhandling.cpp index 3ebdc2fff428ba..61b6a237314c8f 100644 --- a/src/coreclr/vm/exceptionhandling.cpp +++ b/src/coreclr/vm/exceptionhandling.cpp @@ -3651,8 +3651,6 @@ static void NotifyExceptionPassStarted(StackFrameIterator *pThis, Thread *pThrea pExInfo->m_ExceptionFlags.SetUnwindHasStarted(); EEToDebuggerExceptionInterfaceWrapper::ManagedExceptionUnwindBegin(pThread); } - else - { } } From 0d2dd5ee17552b7efaf4af058b206e9d500c5069 Mon Sep 17 00:00:00 2001 From: Jan Kotas Date: Sun, 3 May 2026 16:51:06 -0700 Subject: [PATCH 4/4] Apply suggestion from @jkotas --- src/coreclr/vm/exceptionhandling.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/coreclr/vm/exceptionhandling.cpp b/src/coreclr/vm/exceptionhandling.cpp index 61b6a237314c8f..5764548e50cf7d 100644 --- a/src/coreclr/vm/exceptionhandling.cpp +++ b/src/coreclr/vm/exceptionhandling.cpp @@ -4018,9 +4018,6 @@ CLR_BOOL SfiNextWorker(StackFrameIterator* pThis, uint* uExCollideClauseIdx, CLR // When the exception is propagating to native code during pass 1, notify the debugger if the next // explicit frame is a FuncEvalFrame or DebuggerU2MCatchHandlerFrame. The debugger needs this notification // while the managed stack frames are still present so it can inspect the failure location. - // This was previously done in NotifyExceptionPassStarted using stale StackFrameIterator state, but the - // iterator is no longer guaranteed to be in SFITER_FRAME_FUNCTION state at that point. Here, we have - // live iterator state and direct access to the frame chain. #if defined(DEBUGGING_SUPPORTED) if (pTopExInfo->m_passNumber == 1 && pFrame != FRAME_TOP) {