Skip to content

Commit a087868

Browse files
authored
Fix foreign exception handling on Windows (#113323)
* Fix foreign exception handling on Windows There is a problem in a special case with the new exception handling: * A managed code calls a 3rd party native C++ code * then a C++ exception is thrown in that code * then it is propagated through managed code * then it is caught in a 3rd party native C++ code * then a managed callback is called from the catch handler * then the exception is rethrown using C++ "throw" * then it propagates back to the initial managed caller The problem is as follows: * When a native exception passes through managed frames and it is rethrown once a native frame is encountered, the RaiseException doesn't use the original exception's `EXCEPTION_RECORD`, but creates a new CLR exception instead. * When a managed callback called from the native C++ catch handler throws and catches another exception, the LastThrownObject is cleared. * When the C++ code in the catch handler rethrows the exception and it is propagated to managed code, the ProcessCLRException personality routine is invoked for the first managed frame. It can see that the exception is a CLR exception, but the LastThrownObject is NULL. It asserts in debug builds and crashes a bit later in release builds attempting to dereference the NULL. The fix is to pass the original `EXCEPTION_RECORD` to the `RaiseException` in case an external exception is propagated. And use just `RaiseException` instead of the machinery for regular managed exceptions. The issue was discovered in Autodesk Revit 2025 that does this kind of exception propagation. Close #113158 * Disable the test on Mono * Fix the problem with C++ exception object in reclaimed stack * Fix jongjmp accidentally handled as foreign
1 parent ac5f930 commit a087868

3 files changed

Lines changed: 110 additions & 6 deletions

File tree

src/coreclr/vm/exceptionhandling.cpp

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5662,11 +5662,18 @@ VOID DECLSPEC_NORETURN DispatchManagedException(OBJECTREF throwable, CONTEXT* pE
56625662
ULONG_PTR hr = GetHRFromThrowable(throwable);
56635663

56645664
EXCEPTION_RECORD newExceptionRecord;
5665-
newExceptionRecord.ExceptionCode = EXCEPTION_COMPLUS;
5666-
newExceptionRecord.ExceptionFlags = EXCEPTION_NONCONTINUABLE | EXCEPTION_SOFTWARE_ORIGINATE;
5667-
newExceptionRecord.ExceptionAddress = (void *)(void (*)(OBJECTREF))&DispatchManagedException;
5668-
newExceptionRecord.NumberParameters = MarkAsThrownByUs(newExceptionRecord.ExceptionInformation, hr);
5669-
newExceptionRecord.ExceptionRecord = NULL;
5665+
if (pExceptionRecord != NULL)
5666+
{
5667+
newExceptionRecord = *pExceptionRecord;
5668+
}
5669+
else
5670+
{
5671+
newExceptionRecord.ExceptionCode = EXCEPTION_COMPLUS;
5672+
newExceptionRecord.ExceptionFlags = EXCEPTION_NONCONTINUABLE | EXCEPTION_SOFTWARE_ORIGINATE;
5673+
newExceptionRecord.ExceptionAddress = (void *)(void (*)(OBJECTREF))&DispatchManagedException;
5674+
newExceptionRecord.NumberParameters = MarkAsThrownByUs(newExceptionRecord.ExceptionInformation, hr);
5675+
newExceptionRecord.ExceptionRecord = NULL;
5676+
}
56705677

56715678
ExInfo exInfo(pThread, &newExceptionRecord, pExceptionContext, ExKind::Throw);
56725679

@@ -7716,6 +7723,32 @@ VOID DECLSPEC_NORETURN PropagateLongJmpThroughNativeFrames(jmp_buf *pJmpBuf, int
77167723
WRAPPER_NO_CONTRACT;
77177724
longjmp(*pJmpBuf, retVal);
77187725
}
7726+
7727+
// This is a personality routine that the RtlRestoreContext calls when it is called with
7728+
// pExceptionRecord->ExceptionCode == STATUS_UNWIND_CONSOLIDATE.
7729+
// Before calling this function, it creates a machine frame that hides all the frames
7730+
// upto the frame described by the pContextRecord. This allows us to raise the exception
7731+
// from the target context without removing the frames from the stack. Those frames
7732+
// can contain e.g. a C++ exception object that needs to be preserved during the exception
7733+
// propagation.
7734+
EXTERN_C EXCEPTION_DISPOSITION
7735+
PropagateForeignExceptionThroughNativeFrames(IN PEXCEPTION_RECORD pExceptionRecord,
7736+
IN PVOID pEstablisherFrame,
7737+
IN OUT PCONTEXT pContextRecord,
7738+
IN OUT PDISPATCHER_CONTEXT pDispatcherContext
7739+
)
7740+
{
7741+
STATIC_CONTRACT_MODE_COOPERATIVE;
7742+
STATIC_CONTRACT_GC_TRIGGERS;
7743+
STATIC_CONTRACT_THROWS;
7744+
7745+
_ASSERTE(pExceptionRecord->NumberParameters == 2);
7746+
EXCEPTION_RECORD *pExceptionToPropagateRecord = (EXCEPTION_RECORD*)pExceptionRecord->ExceptionInformation[1];
7747+
GCX_PREEMP_NO_DTOR();
7748+
RaiseException(pExceptionToPropagateRecord->ExceptionCode, pExceptionToPropagateRecord->ExceptionFlags, pExceptionToPropagateRecord->NumberParameters, pExceptionToPropagateRecord->ExceptionInformation);
7749+
UNREACHABLE();
7750+
}
7751+
77197752
#endif // HOST_WINDOWS
77207753

77217754
extern "C" void * QCALLTYPE CallCatchFunclet(QCall::ObjectHandleOnStack exceptionObj, BYTE* pHandlerIP, REGDISPLAY* pvRegDisplay, ExInfo* exInfo)
@@ -7784,6 +7817,7 @@ extern "C" void * QCALLTYPE CallCatchFunclet(QCall::ObjectHandleOnStack exceptio
77847817
#ifdef HOST_WINDOWS
77857818
jmp_buf* pLongJmpBuf = pExInfo->m_pLongJmpBuf;
77867819
int longJmpReturnValue = pExInfo->m_longJmpReturnValue;
7820+
EXCEPTION_RECORD lastExceptionRecord = *pExInfo->m_ptrs.ExceptionRecord;
77877821
#endif // HOST_WINDOWS
77887822

77897823
#ifdef HOST_UNIX
@@ -7885,6 +7919,23 @@ extern "C" void * QCALLTYPE CallCatchFunclet(QCall::ObjectHandleOnStack exceptio
78857919
}
78867920
#endif // HOST_UNIX
78877921
// Throw exception from the caller context
7922+
7923+
#ifdef HOST_WINDOWS
7924+
if ((pLongJmpBuf == NULL) && !IsComPlusException(&lastExceptionRecord) && MapWin32FaultToCOMPlusException(&lastExceptionRecord) == kSEHException)
7925+
{
7926+
// Propagate an external exception to the caller context. This is done in a special way, since the native stack
7927+
// frames below the caller context may contain e.g. C++ exception object that the external exception references.
7928+
// So we rely on a special mode of the RtlRestoreContext with EXCEPTION_RECORD passed in with STATUS_UNWIND_CONSOLIDATE
7929+
// exception code to create a machine frame that hides all the frames upto the caller context before rasing the exception.
7930+
EXCEPTION_RECORD exceptionRecord;
7931+
exceptionRecord.ExceptionCode = STATUS_UNWIND_CONSOLIDATE;
7932+
exceptionRecord.NumberParameters = 2;
7933+
exceptionRecord.ExceptionInformation[0] = (ULONG_PTR)PropagateForeignExceptionThroughNativeFrames;
7934+
exceptionRecord.ExceptionInformation[1] = (ULONG_PTR)&lastExceptionRecord;
7935+
RtlRestoreContext(pvRegDisplay->pCurrentContext, &exceptionRecord);
7936+
}
7937+
#endif // HOST_WINDOWS
7938+
78887939
#if defined(HOST_AMD64)
78897940
ULONG64* returnAddress = (ULONG64*)targetSp;
78907941
*returnAddress = pvRegDisplay->pCurrentContext->Rip;

src/tests/baseservices/exceptions/exceptioninterop/ExceptionInterop.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,4 +156,41 @@ static void ThrowInFrameWithFinally()
156156
}
157157
}
158158
}
159+
160+
[DllImport(nameof(ExceptionInteropNative))]
161+
public static extern void InvokeCallbackCatchCallbackAndRethrow(delegate*unmanaged<void> callBack1, delegate*unmanaged<void> callBack2);
162+
163+
[UnmanagedCallersOnly]
164+
static void CallPInvoke()
165+
{
166+
ThrowException();
167+
}
168+
169+
[UnmanagedCallersOnly]
170+
static void ThrowAndCatchException()
171+
{
172+
try
173+
{
174+
throw new Exception("This one is handled");
175+
}
176+
catch (Exception ex)
177+
{
178+
Console.WriteLine($"Caught {ex}");
179+
}
180+
}
181+
182+
[Fact]
183+
[PlatformSpecific(TestPlatforms.Windows)]
184+
[SkipOnMono("Exception interop not supported on Mono.")]
185+
public static void PropagateAndRethrowCppException()
186+
{
187+
try
188+
{
189+
InvokeCallbackCatchCallbackAndRethrow(&CallPInvoke, &ThrowAndCatchException);
190+
}
191+
catch (Exception ex)
192+
{
193+
Console.WriteLine($"Caught {ex}");
194+
}
195+
}
159196
}

src/tests/baseservices/exceptions/exceptioninterop/ExceptionInteropNative.cpp

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,20 @@ extern "C" DLL_EXPORT void STDMETHODCALLTYPE NativeFunction()
1616
extern "C" DLL_EXPORT void STDMETHODCALLTYPE CallCallback(void (*cb)())
1717
{
1818
cb();
19-
}
19+
}
20+
21+
typedef void (*PFNACTION1)();
22+
extern "C" DLL_EXPORT void InvokeCallbackCatchCallbackAndRethrow(PFNACTION1 callback1, PFNACTION1 callback2)
23+
{
24+
try
25+
{
26+
callback1();
27+
}
28+
catch (std::exception& ex)
29+
{
30+
callback2();
31+
printf("Caught exception %s in native code, rethrowing\n", ex.what());
32+
throw;
33+
}
34+
}
35+

0 commit comments

Comments
 (0)