Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NativeAOT] Objective-C exception propagation #80334

Merged
3 changes: 3 additions & 0 deletions src/coreclr/nativeaot/Bootstrap/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ extern "C" void IDynamicCastableGetInterfaceImplementation();
extern "C" void ObjectiveCMarshalTryGetTaggedMemory();
extern "C" void ObjectiveCMarshalGetIsTrackedReferenceCallback();
extern "C" void ObjectiveCMarshalGetOnEnteredFinalizerQueueCallback();
extern "C" void ObjectiveCMarshalGetUnhandledExceptionPropagationHandler();
#endif

typedef void(*pfn)();
Expand All @@ -134,10 +135,12 @@ static const pfn c_classlibFunctions[] = {
&ObjectiveCMarshalTryGetTaggedMemory,
&ObjectiveCMarshalGetIsTrackedReferenceCallback,
&ObjectiveCMarshalGetOnEnteredFinalizerQueueCallback,
&ObjectiveCMarshalGetUnhandledExceptionPropagationHandler,
#else
nullptr,
nullptr,
nullptr,
nullptr,
#endif
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,8 @@ private static void DispatchEx(scoped ref StackFrameIterator frameIter, ref ExIn
byte* prevOriginalPC = null;
UIntPtr prevFramePtr = UIntPtr.Zero;
bool unwoundReversePInvoke = false;
IntPtr pReversePInvokePropagationCallback = IntPtr.Zero;
IntPtr pReversePInvokePropagationContext = IntPtr.Zero;

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

if (pCatchHandler == null)
#if FEATURE_OBJCMARSHAL
if (unwoundReversePInvoke)
{
// We did not find any managed handlers before hitting a reverse P/Invoke boundary.
// See if the classlib has a handler to propagate the exception to native code.
IntPtr pGetHandlerClasslibFunction = (IntPtr)InternalCalls.RhpGetClasslibFunctionFromCodeAddress((IntPtr)prevControlPC,
ClassLibFunctionId.ObjectiveCMarshalGetUnhandledExceptionPropagationHandler);
if (pGetHandlerClasslibFunction != IntPtr.Zero)
{
var pGetHandler = (delegate*<object, IntPtr, out IntPtr, IntPtr>)pGetHandlerClasslibFunction;
pReversePInvokePropagationCallback = pGetHandler(
exceptionObj, (IntPtr)prevControlPC, out pReversePInvokePropagationContext);
if (pReversePInvokePropagationCallback != IntPtr.Zero)
{
// Tell the second pass to unwind to this frame.
handlingFrameSP = frameIter.SP;
catchingTryRegionIdx = MaxTryRegionIdx;
}
}
}
#endif // FEATURE_OBJCMARSHAL

if (pCatchHandler == null && pReversePInvokePropagationCallback == IntPtr.Zero)
{
OnUnhandledExceptionViaClassLib(exceptionObj);

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

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

// ------------------------------------------------
//
Expand All @@ -673,12 +697,23 @@ private static void DispatchEx(scoped ref StackFrameIterator frameIter, ref ExIn

exInfo._passNumber = 2;
startIdx = MaxTryRegionIdx;
unwoundReversePInvoke = false;
isValid = frameIter.Init(exInfo._pExContext, (exInfo._kind & ExKind.InstructionFaultFlag) != 0);
for (; isValid && ((byte*)frameIter.SP <= (byte*)handlingFrameSP); isValid = frameIter.Next(&startIdx))
for (; isValid && ((byte*)frameIter.SP <= (byte*)handlingFrameSP); isValid = frameIter.Next(&startIdx, &unwoundReversePInvoke))
{
Debug.Assert(isValid, "second-pass EH unwind failed unexpectedly");
DebugScanCallFrame(exInfo._passNumber, frameIter.ControlPC, frameIter.SP);

if (unwoundReversePInvoke)
{
Debug.Assert(pReversePInvokePropagationCallback != IntPtr.Zero, "Unwound to a reverse P/Invoke in the second pass. We should have a propagation handler.");
Debug.Assert(frameIter.SP == handlingFrameSP, "Encountered a different reverse P/Invoke frame in the second pass.");
Debug.Assert(frameIter.PreviousTransitionFrame != IntPtr.Zero, "Should have a transition frame for reverse P/Invoke.");
// Found the native frame that called the reverse P/invoke.
// It is not possible to run managed second pass handlers on a native frame.
break;
}

if ((frameIter.SP == handlingFrameSP)
#if TARGET_ARM64
&& (frameIter.ControlPC == prevControlPC)
Expand All @@ -693,6 +728,18 @@ private static void DispatchEx(scoped ref StackFrameIterator frameIter, ref ExIn
InvokeSecondPass(ref exInfo, startIdx);
}

#if FEATURE_OBJCMARSHAL
if (pReversePInvokePropagationCallback != IntPtr.Zero)
{
InternalCalls.RhpCallPropagateExceptionCallback(
pReversePInvokePropagationContext, pReversePInvokePropagationCallback, frameIter.RegisterSet, ref exInfo, frameIter.PreviousTransitionFrame);
// the helper should jump to propagation handler and not return
Debug.Assert(false, "unreachable");
FallbackFailFast(RhFailFastReason.InternalError, null);
}
#endif // FEATURE_OBJCMARSHAL


// ------------------------------------------------
//
// Call the handler and resume execution
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ internal enum ClassLibFunctionId
ObjectiveCMarshalTryGetTaggedMemory = 10,
ObjectiveCMarshalGetIsTrackedReferenceCallback = 11,
ObjectiveCMarshalGetOnEnteredFinalizerQueueCallback = 12,
ObjectiveCMarshalGetUnhandledExceptionPropagationHandler = 13,
}

internal static class InternalCalls
Expand Down Expand Up @@ -230,6 +231,13 @@ internal static extern unsafe IntPtr RhpCallCatchFunclet(
internal static extern unsafe bool RhpCallFilterFunclet(
object exceptionObj, byte* pFilterIP, void* pvRegDisplay);

#if FEATURE_OBJCMARSHAL
[RuntimeImport(Redhawk.BaseName, "RhpCallPropagateExceptionCallback")]
[MethodImpl(MethodImplOptions.InternalCall)]
internal static extern unsafe IntPtr RhpCallPropagateExceptionCallback(
IntPtr callbackContext, IntPtr callback, void* pvRegDisplay, ref EH.ExInfo exInfo, IntPtr pPreviousTransitionFrame);
#endif // FEATURE_OBJCMARSHAL

[RuntimeImport(Redhawk.BaseName, "RhpFallbackFailFast")]
[MethodImpl(MethodImplOptions.InternalCall)]
internal static extern unsafe void RhpFallbackFailFast();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@ internal unsafe struct StackFrameIterator
private REGDISPLAY _regDisplay;
[FieldOffset(AsmOffsets.OFFSETOF__StackFrameIterator__m_OriginalControlPC)]
private IntPtr _originalControlPC;
[FieldOffset(AsmOffsets.OFFSETOF__StackFrameIterator__m_pPreviousTransitionFrame)]
private IntPtr _pPreviousTransitionFrame;

internal byte* ControlPC { get { return (byte*)_controlPC; } }
internal byte* OriginalControlPC { get { return (byte*)_originalControlPC; } }
internal void* RegisterSet { get { fixed (void* pRegDisplay = &_regDisplay) { return pRegDisplay; } } }
internal UIntPtr SP { get { return _regDisplay.SP; } }
internal UIntPtr FramePointer { get { return _framePointer; } }
internal IntPtr PreviousTransitionFrame { get { return _pPreviousTransitionFrame; } }

internal bool Init(EH.PAL_LIMITED_CONTEXT* pStackwalkCtx, bool instructionFault = false)
{
Expand Down
10 changes: 10 additions & 0 deletions src/coreclr/nativeaot/Runtime/ICodeManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ enum class ClasslibFunctionId
ObjectiveCMarshalTryGetTaggedMemory = 10,
ObjectiveCMarshalGetIsTrackedReferenceCallback = 11,
ObjectiveCMarshalGetOnEnteredFinalizerQueueCallback = 12,
ObjectiveCMarshalGetUnhandledExceptionPropagationHandler = 13,
};

enum class AssociatedDataFlags : unsigned char
Expand All @@ -165,6 +166,14 @@ enum class AssociatedDataFlags : unsigned char
HasUnboxingStubTarget = 1,
};

enum UnwindStackFrameFlags
{
USFF_None = 0,
// If this is a reverse P/Invoke frame, do not continue the unwind
// after extracting the saved transition frame.
USFF_StopUnwindOnTransitionFrame = 1,
};

class ICodeManager
{
public:
Expand All @@ -185,6 +194,7 @@ class ICodeManager
bool isActiveStackFrame) PURE_VIRTUAL

virtual bool UnwindStackFrame(MethodInfo * pMethodInfo,
uint32_t flags,
REGDISPLAY * pRegisterSet, // in/out
PInvokeTransitionFrame** ppPreviousTransitionFrame) PURE_VIRTUAL // out

Expand Down
50 changes: 38 additions & 12 deletions src/coreclr/nativeaot/Runtime/StackFrameIterator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ void StackFrameIterator::EnterInitialInvalidState(Thread * pThreadToWalk)
m_ShouldSkipRegularGcReporting = false;
m_pendingFuncletFramePointer = NULL;
m_pNextExInfo = pThreadToWalk->GetCurExInfo();
m_pPreviousTransitionFrame = NULL;
SetControlPC(0);
}

Expand Down Expand Up @@ -172,6 +173,7 @@ void StackFrameIterator::InternalInit(Thread * pThreadToWalk, PInvokeTransitionF
}

m_dwFlags = dwFlags;
m_pPreviousTransitionFrame = pFrame;

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

Expand All @@ -1432,33 +1436,41 @@ void StackFrameIterator::NextInternal()
uintptr_t DEBUG_preUnwindSP = m_RegDisplay.GetSP();
#endif

PInvokeTransitionFrame* pPreviousTransitionFrame;
FAILFAST_OR_DAC_FAIL(GetCodeManager()->UnwindStackFrame(&m_methodInfo, &m_RegDisplay, &pPreviousTransitionFrame));
uint32_t unwindFlags = USFF_None;
if ((m_dwFlags & SkipNativeFrames) != 0)
{
unwindFlags |= USFF_StopUnwindOnTransitionFrame;
}

FAILFAST_OR_DAC_FAIL(GetCodeManager()->UnwindStackFrame(&m_methodInfo, unwindFlags, &m_RegDisplay,
&m_pPreviousTransitionFrame));

if (m_pPreviousTransitionFrame != NULL)
{
m_dwFlags |= UnwoundReversePInvoke;
}

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

if (pPreviousTransitionFrame != NULL)
if (m_pPreviousTransitionFrame != NULL && (m_dwFlags & SkipNativeFrames) != 0)
{
ASSERT(!doingFuncletUnwind);

if (pPreviousTransitionFrame == TOP_OF_STACK_MARKER)
if (m_pPreviousTransitionFrame == TOP_OF_STACK_MARKER)
{
SetControlPC(0);
}
else
{
// NOTE: If this is an EH stack walk, then reinitializing the iterator using the GC stack
// walk flags is incorrect. That said, this is OK because the exception dispatcher will
// immediately trigger a failfast when it sees the UnwoundReversePInvoke flag.
// NOTE: This can generate a conservative stack range if the recovered PInvoke callsite
// resides in an assembly thunk and not in normal managed code. In this case InternalInit
// will unwind through the thunk and back to the nearest managed frame, and therefore may
// see a conservative range reported by one of the thunks encountered during this "nested"
// unwind.
InternalInit(m_pThread, pPreviousTransitionFrame, GcStackWalkFlags);
InternalInit(m_pThread, m_pPreviousTransitionFrame, GcStackWalkFlags);
m_dwFlags |= UnwoundReversePInvoke;
ASSERT(m_pInstance->IsManaged(m_ControlPC));
}
m_dwFlags |= UnwoundReversePInvoke;
}
else
{
Expand Down Expand Up @@ -1579,11 +1591,12 @@ void StackFrameIterator::NextInternal()
}

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

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

if (collapsingTargetFrame != NULL)
{
Expand Down Expand Up @@ -1692,7 +1705,8 @@ void StackFrameIterator::PrepareToYieldFrame()
if (!IsValid())
return;

ASSERT(m_pInstance->IsManaged(m_ControlPC));
ASSERT(m_pInstance->IsManaged(m_ControlPC) ||
((m_dwFlags & SkipNativeFrames) == 0 && (m_dwFlags & UnwoundReversePInvoke) != 0));

if (m_dwFlags & ApplyReturnAddressAdjustment)
{
Expand Down Expand Up @@ -1748,6 +1762,7 @@ REGDISPLAY * StackFrameIterator::GetRegisterSet()
PTR_VOID StackFrameIterator::GetEffectiveSafePointAddress()
{
ASSERT(IsValid());
ASSERT(m_effectiveSafePointAddress);
return m_effectiveSafePointAddress;
}

Expand Down Expand Up @@ -1780,6 +1795,17 @@ void StackFrameIterator::CalculateCurrentMethodState()
if (m_dwFlags & MethodStateCalculated)
return;

// Check if we are on a native frame.
if ((m_dwFlags & (SkipNativeFrames|UnwoundReversePInvoke)) == UnwoundReversePInvoke)
{
// There is no implementation of ICodeManager for native code.
m_pCodeManager = nullptr;
m_effectiveSafePointAddress = nullptr;
m_FramePointer = nullptr;
m_dwFlags |= MethodStateCalculated;
return;
}

// Assume that the caller is likely to be in the same module
if (m_pCodeManager == NULL || !m_pCodeManager->FindMethodInfo(m_ControlPC, &m_methodInfo))
{
Expand Down
8 changes: 6 additions & 2 deletions src/coreclr/nativeaot/Runtime/StackFrameIterator.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,10 @@ class StackFrameIterator
// The thread was interrupted in the current frame at the current IP by a signal, SuspendThread or similar.
ActiveStackFrame = 0x40,

GcStackWalkFlags = (CollapseFunclets | RemapHardwareFaultsToSafePoint),
// When encountering a reverse P/Invoke, unwind directly to the P/Invoke frame using the saved transition frame.
SkipNativeFrames = 0x80,

GcStackWalkFlags = (CollapseFunclets | RemapHardwareFaultsToSafePoint | SkipNativeFrames),
EHStackWalkFlags = ApplyReturnAddressAdjustment,
StackTraceStackWalkFlags = GcStackWalkFlags
};
Expand Down Expand Up @@ -209,14 +212,15 @@ class StackFrameIterator
GCRefKind m_HijackedReturnValueKind;
PTR_UIntNative m_pConservativeStackRangeLowerBound;
PTR_UIntNative m_pConservativeStackRangeUpperBound;
uint32_t m_dwFlags;
uint32_t m_dwFlags;
PTR_ExInfo m_pNextExInfo;
PTR_VOID m_pendingFuncletFramePointer;
PreservedRegPtrs m_funcletPtrs; // @TODO: Placing the 'scratch space' in the StackFrameIterator is not
// preferred because not all StackFrameIterators require this storage
// space. However, the implementation simpler by doing it this way.
bool m_ShouldSkipRegularGcReporting;
PTR_VOID m_OriginalControlPC;
PTR_PInvokeTransitionFrame m_pPreviousTransitionFrame;
};

#endif // __StackFrameIterator_h__
Loading