Repository navigation
Description
Description
A try region whose body cannot throw, paired with a finally whose first block is the target of a back edge from within the handler, trips a ref-count assert in Compiler::fgRemoveEmptyTryCatchOrTryFault.
Assertion failed 'firstHndBlock->bbRefs == 1'
in 'Runtime_XXXXXX:LoopInTryAndFinally(long):long'
during 'Remove empty try-catch-fault 2'
(IL size 33; hash 0xf561a59d; FullOpts)
The phase removes the handler blocks of an empty try, and assumes the handler's first block carries exactly the single artificial reference the phase itself installed:
runtime/src/coreclr/jit/fgehopt.cpp
Lines 955 to 959 in 399c3eb
| // (3) Remove any handler blocks. | |
| // The first handler block has an artificial ref count | |
| // | |
| assert(firstHndBlock->bbRefs == 1); | |
| firstHndBlock->bbRefs = 0; |
// (3) Remove any handler blocks.
// The first handler block has an artificial ref count
//
assert(firstHndBlock->bbRefs == 1);
firstHndBlock->bbRefs = 0;When the handler entry is also the target of a back edge from inside the handler, bbRefs is 2 and the assert fires.
Repro
using System;
using System.Runtime.CompilerServices;
public static class Repro
{
[MethodImpl(MethodImplOptions.NoInlining)]
public static long LoopInTryAndFinally(long a)
{
long x = a;
try
{
L1:
if ((x & 8) != 0) { x += 4; goto L1; }
}
finally
{
L2:
if ((x & 1) != 0) { x += 1; goto L2; }
}
return x;
}
public static int Main()
{
Console.WriteLine(LoopInTryAndFinally(3));
return 100;
}
}Requires optimized codegen (the phase does not run at Tier0), so DOTNET_TieredCompilation=0 on a checked JIT.
Configurations
Reproduces under crossgen2 with an assert-enabled JIT for all four target architectures — x64, arm64, x86, arm — under FullOpts with no stress modes enabled.
Isolating controls
The failure needs both an empty-able try and a self-edge on the finally entry, and is specific to finally:
| variant | shape | result |
|---|---|---|
| as above | self-loop in try and in finally | assert (x64, arm64, x86, arm) |
| control | self-loop in try only, straight-line finally | clean |
| control | straight-line try, self-loop in finally only | clean |
| control | catch instead of finally |
clean |
Severity
Assert-only as far as I have been able to demonstrate. Running the same method over a ∈ [-40, 80] against a while-loop reference implementation on a release runtime (both tiered and with DOTNET_TieredCompilation=0) produces matching results in every case. In release builds the assert is compiled out and bbRefs is unconditionally set to 0 immediately afterwards.
So this may be a stale-assert / bookkeeping issue rather than a latent bad-codegen bug — but the invariant the phase documents ("The first handler block has an artificial ref count") does not hold, and I did not attempt to prove that no downstream consumer depends on it.
Related
Several other asserts have been reported in this phase, all distinct from this one and all closed: #130248, #115122, #110958, #110954, #114985.
Found by
A compile-only crossgen2 fuzzing leg generating goto-webs with duplicated statement tails inside EH regions. Reduced automatically from 756 bytes to the 12-line repro above.
Note
This issue was AI-generated by Copilot, and reviewed by @AndyAyersMS.
Activity
dotnet-policy-service commentedon Sep 2, 2026
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.
[WIP] Fix assertion failed during remove empty try-catch-fault (#133103)