15

I'm using standard VS2015 compiler targeted for .Net 4.6.2.

Compilator emits infinite loop after failing finally block.

Some examples:

Debug:

IL_0000: nop
.try
{
    IL_0001: nop
    IL_0002: nop
    IL_0003: leave.s IL_000c
} // end .try
finally
{
    IL_0005: nop
    IL_0006: br.s IL_000a
    // loop start (head: IL_000a)
        IL_0008: nop
        IL_0009: nop
        IL_000a: br.s IL_0008
    // end loop
} // end handler
// loop start (head: IL_000c)
    IL_000c: br.s IL_000c
// end loop

Release:

  .try
    {
        IL_0000: leave.s IL_0004
    } // end .try
    finally
    {
        // loop start
            IL_0002: br.s IL_0002
        // end loop
    } // end handler
    // loop start (head: IL_0004)
        IL_0004: br.s IL_0004
    // end loop

Source C# code

    private void _Simple()
    {
        try
        {

        }
        finally
        {
            for (;;) { }
        }
    }

As you see at IL_000c is infinite loop (generated by compilator)

Ok, now I'll show you a bit extended case

Debug:

IL_0000: nop
.try
{
    IL_0001: nop
    .try
    {
        IL_0002: nop
        IL_0003: nop
        IL_0004: leave.s IL_000d
    } // end .try
    finally
    {
        IL_0006: nop
        IL_0007: newobj instance void [mscorlib]System.Exception::.ctor()
        IL_000c: throw
    } // end handler
    // loop start (head: IL_000d)
        IL_000d: br.s IL_000d
    // end loop
} // end .try
finally
{
    IL_000f: nop
    IL_0010: newobj instance void [mscorlib]System.Exception::.ctor()
    IL_0015: throw
} // end handler

Release:

.try
{
    .try
    {
        IL_0000: leave.s IL_0008
    } // end .try
    finally
    {
        IL_0002: newobj instance void [mscorlib]System.Exception::.ctor()
        IL_0007: throw
    } // end handler
    // loop start (head: IL_0008)
        IL_0008: br.s IL_0008
    // end loop
} // end .try
finally
{
    IL_000a: newobj instance void [mscorlib]System.Exception::.ctor()
    IL_000f: throw
} // end handler

After nested finally infinite loop is generated once again, but after second finally is not. (IL_000d)

Source C#

    private void _DoubleFinallyWithThrowingNewException()
    {
        try
        {
            try
            {

            }
            finally
            {
                throw new Exception();
            }
        }
        finally
        {
            throw new Exception();
        }
    }

One again, now there is non explicit exception thrown by method called at finally block.

Debug:

IL_0000: nop
.try
{
    IL_0001: nop
    .try
    {
        IL_0002: nop
        IL_0003: nop
        IL_0004: leave.s IL_0010
    } // end .try
    finally
    {
        IL_0006: nop
        IL_0007: ldarg.0
        IL_0008: call instance void System.Reflection.Emit.FactoryTests::ThrowException()
        IL_000d: nop
        IL_000e: nop
        IL_000f: endfinally
    } // end handler

    IL_0010: nop
    IL_0011: leave.s IL_001d
} // end .try
finally
{
    IL_0013: nop
    IL_0014: ldarg.0
    IL_0015: call instance void System.Reflection.Emit.FactoryTests::ThrowException()
    IL_001a: nop
    IL_001b: nop
    IL_001c: endfinally
} // end handler

IL_001d: ret

Release:

    .try
{
    .try
    {
        IL_0000: leave.s IL_0010
    } // end .try
    finally
    {
        IL_0002: ldarg.0
        IL_0003: call instance void System.Reflection.Emit.FactoryTests::ThrowException()
        IL_0008: endfinally
    } // end handler
} // end .try
finally
{
    IL_0009: ldarg.0
    IL_000a: call instance void System.Reflection.Emit.FactoryTests::ThrowException()
    IL_000f: endfinally
} // end handler

IL_0010: ret

C# Source

    private void ThrowException()
    {
        throw new Exception();
    }

    private void _DoubleFinallyWithThrowingNewExceptionNotInline()
    {
        try
        {
            try
            {

            }
            finally
            {
                ThrowException();
            }
        }
        finally
        {
            ThrowException();
        }
    }

Why after first unreachable finally block infinite loop is generated?

Why EndFinally OpCode is not generated?

@Edit 1

Added some msil at Release mode.

@Edit 2

Added example with non empty try exception

The metadata .maxStack variable setted to 1, and existing .local variables are a bit confusing - there is no code connected with this variables.

Debug:

.maxstack 1
.locals init (
    [0] object someVar,
    [1] valuetype [mscorlib]System.DateTime
)

IL_0000: nop
.try
{
    IL_0001: nop
    .try
    {
        IL_0002: nop
        IL_0003: ldarg.0
        IL_0004: call instance void System.Reflection.Emit.FactoryTests::ThrowException()
        IL_0009: nop
        IL_000a: nop
        IL_000b: leave.s IL_0014
    } // end .try
    finally
    {
        IL_000d: nop
        IL_000e: newobj instance void [mscorlib]System.Exception::.ctor()
        IL_0013: throw
    } // end handler
    // loop start (head: IL_0014)
        IL_0014: br.s IL_0014
    // end loop
} // end .try
finally
{
    IL_0016: nop
    IL_0017: newobj instance void [mscorlib]System.Exception::.ctor()
    IL_001c: throw
} // end handler

The previous object[0] has been skipped, but DateTime is still there. Release:

