From 291131f462f2c1155cc538d99a8d60168fb00b2b Mon Sep 17 00:00:00 2001 From: Milos Kotlar Date: Wed, 15 Apr 2026 16:00:08 +0200 Subject: [PATCH 1/4] Fix interpreter exception handling for debugger interception - Skip controlPC -1 adjustment for interpreter methods (bytecode IP already points to the correct instruction boundary) - Detect native transitions past last interpreter frame in SfiNext - Skip InterpreterFrame chain entries when checking for unhandled exceptions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/exceptionhandling.cpp | 36 +++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/coreclr/vm/exceptionhandling.cpp b/src/coreclr/vm/exceptionhandling.cpp index 00f6db4a17d612..f663115b86b332 100644 --- a/src/coreclr/vm/exceptionhandling.cpp +++ b/src/coreclr/vm/exceptionhandling.cpp @@ -3415,6 +3415,7 @@ extern "C" CLR_BOOL QCALLTYPE EHEnumInitFromStackFrameIterator(StackFrameIterato pJitMan->JitTokenToMethodRegionInfo(MethToken, pMethodRegionInfo); pFrameIter->UpdateIsRuntimeWrappedExceptions(); + return TRUE; } @@ -3832,10 +3833,16 @@ CLR_BOOL SfiInitWorker(StackFrameIterator* pThis, CONTEXT* pStackwalkCtx, CLR_BO if (!pThis->m_crawl.HasFaulted() && !pThis->m_crawl.IsIPadjusted()) { - controlPC -= STACKWALK_CONTROLPC_ADJUST_OFFSET; +#ifdef FEATURE_INTERPRETER + // Skip controlPC adjustment for interpreter methods — the bytecode IP + // already points to the correct instruction boundary for EH clause matching. + if (!pThis->m_crawl.GetCodeInfo()->IsInterpretedCode()) +#endif // FEATURE_INTERPRETER + { + controlPC -= STACKWALK_CONTROLPC_ADJUST_OFFSET; + } } pThis->SetAdjustedControlPC(controlPC); - *pfIsExceptionIntercepted = CheckExceptionInterception(pThis, pExInfo); EH_LOG((LL_INFO100, "SfiInit (pass %d): Exception stack walking starting at IP=%p, SP=%p, method %s::%s\n", pExInfo->m_passNumber, controlPC, GetRegdisplaySP(pThis->m_crawl.GetRegisterSet()), @@ -3992,12 +3999,29 @@ CLR_BOOL SfiNextWorker(StackFrameIterator* pThis, uint* uExCollideClauseIdx, CLR EH_LOG((LL_INFO100, "SfiNext: current frame is filter funclet\n")); isPropagatingToNativeCode = TRUE; } +#ifdef FEATURE_INTERPRETER + // Detect interpreter-to-native transition past the last interpreted frame. + else if (codeInfo.IsInterpretedCode()) + { + EH_LOG((LL_INFO100, "SfiNext: native transition past last interpreter frame\n")); + isPropagatingToNativeCode = TRUE; + } +#endif // FEATURE_INTERPRETER } if (isPropagatingToNativeCode) { pFrame = pThis->m_crawl.GetFrame(); +#ifdef FEATURE_INTERPRETER + // Skip past InterpreterFrames so the unhandled-exception check + // reaches DebuggerU2MCatchHandlerFrame or FRAME_TOP. + while (pFrame != FRAME_TOP && pFrame->GetFrameIdentifier() == FrameIdentifier::InterpreterFrame) + { + pFrame = pFrame->PtrNextFrame(); + } +#endif // FEATURE_INTERPRETER + // Check if there are any further managed frames on the stack or a catch for all exceptions in native code (marked by // DebuggerU2MCatchHandlerFrame with CatchesAllExceptions() returning true). // If not, the exception is unhandled. @@ -4134,7 +4158,12 @@ Exit:; TADDR controlPC = pThis->m_crawl.GetRegisterSet()->ControlPC; if (!pThis->m_crawl.HasFaulted() && !pThis->m_crawl.IsIPadjusted()) { - controlPC -= STACKWALK_CONTROLPC_ADJUST_OFFSET; +#ifdef FEATURE_INTERPRETER + if (!pThis->m_crawl.GetCodeInfo()->IsInterpretedCode()) +#endif // FEATURE_INTERPRETER + { + controlPC -= STACKWALK_CONTROLPC_ADJUST_OFFSET; + } } pThis->SetAdjustedControlPC(controlPC); @@ -4301,6 +4330,7 @@ void DECLSPEC_NORETURN DispatchExSecondPass(ExInfo *pExInfo) pExInfo->m_passNumber = 2; uint startIdx = MaxTryRegionIdx; uint catchingTryRegionIdx = pExInfo->m_idxCurClause; + CLR_BOOL unwoundReversePInvoke = false; CLR_BOOL isExceptionIntercepted = false; CLR_BOOL isValid = SfiInitWorker(pFrameIter, pExInfo->m_pExContext, ((uint8_t)pExInfo->m_kind & (uint8_t)ExKind::InstructionFaultFlag) != 0, &isExceptionIntercepted); From d77ff60aacab5b78fb32fd4f306cfc175df7fab3 Mon Sep 17 00:00:00 2001 From: Milos Kotlar Date: Wed, 22 Apr 2026 15:15:20 +0200 Subject: [PATCH 2/4] Apply -1 controlPC adjustment uniformly for interp; narrow InterpreterFrame skip - Remove the IsInterpretedCode() skip around controlPC -= STACKWALK_CONTROLPC_ADJUST_OFFSET at both SfiInitWorker and SfiNextWorker exit. The throw-site case is already m_crawl.hasFaulted). The remaining propagation case needs the -1 because pFrame->ip is advanced past the call opcode before descending into the callee, same as a JIT return address, and EH clauses are half-open [tryStart, tryEnd). - Change the InterpreterFrame skip in the isPropagatingToNativeCode branch from while to if. Only the one InterpreterFrame pushed by the just-unwound InterpExecMethod needs bridging; consuming adjacent InterpreterFrames could skip past an outer interpreted catch and incorrectly trigger the unhandled exception path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/exceptionhandling.cpp | 38 +++++++++++++++------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/coreclr/vm/exceptionhandling.cpp b/src/coreclr/vm/exceptionhandling.cpp index f663115b86b332..d5d00fe0bc6032 100644 --- a/src/coreclr/vm/exceptionhandling.cpp +++ b/src/coreclr/vm/exceptionhandling.cpp @@ -3833,14 +3833,14 @@ CLR_BOOL SfiInitWorker(StackFrameIterator* pThis, CONTEXT* pStackwalkCtx, CLR_BO if (!pThis->m_crawl.HasFaulted() && !pThis->m_crawl.IsIPadjusted()) { -#ifdef FEATURE_INTERPRETER - // Skip controlPC adjustment for interpreter methods — the bytecode IP - // already points to the correct instruction boundary for EH clause matching. - if (!pThis->m_crawl.GetCodeInfo()->IsInterpretedCode()) -#endif // FEATURE_INTERPRETER - { - controlPC -= STACKWALK_CONTROLPC_ADJUST_OFFSET; - } + // For interpreter methods the same -1 adjustment applies: pFrame->ip is + // advanced past the call opcode before descending into the callee + // (see interpexec.cpp, e.g. `ip += 4; pFrame->ip = ip;`), so on + // propagation we need to land back inside the call site for the + // half-open EH clause match. The faulting case (INTOP_THROW / + // INTOP_RETHROW / hardware fault) is handled by !HasFaulted() above, + // which is set via InterpreterFrame::m_isFaulting -> CONTEXT_EXCEPTION_ACTIVE. + controlPC -= STACKWALK_CONTROLPC_ADJUST_OFFSET; } pThis->SetAdjustedControlPC(controlPC); *pfIsExceptionIntercepted = CheckExceptionInterception(pThis, pExInfo); @@ -4014,9 +4014,16 @@ CLR_BOOL SfiNextWorker(StackFrameIterator* pThis, uint* uExCollideClauseIdx, CLR pFrame = pThis->m_crawl.GetFrame(); #ifdef FEATURE_INTERPRETER - // Skip past InterpreterFrames so the unhandled-exception check - // reaches DebuggerU2MCatchHandlerFrame or FRAME_TOP. - while (pFrame != FRAME_TOP && pFrame->GetFrameIdentifier() == FrameIdentifier::InterpreterFrame) + // The current explicit frame is the InterpreterFrame pushed by the + // last InterpExecMethod we just unwound out of. Skip exactly that + // one entry so the unhandled-exception check below can see the + // DebuggerU2MCatchHandlerFrame / FRAME_TOP that actually terminates + // the chain. Do NOT use a while-loop here: if two InterpreterFrames + // happen to be adjacent in the explicit frame chain (nested + // InterpExecMethod with no intervening explicit frame), skipping + // past both would bypass an outer interpreted catch and cause the + // unhandled-exception path to fire incorrectly. + if (pFrame != FRAME_TOP && pFrame->GetFrameIdentifier() == FrameIdentifier::InterpreterFrame) { pFrame = pFrame->PtrNextFrame(); } @@ -4158,12 +4165,9 @@ Exit:; TADDR controlPC = pThis->m_crawl.GetRegisterSet()->ControlPC; if (!pThis->m_crawl.HasFaulted() && !pThis->m_crawl.IsIPadjusted()) { -#ifdef FEATURE_INTERPRETER - if (!pThis->m_crawl.GetCodeInfo()->IsInterpretedCode()) -#endif // FEATURE_INTERPRETER - { - controlPC -= STACKWALK_CONTROLPC_ADJUST_OFFSET; - } + // Same reasoning as in SfiInitWorker: interp frames also need -1 + // for half-open EH clause matching on propagation through a call. + controlPC -= STACKWALK_CONTROLPC_ADJUST_OFFSET; } pThis->SetAdjustedControlPC(controlPC); From 1eb28385cdc2923b1aefe423cc433969ea4f0341 Mon Sep 17 00:00:00 2001 From: Milos Kotlar Date: Wed, 22 Apr 2026 15:25:53 +0200 Subject: [PATCH 3/4] Remove explanatory comments Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/exceptionhandling.cpp | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/coreclr/vm/exceptionhandling.cpp b/src/coreclr/vm/exceptionhandling.cpp index d5d00fe0bc6032..a5e755c98f48df 100644 --- a/src/coreclr/vm/exceptionhandling.cpp +++ b/src/coreclr/vm/exceptionhandling.cpp @@ -3833,13 +3833,6 @@ CLR_BOOL SfiInitWorker(StackFrameIterator* pThis, CONTEXT* pStackwalkCtx, CLR_BO if (!pThis->m_crawl.HasFaulted() && !pThis->m_crawl.IsIPadjusted()) { - // For interpreter methods the same -1 adjustment applies: pFrame->ip is - // advanced past the call opcode before descending into the callee - // (see interpexec.cpp, e.g. `ip += 4; pFrame->ip = ip;`), so on - // propagation we need to land back inside the call site for the - // half-open EH clause match. The faulting case (INTOP_THROW / - // INTOP_RETHROW / hardware fault) is handled by !HasFaulted() above, - // which is set via InterpreterFrame::m_isFaulting -> CONTEXT_EXCEPTION_ACTIVE. controlPC -= STACKWALK_CONTROLPC_ADJUST_OFFSET; } pThis->SetAdjustedControlPC(controlPC); @@ -4014,15 +4007,6 @@ CLR_BOOL SfiNextWorker(StackFrameIterator* pThis, uint* uExCollideClauseIdx, CLR pFrame = pThis->m_crawl.GetFrame(); #ifdef FEATURE_INTERPRETER - // The current explicit frame is the InterpreterFrame pushed by the - // last InterpExecMethod we just unwound out of. Skip exactly that - // one entry so the unhandled-exception check below can see the - // DebuggerU2MCatchHandlerFrame / FRAME_TOP that actually terminates - // the chain. Do NOT use a while-loop here: if two InterpreterFrames - // happen to be adjacent in the explicit frame chain (nested - // InterpExecMethod with no intervening explicit frame), skipping - // past both would bypass an outer interpreted catch and cause the - // unhandled-exception path to fire incorrectly. if (pFrame != FRAME_TOP && pFrame->GetFrameIdentifier() == FrameIdentifier::InterpreterFrame) { pFrame = pFrame->PtrNextFrame(); @@ -4165,8 +4149,6 @@ Exit:; TADDR controlPC = pThis->m_crawl.GetRegisterSet()->ControlPC; if (!pThis->m_crawl.HasFaulted() && !pThis->m_crawl.IsIPadjusted()) { - // Same reasoning as in SfiInitWorker: interp frames also need -1 - // for half-open EH clause matching on propagation through a call. controlPC -= STACKWALK_CONTROLPC_ADJUST_OFFSET; } pThis->SetAdjustedControlPC(controlPC); From 23fa28fddc7844101588644093873bc96aa09d7a Mon Sep 17 00:00:00 2001 From: Milos Kotlar Date: Mon, 27 Apr 2026 15:30:33 +0200 Subject: [PATCH 4/4] Nit --- src/coreclr/vm/exceptionhandling.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/coreclr/vm/exceptionhandling.cpp b/src/coreclr/vm/exceptionhandling.cpp index a5e755c98f48df..8beb5ac9eb722e 100644 --- a/src/coreclr/vm/exceptionhandling.cpp +++ b/src/coreclr/vm/exceptionhandling.cpp @@ -3415,7 +3415,6 @@ extern "C" CLR_BOOL QCALLTYPE EHEnumInitFromStackFrameIterator(StackFrameIterato pJitMan->JitTokenToMethodRegionInfo(MethToken, pMethodRegionInfo); pFrameIter->UpdateIsRuntimeWrappedExceptions(); - return TRUE; } @@ -3836,6 +3835,7 @@ CLR_BOOL SfiInitWorker(StackFrameIterator* pThis, CONTEXT* pStackwalkCtx, CLR_BO controlPC -= STACKWALK_CONTROLPC_ADJUST_OFFSET; } pThis->SetAdjustedControlPC(controlPC); + *pfIsExceptionIntercepted = CheckExceptionInterception(pThis, pExInfo); EH_LOG((LL_INFO100, "SfiInit (pass %d): Exception stack walking starting at IP=%p, SP=%p, method %s::%s\n", pExInfo->m_passNumber, controlPC, GetRegdisplaySP(pThis->m_crawl.GetRegisterSet()), @@ -4316,7 +4316,6 @@ void DECLSPEC_NORETURN DispatchExSecondPass(ExInfo *pExInfo) pExInfo->m_passNumber = 2; uint startIdx = MaxTryRegionIdx; uint catchingTryRegionIdx = pExInfo->m_idxCurClause; - CLR_BOOL unwoundReversePInvoke = false; CLR_BOOL isExceptionIntercepted = false; CLR_BOOL isValid = SfiInitWorker(pFrameIter, pExInfo->m_pExContext, ((uint8_t)pExInfo->m_kind & (uint8_t)ExKind::InstructionFaultFlag) != 0, &isExceptionIntercepted);