VM: (bug) open virtual delegates compare equal but have different hash codes
Description
Two open delegates created over the same virtual slot (from Base.M and from Derived.M) behave inconsistently on main: Release reports Equals == true with different hash codes, violating the Equals/GetHashCode contract and breaking dictionary lookups; Checked reports Equals == false. .NET 10.0.12 is self-consistent.
Repro
using System;
using System.Collections.Generic;
public class Base { public virtual void M() { } }
public class Derived : Base { public override void M() { } }
public static class Program
{
public static void Main()
{
var d1 = (Action<Derived>)Delegate.CreateDelegate(typeof(Action<Derived>), typeof(Base).GetMethod("M")!);
var d2 = (Action<Derived>)Delegate.CreateDelegate(typeof(Action<Derived>), typeof(Derived).GetMethod("M")!);
Console.WriteLine($"Equals={d1.Equals(d2)} hashEq={d1.GetHashCode() == d2.GetHashCode()}");
var dict = new Dictionary<Action<Derived>, int> { { d1, 1 } };
Console.WriteLine($"ContainsKey(d2)={dict.ContainsKey(d2)}");
}
}Expected
Equals=True hashEq=True
ContainsKey(d2)=TrueActual
main Release:
Equals=True hashEq=False
ContainsKey(d2)=Falsemain Checked:
Equals=False hashEq=False
ContainsKey(d2)=FalseAnalysis
In Release, Delegate.Equals treats matching _methodPtrAux virtual-dispatch stubs as equal, and that stub is keyed by slot, so Base.M and Derived.M produce the same stub. Delegate.GetHashCode instead hashes the MethodDesc, which stays distinct. Checked compiles out the _methodPtrAux shortcut and returns false from Equals.
Either way, Equals and GetHashCode must agree.
Regression?
Yes. .NET 10.0.12 reports Equals=True hashEq=True and finds the dictionary key.
Platform
Windows x64, CoreCLR main (Checked and Release); .NET 10.0.12 is unaffected.
Source: dotnet/runtime