Skip to content

JIT: (bug) failing IDynamicInterfaceCastable cast elided after a successful is test #133679

Description

@EgorBo

if (o is I) { ...; return (I)o; } — the JIT turns the successful is into a durable subtype
assertion and deletes the later castclass. For an IDynamicInterfaceCastable receiver the answer
is instance state, not metadata, so if it changes in between, the required InvalidCastException is
silently lost.

Repro

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

public interface IMarker
{
}

[DynamicInterfaceCastableImplementation]
public interface IMarkerImpl : IMarker
{
}

public sealed class DynamicMarker : IDynamicInterfaceCastable
{
    public bool Enabled = true;

    public bool IsInterfaceImplemented(RuntimeTypeHandle interfaceType, bool throwIfNotImplemented)
        => Enabled && interfaceType.Equals(typeof(IMarker).TypeHandle);

    public RuntimeTypeHandle GetInterfaceImplementation(RuntimeTypeHandle interfaceType)
        => typeof(IMarkerImpl).TypeHandle;
}

public static class Program
{
    [MethodImpl(MethodImplOptions.NoInlining)]
    private static void Disable(object o) => ((DynamicMarker)o).Enabled = false;

    [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
    public static IMarker Test(object obj)
    {
        if (obj is IMarker)
        {
            Disable(obj);
            return (IMarker)obj; // must re-query IsInterfaceImplemented -> must throw
        }

        return null;
    }

    public static int Main()
    {
        int bad = 0;
        for (int i = 0; i < 30; i++)
        {
            try
            {
                Test(new DynamicMarker());
                bad++;
            }
            catch (InvalidCastException)
            {
            }
        }

        Console.WriteLine(bad == 0 ? "PASS" : $"FAIL ({bad}/30 casts wrongly succeeded)");
        return bad == 0 ? 100 : 1;
    }
}

Expected

PASS

Disable makes IsInterfaceImplemented return false, so the castclass must throw.

Actual

FAIL (30/30 casts wrongly succeeded)

IsInterfaceImplemented is never re-queried; the cast is replaced by the object itself.

Notes

  • Windows x64, net11.0, Release. Reproduces with default settings and with
    DOTNET_TieredCompilation=0. Correct under DOTNET_JITMinOpts=1 and with Debug IL.
  • Verified on a Checked build of 97ea9ba17a825a62c1b52a259999566eed748b79; no assert fires.
    DOTNET_JitDump=Test shows Did VN based subtype prop removing CORINFO_HELP_CHKCASTINTERFACE.
  • Mutable IDynamicInterfaceCastable state is a supported contract — see
    src/tests/Interop/IDynamicInterfaceCastable/Program.cs, ValidateErrorHandling.
  • optAssertionVNIsSubtype asks the VM compareTypesForCast(IMarker, IMarker), i.e. it passes the
    asserted interface as the source type, so the VM's IsIDynamicInterfaceCastable() guard in
    compareTypesForCast never sees DynamicMarker and answers Must.
  • The same guard exists in the NativeAOT JIT interface, so optimized NativeAOT is likely affected too
    (not verified).

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions