Skip to content

Commit abf8e94

Browse files
janvorlijkotas
andauthored
Make longjmp over managed frames work (#111259)
* Make longjmp over managed frames work The new exception handling has broken hosting Lua on Windows when a Lua code throws an exception that ends up crossing managed frames. Lua uses longjmp for its exception handling mechanism and the new exception handling reported the longjmp as an unhandled `SEHException`. This change makes the new EH behave like the legacy one w.r.t. the longjmp propagation over managed frames on Windows. It is a best effort fix though just to get rid of the regression related to the legacy exception handling. New code should not rely on this. And it doesn't work on non-windows targets (neither did it work with the legacy exception handling). Close #111242 --------- Co-authored-by: Jan Kotas <[email protected]>
1 parent 3978639 commit abf8e94

File tree

8 files changed

+198
-14
lines changed

8 files changed

+198
-14
lines changed

src/coreclr/vm/exceptionhandling.cpp

+65-13
Original file line numberDiff line numberDiff line change
@@ -970,7 +970,7 @@ ProcessCLRExceptionNew(IN PEXCEPTION_RECORD pExceptionRecord,
970970
else
971971
{
972972
OBJECTREF oref = ExceptionTracker::CreateThrowable(pExceptionRecord, FALSE);
973-
DispatchManagedException(oref, pContextRecord);
973+
DispatchManagedException(oref, pContextRecord, pExceptionRecord);
974974
}
975975
}
976976
#endif // !HOST_UNIX
@@ -5646,7 +5646,7 @@ void FirstChanceExceptionNotification()
56465646
#endif // TARGET_UNIX
56475647
}
56485648

