-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Make longjmp over managed frames work #111259
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 3 commits
82b67ac
3a019a2
ec133dc
a7366aa
19b02b1
7fe2b47
2e11229
e0a9b38
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 |
|---|---|---|
|
|
@@ -932,6 +932,13 @@ ProcessCLRExceptionNew(IN PEXCEPTION_RECORD pExceptionRecord, | |
|
|
||
| Thread* pThread = GetThread(); | ||
|
|
||
| if (pExceptionRecord->ExceptionCode == STATUS_LONGJUMP) | ||
| { | ||
| // This is an exception used to unwind during longjmp function. We just let it pass through managed | ||
| // frames without any interaction. | ||
| return ExceptionContinueSearch; | ||
| } | ||
|
|
||
| if (pThread->HasThreadStateNC(Thread::TSNC_ProcessedUnhandledException)) | ||
| { | ||
| if ((pExceptionRecord->ExceptionFlags & EXCEPTION_UNWINDING)) | ||
|
|
@@ -970,7 +977,7 @@ ProcessCLRExceptionNew(IN PEXCEPTION_RECORD pExceptionRecord, | |
| else | ||
| { | ||
| OBJECTREF oref = ExceptionTracker::CreateThrowable(pExceptionRecord, FALSE); | ||
| DispatchManagedException(oref, pContextRecord); | ||
| DispatchManagedException(oref, pContextRecord, pExceptionRecord); | ||
| } | ||
| } | ||
| #endif // !HOST_UNIX | ||
|
|
@@ -5646,7 +5653,7 @@ void FirstChanceExceptionNotification() | |
| #endif // TARGET_UNIX | ||
| } | ||
|
|
||
| VOID DECLSPEC_NORETURN DispatchManagedException(OBJECTREF throwable, CONTEXT* pExceptionContext) | ||
| VOID DECLSPEC_NORETURN DispatchManagedException(OBJECTREF throwable, CONTEXT* pExceptionContext, EXCEPTION_RECORD* pExceptionRecord) | ||
| { | ||
| STATIC_CONTRACT_THROWS; | ||
| STATIC_CONTRACT_GC_TRIGGERS; | ||
|
|
@@ -5660,14 +5667,28 @@ VOID DECLSPEC_NORETURN DispatchManagedException(OBJECTREF throwable, CONTEXT* pE | |
|
|
||
| ULONG_PTR hr = GetHRFromThrowable(throwable); | ||
|
|
||
| EXCEPTION_RECORD exceptionRecord; | ||
| exceptionRecord.ExceptionCode = EXCEPTION_COMPLUS; | ||
| exceptionRecord.ExceptionFlags = EXCEPTION_NONCONTINUABLE | EXCEPTION_SOFTWARE_ORIGINATE; | ||
| exceptionRecord.ExceptionAddress = (void *)(void (*)(OBJECTREF))&DispatchManagedException; | ||
| exceptionRecord.NumberParameters = MarkAsThrownByUs(exceptionRecord.ExceptionInformation, hr); | ||
| exceptionRecord.ExceptionRecord = NULL; | ||
| EXCEPTION_RECORD newExceptionRecord; | ||
| newExceptionRecord.ExceptionCode = EXCEPTION_COMPLUS; | ||
| newExceptionRecord.ExceptionFlags = EXCEPTION_NONCONTINUABLE | EXCEPTION_SOFTWARE_ORIGINATE; | ||
| newExceptionRecord.ExceptionAddress = (void *)(void (*)(OBJECTREF))&DispatchManagedException; | ||
| newExceptionRecord.NumberParameters = MarkAsThrownByUs(newExceptionRecord.ExceptionInformation, hr); | ||
| newExceptionRecord.ExceptionRecord = NULL; | ||
|
|
||
| ExInfo exInfo(pThread, &exceptionRecord, pExceptionContext, ExKind::Throw); | ||
| ExInfo exInfo(pThread, &newExceptionRecord, pExceptionContext, ExKind::Throw); | ||
|
|
||
| #ifdef HOST_WINDOWS | ||
| // On Windows, this enables the possibility to propagate a longjmp across managed frames. Longjmp | ||
| // behaves like a SEH exception, but only runs the second (unwinding) pass. | ||
| if ((pExceptionRecord != NULL) && (pExceptionRecord->ExceptionCode == STATUS_LONGJUMP)) | ||
| { | ||
| // longjmp over managed frames. The EXCEPTION_RECORD::ExceptionInformation store the | ||
| // jmp_buf and the return value for STATUS_LONGJUMP, so we extract it here. When the | ||
| // exception handling code moves out of the managed frames, we call the longjmp with | ||
| // these arguments again to continue its propagation. | ||
| exInfo.m_pLongJmpBuf = (jmp_buf*)pExceptionRecord->ExceptionInformation[0]; | ||
| exInfo.m_longJmpReturnValue = (int)pExceptionRecord->ExceptionInformation[1]; | ||
| } | ||
| #endif // HOST_WINDOWS | ||
|
|
||
| if (pThread->IsAbortInitiated () && IsExceptionOfType(kThreadAbortException,&throwable)) | ||
| { | ||
|
|
@@ -7694,6 +7715,14 @@ size_t GetSSPForFrameOnCurrentStack(TADDR ip) | |
| } | ||
| #endif // HOST_AMD64 && HOST_WINDOWS | ||
|
|
||
| #ifdef HOST_WINDOWS | ||
| VOID DECLSPEC_NORETURN PropagateLongJmpThroughNativeFrames(jmp_buf *pJmpBuf, int retVal) | ||
| { | ||
| WRAPPER_NO_CONTRACT; | ||
| longjmp(*pJmpBuf, retVal); | ||
| } | ||
| #endif // HOST_WINDOWS | ||
|
|
||
| extern "C" void * QCALLTYPE CallCatchFunclet(QCall::ObjectHandleOnStack exceptionObj, BYTE* pHandlerIP, REGDISPLAY* pvRegDisplay, ExInfo* exInfo) | ||
| { | ||
| QCALL_CONTRACT; | ||
|
|
@@ -7757,6 +7786,11 @@ extern "C" void * QCALLTYPE CallCatchFunclet(QCall::ObjectHandleOnStack exceptio | |
|
|
||
| ExInfo* pExInfo = (PTR_ExInfo)pThread->GetExceptionState()->GetCurrentExceptionTracker(); | ||
|
|
||
| #ifdef HOST_WINDOWS | ||
| jmp_buf* pLongJmpBuf = pExInfo->m_pLongJmpBuf; | ||
| int longJmpReturnValue = pExInfo->m_longJmpReturnValue; | ||
| #endif // HOST_WINDOWS | ||
|
|
||
| #ifdef HOST_UNIX | ||
| Interop::ManagedToNativeExceptionCallback propagateExceptionCallback = pExInfo->m_propagateExceptionCallback; | ||
| void* propagateExceptionContext = pExInfo->m_propagateExceptionContext; | ||
|
|
@@ -7875,29 +7909,43 @@ extern "C" void * QCALLTYPE CallCatchFunclet(QCall::ObjectHandleOnStack exceptio | |
| #elif defined(HOST_RISCV64) || defined(HOST_LOONGARCH64) | ||
| pvRegDisplay->pCurrentContext->Ra = GetIP(pvRegDisplay->pCurrentContext); | ||
| #endif | ||
| SetIP(pvRegDisplay->pCurrentContext, (PCODE)(void (*)(Object*))PropagateExceptionThroughNativeFrames); | ||
| #if defined(HOST_AMD64) | ||
| SetSP(pvRegDisplay->pCurrentContext, targetSp - 8); | ||
| #elif defined(HOST_X86) | ||
| SetSP(pvRegDisplay->pCurrentContext, targetSp - 4); | ||
| #endif | ||
|
|
||
| // The SECOND_ARG_REG is defined only for Windows, it is used to handle longjmp propagation over managed frames | ||
| #ifdef HOST_AMD64 | ||
| #ifdef UNIX_AMD64_ABI | ||
| #define FIRST_ARG_REG Rdi | ||
| #else | ||
| #define FIRST_ARG_REG Rcx | ||
| #define SECOND_ARG_REG Rdx | ||
| #endif | ||
| #elif defined(HOST_X86) | ||
| #define FIRST_ARG_REG Ecx | ||
| #elif defined(HOST_ARM64) | ||
| #define FIRST_ARG_REG X0 | ||
| #define SECOND_ARG_REG X1 | ||
| #elif defined(HOST_ARM) | ||
| #define FIRST_ARG_REG R0 | ||
| #elif defined(HOST_RISCV64) || defined(HOST_LOONGARCH64) | ||
| #define FIRST_ARG_REG A0 | ||
| #endif | ||
|
|
||
| pvRegDisplay->pCurrentContext->FIRST_ARG_REG = (size_t)OBJECTREFToObject(exceptionObj.Get()); | ||
| #ifdef HOST_WINDOWS | ||
| if (pLongJmpBuf != NULL) | ||
| { | ||
| SetIP(pvRegDisplay->pCurrentContext, (PCODE)PropagateLongJmpThroughNativeFrames); | ||
| pvRegDisplay->pCurrentContext->FIRST_ARG_REG = (size_t)pLongJmpBuf; | ||
| pvRegDisplay->pCurrentContext->SECOND_ARG_REG = (size_t)longJmpReturnValue; | ||
| } | ||
| else | ||
| #endif | ||
| { | ||
| SetIP(pvRegDisplay->pCurrentContext, (PCODE)(void (*)(Object*))PropagateExceptionThroughNativeFrames); | ||
| pvRegDisplay->pCurrentContext->FIRST_ARG_REG = (size_t)OBJECTREFToObject(exceptionObj.Get()); | ||
| } | ||
| #undef FIRST_ARG_REG | ||
| ClrRestoreNonvolatileContext(pvRegDisplay->pCurrentContext, targetSSP); | ||
| } | ||
|
|
@@ -8098,7 +8146,7 @@ extern "C" BOOL QCALLTYPE EHEnumNext(EH_CLAUSE_ENUMERATOR* pEHEnum, RhEHClause* | |
| ExtendedEHClauseEnumerator *pExtendedEHEnum = (ExtendedEHClauseEnumerator*)pEHEnum; | ||
| StackFrameIterator *pFrameIter = pExtendedEHEnum->pFrameIter; | ||
|
|
||
| if (pEHEnum->iCurrentPos < pExtendedEHEnum->EHCount) | ||
| while (pEHEnum->iCurrentPos < pExtendedEHEnum->EHCount) | ||
| { | ||
| IJitManager* pJitMan = pFrameIter->m_crawl.GetJitManager(); | ||
| const METHODTOKEN& MethToken = pFrameIter->m_crawl.GetMethodToken(); | ||
|
|
@@ -8151,6 +8199,13 @@ extern "C" BOOL QCALLTYPE EHEnumNext(EH_CLAUSE_ENUMERATOR* pEHEnum, RhEHClause* | |
| { | ||
| result = FALSE; | ||
| } | ||
| #ifdef HOST_WINDOWS | ||
| // When processing longjmp, only finally clauses are considered. | ||
|
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. This is one of the reasons for not supporting setjmp/longjmp. setjmp/longjmp behaves like uncatchable exception - managed code is not generally robust against uncatchable exceptions. 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. I completely agree. The intent is really just to fix the regression on Windows w.r.t. the legacy EH. It is a best effort and not something people should use in new code. I hope that the fact that it doesn't work on Linux is enough reason for trying to stay away from using longjmp over managed code. |
||
| if ((pExInfo->m_pLongJmpBuf == NULL) || (flags & COR_ILEXCEPTION_CLAUSE_FINALLY) || (flags & COR_ILEXCEPTION_CLAUSE_FAULT)) | ||
| #endif // HOST_WINDOWS | ||
| { | ||
| break; | ||
| } | ||
| } | ||
| END_QCALL; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| project (Test11242Lib) | ||
janvorli marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| include_directories(${INC_PLATFORM_DIR}) | ||
|
|
||
| if(CLR_CMAKE_HOST_WIN32) | ||
| set_source_files_properties(Test11242.c PROPERTIES COMPILE_OPTIONS /TC) # compile as C | ||
| else() | ||
| set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fvisibility=hidden") | ||
| set(CMAKE_CPP_FLAGS "${CMAKE_CPP_FLAGS} -fvisibility=hidden -Wno-return-type-c-linkage") | ||
| endif() | ||
|
|
||
| # add the executable | ||
| add_library (Test11242Lib SHARED Test11242.c) | ||
|
|
||
| # add the install targets | ||
| install (TARGETS Test11242Lib DESTINATION bin) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
| // Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
|
|
||
| #include <stdint.h> | ||
| #include <stdio.h> | ||
| #include <setjmp.h> | ||
|
|
||
|
|
||
| #ifdef _MSC_VER | ||
| #define DLLEXPORT __declspec(dllexport) | ||
| #else | ||
| #define DLLEXPORT __attribute__((visibility("default"))) | ||
| #endif // _MSC_VER | ||
|
|
||
| DLLEXPORT void TestSetJmp(void (*managedCallback)(void *)) | ||
| { | ||
| jmp_buf jmpBuf; | ||
| if (!setjmp(jmpBuf)) | ||
| { | ||
| managedCallback(&jmpBuf); | ||
| } | ||
| else | ||
| { | ||
| printf("longjmp called\n"); | ||
| } | ||
| } | ||
|
|
||
| DLLEXPORT void TestLongJmp(void *jmpBuf) | ||
| { | ||
| longjmp(*(jmp_buf*)jmpBuf, 1); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System; | ||
| using System.Runtime.InteropServices; | ||
| using System.Runtime.CompilerServices; | ||
| using Xunit; | ||
|
|
||
| public static class Test11242 | ||
| { | ||
| [DllImport("Test11242Lib")] | ||
| static extern unsafe void TestSetJmp(delegate* unmanaged<void*,void> managedCallback); | ||
|
|
||
| [DllImport("Test11242Lib")] | ||
| static extern unsafe void TestLongJmp(void *jmpBuf); | ||
|
|
||
| static bool ExceptionFilter(Exception ex) | ||
| { | ||
| Assert.Fail("Should not call filter for longjmp SEH exception"); | ||
| return true; | ||
| } | ||
|
|
||
| static bool wasFinallyInvoked = false; | ||
|
|
||
| [UnmanagedCallersOnly] | ||
| static unsafe void ManagedCallback(void *jmpBuf) | ||
| { | ||
| try | ||
| { | ||
| TestLongJmp(jmpBuf); | ||
| } | ||
| catch (Exception ex) when (ExceptionFilter(ex)) | ||
| { | ||
| Assert.Fail("Should not catch longjmp SEH exception via filter"); | ||
| } | ||
| catch | ||
| { | ||
| Assert.Fail("Should not catch longjmp SEH exception via filter"); | ||
| } | ||
| finally | ||
| { | ||
| Console.WriteLine("Finally block executed"); | ||
| wasFinallyInvoked = true; | ||
| } | ||
| Assert.Fail("Should not reach here"); | ||
| } | ||
|
|
||
| [Fact] | ||
| public static unsafe void TestEntryPoint() | ||
| { | ||
| TestSetJmp(&ManagedCallback); | ||
| Assert.True(wasFinallyInvoked); | ||
| Console.WriteLine("Test passed"); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
| <PropertyGroup> | ||
| <!-- Needed for CMakeProjectReference --> | ||
| <RequiresProcessIsolation>true</RequiresProcessIsolation> | ||
| <AllowUnsafeBlocks>true</AllowUnsafeBlocks> | ||
| <CLRTestPriority>1</CLRTestPriority> | ||
| <CLRTestTargetUnsupported Condition="'$(TargetOS)' != 'windows'">true</CLRTestTargetUnsupported> | ||
| </PropertyGroup> | ||
| <PropertyGroup> | ||
| <DebugType>PdbOnly</DebugType> | ||
| <Optimize>True</Optimize> | ||
| </PropertyGroup> | ||
| <ItemGroup> | ||
| <Compile Include="Test11242.cs" /> | ||
| </ItemGroup> | ||
| <ItemGroup> | ||
| <CMakeProjectReference Include="CMakeLists.txt" /> | ||
| </ItemGroup> | ||
| </Project> |
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.
Do we need to unwind Frames to avoid corrupting runtime data structures?
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.
And if we are unwinding Frames, is it that hard to call finallys too to match the old behavior?
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 had it implemented by propagating the exception through the managed frames as usual, so the finallys were called, and then re-issued the longjmp after processing them. The only missing part to do was preventing calling catch handlers. The old EH doesn't call those "naturally", as the longjmp runs only the unwind pass.
And then you've mentioned it was not supported, so I have deleted that code :-).
But anyways, I can change it back to that, the change wasn't complicated. The only thing is that the Unix handling would then be different.
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.
longjump over managed code does not and cannot work well on Unix, so I do not think it is a problem