.maxstack 1
.locals init (
    [0] valuetype [mscorlib]System.DateTime
)

.try
{
    .try
    {
        IL_0000: ldarg.0
        IL_0001: call instance void System.Reflection.Emit.FactoryTests::ThrowException()
        IL_0006: leave.s IL_000e
    } // end .try
    finally
    {
        IL_0008: newobj instance void [mscorlib]System.Exception::.ctor()
        IL_000d: throw
    } // end handler
    // loop start (head: IL_000e)
        IL_000e: br.s IL_000e
    // end loop
} // end .try
finally
{
    IL_0010: newobj instance void [mscorlib]System.Exception::.ctor()
    IL_0015: throw
} // end handler`

C#:

private void _ExceptionLeaveReplacementAtFinallyAfterFinallyNonEmpty()
    {
        try
        {
            try
            {
                ThrowException();
            }
            finally
            {
                throw new Exception();
            }
            object someVar = DateTime.Now.GetHashCode();
        }
        finally
        {
            throw new Exception();
        }
    }

Or (Msil is identical):

    private void _ExceptionLeaveReplacementAtFinallyAfterFinallyNonEmpty()
    {
        try
        {
            try
            {
                ThrowException();
            }
            finally
            {
                throw new Exception();
            }
        }
        finally
        {
            throw new Exception();
        }
        object someVar = DateTime.Now.GetHashCode();
abatishchev
  • 98,240
  • 88
  • 296
  • 433
Pixello
  • 191
  • 2
  • 7
  • 1
    What's the output when you build in RELEASE mode? – xxbbcc Oct 21 '16 at 17:58
  • I'm not sure why the branch opcodes are being emitted, but finally blocks don't need to have an `endfinally` opcode. It's perfectly valid to leave a finally block via a `throw` opcode as well (one of the two opcodes must exist; it's also possible for more than one of them to exist for a given finally block). – Kyle Oct 21 '16 at 18:40
  • Will it happen only when the C# `try` block is completely empty? – Jeppe Stig Nielsen Oct 21 '16 at 18:41
  • @JeppeStigNielsen Testing in LINQPad: no, the compiler will still generate the infinite loop "guard" even if there's something within the try block. – Kyle Oct 21 '16 at 18:49
  • I speculate that the `leave.s` instruction at the end of the try needs some target. The "useless" infinite loop is needed as the target. – Jeppe Stig Nielsen Oct 21 '16 at 18:54

3 Answers3

8

This is by design. As long as you cannot reach that infinite loop :-)
Thanks for reporting this issue though!!!

=== longer version:

When "finally" does not terminate (contains a throw or an infinite loop), the code after the try statement becomes unreachable form the language prospective. Since it is unreachable, it is allowed to have no code there whatsoever, even if, for example, the method needs to return a value.
In fact, because various invariants, which typically hold in normal code are not enforced in unreachable code, compiler defensively removes code that is unreachable even if it is present. This is not just an optimization, it is often required for correctness. Instead of preventing/detecting/fixing violations in unreachable code it is cleaner to just remove it.

Now, IL specification requires that the "leave" opcode points to a valid target instruction. In particular it does not care whether the branch is blocked by a nonterminating finally. But we do not have any valid code to point to, so we need to inject a "landing" piece of code. It must be small. We also know that it would not be ever reachable, but it also must not endanger already established static correctness of the method.

An infinite loop is a smallest piece of code like that.
BTW, another possibility could be "throw null", but historically an infinite loop is used.

No, NOP would not work because it would make the next instruction verifier-reachable, and that can result in violations of other IL rules like "do not drop through the end of the method, must use ret".

VSadov
  • 961
  • 8
  • 4
7

Okay, so I dug through the Roslyn Source and found exactly where this is happening:

There's a private method called RewriteSpecialBlocks in ILBuilder.cs at line 706. It looks like this:

/// <summary>
/// Rewrite any block marked as BlockedByFinally as an "infinite loop".
/// </summary>
/// <remarks>
/// Matches the code generated by the native compiler in
/// ILGENREC::AdjustBlockedLeaveTargets.
/// </remarks>
private void RewriteSpecialBlocks()
{
    var current = leaderBlock;

    while (current != null)
    {
        // The only blocks that should be marked as BlockedByFinally
        // are the special blocks inserted at the end of exception handlers.
        Debug.Assert(current.Reachability != Reachability.BlockedByFinally ||
            IsSpecialEndHandlerBlock(current));

         if (IsSpecialEndHandlerBlock(current))
        {
            if (current.Reachability == Reachability.BlockedByFinally)
            {
                // BranchLabel points to the same block, so the BranchCode
                // is changed from Nop to Br_s.
                current.SetBranchCode(ILOpCode.Br_s);
            }
            else
            {
                // special block becomes a true nop
                current.SetBranch(null, ILOpCode.Nop);
            }
        }
        current = current.NextBlock;
    }

    // Now that the branch code has changed, the block is no longer special.
    Debug.Assert(AllBlocks(block => !IsSpecialEndHandlerBlock(block)));
}

This method is called from here and the comments indicate that this is all part of the unreachable code removal. It still doesn't quite answer why it generates an infinite loop rather than a nop.

Kyle
  • 6,500
  • 2
  • 31
  • 41
5

Thanks for the detailed information here. On the surface this does seem like a bug in the compiler. I've filed the following issue to track this.

https://github.com/dotnet/roslyn/issues/15297

JaredPar
  • 733,204
  • 149
  • 1,241
  • 1,454