Repository navigation
Mono: a method whose first instruction is inside a finally or fault handler is accepted and crashes when called; LLVM AOT asserts handler_bb #135639
Description
Description
This is the Mono side of #135593. Mono accepts methods whose first instruction lies inside a finally, fault or catch handler. Calling the finally or fault method crashes the process with SIGSEGV under the JIT, the interpreter and mini AOT. A possible fix is the same as the CoreCLR one in #135602: reject such methods with InvalidProgramException (details under Other information).
The Mono LLVM AOT compiler aborts on the finally method:
* Assertion at src/mono/mono/mini/mini-llvm.c:12707, condition `handler_bb' not met
For a method whose first instruction lies inside a filter, Mono's JIT throws InvalidProgramException (IL_0000: pop), but the interpreter accepts the method and crashes.
ECMA-335 I.12.4.2.8.1 says filters and handlers "cannot appear at the beginning of a method", so this IL is invalid. III.1.8 leaves the behaviour of invalid CIL unspecified, but when Mono detects other invalid IL, it throws InvalidProgramException (Invalid IL code in ...) rather than crashing.
The CoreCLR JIT fix, #135602, adds the regression test JIT/Regression/JitBlue/Runtime_135593. When run, that test cannot observe this bug on Mono. It detects acceptance with RuntimeHelpers.PrepareMethod. On Mono, ves_icall_System_Runtime_CompilerServices_RuntimeHelpers_PrepareMethod validates its arguments and then stops at // FIXME: Implement, so it compiles nothing and the test returns 101. In the llvmaot leg, the AOT compile of the test assembly hits the assertion above. I am disabling the test on Mono in #135602, with a link to this issue.
See also #135641, a separate Mono EH bug that the test added in #135601 exposed. That bug is on valid IL: a clause nested in the handler of an earlier-listed clause.
Reproduction Steps
EntryProbe.il (assemble with ilasm -dll). The four EntryIn* methods are the ones in the Runtime_135593 test; the last two methods are controls.
.assembly extern System.Runtime { .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) }
.assembly EntryProbe { }
.module EntryProbe.dll
.class public auto ansi abstract sealed beforefieldinit EntryProbe
extends [System.Runtime]System.Object
{
.field public static int32 Counter
.method public hidebysig static void EntryInFinally() cil managed noinlining
{
.maxstack 1
HandlerStart:
endfinally
TryStart:
leave MethodEnd
MethodEnd:
ret
.try TryStart to MethodEnd finally handler HandlerStart to TryStart
}
.method public hidebysig static void EntryInFault() cil managed noinlining
{
.maxstack 1
HandlerStart:
endfault
TryStart:
leave MethodEnd
MethodEnd:
ret
.try TryStart to MethodEnd fault handler HandlerStart to TryStart
}
.method public hidebysig static void EntryInCatch() cil managed noinlining
{
.maxstack 1
HandlerStart:
leave MethodEnd
TryStart:
leave MethodEnd
MethodEnd:
ret
.try TryStart to MethodEnd catch [System.Runtime]System.Exception handler HandlerStart to TryStart
}
.method public hidebysig static void EntryInFilter() cil managed noinlining
{
.maxstack 1
FilterStart:
pop
ldc.i4.1
endfilter
HandlerStart:
pop
leave MethodEnd
TryStart:
leave MethodEnd
MethodEnd:
ret
.try TryStart to MethodEnd filter FilterStart handler HandlerStart to TryStart
}
// Control: valid IL.
.method public hidebysig static void ValidFinally() cil managed noinlining
{
.maxstack 2
.try
{
leave.s Done
}
finally
{
ldsfld int32 EntryProbe::Counter
ldc.i4.1
add
stsfld int32 EntryProbe::Counter
endfinally
}
Done:
ret
}
// Control: invalid IL with no EH at all (pop on an empty stack).
.method public hidebysig static void PopEmptyStack() cil managed noinlining
{
.maxstack 1
pop
ret
}
}
Use a small driver that loads EntryProbe.dll and, for one method per process, either calls RuntimeMethodHandle.GetFunctionPointer or calls the method through a delegate. Run it under corerun from a Mono Core_Root, once with MONO_ENV_OPTIONS unset (JIT) and once with MONO_ENV_OPTIONS=--interpreter. For the AOT columns, put each method in its own assembly. For LLVM AOT, compile it with MONO_ENV_OPTIONS=--aot=llvm,llvm-path=<artifacts/bin/mono/linux.x64.Release> plus the mattr flags that the llvmaot leg passes. For mini AOT, compile it with --aot without LLVM.
Expected behavior
InvalidProgramException for all four EntryIn* methods, as for the PopEmptyStack control. The LLVM AOT compiler should skip such a method or report an error instead of asserting.
Actual behavior
Mono built from #135602's head 6ae25ba7, linux-x64 Release, with MonoEnableLLVM=true:
| Method | JIT, GetFunctionPointer |
JIT, call | Interpreter, call | LLVM AOT compile | mini AOT image, call |
|---|---|---|---|---|---|
| EntryInFinally | compiles | SIGSEGV | SIGSEGV | assertion handler_bb (mini-llvm.c:12707), exit 134 |
SIGSEGV |
| EntryInFault | compiles | SIGSEGV | SIGSEGV | compiles | SIGSEGV |
| EntryInCatch | compiles | returns normally | returns normally | compiles | returns normally |
| EntryInFilter | InvalidProgramException (IL_0000: pop) |
InvalidProgramException |
SIGSEGV | method skipped | InvalidProgramException |
| control: PopEmptyStack | InvalidProgramException |
InvalidProgramException |
InvalidProgramException |
method skipped | InvalidProgramException |
| control: ValidFinally | compiles | returns, finally ran once | returns, finally ran once | compiles | returns |
SIGSEGV here means Mono's native crash report ("Got a SIGSEGV while executing native code"), followed by a process exit with SIGABRT or SIGSEGV. Each cell is one process. RuntimeHelpers.PrepareMethod returned normally for every method under both the JIT and the interpreter, including PopEmptyStack. Under the interpreter, GetFunctionPointer also returns normally for every method, because the interpreter transforms a method on its first call.
The shipped Mono 8.0.29 runtime pack (Microsoft.NETCore.App.Runtime.Mono.linux-x64) gives the same results for the JIT and the interpreter. In CI, the linux-x64 Release AllSubsets_Mono_LLVMAot_RuntimeTests llvmaot leg of #135602 failed in "AOT compile CoreCLR tests" with the same assertion on Runtime_135593.dll.
Regression?
Not that I know of. It also reproduces on Mono 8.0.29.
Known Workarounds
Do not emit methods whose first instruction is inside a handler or filter; such IL is invalid.
Configuration
linux-x64. Mono Release built from 6ae25ba7 with -s mono+libs+clr.hosts+clr.iltools /p:MonoEnableLLVM=true /p:MonoAOTEnableLLVM=true /p:MonoBundleLLVMOptimizer=true; also the shipped Mono 8.0.29 runtime pack.
Other information
As far as I can tell, neither the JIT nor the interpreter checks that offset 0 is outside every handler and filter:
- JIT,
mono_method_to_ir(method-to-ir.c, the "handle exception clauses" loop): the block athandler_offsetis markedBB_EXCEPTION_HANDLER, and nothing checks thathandler_offset != 0. On amd64,OP_ENDFINALLY(mini-amd64.c) reloads RSP from the region's spvar and executesret. Entering the handler from the method entry bypasses the normal handler-call path. The crash report shows an unknown native instruction pointer, which is consistent with an invalid return target, but it does not show how that target was obtained. - Interpreter, transform.c (the clause loop that sets
bb->stack_height): handler and filter blocks get their entry stack state from the clause type alone. The filter at offset 0 therefore starts with one stack slot, so the interpreter acceptspop, and the method crashes when called. The JIT rejects the same method. For the finally and fault cases,MINT_ENDFINALLY(interp.c) reads the return address from the clause's data local: a non-null value becomes the instruction pointer, and a null value takes the exception-handler return path. Entering at the method entry skips theMINT_CALL_HANDLERinstruction that normally writes that return address. - LLVM,
OP_ENDFINALLYin mini-llvm.c:region_to_handlerhas no entry for the region, sog_assert (handler_bb)fires.
A possible fix is the same as the CoreCLR one. In mono_method_to_ir and in the interpreter's transform, reject the method with InvalidProgramException when handler_offset == 0, or when data.filter_offset == 0 for a filter clause. Once Mono rejects these methods, Runtime_135593 can be re-enabled on Mono only if it stops relying on PrepareMethod, for example by using RuntimeMethodHandle.GetFunctionPointer, which does compile under Mono's JIT.
Note
This issue was prepared with AI assistance (Anthropic Claude and OpenAI Codex), under my direction. I reviewed it before posting.
Activity
dotnet-policy-service commentedon Oct 12, 2026
Tagging subscribers to this area: @vitek-karas, @BrzVlad, @kotlarmilos
See info in area-owners.md if you want to be subscribed.
Disable Runtime_135593 on Mono
Drop the ActiveIssue on Runtime_135593's Main