-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Change unhandled exception detection #117620
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -295,6 +295,8 @@ HRESULT CorHost2::ExecuteAssembly(DWORD dwAppDomainId, | |
|
||
AppDomain *pCurDomain = SystemDomain::GetCurrentDomain(); | ||
|
||
UnhandledExceptionMarkerFrame unhandledExceptionMarkerFrame; | ||
|
||
Thread *pThread = GetThreadNULLOk(); | ||
if (pThread == NULL) | ||
{ | ||
|
@@ -305,6 +307,11 @@ HRESULT CorHost2::ExecuteAssembly(DWORD dwAppDomainId, | |
} | ||
} | ||
|
||
{ | ||
GCX_COOP(); | ||
unhandledExceptionMarkerFrame.Push(pThread); | ||
} | ||
|
||
INSTALL_UNHANDLED_MANAGED_EXCEPTION_TRAP; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should the unhandledExceptionMarkerFrame be coupled with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for the suggestion, it looks like it would make sense to put the frame creation / destruction into the |
||
INSTALL_UNWIND_AND_CONTINUE_HANDLER; | ||
|
||
|
@@ -327,7 +334,6 @@ HRESULT CorHost2::ExecuteAssembly(DWORD dwAppDomainId, | |
|
||
{ | ||
GCX_COOP(); | ||
|
||
PTRARRAYREF arguments = NULL; | ||
GCPROTECT_BEGIN(arguments); | ||
|
||
|
@@ -359,6 +365,11 @@ HRESULT CorHost2::ExecuteAssembly(DWORD dwAppDomainId, | |
UNINSTALL_UNWIND_AND_CONTINUE_HANDLER; | ||
UNINSTALL_UNHANDLED_MANAGED_EXCEPTION_TRAP; | ||
|
||
{ | ||
GCX_COOP(); | ||
unhandledExceptionMarkerFrame.Pop(pThread); | ||
} | ||
|
||
#ifdef LOG_EXECUTABLE_ALLOCATOR_STATISTICS | ||
ExecutableAllocator::DumpHolderUsage(); | ||
ExecutionManager::DumpExecutionManagerUsage(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does ExecuteInDefaultAppDomain below need the same change? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I've missed that one. |
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4013,16 +4013,17 @@ extern "C" CLR_BOOL QCALLTYPE SfiNext(StackFrameIterator* pThis, uint* uExCollid | |
// 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. | ||
bool isNotHandledByRuntime = | ||
(pFrame == FRAME_TOP) || | ||
(IsTopmostDebuggerU2MCatchHandlerFrame(pFrame) && !((DebuggerU2MCatchHandlerFrame*)pFrame)->CatchesAllExceptions()) | ||
bool reportUnhandledException = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment about DebuggerU2MCatchHandlerFrame above needs updating |
||
((pFrame != FRAME_TOP) && | ||
(pFrame->GetFrameIdentifier() == FrameIdentifier::UnhandledExceptionMarkerFrame) && | ||
IsExceptionFromManagedCode(pTopExInfo->m_ptrs.ExceptionRecord)) | ||
#ifdef HOST_UNIX | ||
// Don't allow propagating exceptions from managed to non-runtime native code | ||
|| isPropagatingToExternalNativeCode | ||
#endif | ||
; | ||
|
||
if (isNotHandledByRuntime && IsExceptionFromManagedCode(pTopExInfo->m_ptrs.ExceptionRecord)) | ||
if (reportUnhandledException) | ||
{ | ||
EH_LOG((LL_INFO100, "SfiNext (pass %d): no more managed frames on the stack, the exception is unhandled", pTopExInfo->m_passNumber)); | ||
if (pTopExInfo->m_passNumber == 1) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -100,6 +100,9 @@ | |
// +-DebuggerU2MCatchHandlerFrame - marker frame to indicate that native code is going to catch and | ||
// | swallow a managed exception | ||
// | | ||
// +- UnhandledExceptionMarkerFrame - When exception handling passes through this frame, | ||
// | the exception is reported as unhandled. | ||
// | | ||
#ifdef DEBUGGING_SUPPORTED | ||
// +-FuncEvalFrame - frame for debugger function evaluation | ||
#endif // DEBUGGING_SUPPORTED | ||
|
@@ -2077,15 +2080,13 @@ class DebuggerU2MCatchHandlerFrame : public Frame | |
{ | ||
public: | ||
#ifndef DACCESS_COMPILE | ||
DebuggerU2MCatchHandlerFrame(bool catchesAllExceptions) : Frame(FrameIdentifier::DebuggerU2MCatchHandlerFrame), | ||
m_catchesAllExceptions(catchesAllExceptions) | ||
DebuggerU2MCatchHandlerFrame() : Frame(FrameIdentifier::DebuggerU2MCatchHandlerFrame) | ||
{ | ||
WRAPPER_NO_CONTRACT; | ||
Frame::Push(); | ||
} | ||
|
||
DebuggerU2MCatchHandlerFrame(Thread * pThread, bool catchesAllExceptions) : Frame(FrameIdentifier::DebuggerU2MCatchHandlerFrame), | ||
m_catchesAllExceptions(catchesAllExceptions) | ||
DebuggerU2MCatchHandlerFrame(Thread * pThread) : Frame(FrameIdentifier::DebuggerU2MCatchHandlerFrame) | ||
{ | ||
WRAPPER_NO_CONTRACT; | ||
Frame::Push(pThread); | ||
|
@@ -2097,16 +2098,19 @@ class DebuggerU2MCatchHandlerFrame : public Frame | |
LIMITED_METHOD_DAC_CONTRACT; | ||
return TT_U2M; | ||
} | ||
}; | ||
|
||
typedef DPTR(class UnhandledExceptionMarkerFrame) PTR_UnhandledExceptionMarkerFrame; | ||
|
||
bool CatchesAllExceptions() | ||
// When exception handling passes through this frame, the exception is reported as unhandled. | ||
class UnhandledExceptionMarkerFrame : public Frame | ||
{ | ||
public: | ||
#ifndef DACCESS_COMPILE | ||
UnhandledExceptionMarkerFrame() : Frame(FrameIdentifier::UnhandledExceptionMarkerFrame) | ||
{ | ||
LIMITED_METHOD_DAC_CONTRACT; | ||
return m_catchesAllExceptions; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The UnhandledExceptionMarkerFrame class is missing the GetTransitionType() method override that is present in DebuggerU2MCatchHandlerFrame. This inconsistency may cause issues with frame type identification during stack walks. Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||
|
||
private: | ||
// The catch handled marked by the DebuggerU2MCatchHandlerFrame catches all exceptions. | ||
bool m_catchesAllExceptions; | ||
#endif | ||
}; | ||
|
||
// Frame for the Reverse PInvoke (i.e. UnmanagedCallersOnlyAttribute). | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does the new Frame type need to be hooked up into cDac?
cc @max-charlamb
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatively, I am wondering whether it would be better to have a managed code at the root of these threads so that the unhandled exception handling for them works the same way as exception handling for external threads. Would it allow us to get rid of this special casing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure I understand how that would work and how it would work the same way as for external threads. For external threads, we assume that the thread will either catch it or the exception will be reported as c++ unhandled. So we let it flow into the native code. But we cannot do that for the unhandled exceptions that we want to report as such, because at the point we would reach some top level catch, the stack frames of the throwing location would be gone. So when we would break into the debugger or have a dump caused by the unhandled exception, there would be no evidence on why it happened.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there is an unhandled external exception thrown on external thread (and the external thread does not have any catch in native code), we will end up reporting it like a regular unhandled managed exception as far as I can tell.
I have tried this: https://gist.github.com/jkotas/2d4f15056768c1b221a78684b4245fec
Console output:
Stacktrace in the crash dump is - notice that it includes
ThreadFunction
where the exception was thrown originally:I do not see what we are getting by special casing finalizer and other threads created by the runtime.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am trying to understand why this is so complicated. If it is too complicated for no good reason and it is too risky to simplify for .NET 10, it is fine to postpone the simplification to .NET 11.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We would not let the exception flow to the native caller on Unix. The idea is that the finalizer and other threads would start with a regular reverse P/Invoke transition (same way as on Native AOT). Unhandled exceptions stop at reverse P/Invoke transition on non-Windows. I believe that we preserve the stacktrace of the original fault when exception goes unhandled at reverse P/Invoke transition on Unix.
In other words, the main reason for this special casing seems to be that these few places do not enter the runtime via a regular reverse P/Invoke transition. They use a custom scheme that we need to compensate for.
I can believe that the diagnostic tests depend on implementation details and changing anything is going to break them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure how it would work for the Main method entry. We call the Main via CallDescrWorker. Where would we fit in the reverse pinvoke transition?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Runtime C/C++ code would call the reverse PInvoke directly, without CallDescrWorker. The reverse PInvoke would take function pointer that points to Main method, some information about Main method signature (there are a few possible shapes - returning int vs. returning void, etc.), and the arguments to pass to main method.
The extra reverse PInvoke may impact diagnostic as discussed above. It would be show up in stacktraces, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related #117844 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your example works because we report the exception as unhandled in the EH and then re-raise it to flow into the native code to give the external native code a chance to catch it. If we didn't treat it as unhandled, the EH would currently unwind stack upto the native code frame and raise the exception from there. That's the way we use for
CallDescrWorker
. But that way looses the managed frames that triggered the exception.If we used the same mechanism as we currently use for re-raising the exception when we consider it unhandled, but wanted to do it for any reverse pinvoke transition without checking if there are managed frames on the stack on top of that, then we would have a problem. This exception is re-raised when there are all the managed frames upto the one that has thrown the exception still on the stack. So re-raising from that point requires us to make sure that when the exception calls
ProcessCLRException
for all managed frames below the native frame, we don't do anything. That's currently done by marking the thread byTSNC_ProcessedUnhandledException
. But if the exception would end up flowing through the native frames uncaught and then reach managed code again, we want theProcessCLRException
to start doing its job again past that point.I am not trying to say we cannot do that, but it complicates things and will require some extra state to store the address of the boundary frame that would decide whether the
ProcessCLRException
should ignore the exception for a given frame or not. And in case the exception will be caught in the native code between those blocks of managed code, the state will become stale and we would need to clean it up at a right time later.