5649-
VOID DECLSPEC_NORETURN DispatchManagedException(OBJECTREF throwable, CONTEXT* pExceptionContext)
5649+
VOID DECLSPEC_NORETURN DispatchManagedException(OBJECTREF throwable, CONTEXT* pExceptionContext, EXCEPTION_RECORD* pExceptionRecord)
56505650
{
56515651
STATIC_CONTRACT_THROWS;
56525652
STATIC_CONTRACT_GC_TRIGGERS;
@@ -5660,14 +5660,32 @@ VOID DECLSPEC_NORETURN DispatchManagedException(OBJECTREF throwable, CONTEXT* pE
56605660

56615661
ULONG_PTR hr = GetHRFromThrowable(throwable);
56625662

5663-
EXCEPTION_RECORD exceptionRecord;
5664-
exceptionRecord.ExceptionCode = EXCEPTION_COMPLUS;
5665-
exceptionRecord.ExceptionFlags = EXCEPTION_NONCONTINUABLE | EXCEPTION_SOFTWARE_ORIGINATE;
5666-
exceptionRecord.ExceptionAddress = (void *)(void (*)(OBJECTREF))&DispatchManagedException;
5667-
exceptionRecord.NumberParameters = MarkAsThrownByUs(exceptionRecord.ExceptionInformation, hr);
5668-
exceptionRecord.ExceptionRecord = NULL;
5663+
EXCEPTION_RECORD newExceptionRecord;
5664+
newExceptionRecord.ExceptionCode = EXCEPTION_COMPLUS;
5665+
newExceptionRecord.ExceptionFlags = EXCEPTION_NONCONTINUABLE | EXCEPTION_SOFTWARE_ORIGINATE;
5666+
newExceptionRecord.ExceptionAddress = (void *)(void (*)(OBJECTREF))&DispatchManagedException;
5667+
newExceptionRecord.NumberParameters = MarkAsThrownByUs(newExceptionRecord.ExceptionInformation, hr);
5668+
newExceptionRecord.ExceptionRecord = NULL;
56695669

5670-
ExInfo exInfo(pThread, &exceptionRecord, pExceptionContext, ExKind::Throw);
5670+
ExInfo exInfo(pThread, &newExceptionRecord, pExceptionContext, ExKind::Throw);
5671+
5672+
#ifdef HOST_WINDOWS
5673+
// On Windows, this enables the possibility to propagate a longjmp across managed frames. Longjmp
5674+
// behaves like a SEH exception, but only runs the second (unwinding) pass.
5675+
// NOTE: This is a best effort purely for backward compatibility with the legacy exception handling.
5676+
// Skipping over managed frames using setjmp/longjmp is
5677+
// is unsupported and it is not guaranteed to work reliably in all cases.
5678+
// https://learn.microsoft.com/dotnet/standard/native-interop/exceptions-interoperability#setjmplongjmp-behaviors
5679+
if ((pExceptionRecord != NULL) && (pExceptionRecord->ExceptionCode == STATUS_LONGJUMP))
5680+
{
5681+
// longjmp over managed frames. The EXCEPTION_RECORD::ExceptionInformation store the
5682+
// jmp_buf and the return value for STATUS_LONGJUMP, so we extract it here. When the
5683+
// exception handling code moves out of the managed frames, we call the longjmp with
5684+
// these arguments again to continue its propagation.
5685+
exInfo.m_pLongJmpBuf = (jmp_buf*)pExceptionRecord->ExceptionInformation[0];
5686+
exInfo.m_longJmpReturnValue = (int)pExceptionRecord->ExceptionInformation[1];
5687+
}
5688+
#endif // HOST_WINDOWS
56715689

56725690
if (pThread->IsAbortInitiated () && IsExceptionOfType(kThreadAbortException,&throwable))
56735691
{
@@ -7683,6 +7701,14 @@ size_t GetSSPForFrameOnCurrentStack(TADDR ip)
76837701
}
76847702
#endif // HOST_AMD64 && HOST_WINDOWS
76857703

7704+
#ifdef HOST_WINDOWS
7705+
VOID DECLSPEC_NORETURN PropagateLongJmpThroughNativeFrames(jmp_buf *pJmpBuf, int retVal)
7706+
{
7707+
WRAPPER_NO_CONTRACT;
7708+
longjmp(*pJmpBuf, retVal);
7709+
}
7710+
#endif // HOST_WINDOWS
7711+
76867712
extern "C" void * QCALLTYPE CallCatchFunclet(QCall::ObjectHandleOnStack exceptionObj, BYTE* pHandlerIP, REGDISPLAY* pvRegDisplay, ExInfo* exInfo)
76877713
{
76887714
QCALL_CONTRACT;
@@ -7746,6 +7772,11 @@ extern "C" void * QCALLTYPE CallCatchFunclet(QCall::ObjectHandleOnStack exceptio
77467772

77477773
ExInfo* pExInfo = (PTR_ExInfo)pThread->GetExceptionState()->GetCurrentExceptionTracker();
77487774

7775+
#ifdef HOST_WINDOWS
7776+
jmp_buf* pLongJmpBuf = pExInfo->m_pLongJmpBuf;
7777+
int longJmpReturnValue = pExInfo->m_longJmpReturnValue;
7778+
#endif // HOST_WINDOWS
7779+
77497780
#ifdef HOST_UNIX
77507781
Interop::ManagedToNativeExceptionCallback propagateExceptionCallback = pExInfo->m_propagateExceptionCallback;
77517782
void* propagateExceptionContext = pExInfo->m_propagateExceptionContext;
@@ -7864,29 +7895,43 @@ extern "C" void * QCALLTYPE CallCatchFunclet(QCall::ObjectHandleOnStack exceptio
78647895
#elif defined(HOST_RISCV64) || defined(HOST_LOONGARCH64)
78657896
pvRegDisplay->pCurrentContext->Ra = GetIP(pvRegDisplay->pCurrentContext);
78667897
#endif
7867-
SetIP(pvRegDisplay->pCurrentContext, (PCODE)(void (*)(Object*))PropagateExceptionThroughNativeFrames);
78687898
#if defined(HOST_AMD64)
78697899
SetSP(pvRegDisplay->pCurrentContext, targetSp - 8);
78707900
#elif defined(HOST_X86)
78717901
SetSP(pvRegDisplay->pCurrentContext, targetSp - 4);
78727902
#endif
7903+
7904+
// The SECOND_ARG_REG is defined only for Windows, it is used to handle longjmp propagation over managed frames
78737905
#ifdef HOST_AMD64
78747906
#ifdef UNIX_AMD64_ABI
78757907
#define FIRST_ARG_REG Rdi
78767908
#else
78777909
#define FIRST_ARG_REG Rcx
7910+
#define SECOND_ARG_REG Rdx
78787911
#endif
78797912
#elif defined(HOST_X86)
78807913
#define FIRST_ARG_REG Ecx
78817914
#elif defined(HOST_ARM64)
78827915
#define FIRST_ARG_REG X0
7916+
#define SECOND_ARG_REG X1
78837917
#elif defined(HOST_ARM)
78847918
#define FIRST_ARG_REG R0
78857919
#elif defined(HOST_RISCV64) || defined(HOST_LOONGARCH64)
78867920
#define FIRST_ARG_REG A0
78877921
#endif
7888-
7889-
pvRegDisplay->pCurrentContext->FIRST_ARG_REG = (size_t)OBJECTREFToObject(exceptionObj.Get());
7922+
#ifdef HOST_WINDOWS
7923+
if (pLongJmpBuf != NULL)
7924+
{
7925+
SetIP(pvRegDisplay->pCurrentContext, (PCODE)PropagateLongJmpThroughNativeFrames);
7926+
pvRegDisplay->pCurrentContext->FIRST_ARG_REG = (size_t)pLongJmpBuf;
7927+
pvRegDisplay->pCurrentContext->SECOND_ARG_REG = (size_t)longJmpReturnValue;
7928+
}
7929+
else
7930+
#endif
7931+
{
7932+
SetIP(pvRegDisplay->pCurrentContext, (PCODE)(void (*)(Object*))PropagateExceptionThroughNativeFrames);
7933+
pvRegDisplay->pCurrentContext->FIRST_ARG_REG = (size_t)OBJECTREFToObject(exceptionObj.Get());
7934+
}
78907935
#undef FIRST_ARG_REG
78917936
ClrRestoreNonvolatileContext(pvRegDisplay->pCurrentContext, targetSSP);
78927937
}
@@ -8087,7 +8132,7 @@ extern "C" BOOL QCALLTYPE EHEnumNext(EH_CLAUSE_ENUMERATOR* pEHEnum, RhEHClause*
80878132
ExtendedEHClauseEnumerator *pExtendedEHEnum = (ExtendedEHClauseEnumerator*)pEHEnum;
80888133
StackFrameIterator *pFrameIter = pExtendedEHEnum->pFrameIter;
80898134

8090-
if (pEHEnum->iCurrentPos < pExtendedEHEnum->EHCount)
8135+
while (pEHEnum->iCurrentPos < pExtendedEHEnum->EHCount)
80918136
{
80928137
IJitManager* pJitMan = pFrameIter->m_crawl.GetJitManager();
80938138
const METHODTOKEN& MethToken = pFrameIter->m_crawl.GetMethodToken();
@@ -8140,6 +8185,13 @@ extern "C" BOOL QCALLTYPE EHEnumNext(EH_CLAUSE_ENUMERATOR* pEHEnum, RhEHClause*
81408185
{
81418186
result = FALSE;
81428187
}
8188+
#ifdef HOST_WINDOWS
8189+
// When processing longjmp, only finally clauses are considered.
8190+
if ((pExInfo->m_pLongJmpBuf == NULL) || (flags & COR_ILEXCEPTION_CLAUSE_FINALLY) || (flags & COR_ILEXCEPTION_CLAUSE_FAULT))
8191+
#endif // HOST_WINDOWS
8192+
{
8193+
break;
8194+
}
81438195
}
81448196
END_QCALL;
81458197

src/coreclr/vm/exceptionhandling.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ ProcessCLRException(IN PEXCEPTION_RECORD pExceptionRecord,
2222
IN OUT PT_CONTEXT pContextRecord,
2323
IN OUT PT_DISPATCHER_CONTEXT pDispatcherContext);
2424

25-
VOID DECLSPEC_NORETURN DispatchManagedException(OBJECTREF throwable, CONTEXT *pExceptionContext);
25+
VOID DECLSPEC_NORETURN DispatchManagedException(OBJECTREF throwable, CONTEXT *pExceptionContext, EXCEPTION_RECORD *pExceptionRecord = NULL);
2626
VOID DECLSPEC_NORETURN DispatchManagedException(OBJECTREF throwable);
2727
VOID DECLSPEC_NORETURN DispatchManagedException(RuntimeExceptionKind reKind);
2828

src/coreclr/vm/exinfo.cpp

+4
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,10 @@ ExInfo::ExInfo(Thread *pThread, EXCEPTION_RECORD *pExceptionRecord, CONTEXT *pEx
322322
m_CurrentClause({}),
323323
m_pMDToReportFunctionLeave(NULL),
324324
m_lastReportedFunclet({0, 0, 0})
325+
#ifdef HOST_WINDOWS
326+
, m_pLongJmpBuf(NULL),
327+
m_longJmpReturnValue(0)
328+
#endif // HOST_WINDOWS
325329
{
326330
pThread->GetExceptionState()->m_pCurrentTracker = this;
327331
m_pInitialFrame = pThread->GetFrame();

src/coreclr/vm/exinfo.h

+8
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,14 @@ struct ExInfo : public ExceptionTrackerBase
283283
// Info on the last reported funclet used to report references in the parent frame
284284
LastReportedFuncletInfo m_lastReportedFunclet;
285285

286+
#ifdef TARGET_WINDOWS
287+
// Longjmp buffer used to restart longjmp after a block of managed frames when
288+
// longjmp jumps over them. This is possible on Windows only due to the way the
289+
// longjmp is implemented.
290+
jmp_buf *m_pLongJmpBuf;
291+
int m_longJmpReturnValue;
292+
#endif
293+
286294
#if defined(TARGET_UNIX)
287295
void TakeExceptionPointersOwnership(PAL_SEHException* ex);
288296
#endif // TARGET_UNIX
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
project (Test111242Lib)
2+
include_directories(${INC_PLATFORM_DIR})
3+
4+
if(CLR_CMAKE_HOST_WIN32)
5+
set_source_files_properties(Test111242.c PROPERTIES COMPILE_OPTIONS /TC) # compile as C
6+
else()
7+
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fvisibility=hidden")
8+
set(CMAKE_CPP_FLAGS "${CMAKE_CPP_FLAGS} -fvisibility=hidden -Wno-return-type-c-linkage")
9+
endif()
10+
11+
# add the executable
12+
add_library (Test111242Lib SHARED Test111242.c)
13+
14+
# add the install targets
15+
install (TARGETS Test111242Lib DESTINATION bin)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
#include <stdint.h>
5+
#include <stdio.h>
6+
#include <setjmp.h>
7+
8+
9+
#ifdef _MSC_VER
10+
#define DLLEXPORT __declspec(dllexport)
11+
#else
12+
#define DLLEXPORT __attribute__((visibility("default")))
13+
#endif // _MSC_VER
14+
15+
DLLEXPORT void TestSetJmp(void (*managedCallback)(void *))
16+
{
17+
jmp_buf jmpBuf;
18+
if (!setjmp(jmpBuf))
19+
{
20+
managedCallback(&jmpBuf);
21+
}
22+
else
23+
{
24+
printf("longjmp called\n");
25+
}
26+
}
27+
28+
DLLEXPORT void TestLongJmp(void *jmpBuf)
29+
{
30+
longjmp(*(jmp_buf*)jmpBuf, 1);
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Runtime.InteropServices;
6+
using System.Runtime.CompilerServices;
7+
using Xunit;
8+
9+
public static class Test111242
10+
{
11+
[DllImport("Test111242Lib")]
12+
static extern unsafe void TestSetJmp(delegate* unmanaged<void*,void> managedCallback);
13+
14+
[DllImport("Test111242Lib")]
15+
static extern unsafe void TestLongJmp(void *jmpBuf);
16+
17+
static bool ExceptionFilter(Exception ex)
18+
{
19+
Assert.Fail("Should not call filter for longjmp SEH exception");
20+
return true;
21+
}
22+
23+
static bool wasFinallyInvoked = false;
24+
25+
[UnmanagedCallersOnly]
26+
static unsafe void ManagedCallback(void *jmpBuf)
27+
{
28+
try
29+
{
30+
TestLongJmp(jmpBuf);
31+
}
32+
catch (Exception ex) when (ExceptionFilter(ex))
33+
{
34+
Assert.Fail("Should not catch longjmp SEH exception via filter");
35+
}
36+
catch
37+
{
38+
Assert.Fail("Should not catch longjmp SEH exception via catch-all");
39+
}
40+
finally
41+
{
42+
Console.WriteLine("Finally block executed");
43+
wasFinallyInvoked = true;
44+
}
45+
Assert.Fail("Should not reach here");
46+
}
47+
48+
[Fact]
49+
public static unsafe void TestEntryPoint()
50+
{
51+
TestSetJmp(&ManagedCallback);
52+
Assert.True(wasFinallyInvoked);
53+
Console.WriteLine("Test passed");
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<!-- Needed for CMakeProjectReference -->
4+
<RequiresProcessIsolation>true</RequiresProcessIsolation>
5+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
6+
<CLRTestPriority>1</CLRTestPriority>
7+
<CLRTestTargetUnsupported Condition="'$(TargetOS)' != 'windows'">true</CLRTestTargetUnsupported>
8+
</PropertyGroup>
9+
<PropertyGroup>
10+
<DebugType>PdbOnly</DebugType>
11+
<Optimize>True</Optimize>
12+
</PropertyGroup>
13+
<ItemGroup>
14+
<Compile Include="Test111242.cs" />
15+
</ItemGroup>
16+
<ItemGroup>
17+
<CMakeProjectReference Include="CMakeLists.txt" />
18+
</ItemGroup>
19+
</Project>

0 commit comments

Comments
 (0)