Repository navigation
SEHException with new Exception behavior in .NET 9 #111242
Description
Activity
- ghost addedneeds-area-labelAn area label is needed to ensure this gets routed to the appropriate area ownersAn area label is needed to ensure this gets routed to the appropriate area owners
on Jan 9, 2025 - addeduntriagedNew issue has not been triaged by the area ownerNew issue has not been triaged by the area owner
on Jan 9, 2025 - addedarea-ExceptionHandling-coreclronly use for closed issuesonly use for closed issuesand removedneeds-area-labelAn area label is needed to ensure this gets routed to the appropriate area ownersAn area label is needed to ensure this gets routed to the appropriate area owners
on Jan 9, 2025 Rust
1.24.1release notes describe a similar issue
https://blog.rust-lang.org/2018/03/01/Rust-1.24.1.html#do-not-abort-when-unwinding-through-ffi
rust-lang/rust#48572Reacted by kevin-montrose and Badrish ChandramouliThe problem is interesting. At the point .NET gets notified about the error raised by Lua, the call stack looks as shown below. The key point is that there is a block of Lua frames, then a block of .NET managed frames, then again a block of Lua frames and then again .NET managed frames.
.NET gets notified (theProcessCLRExceptionis called) when the SEH stack walk enters the frame 16. TheEXCEPTION_RECORDthat it gets as an argument has exception code 0x80000026, which is what Lua passed in and that code meansSTATUS_LONGJUMP. Since that's not .NET exception, we create a .NET exception, we create theSystem.Runtime.InteropServices.SEHExceptionfor it and propagate it through the .NET frames 16, 17, 18 and 19. Since there is nothing to catch that exception in those frames, the stack walk reaches the lua frame (1a). When we reach a native frame and we know there are some more .NET managed frames towards the bottom of the stack, we rethrow the exception we were propagating through the .NET frames, which is theSEHException, from the context of the frame 1a. But its exception code is obviously not theSTATUS_LONGJUMPand so it is not caught by the Lua code and ends up being unhandled.I need to figure out how to preserve the exception code to rethrow and then rethrow the original SEH exception.
# Child-SP RetAddr Call Site 0d 0000007e`67f8b9f0 00007ffb`1e003edf coreclr!ProcessCLRException+0x6e [D:\git\runtime7\src\coreclr\vm\exceptionhandling.cpp @ 998] 0e 0000007e`67f8c390 00007ffb`1dec0502 ntdll!RtlpExecuteHandlerForUnwind+0xf [minkernel\ntos\rtl\amd64\xcptmisc.asm @ 254] 0f 0000007e`67f8c3c0 00007ffb`1debe53c ntdll!RtlUnwindEx+0x352 [minkernel\ntos\rtl\amd64\exdsptch.c @ 1538] 10 0000007e`67f8caf0 00007ffa`9588d789 ntdll!RtlUnwind+0xfc [minkernel\ntos\rtl\amd64\exdsptch.c @ 1077] 11 0000007e`67f8d060 00007ffa`9583e199 lua54!luaopen_utf8+0x34e19 12 0000007e`67f8d0b0 00007ffa`9583c1d9 lua54!lua_setlocal+0x13d9 13 0000007e`67f8d0e0 00007ffa`958316e0 lua54!luaopen_debug+0xaf9 14 0000007e`67f8d110 00007ffa`95833d6c lua54!lua_error+0x30 15 0000007e`67f8d140 00007ff9`d0a9ff47 lua54!luaL_error+0x4c 16 0000007e`67f8d180 00007ff9`d0a9fe12 KeraLua!ILStubClass.IL_STUB_PInvoke(IntPtr, System.String)+0x117 17 0000007e`67f8d270 00007ff9`d0a9f9a3 KeraLua!KeraLua.Lua.Error+0x42 18 0000007e`67f8d2b0 00007ff9`d0a9f8d4 repro!LongJumpRepro.Program.RaiseLuaError+0x73 19 0000007e`67f8d310 00007ffa`9583e7ce KeraLua!ILStubClass.IL_STUB_ReversePInvoke(Int64)+0x44 1a 0000007e`67f8d370 00007ffa`9583d8f4 lua54!lua_yieldk+0x31e 1b 0000007e`67f8d3c0 00007ffa`9583d02f lua54!lua_setlocal+0xb34 1c 0000007e`67f8d410 00007ffa`9583de63 lua54!lua_setlocal+0x26f 1d 0000007e`67f8d440 00007ffa`9583d6d0 lua54!lua_setlocal+0x10a3 1e 0000007e`67f8d5c0 00007ffa`9583203d lua54!lua_setlocal+0x910 1f 0000007e`67f8d600 00007ff9`d0a9f82c lua54!lua_pcallk+0x10d 20 0000007e`67f8d650 00007ff9`d0a9de44 KeraLua!KeraLua.Lua.PCall+0x9c 21 0000007e`67f8d720 00007ffa`2f823753 repro!LongJumpRepro.Program.Main+0x204 [D:\issues\111242\Program.cs @ 24] 22 0000007e`67f8d840 00007ffa`2f2a81a0 coreclr!CallDescrWorkerInternal+0x83 [D:\git\runtime7\src\coreclr\vm\amd64\CallDescrWorkerAMD64.asm @ 74] 23 0000007e`67f8d880 00007ffa`2f2a8d43 coreclr!CallDescrWorkerWithHandler+0x130 [D:\git\runtime7\src\coreclr\vm\callhelpers.cpp @ 63] 24 0000007e`67f8d8e0 00007ffa`2ee1e714 coreclr!MethodDescCallSite::CallTargetWorker+0xb93 [D:\git\runtime7\src\coreclr\vm\callhelpers.cpp @ 585] 25 0000007e`67f8e0d0 00007ffa`2ee6f747 coreclr!MethodDescCallSite::Call+0x24 [D:\git\runtime7\src\coreclr\vm\callhelpers.h @ 465] 26 0000007e`67f8e100 00007ffa`2ee6f10a coreclr!RunMainInternal+0x287 [D:\git\runtime7\src\coreclr\vm\assembly.cpp @ 1234] 27 0000007e`67f8e300 00007ffa`2ee6f213 coreclr!``RunMain'::`30'::__Body::Run'::`5'::__Body::Run+0x5a [D:\git\runtime7\src\coreclr\vm\assembly.cpp @ 1306] 28 0000007e`67f8e350 00007ffa`2ee6f445 coreclr!`RunMain'::`30'::__Body::Run+0xa3 [D:\git\runtime7\src\coreclr\vm\assembly.cpp @ 1308] 29 0000007e`67f8e3f0 00007ffa`2ee67872 coreclr!RunMain+0x1c5 [D:\git\runtime7\src\coreclr\vm\assembly.cpp @ 1308] 2a 0000007e`67f8e4f0 00007ffa`2ef5aa93 coreclr!Assembly::ExecuteMainMethod+0x542 [D:\git\runtime7\src\coreclr\vm\assembly.cpp @ 1434] 2b 0000007e`67f8eb20 00007ffa`2fb2db78 coreclr!CorHost2::ExecuteAssembly+0x5b3 [D:\git\runtime7\src\coreclr\vm\corhost.cpp @ 349] 2c 0000007e`67f8f010 00007ffa`958ded6b coreclr!coreclr_execute_assembly+0x138 [D:\git\runtime7\src\coreclr\dlls\mscoree\exports.cpp @ 494] 2d (Inline Function) --------`-------- hostpolicy!coreclr_t::execute_assembly+0x2d [D:\git\runtime7\src\native\corehost\hostpolicy\coreclr.cpp @ 108] 2e 0000007e`67f8f100 00007ffa`958df04c hostpolicy!run_app_for_context+0x68b [D:\git\runtime7\src\native\corehost\hostpolicy\hostpolicy.cpp @ 256] 2f 0000007e`67f8f230 00007ffa`958df983 hostpolicy!run_app+0x3c [D:\git\runtime7\src\native\corehost\hostpolicy\hostpolicy.cpp @ 285] 30 0000007e`67f8f270 00007ffa`f4d8d896 hostpolicy!corehost_main+0x163 [D:\git\runtime7\src\native\corehost\hostpolicy\hostpolicy.cpp @ 426] 31 0000007e`67f8f370 00007ffa`f4d8fea5 hostfxr!execute_app+0x2e6 [D:\git\runtime7\src\native\corehost\fxr\fx_muxer.cpp @ 145] 32 0000007e`67f8f400 00007ffa`f4d91f52 hostfxr!`anonymous namespace'::read_config_and_execute+0xa5 [D:\git\runtime7\src\native\corehost\fxr\fx_muxer.cpp @ 532] 33 0000007e`67f8f4f0 00007ffa`f4d90472 hostfxr!fx_muxer_t::handle_exec_host_command+0x142 [D:\git\runtime7\src\native\corehost\fxr\fx_muxer.cpp @ 1007] 34 0000007e`67f8f590 00007ffa`f4d88440 hostfxr!fx_muxer_t::execute+0x482 [D:\git\runtime7\src\native\corehost\fxr\fx_muxer.cpp @ 578] 35 0000007e`67f8f6d0 00007ff6`63bd8bb2 hostfxr!hostfxr_main_startupinfo+0xa0 [D:\git\runtime7\src\native\corehost\fxr\hostfxr.cpp @ 63] 36 0000007e`67f8f7d0 00007ff6`63bd904e dotnet!exe_start+0x7e2 [D:\git\runtime7\src\native\corehost\corehost.cpp @ 253] 37 0000007e`67f8f9b0 00007ff6`63bda3a8 dotnet!wmain+0x11e [D:\git\runtime7\src\native\corehost\corehost.cpp @ 324] 38 (Inline Function) --------`-------- dotnet!invoke_main+0x22 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 90] 39 0000007e`67f8fa20 00007ffb`1cece8d7 dotnet!__scrt_common_main_seh+0x10c [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288] 3a 0000007e`67f8fa60 00007ffb`1df7fbcc KERNEL32!BaseThreadInitThunk+0x17 [clientcore\base\win32\client\thread.c @ 77] 3b 0000007e`67f8fa90 00000000`00000000 ntdll!RtlUserThreadStart+0x2c [minkernel\ldr\rtlstrt.c @ 1184]It is even more interesting. Re-raising the same SEH exception as .NET got notified about doesn't help for this case, because longjmp doesn't actually use RaiseException. So we will need to re-issue the longjmp, extracting the arguments from the
EXCEPTION_RECORD. Or alternatively, maybe ignore theSTATUS_LONGJUMPin the new EH. I have to check how the old EH behaves w.r.t. processing theSTATUS_LONGJUMPthat skips over managed frames.So we cannot ignore it, the old EH also doesn't ignore it, so there can be e.g. finally handlers invoked while the longjmp jumps over managed frames. I've experimented with re-running longjmp instead of doing RaiseException when the exception escapes managed frames and is propagated into native ones and it seems to work correctly.
Using longjump to skip over managed frames had all sort of issues, even in the old EH scheme. We have it explicitly documented as unsupported in https://learn.microsoft.com/en-us/dotnet/standard/native-interop/exceptions-interoperability#setjmplongjmp-behaviors
Ok, then I guess that fixing this specific issue by making it just skip over the managed frames without invoking finallys is the right way to do it. That would be also aligned with Unix where we could not even see there is a longjmp over managed frames.
- added a commit that references this issue
on Jan 10, 2025 - addedin-prThere is an active PR which will close this issue when it is mergedThere is an active PR which will close this issue when it is merged
on Jan 10, 2025 On Unix, longjump does not work at all since we are not able to unwind transition frames. It means that the key runtime data structures get corrupted and you are very likely going to crash soon after. #1445 has an example of the crash that it leads to.
- added a commit that references this issue
on Jan 21, 2025 - removeduntriagedNew issue has not been triaged by the area ownerNew issue has not been triaged by the area owner
on Jan 21, 2025 - locked and limited conversation to collaborators
on Feb 21, 2025
Description
Starting in .NET 9, pinvoking Lua's
luaL_errorcan raise aSEHException.I believe this is due to
luaL_errorbeing a relatively thin wrapper overlongjmp.I narrowed this down to the newly defaulted on improved exception handling in .NET 9.
Note that while I use KeraLua in my repro, the same thing happens if you pinvoke Lua directly - KeraLua is just a convenient way to package up a pre-built Lua 5.4 binary, and eliminate some of my own pinvoke definition from the repro.
Reproduction Steps
csprojProgram.csExpected behavior
Expected behavior is either the .NET 8:


or .NET 9 with DOTNET_LegacyExceptionHandling=1 behavior:
Where
luaL_errorworks when pinvoked.Actual behavior
A

SEHExceptionis raised:Regression?
Yes, this worked in earlier versions of .NET. I have confirmed it worked in .NET 8, and this was reported when first running the code on .NET 9.
Known Workarounds
Setting DOTNET_LegacyExceptionHandling=1 fixes the issue in .NET 9.
Discussion on the PR that introduced it suggests it is temporary however, if it is removed in a later .NET release we will no longer have a workaround.
Configuration
Other information
While I admit longjmp is weird, embedding Lua is pretty popular.