BasicBlock::VisitEHEnclosedHandlerSecondPassSuccs enumerates a bogus EH successor edge from a filter region to a finally nested inside a sibling (mutually protecting) catch handler. Compiler::BlockPredsWithEH does not create the matching edge, so phi placement and phi-arg insertion disagree and the JIT asserts during SSA: insert phis.
Minimal Repro
No stress knobs needed; the failure happens while JITting Test at full opts.
using System;
using System.Runtime.CompilerServices;
public static class Program
{
public static int Sink;
[MethodImpl(MethodImplOptions.NoInlining)]
private static void Throw() => throw new InvalidOperationException();
[MethodImpl(MethodImplOptions.NoInlining)]
private static void Work(int v) => Sink = v;
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
public static int Test(int arg)
{
int x = 0;
try
{
Throw();
}
catch (ArgumentException)
{
try { Work(arg); } // nested try/finally inside the FIRST catch handler
finally { Sink = x; } // 'x' is live-in to this finally
}
catch (Exception) when ((x = arg + 1) > 0) // mutually-protecting filter defines 'x'
{
Sink = x;
}
return x;
}
public static int Main()
{
Console.WriteLine("Test(5) = " + Test(5));
return 100;
}
}
Expected
exit code 100 (this is what .NET 10.0.12 prints).
Actual
Assert failure(PID 48836 [0x0000bec4], Thread: 67488 [0x107a0]): Assertion failed
'phiFound || ((ehDsc != nullptr) && !m_compiler->m_dfsTree->Contains(ehDsc->ebdTryBeg))'
in 'Program:Test(int):int' during 'SSA: insert phis' (IL size 64; hash 0x5b844c46; FullOpts)
File: src\coreclr\jit\ssabuilder.cpp:685
Process exit code 0xC0000602.
Notes
- Root cause:
BasicBlock::VisitEHEnclosedHandlerSecondPassSuccs (compiler.hpp ~483-516) reuses one inTry out-param across hops of mixed kinds. The EH table has EH#0 (finally) nested in the handler of EH#1, and EH#2 mutually protecting EH#1. Walking up from the inner finally goes EH#0 -> (handler link, inTry=false) EH#1 -> (mutual-protect try link, inTry=true) EH#2; only the last inTry is tested, so the inner finally is misclassified as an enclosed try/finally of the filter's try region.
- Semantically that edge cannot exist: second-pass unwind for
catch (B) never enters a finally that only exists inside sibling handler A.
- Consumers disagree:
SsaBuilder::AddDefToEHSuccessorPhis uses VisitEHSuccs, while Compiler::BlockPredsWithEH (block.cpp ~229-253) never adds that edge.
- Debug-only assert, but the bogus successor edge exists in Release codegen too. Target-independent.
main @ 498a04c34eae9db1a1b11e5de378d139d032f66f.
BasicBlock::VisitEHEnclosedHandlerSecondPassSuccsenumerates a bogus EH successor edge from a filter region to afinallynested inside a sibling (mutually protecting) catch handler.Compiler::BlockPredsWithEHdoes not create the matching edge, so phi placement and phi-arg insertion disagree and the JIT asserts duringSSA: insert phis.Minimal Repro
No stress knobs needed; the failure happens while JITting
Testat full opts.Expected
exit code 100 (this is what .NET 10.0.12 prints).
Actual
Process exit code
0xC0000602.Notes
BasicBlock::VisitEHEnclosedHandlerSecondPassSuccs(compiler.hpp~483-516) reuses oneinTryout-param across hops of mixed kinds. The EH table has EH#0 (finally) nested in the handler of EH#1, and EH#2 mutually protecting EH#1. Walking up from the inner finally goesEH#0 -> (handler link, inTry=false) EH#1 -> (mutual-protect try link, inTry=true) EH#2; only the lastinTryis tested, so the inner finally is misclassified as an enclosed try/finally of the filter's try region.catch (B)never enters a finally that only exists inside sibling handlerA.SsaBuilder::AddDefToEHSuccessorPhisusesVisitEHSuccs, whileCompiler::BlockPredsWithEH(block.cpp~229-253) never adds that edge.main@498a04c34eae9db1a1b11e5de378d139d032f66f.