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
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).
if (o is I) { ...; return (I)o; }— the JIT turns the successfulisinto a durable subtypeassertion and deletes the later
castclass. For anIDynamicInterfaceCastablereceiver the answeris instance state, not metadata, so if it changes in between, the required
InvalidCastExceptionissilently lost.
Repro
Expected
DisablemakesIsInterfaceImplementedreturnfalse, so thecastclassmust throw.Actual
IsInterfaceImplementedis never re-queried; the cast is replaced by the object itself.Notes
net11.0, Release. Reproduces with default settings and withDOTNET_TieredCompilation=0. Correct underDOTNET_JITMinOpts=1and with Debug IL.97ea9ba17a825a62c1b52a259999566eed748b79; no assert fires.DOTNET_JitDump=TestshowsDid VN based subtype propremovingCORINFO_HELP_CHKCASTINTERFACE.IDynamicInterfaceCastablestate is a supported contract — seesrc/tests/Interop/IDynamicInterfaceCastable/Program.cs,ValidateErrorHandling.optAssertionVNIsSubtypeasks the VMcompareTypesForCast(IMarker, IMarker), i.e. it passes theasserted interface as the source type, so the VM's
IsIDynamicInterfaceCastable()guard incompareTypesForCastnever seesDynamicMarkerand answersMust.(not verified).