Skip to content

Commit 187508c

Browse files
authored
[NativeAOT] Objective-C exception propagation (#80334)
* [NativeAOT] Objective-C exception interception * Fix Windows build * Add back UnwoundReversePInvoke flag after re-initing. * Switch to preemptive mode before jumping to propagation callback Co-authored-by: Jan Kotas <[email protected]> * Fix windows AMD64 offsets.
1 parent c50c745 commit 187508c

File tree

20 files changed

+554
-53
lines changed

20 files changed

+554
-53
lines changed

src/coreclr/nativeaot/Bootstrap/main.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ extern "C" void IDynamicCastableGetInterfaceImplementation();
115115
extern "C" void ObjectiveCMarshalTryGetTaggedMemory();
116116
extern "C" void ObjectiveCMarshalGetIsTrackedReferenceCallback();
117117
extern "C" void ObjectiveCMarshalGetOnEnteredFinalizerQueueCallback();
118+
extern "C" void ObjectiveCMarshalGetUnhandledExceptionPropagationHandler();
118119
#endif
119120

120121
typedef void(*pfn)();
@@ -134,10 +135,12 @@ static const pfn c_classlibFunctions[] = {
134135
&ObjectiveCMarshalTryGetTaggedMemory,
135136
&ObjectiveCMarshalGetIsTrackedReferenceCallback,
136137
&ObjectiveCMarshalGetOnEnteredFinalizerQueueCallback,
138+
&ObjectiveCMarshalGetUnhandledExceptionPropagationHandler,
137139
#else
138140
nullptr,
139141
nullptr,
140142
nullptr,
143+
nullptr,
141144
#endif
142145
};
143146

src/coreclr/nativeaot/Runtime.Base/src/System/Runtime/ExceptionHandling.cs

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,8 @@ private static void DispatchEx(scoped ref StackFrameIterator frameIter, ref ExIn
611611
byte* prevOriginalPC = null;
612612
UIntPtr prevFramePtr = UIntPtr.Zero;
613613
bool unwoundReversePInvoke = false;
614+
IntPtr pReversePInvokePropagationCallback = IntPtr.Zero;
615+
IntPtr pReversePInvokePropagationContext = IntPtr.Zero;
614616

615617
bool isValid = frameIter.Init(exInfo._pExContext, (exInfo._kind & ExKind.InstructionFaultFlag) != 0);
616618
Debug.Assert(isValid, "RhThrowEx called with an unexpected context");
@@ -643,7 +645,29 @@ private static void DispatchEx(scoped ref StackFrameIterator frameIter, ref ExIn
643645
}
644646
}
645647

646-
if (pCatchHandler == null)
648+
#if FEATURE_OBJCMARSHAL
649+
if (unwoundReversePInvoke)
650+
{
651+
// We did not find any managed handlers before hitting a reverse P/Invoke boundary.
652+
// See if the classlib has a handler to propagate the exception to native code.
653+
IntPtr pGetHandlerClasslibFunction = (IntPtr)InternalCalls.RhpGetClasslibFunctionFromCodeAddress((IntPtr)prevControlPC,
654+
ClassLibFunctionId.ObjectiveCMarshalGetUnhandledExceptionPropagationHandler);
655+
if (pGetHandlerClasslibFunction != IntPtr.Zero)
656+
{
657+
var pGetHandler = (delegate*<object, IntPtr, out IntPtr, IntPtr>)pGetHandlerClasslibFunction;
658+
pReversePInvokePropagationCallback = pGetHandler(
659+
exceptionObj, (IntPtr)prevControlPC, out pReversePInvokePropagationContext);
660+
if (pReversePInvokePropagationCallback != IntPtr.Zero)
661+
{
662+
// Tell the second pass to unwind to this frame.
663+
handlingFrameSP = frameIter.SP;
664+
catchingTryRegionIdx = MaxTryRegionIdx;
665+
}
666+
}
667+
}
668+
#endif // FEATURE_OBJCMARSHAL
669+
670+
if (pCatchHandler == null && pReversePInvokePropagationCallback == IntPtr.Zero)
647671
{
648672
OnUnhandledExceptionViaClassLib(exceptionObj);
649673

@@ -655,8 +679,8 @@ private static void DispatchEx(scoped ref StackFrameIterator frameIter, ref ExIn
655679
}
656680

657681
// We FailFast above if the exception goes unhandled. Therefore, we cannot run the second pass
658-
// without a catch handler.
659-
Debug.Assert(pCatchHandler != null, "We should have a handler if we're starting the second pass");
682+
// without a catch handler or propagation callback.
683+
Debug.Assert(pCatchHandler != null || pReversePInvokePropagationCallback != IntPtr.Zero, "We should have a handler if we're starting the second pass");
660684

661685
// ------------------------------------------------
662686
//
@@ -673,12 +697,23 @@ private static void DispatchEx(scoped ref StackFrameIterator frameIter, ref ExIn
673697

674698
exInfo._passNumber = 2;
675699
startIdx = MaxTryRegionIdx;
700+
unwoundReversePInvoke = false;
676701
isValid = frameIter.Init(exInfo._pExContext, (exInfo._kind & ExKind.InstructionFaultFlag) != 0);
677-
for (; isValid && ((byte*)frameIter.SP <= (byte*)handlingFrameSP); isValid = frameIter.Next(&startIdx))
702+
for (; isValid && ((byte*)frameIter.SP <= (byte*)handlingFrameSP); isValid = frameIter.Next(&startIdx, &unwoundReversePInvoke))
678703
{
679704
Debug.Assert(isValid, "second-pass EH unwind failed unexpectedly");
680705
DebugScanCallFrame(exInfo._passNumber, frameIter.ControlPC, frameIter.SP);
681706

707+
if (unwoundReversePInvoke)
708+
{
709+
Debug.Assert(pReversePInvokePropagationCallback != IntPtr.Zero, "Unwound to a reverse P/Invoke in the second pass. We should have a propagation handler.");
710+
Debug.Assert(frameIter.SP == handlingFrameSP, "Encountered a different reverse P/Invoke frame in the second pass.");
711+
Debug.Assert(frameIter.PreviousTransitionFrame != IntPtr.Zero, "Should have a transition frame for reverse P/Invoke.");
712+
// Found the native frame that called the reverse P/invoke.
713+
// It is not possible to run managed second pass handlers on a native frame.
714+
break;
715+
}
716+
682717
if ((frameIter.SP == handlingFrameSP)
683718
#if TARGET_ARM64
684719
&& (frameIter.ControlPC == prevControlPC)
@@ -693,6 +728,18 @@ private static void DispatchEx(scoped ref StackFrameIterator frameIter, ref ExIn
693728
InvokeSecondPass(ref exInfo, startIdx);
694729
}
695730

731+
#if FEATURE_OBJCMARSHAL
732+
if (pReversePInvokePropagationCallback != IntPtr.Zero)
733+
{
734+
InternalCalls.RhpCallPropagateExceptionCallback(
735+
pReversePInvokePropagationContext, pReversePInvokePropagationCallback, frameIter.RegisterSet, ref exInfo, frameIter.PreviousTransitionFrame);
736+
// the helper should jump to propagation handler and not return
737+
Debug.Assert(false, "unreachable");
738+
FallbackFailFast(RhFailFastReason.InternalError, null);
739+
}
740+
#endif // FEATURE_OBJCMARSHAL
741+
742+
696743
// ------------------------------------------------
697744
//
698745
// Call the handler and resume execution

src/coreclr/nativeaot/Runtime.Base/src/System/Runtime/InternalCalls.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ internal enum ClassLibFunctionId
4949
ObjectiveCMarshalTryGetTaggedMemory = 10,
5050
ObjectiveCMarshalGetIsTrackedReferenceCallback = 11,
5151
ObjectiveCMarshalGetOnEnteredFinalizerQueueCallback = 12,
52+
ObjectiveCMarshalGetUnhandledExceptionPropagationHandler = 13,
5253
}
5354

5455
internal static class InternalCalls
@@ -232,6 +233,13 @@ internal static extern unsafe IntPtr RhpCallCatchFunclet(
232233
internal static extern unsafe bool RhpCallFilterFunclet(
233234
object exceptionObj, byte* pFilterIP, void* pvRegDisplay);
234235

236+
#if FEATURE_OBJCMARSHAL
237+
[RuntimeImport(Redhawk.BaseName, "RhpCallPropagateExceptionCallback")]
238+
[MethodImpl(MethodImplOptions.InternalCall)]
239+
internal static extern unsafe IntPtr RhpCallPropagateExceptionCallback(
240+
IntPtr callbackContext, IntPtr callback, void* pvRegDisplay, ref EH.ExInfo exInfo, IntPtr pPreviousTransitionFrame);
241+
#endif // FEATURE_OBJCMARSHAL
242+
235243
[RuntimeImport(Redhawk.BaseName, "RhpFallbackFailFast")]
236244
[MethodImpl(MethodImplOptions.InternalCall)]
237245
internal static extern unsafe void RhpFallbackFailFast();

src/coreclr/nativeaot/Runtime.Base/src/System/Runtime/StackFrameIterator.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,15 @@ internal unsafe struct StackFrameIterator
2323
private REGDISPLAY _regDisplay;
2424
[FieldOffset(AsmOffsets.OFFSETOF__StackFrameIterator__m_OriginalControlPC)]
2525
private IntPtr _originalControlPC;
26+
[FieldOffset(AsmOffsets.OFFSETOF__StackFrameIterator__m_pPreviousTransitionFrame)]
27+
private IntPtr _pPreviousTransitionFrame;
2628

2729
internal byte* ControlPC { get { return (byte*)_controlPC; } }
2830
internal byte* OriginalControlPC { get { return (byte*)_originalControlPC; } }
2931
internal void* RegisterSet { get { fixed (void* pRegDisplay = &_regDisplay) { return pRegDisplay; } } }
3032
internal UIntPtr SP { get { return _regDisplay.SP; } }
3133
internal UIntPtr FramePointer { get { return _framePointer; } }
34+
internal IntPtr PreviousTransitionFrame { get { return _pPreviousTransitionFrame; } }
3235

3336
internal bool Init(EH.PAL_LIMITED_CONTEXT* pStackwalkCtx, bool instructionFault = false)
3437
{

src/coreclr/nativeaot/Runtime/ICodeManager.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ enum class ClasslibFunctionId
157157
ObjectiveCMarshalTryGetTaggedMemory = 10,
158158
ObjectiveCMarshalGetIsTrackedReferenceCallback = 11,
159159
ObjectiveCMarshalGetOnEnteredFinalizerQueueCallback = 12,
160+
ObjectiveCMarshalGetUnhandledExceptionPropagationHandler = 13,
160161
};
161162

162163
enum class AssociatedDataFlags : unsigned char
@@ -165,6 +166,14 @@ enum class AssociatedDataFlags : unsigned char
165166
HasUnboxingStubTarget = 1,
166167
};
167168

169+
enum UnwindStackFrameFlags
170+
{
171+
USFF_None = 0,
172+
// If this is a reverse P/Invoke frame, do not continue the unwind
173+
// after extracting the saved transition frame.
174+
USFF_StopUnwindOnTransitionFrame = 1,
175+
};
176+
168177
class ICodeManager
169178
{
170179
public:
@@ -185,6 +194,7 @@ class ICodeManager
185194
bool isActiveStackFrame) PURE_VIRTUAL
186195

187196
virtual bool UnwindStackFrame(MethodInfo * pMethodInfo,
197+
uint32_t flags,
188198
REGDISPLAY * pRegisterSet, // in/out
189199
PInvokeTransitionFrame** ppPreviousTransitionFrame) PURE_VIRTUAL // out
190200

src/coreclr/nativeaot/Runtime/StackFrameIterator.cpp

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ void StackFrameIterator::EnterInitialInvalidState(Thread * pThreadToWalk)
140140
m_ShouldSkipRegularGcReporting = false;
141141
m_pendingFuncletFramePointer = NULL;
142142
m_pNextExInfo = pThreadToWalk->GetCurExInfo();
143+
m_pPreviousTransitionFrame = NULL;
143144
SetControlPC(0);
144145
}
145146

@@ -171,6 +172,7 @@ void StackFrameIterator::InternalInit(Thread * pThreadToWalk, PInvokeTransitionF
171172
}
172173

173174
m_dwFlags = dwFlags;
175+
m_pPreviousTransitionFrame = pFrame;
174176

175177
// We need to walk the ExInfo chain in parallel with the stackwalk so that we know when we cross over
176178
// exception throw points. So we must find our initial point in the ExInfo chain here so that we can
@@ -1412,6 +1414,8 @@ void StackFrameIterator::NextInternal()
14121414
{
14131415
UnwindOutOfCurrentManagedFrame:
14141416
ASSERT(m_dwFlags & MethodStateCalculated);
1417+
// Due to the lack of an ICodeManager for native code, we can't unwind from a native frame.
1418+
ASSERT((m_dwFlags & (SkipNativeFrames|UnwoundReversePInvoke)) != UnwoundReversePInvoke);
14151419
m_dwFlags &= ~(ExCollide|MethodStateCalculated|UnwoundReversePInvoke|ActiveStackFrame);
14161420
ASSERT(IsValid());
14171421

@@ -1431,33 +1435,41 @@ void StackFrameIterator::NextInternal()
14311435
uintptr_t DEBUG_preUnwindSP = m_RegDisplay.GetSP();
14321436
#endif
14331437

1434-
PInvokeTransitionFrame* pPreviousTransitionFrame;
1435-
FAILFAST_OR_DAC_FAIL(GetCodeManager()->UnwindStackFrame(&m_methodInfo, &m_RegDisplay, &pPreviousTransitionFrame));
1438+
uint32_t unwindFlags = USFF_None;
1439+
if ((m_dwFlags & SkipNativeFrames) != 0)
1440+
{
1441+
unwindFlags |= USFF_StopUnwindOnTransitionFrame;
1442+
}
1443+
1444+
FAILFAST_OR_DAC_FAIL(GetCodeManager()->UnwindStackFrame(&m_methodInfo, unwindFlags, &m_RegDisplay,
1445+
&m_pPreviousTransitionFrame));
1446+
1447+
if (m_pPreviousTransitionFrame != NULL)
1448+
{
1449+
m_dwFlags |= UnwoundReversePInvoke;
1450+
}
14361451

14371452
bool doingFuncletUnwind = GetCodeManager()->IsFunclet(&m_methodInfo);
14381453

1439-
if (pPreviousTransitionFrame != NULL)
1454+
if (m_pPreviousTransitionFrame != NULL && (m_dwFlags & SkipNativeFrames) != 0)
14401455
{
14411456
ASSERT(!doingFuncletUnwind);
14421457

1443-
if (pPreviousTransitionFrame == TOP_OF_STACK_MARKER)
1458+
if (m_pPreviousTransitionFrame == TOP_OF_STACK_MARKER)
14441459
{
14451460
SetControlPC(0);
14461461
}
14471462
else
14481463
{
1449-
// NOTE: If this is an EH stack walk, then reinitializing the iterator using the GC stack
1450-
// walk flags is incorrect. That said, this is OK because the exception dispatcher will
1451-
// immediately trigger a failfast when it sees the UnwoundReversePInvoke flag.
14521464
// NOTE: This can generate a conservative stack range if the recovered PInvoke callsite
14531465
// resides in an assembly thunk and not in normal managed code. In this case InternalInit
14541466
// will unwind through the thunk and back to the nearest managed frame, and therefore may
14551467
// see a conservative range reported by one of the thunks encountered during this "nested"
14561468
// unwind.
1457-
InternalInit(m_pThread, pPreviousTransitionFrame, GcStackWalkFlags);
1469+
InternalInit(m_pThread, m_pPreviousTransitionFrame, GcStackWalkFlags);
1470+
m_dwFlags |= UnwoundReversePInvoke;
14581471
ASSERT(m_pInstance->IsManaged(m_ControlPC));
14591472
}
1460-
m_dwFlags |= UnwoundReversePInvoke;
14611473
}
14621474
else
14631475
{
@@ -1578,11 +1590,12 @@ void StackFrameIterator::NextInternal()
15781590
}
15791591

15801592
// Now that all assembly thunks and ExInfo collisions have been processed, it is guaranteed
1581-
// that the next managed frame has been located. The located frame must now be yielded
1593+
// that the next managed frame has been located. Or the next native frame
1594+
// if we are not skipping them. The located frame must now be yielded
15821595
// from the iterator with the one and only exception being cases where a managed frame must
15831596
// be skipped due to funclet collapsing.
15841597

1585-
ASSERT(m_pInstance->IsManaged(m_ControlPC));
1598+
ASSERT(m_pInstance->IsManaged(m_ControlPC) || (m_pPreviousTransitionFrame != NULL && (m_dwFlags & SkipNativeFrames) == 0));
15861599

15871600
if (collapsingTargetFrame != NULL)
15881601
{
@@ -1691,7 +1704,8 @@ void StackFrameIterator::PrepareToYieldFrame()
16911704
if (!IsValid())
16921705
return;
16931706

1694-
ASSERT(m_pInstance->IsManaged(m_ControlPC));
1707+
ASSERT(m_pInstance->IsManaged(m_ControlPC) ||
1708+
((m_dwFlags & SkipNativeFrames) == 0 && (m_dwFlags & UnwoundReversePInvoke) != 0));
16951709

16961710
if (m_dwFlags & ApplyReturnAddressAdjustment)
16971711
{
@@ -1747,6 +1761,7 @@ REGDISPLAY * StackFrameIterator::GetRegisterSet()
17471761
PTR_VOID StackFrameIterator::GetEffectiveSafePointAddress()
17481762
{
17491763
ASSERT(IsValid());
1764+
ASSERT(m_effectiveSafePointAddress);
17501765
return m_effectiveSafePointAddress;
17511766
}
17521767

@@ -1779,6 +1794,17 @@ void StackFrameIterator::CalculateCurrentMethodState()
17791794
if (m_dwFlags & MethodStateCalculated)
17801795
return;
17811796

1797+
// Check if we are on a native frame.
1798+
if ((m_dwFlags & (SkipNativeFrames|UnwoundReversePInvoke)) == UnwoundReversePInvoke)
1799+
{
1800+
// There is no implementation of ICodeManager for native code.
1801+
m_pCodeManager = nullptr;
1802+
m_effectiveSafePointAddress = nullptr;
1803+
m_FramePointer = nullptr;
1804+
m_dwFlags |= MethodStateCalculated;
1805+
return;
1806+
}
1807+
17821808
// Assume that the caller is likely to be in the same module
17831809
if (m_pCodeManager == NULL || !m_pCodeManager->FindMethodInfo(m_ControlPC, &m_methodInfo))
17841810
{

src/coreclr/nativeaot/Runtime/StackFrameIterator.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,10 @@ class StackFrameIterator
147147
// The thread was interrupted in the current frame at the current IP by a signal, SuspendThread or similar.
148148
ActiveStackFrame = 0x40,
149149

150-
GcStackWalkFlags = (CollapseFunclets | RemapHardwareFaultsToSafePoint),
150+
// When encountering a reverse P/Invoke, unwind directly to the P/Invoke frame using the saved transition frame.
151+
SkipNativeFrames = 0x80,
152+
153+
GcStackWalkFlags = (CollapseFunclets | RemapHardwareFaultsToSafePoint | SkipNativeFrames),
151154
EHStackWalkFlags = ApplyReturnAddressAdjustment,
152155
StackTraceStackWalkFlags = GcStackWalkFlags
153156
};
@@ -209,14 +212,15 @@ class StackFrameIterator
209212
GCRefKind m_HijackedReturnValueKind;
210213
PTR_UIntNative m_pConservativeStackRangeLowerBound;
211214
PTR_UIntNative m_pConservativeStackRangeUpperBound;
212-
uint32_t m_dwFlags;
215+
uint32_t m_dwFlags;
213216
PTR_ExInfo m_pNextExInfo;
214217
PTR_VOID m_pendingFuncletFramePointer;
215218
PreservedRegPtrs m_funcletPtrs; // @TODO: Placing the 'scratch space' in the StackFrameIterator is not
216219
// preferred because not all StackFrameIterators require this storage
217220
// space. However, the implementation simpler by doing it this way.
218221
bool m_ShouldSkipRegularGcReporting;
219222
PTR_VOID m_OriginalControlPC;
223+
PTR_PInvokeTransitionFrame m_pPreviousTransitionFrame;
220224
};
221225

222226
#endif // __StackFrameIterator_h__

0 commit comments

Comments
 (0)