Skip to content

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

Merged
merged 8 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 65 additions & 13 deletions src/coreclr/vm/exceptionhandling.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -970,7 +970,7 @@ ProcessCLRExceptionNew(IN PEXCEPTION_RECORD pExceptionRecord,
else
{
OBJECTREF oref = ExceptionTracker::CreateThrowable(pExceptionRecord, FALSE);
DispatchManagedException(oref, pContextRecord);
DispatchManagedException(oref, pContextRecord, pExceptionRecord);
}
}
#endif // !HOST_UNIX
Expand Down Expand Up @@ -5646,7 +5646,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;
Expand All @@ -5660,14 +5660,32 @@ 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.
// NOTE: This is a best effort purely for backward compatibility with the legacy exception handling.
// Skipping over managed frames using setjmp/longjmp is
// is unsupported and it is not guaranteed to work reliably in all cases.
// https://learn.microsoft.com/dotnet/standard/native-interop/exceptions-interoperability#setjmplongjmp-behaviors
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))
{
Expand Down Expand Up @@ -7694,6 +7712,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;
Expand Down Expand Up @@ -7757,6 +7783,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;
Expand Down Expand Up @@ -7875,29 +7906,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);
}
Expand Down Expand Up @@ -8098,7 +8143,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();
Expand Down Expand Up @@ -8151,6 +8196,13 @@ extern "C" BOOL QCALLTYPE EHEnumNext(EH_CLAUSE_ENUMERATOR* pEHEnum, RhEHClause*
{
result = FALSE;
}
#ifdef HOST_WINDOWS
// When processing longjmp, only finally clauses are considered.
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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;

Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/vm/exceptionhandling.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ ProcessCLRException(IN PEXCEPTION_RECORD pExceptionRecord,
IN OUT PT_CONTEXT pContextRecord,
IN OUT PT_DISPATCHER_CONTEXT pDispatcherContext);

VOID DECLSPEC_NORETURN DispatchManagedException(OBJECTREF throwable, CONTEXT *pExceptionContext);
VOID DECLSPEC_NORETURN DispatchManagedException(OBJECTREF throwable, CONTEXT *pExceptionContext, EXCEPTION_RECORD *pExceptionRecord = NULL);
VOID DECLSPEC_NORETURN DispatchManagedException(OBJECTREF throwable);
VOID DECLSPEC_NORETURN DispatchManagedException(RuntimeExceptionKind reKind);

Expand Down
4 changes: 4 additions & 0 deletions src/coreclr/vm/exinfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,10 @@ ExInfo::ExInfo(Thread *pThread, EXCEPTION_RECORD *pExceptionRecord, CONTEXT *pEx
m_CurrentClause({}),
m_pMDToReportFunctionLeave(NULL),
m_lastReportedFunclet({0, 0, 0})
#ifdef HOST_WINDOWS
, m_pLongJmpBuf(NULL),
m_longJmpReturnValue(0)
#endif // HOST_WINDOWS
{
pThread->GetExceptionState()->m_pCurrentTracker = this;
m_pInitialFrame = pThread->GetFrame();
Expand Down
8 changes: 8 additions & 0 deletions src/coreclr/vm/exinfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,14 @@ struct ExInfo : public ExceptionTrackerBase
// Info on the last reported funclet used to report references in the parent frame
LastReportedFuncletInfo m_lastReportedFunclet;

#ifdef TARGET_WINDOWS
// Longjmp buffer used to restart longjmp after a block of managed frames when
// longjmp jumps over them. This is possible on Windows only due to the way the
// longjmp is implemented.
jmp_buf *m_pLongJmpBuf;
int m_longJmpReturnValue;
#endif

#if defined(TARGET_UNIX)
void TakeExceptionPointersOwnership(PAL_SEHException* ex);
#endif // TARGET_UNIX
Expand Down
15 changes: 15 additions & 0 deletions src/tests/Regressions/coreclr/GitHub_111242/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
project (Test111242Lib)
include_directories(${INC_PLATFORM_DIR})

if(CLR_CMAKE_HOST_WIN32)
set_source_files_properties(Test111242.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 (Test111242Lib SHARED Test111242.c)

# add the install targets
install (TARGETS Test111242Lib DESTINATION bin)
31 changes: 31 additions & 0 deletions src/tests/Regressions/coreclr/GitHub_111242/Test111242.c
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);
}
55 changes: 55 additions & 0 deletions src/tests/Regressions/coreclr/GitHub_111242/Test111242.cs
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 Test111242
{
[DllImport("Test111242Lib")]
static extern unsafe void TestSetJmp(delegate* unmanaged<void*,void> managedCallback);

[DllImport("Test111242Lib")]
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 catch-all");
}
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");
}
}
19 changes: 19 additions & 0 deletions src/tests/Regressions/coreclr/GitHub_111242/Test111242.csproj
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="Test111242.cs" />
</ItemGroup>
<ItemGroup>
<CMakeProjectReference Include="CMakeLists.txt" />
</ItemGroup>
</Project>
Loading