Skip to content

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

Open

Description

@pcshrosbree

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 at handler_offset is marked BB_EXCEPTION_HANDLER, and nothing checks that handler_offset != 0. On amd64, OP_ENDFINALLY (mini-amd64.c) reloads RSP from the region's spvar and executes ret. 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 accepts pop, 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 the MINT_CALL_HANDLER instruction that normally writes that return address.
  • LLVM, OP_ENDFINALLY in mini-llvm.c: region_to_handler has no entry for the region, so g_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

  1. dotnet-policy-service commented on Oct 12, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @vitek-karas, @BrzVlad, @kotlarmilos
    See info in area-owners.md if you want to be subscribed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions