Skip to content

Commit 2cb402c

Browse files
janvorlijkotas
andauthored
Fix exception handling in the prestub worker - take 2 (#112666)
* Fix exception handling in the prestub worker There is a bug in the new exception handling when ThePreStub is called from CallDescrWorkerInternal and the exception is propagated through that. One of such cases is when a managed class is being initialized during JITting and the constructor is in an assembly that's not found. The bug results in skipping all the native frames upto a managed frame that called that native chain that lead to the exception. In the specific case I've mentioned, a lock in the native code is left in locked state. That later leads to a hang. This was case was observed with Roslyn invoking an analyzer where one of the dependencies was missing. The fix is to ensure that when ThePreStub is called by CallDescrWorkerInternal, the exception is not caught in the PreStubWorker. It is left flowing into the native calling code instead. The same treatment is applied to ExternalMethodFixupWorker and VSD_ResolveWorker too. On Windows, we also need to prevent the ProcessCLRException invocation to call into the managed exception handling code. * Fix missing ContextFlags setting I was hitting intermittent crashes in the unhandled exception test with GC stress C enabled due to this when GetSSP tried to access the SSP in the extended part of the context. * Fix couple of issues * The previous set of changes removed popping of ExInfos too. That's not correct though. But it should be done in a different place than the ProcessCLRExceptionNew. * There was a problem (even before the fix) that an exception caught in DispatchInfo::InvokeMember was reported (via console log and to the debugger) as unhandled. This change also adds a new flavor of the unhandled exception test that throws an unhandled exception on a secondary thread to exercise the related code path. --------- Co-authored-by: Jan Kotas <[email protected]>
1 parent 306ced2 commit 2cb402c

File tree

17 files changed

+318
-77
lines changed

17 files changed

+318
-77
lines changed

src/coreclr/vm/dispatchinfo.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2164,7 +2164,7 @@ HRESULT DispatchInfo::InvokeMember(SimpleComCallWrapper *pSimpleWrap, DISPID id,
21642164
// The sole purpose of having this frame is to tell the debugger that we have a catch handler here
21652165
// which may swallow managed exceptions. The debugger needs this in order to send a
21662166
// CatchHandlerFound (CHF) notification.
2167-
DebuggerU2MCatchHandlerFrame catchFrame;
2167+
DebuggerU2MCatchHandlerFrame catchFrame(true /* catchesAllExceptions */);
21682168
EX_TRY
21692169
{
21702170
InvokeMemberDebuggerWrapper(pDispMemberInfo,

src/coreclr/vm/excep.cpp

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3053,14 +3053,17 @@ void StackTraceInfo::AppendElement(OBJECTHANDLE hThrowable, UINT_PTR currentIP,
30533053
// This is a workaround to fix the generation of stack traces from exception objects so that
30543054
// they point to the line that actually generated the exception instead of the line
30553055
// following.
3056-
if (pCf->IsIPadjusted())
3056+
if (pCf != NULL)
30573057
{
3058-
stackTraceElem.flags |= STEF_IP_ADJUSTED;
3059-
}
3060-
else if (!pCf->HasFaulted() && stackTraceElem.ip != 0)
3061-
{
3062-
stackTraceElem.ip -= STACKWALK_CONTROLPC_ADJUST_OFFSET;
3063-
stackTraceElem.flags |= STEF_IP_ADJUSTED;
3058+
if (pCf->IsIPadjusted())
3059+
{
3060+
stackTraceElem.flags |= STEF_IP_ADJUSTED;
3061+
}
3062+
else if (!pCf->HasFaulted() && stackTraceElem.ip != 0)
3063+
{
3064+
stackTraceElem.ip -= STACKWALK_CONTROLPC_ADJUST_OFFSET;
3065+
stackTraceElem.flags |= STEF_IP_ADJUSTED;
3066+
}
30643067
}
30653068

30663069
#ifndef TARGET_UNIX // Watson is supported on Windows only

src/coreclr/vm/exceptionhandling.cpp

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -932,14 +932,13 @@ ProcessCLRExceptionNew(IN PEXCEPTION_RECORD pExceptionRecord,
932932

933933
Thread* pThread = GetThread();
934934

935-
if (pThread->HasThreadStateNC(Thread::TSNC_ProcessedUnhandledException))
935+
// Skip native frames of asm helpers that have the ProcessCLRException set as their personality routine.
936+
// There is nothing to do for those with the new exception handling.
937+
// Also skip all frames when processing unhandled exceptions. That allows them to reach the host app
938+
// level and let 3rd party the chance to handle them.
939+
if (!ExecutionManager::IsManagedCode((PCODE)pDispatcherContext->ControlPc) ||
940+
pThread->HasThreadStateNC(Thread::TSNC_ProcessedUnhandledException))
936941
{
937-
if ((pExceptionRecord->ExceptionFlags & EXCEPTION_UNWINDING))
938-
{
939-
GCX_COOP();
940-
PopExplicitFrames(pThread, (void*)pDispatcherContext->EstablisherFrame, (void*)GetSP(pDispatcherContext->ContextRecord));
941-
ExInfo::PopExInfos(pThread, (void*)pDispatcherContext->EstablisherFrame);
942-
}
943942
return ExceptionContinueSearch;
944943
}
945944

@@ -6147,6 +6146,7 @@ BOOL IsSafeToUnwindFrameChain(Thread* pThread, LPVOID MemoryStackFpForFrameChain
61476146
// Otherwise "unwind" to managed method
61486147
REGDISPLAY rd;
61496148
CONTEXT ctx;
6149+
ctx.ContextFlags = CONTEXT_CONTROL;
61506150
SetIP(&ctx, 0);
61516151
SetSP(&ctx, 0);
61526152
FillRegDisplay(&rd, &ctx);
@@ -6192,9 +6192,16 @@ void CleanUpForSecondPass(Thread* pThread, bool fIsSO, LPVOID MemoryStackFpForFr
61926192
// Instead, we rely on the END_SO_TOLERANT_CODE macro to call ClearExceptionStateAfterSO(). Of course,
61936193
// we may leak in the UMThunkStubCommon() case where we don't have this macro lower on the stack
61946194
// (stack grows up).
6195-
if (!fIsSO && !g_isNewExceptionHandlingEnabled)
6195+
if (!fIsSO)
61966196
{
6197-
ExceptionTracker::PopTrackerIfEscaping(MemoryStackFp);
6197+
if (g_isNewExceptionHandlingEnabled)
6198+
{
6199+
ExInfo::PopExInfos(pThread, MemoryStackFp);
6200+
}
6201+
else
6202+
{
6203+
ExceptionTracker::PopTrackerIfEscaping(MemoryStackFp);
6204+
}
61986205
}
61996206
}
62006207

@@ -8522,7 +8529,7 @@ static StackWalkAction MoveToNextNonSkippedFrame(StackFrameIterator* pStackFrame
85228529
return retVal;
85238530
}
85248531

8525-
extern "C" size_t CallDescrWorkerInternalReturnAddressOffset;
8532+
bool IsCallDescrWorkerInternalReturnAddress(PCODE pCode);
85268533

85278534
extern "C" bool QCALLTYPE SfiNext(StackFrameIterator* pThis, uint* uExCollideClauseIdx, bool* fUnwoundReversePInvoke, bool* pfIsExceptionIntercepted)
85288535
{
@@ -8577,8 +8584,7 @@ extern "C" bool QCALLTYPE SfiNext(StackFrameIterator* pThis, uint* uExCollideCla
85778584
}
85788585
else
85798586
{
8580-
size_t CallDescrWorkerInternalReturnAddress = (size_t)CallDescrWorkerInternal + CallDescrWorkerInternalReturnAddressOffset;
8581-
if (GetIP(pThis->m_crawl.GetRegisterSet()->pCallerContext) == CallDescrWorkerInternalReturnAddress)
8587+
if (IsCallDescrWorkerInternalReturnAddress(GetIP(pThis->m_crawl.GetRegisterSet()->pCallerContext)))
85828588
{
85838589
invalidRevPInvoke = true;
85848590
}
@@ -8601,8 +8607,12 @@ extern "C" bool QCALLTYPE SfiNext(StackFrameIterator* pThis, uint* uExCollideCla
86018607
_ASSERTE(pThis->GetFrameState() != StackFrameIterator::SFITER_SKIPPED_FRAME_FUNCTION);
86028608

86038609
pFrame = pThis->m_crawl.GetFrame();
8604-
// Check if there are any further managed frames on the stack, if not, the exception is unhandled.
8605-
if ((pFrame == FRAME_TOP) || IsTopmostDebuggerU2MCatchHandlerFrame(pFrame))
8610+
8611+
// Check if there are any further managed frames on the stack or a catch for all exceptions in native code (marked by
8612+
// DebuggerU2MCatchHandlerFrame with CatchesAllExceptions() returning true).
8613+
// If not, the exception is unhandled.
8614+
if ((pFrame == FRAME_TOP) ||
8615+
(IsTopmostDebuggerU2MCatchHandlerFrame(pFrame) && !((DebuggerU2MCatchHandlerFrame*)pFrame)->CatchesAllExceptions()))
86068616
{
86078617
if (pTopExInfo->m_passNumber == 1)
86088618
{

src/coreclr/vm/exceptmacros.h

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,15 +277,22 @@ VOID DECLSPEC_NORETURN UnwindAndContinueRethrowHelperAfterCatch(Frame* pEntryFra
277277
#ifdef TARGET_UNIX
278278
VOID DECLSPEC_NORETURN DispatchManagedException(PAL_SEHException& ex, bool isHardwareException);
279279

280-
#define INSTALL_MANAGED_EXCEPTION_DISPATCHER \
280+
#define INSTALL_MANAGED_EXCEPTION_DISPATCHER_EX \
281281
PAL_SEHException exCopy; \
282282
bool hasCaughtException = false; \
283283
try {
284284

285-
#define UNINSTALL_MANAGED_EXCEPTION_DISPATCHER \
285+
#define INSTALL_MANAGED_EXCEPTION_DISPATCHER \
286+
INSTALL_MANAGED_EXCEPTION_DISPATCHER_EX
287+
288+
#define UNINSTALL_MANAGED_EXCEPTION_DISPATCHER_EX(nativeRethrow) \
286289
} \
287290
catch (PAL_SEHException& ex) \
288291
{ \
292+
if (nativeRethrow) \
293+
{ \
294+
throw; \
295+
} \
289296
exCopy = std::move(ex); \
290297
hasCaughtException = true; \
291298
} \
@@ -294,6 +301,9 @@ VOID DECLSPEC_NORETURN DispatchManagedException(PAL_SEHException& ex, bool isHar
294301
DispatchManagedException(exCopy, false);\
295302
}
296303

304+
#define UNINSTALL_MANAGED_EXCEPTION_DISPATCHER \
305+
UNINSTALL_MANAGED_EXCEPTION_DISPATCHER_EX(false)
306+
297307
// Install trap that catches unhandled managed exception and dumps its stack
298308
#define INSTALL_UNHANDLED_MANAGED_EXCEPTION_TRAP \
299309
try {
@@ -315,7 +325,9 @@ VOID DECLSPEC_NORETURN DispatchManagedException(PAL_SEHException& ex, bool isHar
315325
#else // TARGET_UNIX
316326

317327
#define INSTALL_MANAGED_EXCEPTION_DISPATCHER
328+
#define INSTALL_MANAGED_EXCEPTION_DISPATCHER_EX
318329
#define UNINSTALL_MANAGED_EXCEPTION_DISPATCHER
330+
#define UNINSTALL_MANAGED_EXCEPTION_DISPATCHER_EX
319331

320332
#define INSTALL_UNHANDLED_MANAGED_EXCEPTION_TRAP
321333
#define UNINSTALL_UNHANDLED_MANAGED_EXCEPTION_TRAP

src/coreclr/vm/frames.h

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2497,13 +2497,15 @@ class DebuggerU2MCatchHandlerFrame : public Frame
24972497
{
24982498
public:
24992499
#ifndef DACCESS_COMPILE
2500-
DebuggerU2MCatchHandlerFrame() : Frame(FrameIdentifier::DebuggerU2MCatchHandlerFrame)
2500+
DebuggerU2MCatchHandlerFrame(bool catchesAllExceptions) : Frame(FrameIdentifier::DebuggerU2MCatchHandlerFrame),
2501+
m_catchesAllExceptions(catchesAllExceptions)
25012502
{
25022503
WRAPPER_NO_CONTRACT;
25032504
Frame::Push();
25042505
}
25052506

2506-
DebuggerU2MCatchHandlerFrame(Thread * pThread) : Frame(FrameIdentifier::DebuggerU2MCatchHandlerFrame)
2507+
DebuggerU2MCatchHandlerFrame(Thread * pThread, bool catchesAllExceptions) : Frame(FrameIdentifier::DebuggerU2MCatchHandlerFrame),
2508+
m_catchesAllExceptions(catchesAllExceptions)
25072509
{
25082510
WRAPPER_NO_CONTRACT;
25092511
Frame::Push(pThread);
@@ -2515,6 +2517,16 @@ class DebuggerU2MCatchHandlerFrame : public Frame
25152517
LIMITED_METHOD_DAC_CONTRACT;
25162518
return TT_U2M;
25172519
}
2520+
2521+
bool CatchesAllExceptions()
2522+
{
2523+
LIMITED_METHOD_DAC_CONTRACT;
2524+
return m_catchesAllExceptions;
2525+
}
2526+
2527+
private:
2528+
// The catch handled marked by the DebuggerU2MCatchHandlerFrame catches all exceptions.
2529+
bool m_catchesAllExceptions;
25182530
};
25192531

25202532
// Frame for the Reverse PInvoke (i.e. UnmanagedCallersOnlyAttribute).

src/coreclr/vm/prestub.cpp

Lines changed: 73 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2538,6 +2538,20 @@ Stub * MakeInstantiatingStubWorker(MethodDesc *pMD)
25382538
}
25392539
#endif // defined(FEATURE_SHARE_GENERIC_CODE)
25402540

2541+
extern "C" size_t CallDescrWorkerInternalReturnAddressOffset;
2542+
2543+
bool IsCallDescrWorkerInternalReturnAddress(PCODE pCode)
2544+
{
2545+
LIMITED_METHOD_CONTRACT;
2546+
#ifdef FEATURE_EH_FUNCLETS
2547+
size_t CallDescrWorkerInternalReturnAddress = (size_t)CallDescrWorkerInternal + CallDescrWorkerInternalReturnAddressOffset;
2548+
2549+
return pCode == CallDescrWorkerInternalReturnAddress;
2550+
#else // FEATURE_EH_FUNCLETS
2551+
return false;
2552+
#endif // FEATURE_EH_FUNCLETS
2553+
}
2554+
25412555
//=============================================================================
25422556
// This function generates the real code when from Preemptive mode.
25432557
// It is specifically designed to work with the UnmanagedCallersOnlyAttribute.
@@ -2633,60 +2647,76 @@ extern "C" PCODE STDCALL PreStubWorker(TransitionBlock* pTransitionBlock, Method
26332647

26342648
pPFrame->Push(CURRENT_THREAD);
26352649

2636-
INSTALL_MANAGED_EXCEPTION_DISPATCHER;
2637-
INSTALL_UNWIND_AND_CONTINUE_HANDLER;
2650+
EX_TRY
2651+
{
2652+
bool propagateExceptionToNativeCode = IsCallDescrWorkerInternalReturnAddress(pTransitionBlock->m_ReturnAddress);
26382653

2639-
// Make sure the method table is restored, and method instantiation if present
2640-
pMD->CheckRestore();
2641-
CONSISTENCY_CHECK(GetAppDomain()->CheckCanExecuteManagedCode(pMD));
2654+
INSTALL_MANAGED_EXCEPTION_DISPATCHER_EX;
2655+
INSTALL_UNWIND_AND_CONTINUE_HANDLER_EX;
26422656

2643-
MethodTable* pDispatchingMT = NULL;
2644-
if (pMD->IsVtableMethod())
2645-
{
2646-
OBJECTREF curobj = pPFrame->GetThis();
2657+
// Make sure the method table is restored, and method instantiation if present
2658+
pMD->CheckRestore();
2659+
CONSISTENCY_CHECK(GetAppDomain()->CheckCanExecuteManagedCode(pMD));
26472660

2648-
if (curobj != NULL) // Check for virtual function called non-virtually on a NULL object
2661+
MethodTable* pDispatchingMT = NULL;
2662+
if (pMD->IsVtableMethod())
26492663
{
2650-
pDispatchingMT = curobj->GetMethodTable();
2664+
OBJECTREF curobj = pPFrame->GetThis();
26512665

2652-
if (pDispatchingMT->IsIDynamicInterfaceCastable())
2666+
if (curobj != NULL) // Check for virtual function called non-virtually on a NULL object
26532667
{
2654-
MethodTable* pMDMT = pMD->GetMethodTable();
2655-
TypeHandle objectType(pDispatchingMT);
2656-
TypeHandle methodType(pMDMT);
2668+
pDispatchingMT = curobj->GetMethodTable();
26572669

2658-
GCStress<cfg_any>::MaybeTrigger();
2659-
INDEBUG(curobj = NULL); // curobj is unprotected and CanCastTo() can trigger GC
2660-
if (!objectType.CanCastTo(methodType))
2670+
if (pDispatchingMT->IsIDynamicInterfaceCastable())
26612671
{
2662-
// Apparently IDynamicInterfaceCastable magic was involved when we chose this method to be called
2663-
// that's why we better stick to the MethodTable it belongs to, otherwise
2664-
// DoPrestub() will fail not being able to find implementation for pMD in pDispatchingMT.
2672+
MethodTable* pMDMT = pMD->GetMethodTable();
2673+
TypeHandle objectType(pDispatchingMT);
2674+
TypeHandle methodType(pMDMT);
2675+
2676+
GCStress<cfg_any>::MaybeTrigger();
2677+
INDEBUG(curobj = NULL); // curobj is unprotected and CanCastTo() can trigger GC
2678+
if (!objectType.CanCastTo(methodType))
2679+
{
2680+
// Apparently IDynamicInterfaceCastable magic was involved when we chose this method to be called
2681+
// that's why we better stick to the MethodTable it belongs to, otherwise
2682+
// DoPrestub() will fail not being able to find implementation for pMD in pDispatchingMT.
26652683

2666-
pDispatchingMT = pMDMT;
2684+
pDispatchingMT = pMDMT;
2685+
}
26672686
}
2668-
}
26692687

2670-
// For value types, the only virtual methods are interface implementations.
2671-
// Thus pDispatching == pMT because there
2672-
// is no inheritance in value types. Note the BoxedEntryPointStubs are shared
2673-
// between all sharable generic instantiations, so the == test is on
2674-
// canonical method tables.
2688+
// For value types, the only virtual methods are interface implementations.
2689+
// Thus pDispatching == pMT because there
2690+
// is no inheritance in value types. Note the BoxedEntryPointStubs are shared
2691+
// between all sharable generic instantiations, so the == test is on
2692+
// canonical method tables.
26752693
#ifdef _DEBUG
2676-
MethodTable* pMDMT = pMD->GetMethodTable(); // put this here to see what the MT is in debug mode
2677-
_ASSERTE(!pMD->GetMethodTable()->IsValueType() ||
2678-
(pMD->IsUnboxingStub() && (pDispatchingMT->GetCanonicalMethodTable() == pMDMT->GetCanonicalMethodTable())));
2694+
MethodTable* pMDMT = pMD->GetMethodTable(); // put this here to see what the MT is in debug mode
2695+
_ASSERTE(!pMD->GetMethodTable()->IsValueType() ||
2696+
(pMD->IsUnboxingStub() && (pDispatchingMT->GetCanonicalMethodTable() == pMDMT->GetCanonicalMethodTable())));
26792697
#endif // _DEBUG
2698+
}
26802699
}
2681-
}
26822700

2683-
GCX_PREEMP_THREAD_EXISTS(CURRENT_THREAD);
2701+
GCX_PREEMP_THREAD_EXISTS(CURRENT_THREAD);
2702+
{
2703+
pbRetVal = pMD->DoPrestub(pDispatchingMT, CallerGCMode::Coop);
2704+
}
2705+
2706+
UNINSTALL_UNWIND_AND_CONTINUE_HANDLER_EX(propagateExceptionToNativeCode);
2707+
UNINSTALL_MANAGED_EXCEPTION_DISPATCHER_EX(propagateExceptionToNativeCode);
2708+
}
2709+
EX_CATCH
26842710
{
2685-
pbRetVal = pMD->DoPrestub(pDispatchingMT, CallerGCMode::Coop);
2711+
if (g_isNewExceptionHandlingEnabled)
2712+
{
2713+
OBJECTHANDLE ohThrowable = CURRENT_THREAD->LastThrownObjectHandle();
2714+
_ASSERTE(ohThrowable);
2715+
StackTraceInfo::AppendElement(ohThrowable, 0, (UINT_PTR)pTransitionBlock, pMD, NULL);
2716+
}
2717+
EX_RETHROW;
26862718
}
2687-
2688-
UNINSTALL_UNWIND_AND_CONTINUE_HANDLER;
2689-
UNINSTALL_MANAGED_EXCEPTION_DISPATCHER;
2719+
EX_END_CATCH(SwallowAllExceptions)
26902720

26912721
{
26922722
HardwareExceptionHolder;
@@ -3156,8 +3186,10 @@ EXTERN_C PCODE STDCALL ExternalMethodFixupWorker(TransitionBlock * pTransitionBl
31563186

31573187
pEMFrame->Push(CURRENT_THREAD); // Push the new ExternalMethodFrame onto the frame stack
31583188

3159-
INSTALL_MANAGED_EXCEPTION_DISPATCHER;
3160-
INSTALL_UNWIND_AND_CONTINUE_HANDLER;
3189+
bool propagateExceptionToNativeCode = IsCallDescrWorkerInternalReturnAddress(pTransitionBlock->m_ReturnAddress);
3190+
3191+
INSTALL_MANAGED_EXCEPTION_DISPATCHER_EX;
3192+
INSTALL_UNWIND_AND_CONTINUE_HANDLER_EX;
31613193

31623194
bool fVirtual = false;
31633195
MethodDesc * pMD = NULL;
@@ -3399,8 +3431,8 @@ EXTERN_C PCODE STDCALL ExternalMethodFixupWorker(TransitionBlock * pTransitionBl
33993431
}
34003432
// Ready to return
34013433

3402-
UNINSTALL_UNWIND_AND_CONTINUE_HANDLER;
3403-
UNINSTALL_MANAGED_EXCEPTION_DISPATCHER;
3434+
UNINSTALL_UNWIND_AND_CONTINUE_HANDLER_EX(propagateExceptionToNativeCode);
3435+
UNINSTALL_MANAGED_EXCEPTION_DISPATCHER_EX(propagateExceptionToNativeCode);
34043436

34053437
pEMFrame->Pop(CURRENT_THREAD); // Pop the ExternalMethodFrame from the frame stack
34063438

src/coreclr/vm/threads.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7206,7 +7206,7 @@ static void ManagedThreadBase_DispatchOuter(ManagedThreadCallState *pCallState)
72067206
// The sole purpose of having this frame is to tell the debugger that we have a catch handler here
72077207
// which may swallow managed exceptions. The debugger needs this in order to send a
72087208
// CatchHandlerFound (CHF) notification.
7209-
DebuggerU2MCatchHandlerFrame catchFrame;
7209+
DebuggerU2MCatchHandlerFrame catchFrame(false /* catchesAllExceptions */);
72107210

72117211
TryParam param(pCallState);
72127212
param.pFrame = &catchFrame;

0 commit comments

Comments
 (0)