From 317030a9c567816922326714ad91763dd48e6cdb Mon Sep 17 00:00:00 2001 From: Iurie Caraion Date: Sat, 10 Oct 2026 11:09:42 +0300 Subject: [PATCH 1/5] Keep static abstract methods out of invoke map #125875 removed the exclusion of static virtual methods from IsMethodSupportedInReflectionInvoke, so static abstract interface methods got NativeAOT reflection invoke map entries without an entry point. Reflection then called through a null entry point: MethodInfo.Invoke, MethodInvoker.Invoke and PropertyInfo.GetValue crashed the process, a delegate from CreateDelegate crashed when called, and RuntimeMethodHandle.GetFunctionPointer returned zero. Skip static abstract methods in ShouldMethodBeInInvokeMap. Reflection reports them as not invokable with NotSupportedException, as before #125875. IsMethodSupportedInReflectionInvoke is unchanged, so the dependency and dataflow analysis #125875 enabled for these methods stays, and static virtual methods with a body stay invokable. ReflectionVirtualInvokeMapNode now calls ShouldMethodBeInInvokeMap instead of repeating its checks, so the virtual invoke map, which is only read for methods with an invoke map entry, matches it. Fix #135565 Co-Authored-By: Claude Opus 5.5 --- .../ReflectionVirtualInvokeMapNode.cs | 8 +- .../Compiler/MetadataManager.cs | 4 + .../SmokeTests/Reflection/Reflection.cs | 77 +++++++++++++++++++ 3 files changed, 83 insertions(+), 6 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ReflectionVirtualInvokeMapNode.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ReflectionVirtualInvokeMapNode.cs index 51371d4fa06f54..5619337201b9f2 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ReflectionVirtualInvokeMapNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ReflectionVirtualInvokeMapNode.cs @@ -128,12 +128,8 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) MethodDesc method = mappingEntry.Entity; - // The current format requires us to have an MethodTable for the owning type. We might want to lift this. - if (!factory.MetadataManager.TypeGeneratesEEType(method.OwningType)) - continue; - - // We have a method body, we have a metadata token, but we can't get an invoke stub. Bail. - if (!factory.MetadataManager.IsReflectionInvokable(method)) + // The runtime reads this map only for methods that have an invoke map entry + if (!factory.MetadataManager.ShouldMethodBeInInvokeMap(method)) continue; // Only virtual methods are interesting diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/MetadataManager.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/MetadataManager.cs index 7de1176f3cbbe4..87526e1e2c2b39 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/MetadataManager.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/MetadataManager.cs @@ -464,6 +464,10 @@ public bool ShouldMethodBeInInvokeMap(MethodDesc method) if (!IsReflectionInvokable(method)) return false; + // Static abstract interface methods have no body to invoke + if (method.Signature.IsStatic && method.IsAbstract) + return false; + return true; } diff --git a/src/tests/nativeaot/SmokeTests/Reflection/Reflection.cs b/src/tests/nativeaot/SmokeTests/Reflection/Reflection.cs index 7e1423bd1b87b4..cd58c85d16181c 100644 --- a/src/tests/nativeaot/SmokeTests/Reflection/Reflection.cs +++ b/src/tests/nativeaot/SmokeTests/Reflection/Reflection.cs @@ -91,6 +91,7 @@ private static int Main() TestInvokeMemberParamsCornerCase.Run(); TestDefaultInterfaceInvoke.Run(); TestCovariantReturnInvoke.Run(); + TestStaticAbstractInvoke.Run(); TypeConstructionTest.Run(); TestThreadStaticFields.Run(); TestByRefReturnInvoke.Run(); @@ -551,6 +552,82 @@ public static void Run() } } + class TestStaticAbstractInvoke + { + interface IStaticAbstract + { + static abstract int Get(); + static abstract int Count { get; } + } + + interface IStaticAbstract + { + static abstract void Set(T value); + } + + interface IStaticVirtual + { + static virtual int Get() => 42; + } + + public static void Run() + { + Console.WriteLine(nameof(TestStaticAbstractInvoke)); + + // A static abstract method has no body to invoke, bind a delegate to or take the address of + MethodInfo mi = typeof(IStaticAbstract).GetMethod(nameof(IStaticAbstract.Get)); + + try + { + mi.Invoke(null, Array.Empty()); + throw new Exception("Invoke"); + } + catch (NotSupportedException) + { + } + + try + { + mi.CreateDelegate>(); + throw new Exception("CreateDelegate"); + } + catch (NotSupportedException) + { + } + + try + { + mi.MethodHandle.GetFunctionPointer(); + throw new Exception("GetFunctionPointer"); + } + catch (NotSupportedException) + { + } + + try + { + typeof(IStaticAbstract).GetProperty(nameof(IStaticAbstract.Count)).GetValue(null); + throw new Exception("GetValue"); + } + catch (NotSupportedException) + { + } + + try + { + typeof(IStaticAbstract).GetMethod(nameof(IStaticAbstract.Set)).Invoke(null, new object[] { "" }); + throw new Exception("Generic Invoke"); + } + catch (NotSupportedException) + { + } + + // A static virtual method with a body is invokable + if ((int)typeof(IStaticVirtual).GetMethod(nameof(IStaticVirtual.Get)).Invoke(null, Array.Empty()) != 42) + throw new Exception("Static virtual"); + } + } + class TestInstanceFields { public class FieldInvokeSample From 984d0050a4371b35677d3caca4eabbc534ffce75 Mon Sep 17 00:00:00 2001 From: Iurie Caraion Date: Sat, 10 Oct 2026 17:50:27 +0300 Subject: [PATCH 2/5] Move static abstract invoke tests to System.Reflection.Tests Co-Authored-By: Claude Opus 5.5 --- .../MethodInfoTests.cs | 32 ++++++++ .../PropertyInfoTests.cs | 13 ++++ .../SmokeTests/Reflection/Reflection.cs | 77 ------------------- 3 files changed, 45 insertions(+), 77 deletions(-) diff --git a/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInfoTests.cs b/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInfoTests.cs index 61a8f99cdbb874..78c945791c0fd2 100644 --- a/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInfoTests.cs +++ b/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInfoTests.cs @@ -1284,6 +1284,38 @@ public void CreateDelegate_StaticVirtual() Assert.Equal(testString, returnValue); } + [Fact] + public void Invoke_StaticVirtual() + { + MethodInfo miStaticVirtual = GetMethod(typeof(IStaticInterface), nameof(IStaticInterface.StaticVirtual)); + const string testString = "test"; + + Assert.Equal(testString, miStaticVirtual.Invoke(null, new object[] { testString })); + } + + private interface IStaticAbstractInterface + { + static abstract int StaticAbstract(); + } + + private interface IStaticAbstractInterface + { + static abstract void StaticAbstract(T value); + } + + // A static abstract method has no body to invoke, bind a delegate to or take the address of + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot))] + public void StaticAbstract_NativeAot_ThrowsNotSupportedException() + { + MethodInfo miStaticAbstract = GetMethod(typeof(IStaticAbstractInterface), nameof(IStaticAbstractInterface.StaticAbstract)); + Assert.Throws(() => miStaticAbstract.Invoke(null, null)); + Assert.Throws(() => miStaticAbstract.CreateDelegate>()); + Assert.Throws(() => miStaticAbstract.MethodHandle.GetFunctionPointer()); + + MethodInfo miGenericStaticAbstract = GetMethod(typeof(IStaticAbstractInterface), nameof(IStaticAbstractInterface.StaticAbstract)); + Assert.Throws(() => miGenericStaticAbstract.Invoke(null, new object[] { "" })); + } + [Theory] [InlineData(typeof(MI_BaseClass), nameof(MI_BaseClass.VirtualMethod), null, typeof(ArgumentNullException))] [InlineData(typeof(MI_BaseClass), nameof(MI_BaseClass.VirtualMethod), typeof(Delegate_Void_Int), typeof(ArgumentException))] diff --git a/src/libraries/System.Runtime/tests/System.Reflection.Tests/PropertyInfoTests.cs b/src/libraries/System.Runtime/tests/System.Reflection.Tests/PropertyInfoTests.cs index 1096014b8b4730..20f2d1b062251d 100644 --- a/src/libraries/System.Runtime/tests/System.Reflection.Tests/PropertyInfoTests.cs +++ b/src/libraries/System.Runtime/tests/System.Reflection.Tests/PropertyInfoTests.cs @@ -160,6 +160,19 @@ public void SetValue_Invalid(Type type, string name, object obj, object value, o Assert.Throws(exceptionType, () => PropertyInfo.SetValue(obj, value, index)); } + private interface IStaticAbstractProperty + { + static abstract int Value { get; } + } + + // The getter of a static abstract property has no body to invoke + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot))] + public void GetValue_StaticAbstract_NativeAot_ThrowsNotSupportedException() + { + PropertyInfo propertyInfo = GetProperty(typeof(IStaticAbstractProperty), nameof(IStaticAbstractProperty.Value)); + Assert.Throws(() => propertyInfo.GetValue(null)); + } + [Theory] [InlineData(nameof(PropertyInfoMembers.PublicGetIntProperty))] [InlineData(nameof(PropertyInfoMembers.PublicGetPublicSetStringProperty))] diff --git a/src/tests/nativeaot/SmokeTests/Reflection/Reflection.cs b/src/tests/nativeaot/SmokeTests/Reflection/Reflection.cs index cd58c85d16181c..7e1423bd1b87b4 100644 --- a/src/tests/nativeaot/SmokeTests/Reflection/Reflection.cs +++ b/src/tests/nativeaot/SmokeTests/Reflection/Reflection.cs @@ -91,7 +91,6 @@ private static int Main() TestInvokeMemberParamsCornerCase.Run(); TestDefaultInterfaceInvoke.Run(); TestCovariantReturnInvoke.Run(); - TestStaticAbstractInvoke.Run(); TypeConstructionTest.Run(); TestThreadStaticFields.Run(); TestByRefReturnInvoke.Run(); @@ -552,82 +551,6 @@ public static void Run() } } - class TestStaticAbstractInvoke - { - interface IStaticAbstract - { - static abstract int Get(); - static abstract int Count { get; } - } - - interface IStaticAbstract - { - static abstract void Set(T value); - } - - interface IStaticVirtual - { - static virtual int Get() => 42; - } - - public static void Run() - { - Console.WriteLine(nameof(TestStaticAbstractInvoke)); - - // A static abstract method has no body to invoke, bind a delegate to or take the address of - MethodInfo mi = typeof(IStaticAbstract).GetMethod(nameof(IStaticAbstract.Get)); - - try - { - mi.Invoke(null, Array.Empty()); - throw new Exception("Invoke"); - } - catch (NotSupportedException) - { - } - - try - { - mi.CreateDelegate>(); - throw new Exception("CreateDelegate"); - } - catch (NotSupportedException) - { - } - - try - { - mi.MethodHandle.GetFunctionPointer(); - throw new Exception("GetFunctionPointer"); - } - catch (NotSupportedException) - { - } - - try - { - typeof(IStaticAbstract).GetProperty(nameof(IStaticAbstract.Count)).GetValue(null); - throw new Exception("GetValue"); - } - catch (NotSupportedException) - { - } - - try - { - typeof(IStaticAbstract).GetMethod(nameof(IStaticAbstract.Set)).Invoke(null, new object[] { "" }); - throw new Exception("Generic Invoke"); - } - catch (NotSupportedException) - { - } - - // A static virtual method with a body is invokable - if ((int)typeof(IStaticVirtual).GetMethod(nameof(IStaticVirtual.Get)).Invoke(null, Array.Empty()) != 42) - throw new Exception("Static virtual"); - } - } - class TestInstanceFields { public class FieldInvokeSample From 47ced63c1ac82553d639dc6b549757c8d75e6a71 Mon Sep 17 00:00:00 2001 From: Iurie Caraion Date: Sun, 11 Oct 2026 11:47:39 +0300 Subject: [PATCH 3/5] Throw MemberAccessException for static abstract Reflection on a static abstract interface method failed late or crashed. On CoreCLR, MethodInfo.Invoke, MethodInvoker.Invoke and PropertyInfo.GetValue called into a method with no body and failed with BadImageFormatException, MethodInfo.CreateDelegate and Delegate.CreateDelegate by name returned a delegate that failed when called, and RuntimeMethodHandle.GetFunctionPointer returned a non-null pointer. On NativeAOT, the compiler change earlier in this PR made these calls throw NotSupportedException instead of crashing. Both runtimes now throw MemberAccessException naming the method from Invoke, MethodInvoker.Invoke, GetValue, SetValue, MethodInfo.CreateDelegate, Delegate.CreateDelegate(Type, [object,] MethodInfo, bool) and GetFunctionPointer, before any call. The existing checks still come first: open generic types, delegate binding, and the argument count passed to MethodInvoker.Invoke. On CoreCLR, RuntimeMethodInfo sets NoInvoke for these methods (shared with Mono), CreateDelegateInternal throws once the delegate binds, so a delegate type that doesn't match is still a bind failure, and GetFunctionPointer throws when the runtime returns null for them. On NativeAOT, GetUncachedMethodInvoker returns an invoker that throws from every member, after the argument count check for a span of arguments, and Type.GetInterfaceMap throws NotSupportedException at such a method instead of failing a cast. Delegate.CreateDelegate by name skips these methods on both runtimes, so it fails to bind. COMDelegate::BindToMethod no longer virtualizes a static method when it binds a closed delegate, as #125875 did for open ones. A delegate closed over a non-null first argument asserted there on a Checked runtime, so it never reached the new check, and the same delegate over a static virtual method with a body now works. The protected Delegate(Type, string) constructor on CoreCLR ignored the bind result and left the delegate unchanged when no method bound, which skipping static abstract methods would have extended to them. It now throws ArgumentException, as Delegate(object, string) does. Fix #101320 Co-Authored-By: Claude Opus 5.5 --- .../src/System/Delegate.CoreCLR.cs | 19 ++- .../src/System/RuntimeHandles.cs | 8 + .../Reflection/Augments/ReflectionAugments.cs | 3 +- .../src/System.Private.CoreLib.csproj | 1 + .../MethodInfos/RuntimeNamedMethodInfo.cs | 21 +++ .../StaticAbstractMethodInvoker.cs | 76 ++++++++++ ...cutionEnvironmentImplementation.Runtime.cs | 4 + src/coreclr/vm/comdelegate.cpp | 8 +- src/coreclr/vm/runtimehandles.cpp | 13 +- .../src/Resources/Strings.resx | 3 + .../System/Reflection/RuntimeMethodInfo.cs | 8 + .../MethodCommonTests.cs | 31 ++++ .../MethodInfoTests.cs | 141 ++++++++++++++++-- .../MethodInvokerTests.cs | 21 +++ .../PropertyInfoTests.cs | 19 ++- .../System/DelegateTests.cs | 28 ++++ .../System/Type/TypeTests.cs | 7 + 17 files changed, 382 insertions(+), 29 deletions(-) create mode 100644 src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/MethodInfos/StaticAbstractMethodInvoker.cs diff --git a/src/coreclr/System.Private.CoreLib/src/System/Delegate.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Delegate.CoreCLR.cs index d47f3bdc3b6137..e59723213d1033 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Delegate.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Delegate.CoreCLR.cs @@ -100,10 +100,11 @@ protected Delegate([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Al // The name matching was case insensitive (no idea why this is // different from the constructor above) and we preserve that as // well. - BindToMethodName(null, rtTarget, method, - DelegateBindingFlags.StaticMethodOnly | - DelegateBindingFlags.OpenDelegateOnly | - DelegateBindingFlags.CaselessMatching); + if (!BindToMethodName(null, rtTarget, method, + DelegateBindingFlags.StaticMethodOnly | + DelegateBindingFlags.OpenDelegateOnly | + DelegateBindingFlags.CaselessMatching)) + throw new ArgumentException(SR.Arg_DlgtTargMeth); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -428,7 +429,15 @@ internal static Delegate CreateDelegateForDynamicMethod(Type type, object? targe { Delegate d = InternalAlloc(rtType); - return d.BindToMethodInfo(firstArgument, rtMethod, rtMethod.GetDeclaringTypeInternal(), flags) ? d : null; + if (!d.BindToMethodInfo(firstArgument, rtMethod, rtMethod.GetDeclaringTypeInternal(), flags)) + return null; + + // A static abstract interface method has no implementation to call. Checked after the bind, so a delegate + // type that doesn't match is still a bind failure, as on NativeAOT. + if (rtMethod.IsStatic && rtMethod.IsAbstract) + throw new MemberAccessException(SR.Format(SR.Acc_CallStaticAbstract, rtMethod.DeclaringType, rtMethod.Name)); + + return d; } // diff --git a/src/coreclr/System.Private.CoreLib/src/System/RuntimeHandles.cs b/src/coreclr/System.Private.CoreLib/src/System/RuntimeHandles.cs index 791f12174cc4b5..7f257f342d509b 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/RuntimeHandles.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/RuntimeHandles.cs @@ -1093,6 +1093,14 @@ public IntPtr GetFunctionPointer() { IntPtr ptr = GetFunctionPointer(IRuntimeMethodInfo.GetValue(EnsureNonNullMethodInfo(m_value))); GC.KeepAlive(m_value); + + // Null only for a static abstract interface method, which has no implementation to call + if (ptr == IntPtr.Zero) + { + MethodBase method = RuntimeType.GetMethodBase(m_value)!; + throw new MemberAccessException(SR.Format(SR.Acc_CallStaticAbstract, method.DeclaringType, method.Name)); + } + return ptr; } diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Reflection/Augments/ReflectionAugments.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Reflection/Augments/ReflectionAugments.cs index 3e07c1f29f4e93..8566654e0e0acf 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Reflection/Augments/ReflectionAugments.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Reflection/Augments/ReflectionAugments.cs @@ -358,7 +358,8 @@ private static RuntimeMethodInfo LookupMethodForCreateDelegate(RuntimeTypeInfo r while (type != null) { MethodInfo? methodInfo = type.GetMethod(method, 0, bindingFlags, parameterTypes); - if (methodInfo != null && methodInfo.ReturnType.Equals(invokeMethod.ReturnType)) + // A static abstract interface method has no implementation to call + if (methodInfo != null && !(methodInfo.IsStatic && methodInfo.IsAbstract) && methodInfo.ReturnType.Equals(invokeMethod.ReturnType)) return (RuntimeMethodInfo)methodInfo; // This cast is safe since we already verified that containingType is runtime implemented. type = type.BaseType; diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj b/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj index 24511cbb49a8b9..32c182165d4994 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj @@ -433,6 +433,7 @@ + diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/MethodInfos/RuntimeNamedMethodInfo.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/MethodInfos/RuntimeNamedMethodInfo.cs index 0741ba6ef10db9..43123ea23a6bd3 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/MethodInfos/RuntimeNamedMethodInfo.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/MethodInfos/RuntimeNamedMethodInfo.cs @@ -289,13 +289,34 @@ private RuntimeTypeInfo[] GenericTypeParameters internal sealed override MethodBaseInvoker GetUncachedMethodInvoker(RuntimeTypeInfo[] methodArguments, MemberInfo exceptionPertainant) { + // A static abstract interface method has no implementation to call. An open generic one is left to + // OpenMethodInvoker's InvalidOperationException, as on CoreCLR. + if (IsStatic && IsAbstract && !IsOpenGeneric(_common.DeclaringType, methodArguments)) + return new StaticAbstractMethodInvoker(this); + MethodBaseInvoker invoker = _common.GetUncachedMethodInvoker(methodArguments, exceptionPertainant, out Exception exception); + if (invoker == null) throw exception; return invoker; } + // The same open checks as ExecutionEnvironment.GetMethodInvoker, which returns OpenMethodInvoker for them + private static bool IsOpenGeneric(RuntimeTypeInfo declaringType, RuntimeTypeInfo[] methodArguments) + { + if (declaringType.ContainsGenericParameters) + return true; + + for (int i = 0; i < methodArguments.Length; i++) + { + if (methodArguments[i].ContainsGenericParameters) + return true; + } + + return false; + } + protected sealed override MethodBaseInvoker UncachedMethodInvoker { get diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/MethodInfos/StaticAbstractMethodInvoker.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/MethodInfos/StaticAbstractMethodInvoker.cs new file mode 100644 index 00000000000000..0918b0323fed08 --- /dev/null +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/MethodInfos/StaticAbstractMethodInvoker.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Reflection; + +using Internal.Reflection.Core.Execution; + +namespace System.Reflection.Runtime.MethodInfos +{ + // A static abstract interface method has no implementation to call: invoking it, binding a delegate to it or + // taking its address throws MemberAccessException, as on CoreCLR. + internal sealed class StaticAbstractMethodInvoker : MethodBaseInvoker + { + private readonly MethodInfo _method; + + public StaticAbstractMethodInvoker(MethodInfo method) + { + _method = method; + } + + private MemberAccessException CreateException() + { + return new MemberAccessException(SR.Format(SR.Acc_CallStaticAbstract, _method.DeclaringType, _method.Name)); + } + + protected sealed override object? Invoke(object? thisObject, object?[]? arguments, BinderBundle binderBundle, bool wrapInTargetInvocationException) + { + throw CreateException(); + } + + protected sealed override object CreateInstance(object?[]? arguments, BinderBundle binderBundle, bool wrapInTargetInvocationException) + { + throw CreateException(); + } + + protected internal sealed override object CreateInstance(Span arguments) + { + throw CreateException(); + } + + protected internal sealed override object CreateInstanceWithFewArgs(Span arguments) + { + throw CreateException(); + } + + protected internal sealed override object? Invoke(object? thisObject, Span arguments) + { + // MethodInvoker.Invoke(object, Span) leaves the argument count check to the invoker, unlike its + // fixed-arity overloads, which check before calling InvokeDirectWithFewArgs. Check the count here first, as + // DynamicInvokeInfo does for other methods, so a wrong count throws TargetParameterCountException. + if (arguments.Length != _method.GetParametersAsSpan().Length) + throw new TargetParameterCountException(SR.Arg_ParmCnt); + + throw CreateException(); + } + + protected internal sealed override object? InvokeDirectWithFewArgs(object? thisObject, Span arguments) + { + throw CreateException(); + } + + public sealed override Delegate CreateDelegate(RuntimeTypeHandle delegateType, object target, bool isStatic, bool isVirtual, bool isOpen) + { + throw CreateException(); + } + + public sealed override IntPtr LdFtnResult + { + get + { + throw CreateException(); + } + } + } +} diff --git a/src/coreclr/nativeaot/System.Private.Reflection.Execution/src/Internal/Reflection/Execution/ExecutionEnvironmentImplementation.Runtime.cs b/src/coreclr/nativeaot/System.Private.Reflection.Execution/src/Internal/Reflection/Execution/ExecutionEnvironmentImplementation.Runtime.cs index 59d7f11651d47a..546532d20948b8 100644 --- a/src/coreclr/nativeaot/System.Private.Reflection.Execution/src/Internal/Reflection/Execution/ExecutionEnvironmentImplementation.Runtime.cs +++ b/src/coreclr/nativeaot/System.Private.Reflection.Execution/src/Internal/Reflection/Execution/ExecutionEnvironmentImplementation.Runtime.cs @@ -29,6 +29,10 @@ public sealed override void GetInterfaceMap(Type instanceType, [DynamicallyAcces var tMethods = new MethodInfo[ifaceMethods.Length]; for (int i = 0; i < ifaceMethods.Length; i++) { + // A static abstract method is not implemented through the instance type's virtual slots (https://github.com/dotnet/runtime/issues/89157) + if (ifaceMethods[i].IsStatic && ifaceMethods[i].IsAbstract) + throw new NotSupportedException(); + var invoker = (VirtualMethodInvoker)GetMethodInvoker(ifaceMethods[i]); IntPtr classRtMethodHandle = invoker.ResolveTarget(instanceType.TypeHandle); diff --git a/src/coreclr/vm/comdelegate.cpp b/src/coreclr/vm/comdelegate.cpp index ac41bcefaff092..dc3acc7548854d 100644 --- a/src/coreclr/vm/comdelegate.cpp +++ b/src/coreclr/vm/comdelegate.cpp @@ -1059,6 +1059,10 @@ extern "C" BOOL QCALLTYPE Delegate_BindToMethodName(MethodTable* pDelegateMT, Me if (pCurMethod->IsAsyncVariantMethod()) continue; + // We can't match static abstract interface methods (since they have no implementation to call). + if (pCurMethod->IsStatic() && pCurMethod->IsAbstract()) + continue; + if (StrCompFunc(pszMethodName, pCurMethod->GetName()) == 0) { // found a matching string, get an associated method desc if needed @@ -1267,7 +1271,9 @@ void COMDelegate::BindToMethod(MethodTable* pDelegateMT, // We can get virtual delegates closed over null through this code path, so be careful to handle that case (no need to // virtualize since we're just going to throw NullRefException at invocation time). // - if (pTargetMethod->IsVirtual() + // A static method's first argument is not "this", so never virtualize the call on it. + if (!pTargetMethod->IsStatic() + && pTargetMethod->IsVirtual() && pTargetMT != NULL && pTargetMethod->GetMethodTable() != pTargetMT) { diff --git a/src/coreclr/vm/runtimehandles.cpp b/src/coreclr/vm/runtimehandles.cpp index 3a9bfb4e0fd14d..c25e816e744975 100644 --- a/src/coreclr/vm/runtimehandles.cpp +++ b/src/coreclr/vm/runtimehandles.cpp @@ -1308,10 +1308,15 @@ extern "C" void * QCALLTYPE RuntimeMethodHandle_GetFunctionPointer(MethodDesc * BEGIN_QCALL; - // Ensure the method is active and all types have been loaded so the function pointer can be used. - pMethod->EnsureActive(); - pMethod->PrepareForUseAsAFunctionPointer(); - funcPtr = (void*)pMethod->GetMultiCallableAddrOfCode(CORINFO_ACCESS_UNMANAGED_CALLER_MAYBE); + // A static abstract interface method has no implementation to call: return null, the caller throws. An open + // generic one throws InvalidOperationException from GetMultiCallableAddrOfCode first, as for any other method. + if (!(pMethod->IsStatic() && pMethod->IsAbstract()) || pMethod->ContainsGenericVariables()) + { + // Ensure the method is active and all types have been loaded so the function pointer can be used. + pMethod->EnsureActive(); + pMethod->PrepareForUseAsAFunctionPointer(); + funcPtr = (void*)pMethod->GetMultiCallableAddrOfCode(CORINFO_ACCESS_UNMANAGED_CALLER_MAYBE); + } END_QCALL; diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index 8d6acae094adc5..f10d348e6f3866 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Cannot call static abstract method '{0}.{1}'. + Cannot create an abstract class. diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/RuntimeMethodInfo.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/RuntimeMethodInfo.cs index d4fafd9957330c..3a5e224a0e87bf 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/RuntimeMethodInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/RuntimeMethodInfo.cs @@ -22,6 +22,7 @@ internal InvocationFlags ComputeAndUpdateInvocationFlags() || IsDisallowedByRefType(returnType) // Return type is an invalid by-ref (i.e., by-ref-like or void*) || (CallingConvention & CallingConventions.VarArgs) == CallingConventions.VarArgs // Managed varargs || IsDisallowedAsyncHelper + || IsStaticAbstract // Static abstract interface method has no body to call ) { invocationFlags = InvocationFlags.NoInvoke; @@ -79,6 +80,10 @@ internal void ThrowNoInvokeException() { throw new InvalidOperationException(SR.Arg_UnboundGenParam); } + else if (IsStaticAbstract) + { + throw new MemberAccessException(SR.Format(SR.Acc_CallStaticAbstract, declaringType, Name)); + } // method is abstract class else if (IsAbstract) { @@ -100,6 +105,9 @@ internal void ThrowNoInvokeException() throw new TargetException(); } + private bool IsStaticAbstract => + (Attributes & (MethodAttributes.Static | MethodAttributes.Abstract)) == (MethodAttributes.Static | MethodAttributes.Abstract); + [DebuggerStepThrough] [DebuggerHidden] public override object? Invoke( diff --git a/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodCommonTests.cs b/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodCommonTests.cs index 21a8227568ed25..8a1d657eeb2974 100644 --- a/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodCommonTests.cs +++ b/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodCommonTests.cs @@ -309,6 +309,37 @@ private class TestClassThatThrows public static void Throw() => throw new Exception("Here"); } + [Theory] + [InlineData(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.NoArgs), null)] + [InlineData(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.OneArg), new object[] { 42 })] + public void Invoke_StaticAbstract_ThrowsMemberAccessException(Type type, string methodName, object?[]? parameters) + { + MethodInfo method = type.GetMethod(methodName)!; + MemberAccessException ex = Assert.Throws(() => Invoke(method, null, parameters)); + Assert.Contains($"{type}.{methodName}", ex.Message); + } + + [Theory] + [InlineData(typeof(int))] + [InlineData(typeof(string))] + public void Invoke_StaticAbstractGenericMethod_ThrowsMemberAccessException(Type typeArgument) + { + MethodInfo method = typeof(IStaticAbstractMethods).GetMethod(nameof(IStaticAbstractMethods.GenericNoArgs))!.MakeGenericMethod(typeArgument); + MemberAccessException ex = Assert.Throws(() => Invoke(method, null, null)); + Assert.Contains($"{typeof(IStaticAbstractMethods)}.{nameof(IStaticAbstractMethods.GenericNoArgs)}", ex.Message); + } + + protected interface IStaticAbstractMethods + { + static abstract void NoArgs(); + static abstract void GenericNoArgs(); + } + + protected interface IStaticAbstractMethods + { + static abstract T OneArg(T value); + } + public static IEnumerable PropertyTestData() { yield return new object[] { "Bool", TestClass.BoolValue }; diff --git a/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInfoTests.cs b/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInfoTests.cs index 78c945791c0fd2..d5fb4317b3e496 100644 --- a/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInfoTests.cs +++ b/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInfoTests.cs @@ -1271,6 +1271,17 @@ public void CreateDelegate_ValueTypeParameters() private interface IStaticInterface { public static virtual string? StaticVirtual(string? s) => s; + public static virtual LargeStruct StaticVirtualLargeStruct(string s) => new LargeStruct { A = s.Length, B = 2, C = 3, D = 4 }; + } + + private struct LargeStruct + { + public long A, B, C, D; + } + + private interface IStaticAbstractLargeStruct + { + static abstract LargeStruct StaticAbstractLargeStruct(string s); } [Fact] @@ -1293,27 +1304,131 @@ public void Invoke_StaticVirtual() Assert.Equal(testString, miStaticVirtual.Invoke(null, new object[] { testString })); } - private interface IStaticAbstractInterface + // Mono's delegate binding is not changed by this fix + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime))] + public void CreateDelegate_StaticVirtual_ClosedOverFirstArgument() + { + MethodInfo miStaticVirtual = GetMethod(typeof(IStaticInterface), nameof(IStaticInterface.StaticVirtual)); + const string testString = "test"; + + Func methodDelegate = miStaticVirtual.CreateDelegate>(testString); + Assert.Equal(testString, methodDelegate()); + } + + // A struct returned through a hidden buffer takes another path when a delegate closes over a static method + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime))] + public void CreateDelegate_StaticVirtual_ClosedOverFirstArgument_LargeStructReturn() + { + MethodInfo miStaticVirtual = GetMethod(typeof(IStaticInterface), nameof(IStaticInterface.StaticVirtualLargeStruct)); + + Func methodDelegate = miStaticVirtual.CreateDelegate>("test"); + LargeStruct returnValue = methodDelegate(); + Assert.Equal(4, returnValue.A); + Assert.Equal(2, returnValue.B); + Assert.Equal(3, returnValue.C); + Assert.Equal(4, returnValue.D); + } + + // Mono binds delegates and takes function pointers without checking for static abstract methods + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime))] + public void CreateDelegate_StaticAbstract_ThrowsMemberAccessException() + { + MethodInfo noArgs = GetMethod(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.NoArgs)); + MemberAccessException ex = Assert.Throws(() => noArgs.CreateDelegate()); + Assert.Contains($"{typeof(IStaticAbstractMethods)}.{nameof(IStaticAbstractMethods.NoArgs)}", ex.Message); + Assert.Throws(() => Delegate.CreateDelegate(typeof(Action), noArgs, throwOnBindFailure: false)); + Assert.Throws(() => Delegate.CreateDelegate(typeof(Action), null, noArgs, throwOnBindFailure: false)); + + MethodInfo oneArg = GetMethod(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.OneArg)); + ex = Assert.Throws(() => oneArg.CreateDelegate>()); + Assert.Contains($"{typeof(IStaticAbstractMethods)}.{nameof(IStaticAbstractMethods.OneArg)}", ex.Message); + + MethodInfo genericNoArgs = GetMethod(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.GenericNoArgs)).MakeGenericMethod(typeof(int)); + ex = Assert.Throws(() => genericNoArgs.CreateDelegate()); + Assert.Contains($"{typeof(IStaticAbstractMethods)}.{nameof(IStaticAbstractMethods.GenericNoArgs)}", ex.Message); + + // Instantiated over a reference type + MethodInfo genericNoArgsString = GetMethod(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.GenericNoArgs)).MakeGenericMethod(typeof(string)); + Assert.Throws(() => genericNoArgsString.CreateDelegate()); + MethodInfo oneStringArg = GetMethod(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.OneArg)); + Assert.Throws(() => oneStringArg.CreateDelegate>()); + + // Closed over the first argument + Assert.Throws(() => oneStringArg.CreateDelegate>("test")); + Assert.Throws(() => Delegate.CreateDelegate(typeof(Func), "test", oneStringArg, throwOnBindFailure: false)); + Assert.Throws(() => oneStringArg.CreateDelegate>(null)); + MethodInfo largeStruct = GetMethod(typeof(IStaticAbstractLargeStruct), nameof(IStaticAbstractLargeStruct.StaticAbstractLargeStruct)); + Assert.Throws(() => largeStruct.CreateDelegate>("test")); + + // A delegate type that doesn't match fails to bind, as for any other method + Assert.Throws(() => noArgs.CreateDelegate>()); + + // On an open generic type the signature check fails first + MethodInfo openOneArg = GetMethod(typeof(IStaticAbstractMethods<>), nameof(IStaticAbstractMethods.OneArg)); + if (PlatformDetection.IsNativeAot) + { + Assert.Throws(() => openOneArg.CreateDelegate>()); + } + else + { + Assert.Throws(() => openOneArg.CreateDelegate>()); + } + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime))] + public void CreateDelegate_ByName_StaticAbstract_DoesNotBind() { - static abstract int StaticAbstract(); + Assert.Throws(() => Delegate.CreateDelegate(typeof(Action), typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.NoArgs))); + Assert.Null(Delegate.CreateDelegate(typeof(Action), typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.NoArgs), ignoreCase: false, throwOnBindFailure: false)); + + // A static virtual method with a body still binds by name + Func byName = (Func)Delegate.CreateDelegate(typeof(Func), typeof(IStaticInterface), nameof(IStaticInterface.StaticVirtual)); + Assert.Equal("test", byName("test")); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime))] + public void GetFunctionPointer_StaticAbstract_ThrowsMemberAccessException() + { + MethodInfo noArgs = GetMethod(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.NoArgs)); + MemberAccessException ex = Assert.Throws(() => noArgs.MethodHandle.GetFunctionPointer()); + Assert.Contains($"{typeof(IStaticAbstractMethods)}.{nameof(IStaticAbstractMethods.NoArgs)}", ex.Message); + + MethodInfo oneArg = GetMethod(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.OneArg)); + ex = Assert.Throws(() => oneArg.MethodHandle.GetFunctionPointer()); + Assert.Contains($"{typeof(IStaticAbstractMethods)}.{nameof(IStaticAbstractMethods.OneArg)}", ex.Message); + + MethodInfo genericNoArgs = GetMethod(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.GenericNoArgs)).MakeGenericMethod(typeof(string)); + ex = Assert.Throws(() => genericNoArgs.MethodHandle.GetFunctionPointer()); + Assert.Contains($"{typeof(IStaticAbstractMethods)}.{nameof(IStaticAbstractMethods.GenericNoArgs)}", ex.Message); + + // On an open generic type the open generic error comes first, as for any other method + MethodInfo openOneArg = GetMethod(typeof(IStaticAbstractMethods<>), nameof(IStaticAbstractMethods.OneArg)); + Assert.Throws(() => openOneArg.MethodHandle.GetFunctionPointer()); + + // A static virtual method with a body still has an address + Assert.NotEqual(IntPtr.Zero, GetMethod(typeof(IStaticInterface), nameof(IStaticInterface.StaticVirtual)).MethodHandle.GetFunctionPointer()); } - private interface IStaticAbstractInterface + [Fact] + public void Invoke_StaticAbstract_WrongArgumentCount_ThrowsMemberAccessException() { - static abstract void StaticAbstract(T value); + // MethodInfo.Invoke rejects a method it can't invoke before it checks the argument count + MethodInfo oneArg = GetMethod(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.OneArg)); + Assert.Throws(() => oneArg.Invoke(null, null)); + Assert.Throws(() => oneArg.Invoke(null, new object[] { 42, 43 })); } - // A static abstract method has no body to invoke, bind a delegate to or take the address of - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot))] - public void StaticAbstract_NativeAot_ThrowsNotSupportedException() + [Fact] + public void Invoke_StaticAbstract_OpenGeneric_ThrowsInvalidOperationException() { - MethodInfo miStaticAbstract = GetMethod(typeof(IStaticAbstractInterface), nameof(IStaticAbstractInterface.StaticAbstract)); - Assert.Throws(() => miStaticAbstract.Invoke(null, null)); - Assert.Throws(() => miStaticAbstract.CreateDelegate>()); - Assert.Throws(() => miStaticAbstract.MethodHandle.GetFunctionPointer()); + MethodInfo openOneArg = GetMethod(typeof(IStaticAbstractMethods<>), nameof(IStaticAbstractMethods.OneArg)); + Assert.Throws(() => openOneArg.Invoke(null, new object[] { 42 })); - MethodInfo miGenericStaticAbstract = GetMethod(typeof(IStaticAbstractInterface), nameof(IStaticAbstractInterface.StaticAbstract)); - Assert.Throws(() => miGenericStaticAbstract.Invoke(null, new object[] { "" })); + // A generic method definition, and the method constructed over an open type argument + MethodInfo genericNoArgs = GetMethod(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.GenericNoArgs)); + Assert.Throws(() => genericNoArgs.Invoke(null, null)); + MethodInfo openArgument = genericNoArgs.MakeGenericMethod(typeof(IStaticAbstractMethods<>).GetGenericArguments()[0]); + Assert.Throws(() => openArgument.Invoke(null, null)); } [Theory] diff --git a/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInvokerTests.cs b/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInvokerTests.cs index d936dd1d7fd58a..46fc0937fbab58 100644 --- a/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInvokerTests.cs +++ b/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInvokerTests.cs @@ -56,6 +56,27 @@ public void Invoke_RefReturnAliasesArgument(string methodName) public static ref object? RefReturningArgumentMany(ref object? value, object? a, object? b, object? c, object? d) => ref value; + [Fact] + public void Invoke_StaticAbstract_FixedArguments_ThrowsMemberAccessException() + { + MethodInvoker noArgs = MethodInvoker.Create(typeof(IStaticAbstractMethods).GetMethod(nameof(IStaticAbstractMethods.NoArgs))!); + Assert.Throws(() => noArgs.Invoke(null)); + + MethodInvoker oneArg = MethodInvoker.Create(typeof(IStaticAbstractMethods).GetMethod(nameof(IStaticAbstractMethods.OneArg))!); + Assert.Throws(() => oneArg.Invoke(null, 42)); + } + + [Fact] + public void Invoke_StaticAbstract_WrongArgumentCount_ThrowsTargetParameterCountException() + { + MethodInvoker oneArg = MethodInvoker.Create(typeof(IStaticAbstractMethods).GetMethod(nameof(IStaticAbstractMethods.OneArg))!); + Assert.Throws(() => oneArg.Invoke(null, new Span(new object?[] { 42 }))); + Assert.Throws(() => oneArg.Invoke(null, Span.Empty)); + Assert.Throws(() => oneArg.Invoke(null, new Span(new object?[] { 42, 43 }))); + Assert.Throws(() => oneArg.Invoke(null)); + Assert.Throws(() => oneArg.Invoke(null, 42, 43)); + } + [Theory] [InlineData(false)] [InlineData(true)] diff --git a/src/libraries/System.Runtime/tests/System.Reflection.Tests/PropertyInfoTests.cs b/src/libraries/System.Runtime/tests/System.Reflection.Tests/PropertyInfoTests.cs index 20f2d1b062251d..a57053bb2b036a 100644 --- a/src/libraries/System.Runtime/tests/System.Reflection.Tests/PropertyInfoTests.cs +++ b/src/libraries/System.Runtime/tests/System.Reflection.Tests/PropertyInfoTests.cs @@ -162,15 +162,24 @@ public void SetValue_Invalid(Type type, string name, object obj, object value, o private interface IStaticAbstractProperty { - static abstract int Value { get; } + static abstract int Value { get; set; } } - // The getter of a static abstract property has no body to invoke - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot))] - public void GetValue_StaticAbstract_NativeAot_ThrowsNotSupportedException() + // Mono's GetValue calls the getter through a delegate of its own + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime))] + public void GetValue_StaticAbstract_ThrowsMemberAccessException() { PropertyInfo propertyInfo = GetProperty(typeof(IStaticAbstractProperty), nameof(IStaticAbstractProperty.Value)); - Assert.Throws(() => propertyInfo.GetValue(null)); + MemberAccessException ex = Assert.Throws(() => propertyInfo.GetValue(null)); + Assert.Contains($"{nameof(IStaticAbstractProperty)}.get_{nameof(IStaticAbstractProperty.Value)}", ex.Message); + } + + [Fact] + public void SetValue_StaticAbstract_ThrowsMemberAccessException() + { + PropertyInfo propertyInfo = GetProperty(typeof(IStaticAbstractProperty), nameof(IStaticAbstractProperty.Value)); + MemberAccessException ex = Assert.Throws(() => propertyInfo.SetValue(null, 1)); + Assert.Contains($"{nameof(IStaticAbstractProperty)}.set_{nameof(IStaticAbstractProperty.Value)}", ex.Message); } [Theory] diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DelegateTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DelegateTests.cs index 4275fd0f8becaa..7fa92509b096b0 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DelegateTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DelegateTests.cs @@ -592,6 +592,34 @@ public static void TypeEquivalentDelegatesPointingToSameMethod_AreEqualAndHaveSa private static void DelegateEquivalentTypeTargetMethod() { } + // Only a delegate type could call the protected Delegate(Type, string), and its constructor is runtime implemented; + // reflection runs it on an existing delegate. NativeAOT's reflection doesn't run a constructor of the abstract + // Delegate; Mono has its own constructor. + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime), nameof(PlatformDetection.IsNotNativeAot))] + public static void ProtectedConstructor_TypeAndName_ThrowsWhenNoMethodBinds() + { + ConstructorInfo constructor = typeof(Delegate).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, new[] { typeof(Type), typeof(string) }); + Func d = new Func(ProtectedConstructorBefore); + + TargetInvocationException ex = Assert.Throws(() => constructor.Invoke(d, new object[] { typeof(IProtectedConstructorStaticAbstract), nameof(IProtectedConstructorStaticAbstract.M) })); + Assert.IsType(ex.InnerException); + ex = Assert.Throws(() => constructor.Invoke(d, new object[] { typeof(DelegateTests), "NoSuchMethod" })); + Assert.IsType(ex.InnerException); + + // The match is case insensitive + constructor.Invoke(d, new object[] { typeof(DelegateTests), "protectedconstructorafter" }); + Assert.Equal(2, d()); + } + + private static int ProtectedConstructorBefore() => 1; + + public static int ProtectedConstructorAfter() => 2; + + private interface IProtectedConstructorStaticAbstract + { + static abstract int M(); + } + class Class { internal void M() { } } struct Struct { internal void M() { } } diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Type/TypeTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Type/TypeTests.cs index 75bff461a01084..837160cddf9f0b 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Type/TypeTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Type/TypeTests.cs @@ -1376,6 +1376,13 @@ public void GetInterfaceMap(Type interfaceType, Type classType, Tuple(() => typeof(SIMs.C1).GetInterfaceMap(typeof(SIMs.I1))); + } + interface ISimpleInterface { void Method(); From 42614b35693cf8520c7a1427e8646acb798614b0 Mon Sep 17 00:00:00 2001 From: Iurie Caraion Date: Tue, 13 Oct 2026 01:30:18 +0300 Subject: [PATCH 4/5] Check static abstract in BindToMethodInfo Delegate_BindToMethodInfo now rejects a static abstract method right after its generic method definition check, before it checks the delegate signature, instead of a managed check after the bind. It throws MemberAccessException with an unformatted message, as the VM throws managed resource strings without arguments. RuntimeMethodHandle.GetFunctionPointer behaves as for an abstract instance method: for a method with no unbound generic parameters, CoreCLR returns the entry point again, and NativeAOT's static abstract invoker throws PlatformNotSupportedException, as VirtualMethodInvoker does for an abstract method. Both checks test the declaring type's cached flags before IsAbstract(), which reads metadata. Only abstract types, interfaces included, can declare an abstract method. Sealed classes are skipped too, so a C# static class (abstract and sealed) reads no metadata; a static abstract method in an abstract sealed class, which C# can't declare, is missed. Delegate_BindToMethodName also tests only once a name matches. Co-Authored-By: Claude Opus 5.5 --- .../src/System/Delegate.CoreCLR.cs | 10 +- .../src/System/RuntimeHandles.cs | 8 -- .../StaticAbstractMethodInvoker.cs | 7 +- src/coreclr/vm/comdelegate.cpp | 26 +++- src/coreclr/vm/runtimehandles.cpp | 13 +- .../src/Resources/Strings.resx | 3 + .../MethodCommonTests.cs | 5 + .../MethodInfoTests.cs | 120 +++++++++++++++--- .../MethodInvokerTests.cs | 9 ++ .../System/DelegateTests.cs | 4 + 10 files changed, 151 insertions(+), 54 deletions(-) diff --git a/src/coreclr/System.Private.CoreLib/src/System/Delegate.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Delegate.CoreCLR.cs index e59723213d1033..fa2a824fb57e95 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Delegate.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Delegate.CoreCLR.cs @@ -429,15 +429,7 @@ internal static Delegate CreateDelegateForDynamicMethod(Type type, object? targe { Delegate d = InternalAlloc(rtType); - if (!d.BindToMethodInfo(firstArgument, rtMethod, rtMethod.GetDeclaringTypeInternal(), flags)) - return null; - - // A static abstract interface method has no implementation to call. Checked after the bind, so a delegate - // type that doesn't match is still a bind failure, as on NativeAOT. - if (rtMethod.IsStatic && rtMethod.IsAbstract) - throw new MemberAccessException(SR.Format(SR.Acc_CallStaticAbstract, rtMethod.DeclaringType, rtMethod.Name)); - - return d; + return d.BindToMethodInfo(firstArgument, rtMethod, rtMethod.GetDeclaringTypeInternal(), flags) ? d : null; } // diff --git a/src/coreclr/System.Private.CoreLib/src/System/RuntimeHandles.cs b/src/coreclr/System.Private.CoreLib/src/System/RuntimeHandles.cs index 7f257f342d509b..791f12174cc4b5 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/RuntimeHandles.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/RuntimeHandles.cs @@ -1093,14 +1093,6 @@ public IntPtr GetFunctionPointer() { IntPtr ptr = GetFunctionPointer(IRuntimeMethodInfo.GetValue(EnsureNonNullMethodInfo(m_value))); GC.KeepAlive(m_value); - - // Null only for a static abstract interface method, which has no implementation to call - if (ptr == IntPtr.Zero) - { - MethodBase method = RuntimeType.GetMethodBase(m_value)!; - throw new MemberAccessException(SR.Format(SR.Acc_CallStaticAbstract, method.DeclaringType, method.Name)); - } - return ptr; } diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/MethodInfos/StaticAbstractMethodInvoker.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/MethodInfos/StaticAbstractMethodInvoker.cs index 0918b0323fed08..9a0da72422584c 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/MethodInfos/StaticAbstractMethodInvoker.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/MethodInfos/StaticAbstractMethodInvoker.cs @@ -8,8 +8,9 @@ namespace System.Reflection.Runtime.MethodInfos { - // A static abstract interface method has no implementation to call: invoking it, binding a delegate to it or - // taking its address throws MemberAccessException, as on CoreCLR. + // A static abstract interface method has no implementation to call: invoking it or binding a delegate to it throws + // MemberAccessException, as on CoreCLR. Taking its address throws PlatformNotSupportedException, as for other + // abstract methods. internal sealed class StaticAbstractMethodInvoker : MethodBaseInvoker { private readonly MethodInfo _method; @@ -69,7 +70,7 @@ public sealed override IntPtr LdFtnResult { get { - throw CreateException(); + throw new PlatformNotSupportedException(); } } } diff --git a/src/coreclr/vm/comdelegate.cpp b/src/coreclr/vm/comdelegate.cpp index dc3acc7548854d..c3725c2ec95ba9 100644 --- a/src/coreclr/vm/comdelegate.cpp +++ b/src/coreclr/vm/comdelegate.cpp @@ -1017,6 +1017,20 @@ static PCODE GetVirtualCallStub(MethodDesc *method, TypeHandle scopeType) ); } +// Only abstract types (interfaces included) can declare a static abstract method, and the type's flags are cached, +// unlike the method's attributes. Sealed classes are skipped, so the methods of a C# static class (abstract and sealed) +// read no metadata here; this misses a static abstract method in an abstract sealed class, which C# can't declare. +static bool IsStaticAbstractMethod(MethodDesc* pMD) +{ + WRAPPER_NO_CONTRACT; + + if (!pMD->IsStatic()) + return false; + + MethodTable* pMT = pMD->GetMethodTable(); + return pMT->IsAbstract() && (pMT->IsInterface() || !pMT->IsSealed()) && pMD->IsAbstract(); +} + extern "C" BOOL QCALLTYPE Delegate_BindToMethodName(MethodTable* pDelegateMT, MethodTable *pTargetMT, QCall::TypeHandle pMethodType, LPCUTF8 pszMethodName, DelegateBindingFlags flags, QCall::ObjectHandleOnStack targetParameter, BindToMethodDetails *pBindToMethodDetails, QCallExceptionStatus* qcallError) @@ -1059,12 +1073,12 @@ extern "C" BOOL QCALLTYPE Delegate_BindToMethodName(MethodTable* pDelegateMT, Me if (pCurMethod->IsAsyncVariantMethod()) continue; - // We can't match static abstract interface methods (since they have no implementation to call). - if (pCurMethod->IsStatic() && pCurMethod->IsAbstract()) - continue; - if (StrCompFunc(pszMethodName, pCurMethod->GetName()) == 0) { + // We can't match static abstract methods (since they have no implementation to call). + if (IsStaticAbstractMethod(pCurMethod)) + continue; + // found a matching string, get an associated method desc if needed // Use unboxing stubs for instance and virtual methods on value types. // If this is a open delegate to an instance method BindToMethod will rebind it to the non-unboxing method. @@ -1142,6 +1156,10 @@ extern "C" BOOL QCALLTYPE Delegate_BindToMethodInfo(MethodTable* pDelegateMT, Me if (method->IsGenericMethodDefinition()) COMPlusThrow(kArgumentException, W("Arg_DlgtTargMeth")); + // A static abstract method has no implementation to call. + if (IsStaticAbstractMethod(method)) + COMPlusThrow(kMemberAccessException, W("Acc_BindStaticAbstract")); + // get the invoke of the delegate MethodDesc* pInvokeMeth = COMDelegate::FindDelegateInvokeMethod(pDelegateMT); _ASSERTE(pInvokeMeth); diff --git a/src/coreclr/vm/runtimehandles.cpp b/src/coreclr/vm/runtimehandles.cpp index c25e816e744975..3a9bfb4e0fd14d 100644 --- a/src/coreclr/vm/runtimehandles.cpp +++ b/src/coreclr/vm/runtimehandles.cpp @@ -1308,15 +1308,10 @@ extern "C" void * QCALLTYPE RuntimeMethodHandle_GetFunctionPointer(MethodDesc * BEGIN_QCALL; - // A static abstract interface method has no implementation to call: return null, the caller throws. An open - // generic one throws InvalidOperationException from GetMultiCallableAddrOfCode first, as for any other method. - if (!(pMethod->IsStatic() && pMethod->IsAbstract()) || pMethod->ContainsGenericVariables()) - { - // Ensure the method is active and all types have been loaded so the function pointer can be used. - pMethod->EnsureActive(); - pMethod->PrepareForUseAsAFunctionPointer(); - funcPtr = (void*)pMethod->GetMultiCallableAddrOfCode(CORINFO_ACCESS_UNMANAGED_CALLER_MAYBE); - } + // Ensure the method is active and all types have been loaded so the function pointer can be used. + pMethod->EnsureActive(); + pMethod->PrepareForUseAsAFunctionPointer(); + funcPtr = (void*)pMethod->GetMultiCallableAddrOfCode(CORINFO_ACCESS_UNMANAGED_CALLER_MAYBE); END_QCALL; diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index f10d348e6f3866..1ca53374d5e624 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Cannot bind a delegate to a static abstract method. + Cannot call static abstract method '{0}.{1}'. diff --git a/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodCommonTests.cs b/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodCommonTests.cs index 8a1d657eeb2974..1e5ad307318726 100644 --- a/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodCommonTests.cs +++ b/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodCommonTests.cs @@ -312,6 +312,9 @@ private class TestClassThatThrows [Theory] [InlineData(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.NoArgs), null)] [InlineData(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.OneArg), new object[] { 42 })] + [InlineData(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.OneArg), new object[] { "abc" })] + [InlineData(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.SpanReturn), null)] + [InlineData(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.RefSpanReturn), null)] public void Invoke_StaticAbstract_ThrowsMemberAccessException(Type type, string methodName, object?[]? parameters) { MethodInfo method = type.GetMethod(methodName)!; @@ -333,6 +336,8 @@ protected interface IStaticAbstractMethods { static abstract void NoArgs(); static abstract void GenericNoArgs(); + static abstract Span SpanReturn(); + static abstract ref Span RefSpanReturn(); } protected interface IStaticAbstractMethods diff --git a/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInfoTests.cs b/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInfoTests.cs index d5fb4317b3e496..5ada620f288fc7 100644 --- a/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInfoTests.cs +++ b/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInfoTests.cs @@ -1284,6 +1284,18 @@ private interface IStaticAbstractLargeStruct static abstract LargeStruct StaticAbstractLargeStruct(string s); } + private interface IStaticAbstractFirstCaseTwins + { + static abstract int Twin(); + static virtual int twin() => 2; + } + + private interface IStaticVirtualFirstCaseTwins + { + static virtual int twin() => 2; + static abstract int Twin(); + } + [Fact] public void CreateDelegate_StaticVirtual() { @@ -1329,23 +1341,23 @@ public void CreateDelegate_StaticVirtual_ClosedOverFirstArgument_LargeStructRetu Assert.Equal(4, returnValue.D); } - // Mono binds delegates and takes function pointers without checking for static abstract methods + // Mono binds delegates without checking for static abstract methods [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime))] public void CreateDelegate_StaticAbstract_ThrowsMemberAccessException() { MethodInfo noArgs = GetMethod(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.NoArgs)); MemberAccessException ex = Assert.Throws(() => noArgs.CreateDelegate()); - Assert.Contains($"{typeof(IStaticAbstractMethods)}.{nameof(IStaticAbstractMethods.NoArgs)}", ex.Message); + AssertNamesMethodOnNativeAot(ex, $"{typeof(IStaticAbstractMethods)}.{nameof(IStaticAbstractMethods.NoArgs)}"); Assert.Throws(() => Delegate.CreateDelegate(typeof(Action), noArgs, throwOnBindFailure: false)); Assert.Throws(() => Delegate.CreateDelegate(typeof(Action), null, noArgs, throwOnBindFailure: false)); MethodInfo oneArg = GetMethod(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.OneArg)); ex = Assert.Throws(() => oneArg.CreateDelegate>()); - Assert.Contains($"{typeof(IStaticAbstractMethods)}.{nameof(IStaticAbstractMethods.OneArg)}", ex.Message); + AssertNamesMethodOnNativeAot(ex, $"{typeof(IStaticAbstractMethods)}.{nameof(IStaticAbstractMethods.OneArg)}"); MethodInfo genericNoArgs = GetMethod(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.GenericNoArgs)).MakeGenericMethod(typeof(int)); ex = Assert.Throws(() => genericNoArgs.CreateDelegate()); - Assert.Contains($"{typeof(IStaticAbstractMethods)}.{nameof(IStaticAbstractMethods.GenericNoArgs)}", ex.Message); + AssertNamesMethodOnNativeAot(ex, $"{typeof(IStaticAbstractMethods)}.{nameof(IStaticAbstractMethods.GenericNoArgs)}"); // Instantiated over a reference type MethodInfo genericNoArgsString = GetMethod(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.GenericNoArgs)).MakeGenericMethod(typeof(string)); @@ -1359,19 +1371,36 @@ public void CreateDelegate_StaticAbstract_ThrowsMemberAccessException() Assert.Throws(() => oneStringArg.CreateDelegate>(null)); MethodInfo largeStruct = GetMethod(typeof(IStaticAbstractLargeStruct), nameof(IStaticAbstractLargeStruct.StaticAbstractLargeStruct)); Assert.Throws(() => largeStruct.CreateDelegate>("test")); + Assert.Throws(() => oneArg.CreateDelegate>(42)); - // A delegate type that doesn't match fails to bind, as for any other method - Assert.Throws(() => noArgs.CreateDelegate>()); - - // On an open generic type the signature check fails first + // CoreCLR checks the method before the delegate signature, as it does for a generic method definition, which + // it still rejects first; NativeAOT checks the signature first + MethodInfo genericDefinition = GetMethod(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.GenericNoArgs)); MethodInfo openOneArg = GetMethod(typeof(IStaticAbstractMethods<>), nameof(IStaticAbstractMethods.OneArg)); if (PlatformDetection.IsNativeAot) { + Assert.Throws(() => noArgs.CreateDelegate>()); + Assert.Null(Delegate.CreateDelegate(typeof(Action), noArgs, throwOnBindFailure: false)); + Assert.Throws(() => genericDefinition.CreateDelegate()); Assert.Throws(() => openOneArg.CreateDelegate>()); + Assert.Throws(() => Delegate.CreateDelegate(typeof(Func), openOneArg, throwOnBindFailure: false)); } else { - Assert.Throws(() => openOneArg.CreateDelegate>()); + Assert.Throws(() => noArgs.CreateDelegate>()); + Assert.Throws(() => Delegate.CreateDelegate(typeof(Action), noArgs, throwOnBindFailure: false)); + Assert.Throws(() => genericDefinition.CreateDelegate()); + Assert.Throws(() => openOneArg.CreateDelegate>()); + Assert.Throws(() => Delegate.CreateDelegate(typeof(Func), openOneArg, throwOnBindFailure: false)); + } + + // CoreCLR throws from the VM, whose message doesn't name the method + static void AssertNamesMethodOnNativeAot(MemberAccessException ex, string method) + { + if (PlatformDetection.IsNativeAot) + { + Assert.Contains(method, ex.Message); + } } } @@ -1380,30 +1409,79 @@ public void CreateDelegate_ByName_StaticAbstract_DoesNotBind() { Assert.Throws(() => Delegate.CreateDelegate(typeof(Action), typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.NoArgs))); Assert.Null(Delegate.CreateDelegate(typeof(Action), typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.NoArgs), ignoreCase: false, throwOnBindFailure: false)); + Assert.Throws(() => Delegate.CreateDelegate(typeof(Func), typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.OneArg))); // A static virtual method with a body still binds by name Func byName = (Func)Delegate.CreateDelegate(typeof(Func), typeof(IStaticInterface), nameof(IStaticInterface.StaticVirtual)); Assert.Equal("test", byName("test")); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime))] - public void GetFunctionPointer_StaticAbstract_ThrowsMemberAccessException() + // A case-insensitive match skips the static abstract method whatever the declaration order. NativeAOT reports + // the two names as ambiguous before it looks at either method. + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime), nameof(PlatformDetection.IsNotNativeAot))] + public void CreateDelegate_ByName_IgnoreCase_SkipsStaticAbstract() { - MethodInfo noArgs = GetMethod(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.NoArgs)); - MemberAccessException ex = Assert.Throws(() => noArgs.MethodHandle.GetFunctionPointer()); - Assert.Contains($"{typeof(IStaticAbstractMethods)}.{nameof(IStaticAbstractMethods.NoArgs)}", ex.Message); + foreach (Type type in new[] { typeof(IStaticAbstractFirstCaseTwins), typeof(IStaticVirtualFirstCaseTwins) }) + { + Func byName = (Func)Delegate.CreateDelegate(typeof(Func), type, "Twin", ignoreCase: true); + Assert.Equal(2, byName()); - MethodInfo oneArg = GetMethod(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.OneArg)); - ex = Assert.Throws(() => oneArg.MethodHandle.GetFunctionPointer()); - Assert.Contains($"{typeof(IStaticAbstractMethods)}.{nameof(IStaticAbstractMethods.OneArg)}", ex.Message); + byName = (Func)Delegate.CreateDelegate(typeof(Func), type, "Twin", ignoreCase: true, throwOnBindFailure: false)!; + Assert.Equal(2, byName()); + } + } - MethodInfo genericNoArgs = GetMethod(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.GenericNoArgs)).MakeGenericMethod(typeof(string)); - ex = Assert.Throws(() => genericNoArgs.MethodHandle.GetFunctionPointer()); - Assert.Contains($"{typeof(IStaticAbstractMethods)}.{nameof(IStaticAbstractMethods.GenericNoArgs)}", ex.Message); + // A static abstract method in an abstract class or in a sealed interface, which C# can't declare, is rejected as one + // in an interface. The method comes from its token: Type.GetMethod would list the type's methods, and a Checked + // CoreLib asserts there that a non-virtual method of a class isn't abstract. + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsCoreCLR), nameof(PlatformDetection.IsReflectionEmitSupported))] + [InlineData(TypeAttributes.Public | TypeAttributes.Abstract, (MethodAttributes)0)] + [InlineData(TypeAttributes.Public | TypeAttributes.Interface | TypeAttributes.Abstract | TypeAttributes.Sealed, MethodAttributes.Virtual)] + public void CreateDelegate_EmittedStaticAbstract_ThrowsMemberAccessException(TypeAttributes typeAttributes, MethodAttributes virtualAttribute) + { + AssemblyBuilder assembly = AssemblyBuilder.DefineDynamicAssembly( + new AssemblyName(nameof(CreateDelegate_EmittedStaticAbstract_ThrowsMemberAccessException)), AssemblyBuilderAccess.RunAndCollect); + TypeBuilder typeBuilder = assembly.DefineDynamicModule("Module").DefineType("Target", typeAttributes); + MethodBuilder methodBuilder = typeBuilder.DefineMethod( + "M", MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.Abstract | virtualAttribute, + typeof(int), Type.EmptyTypes); + Type type = typeBuilder.CreateType()!; + MethodInfo method = (MethodInfo)type.Module.ResolveMethod(methodBuilder.MetadataToken)!; + + Assert.Throws(() => method.CreateDelegate>()); + Assert.Throws(() => Delegate.CreateDelegate(typeof(Func), type, "M")); + Assert.Null(Delegate.CreateDelegate(typeof(Func), type, "M", ignoreCase: false, throwOnBindFailure: false)); + } + + // As for an abstract instance method: an entry point on CoreCLR, PlatformNotSupportedException on NativeAOT + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime))] + public void GetFunctionPointer_StaticAbstract_MatchesAbstractMethod() + { + MethodInfo[] methods = + { + GetMethod(typeof(MI_AbstractBaseClass), nameof(MI_AbstractBaseClass.AbstractMethod)), + GetMethod(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.NoArgs)), + GetMethod(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.OneArg)), + GetMethod(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.GenericNoArgs)).MakeGenericMethod(typeof(string)), + }; + + foreach (MethodInfo method in methods) + { + if (PlatformDetection.IsNativeAot) + { + Assert.Throws(() => method.MethodHandle.GetFunctionPointer()); + } + else + { + Assert.NotEqual(IntPtr.Zero, method.MethodHandle.GetFunctionPointer()); + } + } - // On an open generic type the open generic error comes first, as for any other method + // On an open generic type or a generic method definition the open generic error comes first, as for any other method MethodInfo openOneArg = GetMethod(typeof(IStaticAbstractMethods<>), nameof(IStaticAbstractMethods.OneArg)); Assert.Throws(() => openOneArg.MethodHandle.GetFunctionPointer()); + MethodInfo genericDefinition = GetMethod(typeof(IStaticAbstractMethods), nameof(IStaticAbstractMethods.GenericNoArgs)); + Assert.Throws(() => genericDefinition.MethodHandle.GetFunctionPointer()); // A static virtual method with a body still has an address Assert.NotEqual(IntPtr.Zero, GetMethod(typeof(IStaticInterface), nameof(IStaticInterface.StaticVirtual)).MethodHandle.GetFunctionPointer()); diff --git a/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInvokerTests.cs b/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInvokerTests.cs index 46fc0937fbab58..9cafa6c25a3366 100644 --- a/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInvokerTests.cs +++ b/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInvokerTests.cs @@ -64,6 +64,15 @@ public void Invoke_StaticAbstract_FixedArguments_ThrowsMemberAccessException() MethodInvoker oneArg = MethodInvoker.Create(typeof(IStaticAbstractMethods).GetMethod(nameof(IStaticAbstractMethods.OneArg))!); Assert.Throws(() => oneArg.Invoke(null, 42)); + + MethodInvoker oneStringArg = MethodInvoker.Create(typeof(IStaticAbstractMethods).GetMethod(nameof(IStaticAbstractMethods.OneArg))!); + Assert.Throws(() => oneStringArg.Invoke(null, "abc")); + + MethodInvoker spanReturn = MethodInvoker.Create(typeof(IStaticAbstractMethods).GetMethod(nameof(IStaticAbstractMethods.SpanReturn))!); + Assert.Throws(() => spanReturn.Invoke(null)); + + MethodInvoker refSpanReturn = MethodInvoker.Create(typeof(IStaticAbstractMethods).GetMethod(nameof(IStaticAbstractMethods.RefSpanReturn))!); + Assert.Throws(() => refSpanReturn.Invoke(null)); } [Fact] diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DelegateTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DelegateTests.cs index 7fa92509b096b0..5bfb37e040e78a 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DelegateTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DelegateTests.cs @@ -605,6 +605,8 @@ public static void ProtectedConstructor_TypeAndName_ThrowsWhenNoMethodBinds() Assert.IsType(ex.InnerException); ex = Assert.Throws(() => constructor.Invoke(d, new object[] { typeof(DelegateTests), "NoSuchMethod" })); Assert.IsType(ex.InnerException); + ex = Assert.Throws(() => constructor.Invoke(d, new object[] { typeof(DelegateTests), nameof(ProtectedConstructorWrongSignature) })); + Assert.IsType(ex.InnerException); // The match is case insensitive constructor.Invoke(d, new object[] { typeof(DelegateTests), "protectedconstructorafter" }); @@ -615,6 +617,8 @@ public static void ProtectedConstructor_TypeAndName_ThrowsWhenNoMethodBinds() public static int ProtectedConstructorAfter() => 2; + public static void ProtectedConstructorWrongSignature() { } + private interface IProtectedConstructorStaticAbstract { static abstract int M(); From 7220f157dcb527c3977b9859791252f9744531e9 Mon Sep 17 00:00:00 2001 From: Iurie Caraion Date: Thu, 15 Oct 2026 09:58:34 +0300 Subject: [PATCH 5/5] Move static abstract check to MethodDesc MethodDesc::IsStaticAbstract() replaces the file-local helper in comdelegate.cpp and no longer skips sealed classes, so a static abstract method of an abstract sealed class, which IL allows, is rejected too. It still tests the declaring type's cached IsAbstract() before the method's IsAbstract(), which reads metadata: the type loader rejects an abstract method in a type that isn't abstract. Delegate_BindToMethodName skips static abstract methods right after its generic method definition check, as Delegate_BindToMethodInfo rejects them there. RuntimeMethodInfo.ThrowNoInvokeException loses its IsAbstract branch. Only an abstract method with another reason not to invoke it reached the branch, such as one returning a ref to a ref struct; that method now gets the NotSupportedException a method with a body gets, instead of MemberAccessException. Co-Authored-By: Claude Opus 5.5 --- src/coreclr/vm/comdelegate.cpp | 24 +++----------- src/coreclr/vm/method.hpp | 10 ++++++ .../System/Reflection/RuntimeMethodInfo.cs | 5 --- .../MethodInfoTests.cs | 7 ++-- .../System/Reflection/InvokeRefReturn.cs | 33 +++++++++++++++++++ 5 files changed, 52 insertions(+), 27 deletions(-) diff --git a/src/coreclr/vm/comdelegate.cpp b/src/coreclr/vm/comdelegate.cpp index c3725c2ec95ba9..1fe00b29a021ee 100644 --- a/src/coreclr/vm/comdelegate.cpp +++ b/src/coreclr/vm/comdelegate.cpp @@ -1017,20 +1017,6 @@ static PCODE GetVirtualCallStub(MethodDesc *method, TypeHandle scopeType) ); } -// Only abstract types (interfaces included) can declare a static abstract method, and the type's flags are cached, -// unlike the method's attributes. Sealed classes are skipped, so the methods of a C# static class (abstract and sealed) -// read no metadata here; this misses a static abstract method in an abstract sealed class, which C# can't declare. -static bool IsStaticAbstractMethod(MethodDesc* pMD) -{ - WRAPPER_NO_CONTRACT; - - if (!pMD->IsStatic()) - return false; - - MethodTable* pMT = pMD->GetMethodTable(); - return pMT->IsAbstract() && (pMT->IsInterface() || !pMT->IsSealed()) && pMD->IsAbstract(); -} - extern "C" BOOL QCALLTYPE Delegate_BindToMethodName(MethodTable* pDelegateMT, MethodTable *pTargetMT, QCall::TypeHandle pMethodType, LPCUTF8 pszMethodName, DelegateBindingFlags flags, QCall::ObjectHandleOnStack targetParameter, BindToMethodDetails *pBindToMethodDetails, QCallExceptionStatus* qcallError) @@ -1069,16 +1055,16 @@ extern "C" BOOL QCALLTYPE Delegate_BindToMethodName(MethodTable* pDelegateMT, Me if (pCurMethod->IsGenericMethodDefinition()) continue; + // We can't match static abstract methods (since they have no implementation to call). + if (pCurMethod->IsStaticAbstract()) + continue; + // We can't match async variants (since we have only name). if (pCurMethod->IsAsyncVariantMethod()) continue; if (StrCompFunc(pszMethodName, pCurMethod->GetName()) == 0) { - // We can't match static abstract methods (since they have no implementation to call). - if (IsStaticAbstractMethod(pCurMethod)) - continue; - // found a matching string, get an associated method desc if needed // Use unboxing stubs for instance and virtual methods on value types. // If this is a open delegate to an instance method BindToMethod will rebind it to the non-unboxing method. @@ -1157,7 +1143,7 @@ extern "C" BOOL QCALLTYPE Delegate_BindToMethodInfo(MethodTable* pDelegateMT, Me COMPlusThrow(kArgumentException, W("Arg_DlgtTargMeth")); // A static abstract method has no implementation to call. - if (IsStaticAbstractMethod(method)) + if (method->IsStaticAbstract()) COMPlusThrow(kMemberAccessException, W("Acc_BindStaticAbstract")); // get the invoke of the delegate diff --git a/src/coreclr/vm/method.hpp b/src/coreclr/vm/method.hpp index 1f860e4c8cd3c3..5d70a2e9eb5cb0 100644 --- a/src/coreclr/vm/method.hpp +++ b/src/coreclr/vm/method.hpp @@ -1097,6 +1097,16 @@ class MethodDesc #endif // FEATURE_DEFAULT_INTERFACES } + // Is this a static abstract method (it has no implementation to call) + inline BOOL IsStaticAbstract() + { + WRAPPER_NO_CONTRACT; + + // Only abstract types (interfaces included) can declare an abstract method, and the type's flags are cached, + // unlike the method's attributes. + return IsStatic() && GetMethodTable()->IsAbstract() && IsAbstract(); + } + inline BOOL HasNonVtableSlot(); void SetHasNonVtableSlot() diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/RuntimeMethodInfo.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/RuntimeMethodInfo.cs index 3a5e224a0e87bf..ad9b0117e9503f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/RuntimeMethodInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/RuntimeMethodInfo.cs @@ -84,11 +84,6 @@ internal void ThrowNoInvokeException() { throw new MemberAccessException(SR.Format(SR.Acc_CallStaticAbstract, declaringType, Name)); } - // method is abstract class - else if (IsAbstract) - { - throw new MemberAccessException(); - } else if (ReturnType.IsByRef) { Type elementType = ReturnType.GetElementType()!; diff --git a/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInfoTests.cs b/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInfoTests.cs index 5ada620f288fc7..0d091336d4fde7 100644 --- a/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInfoTests.cs +++ b/src/libraries/System.Runtime/tests/System.Reflection.Tests/MethodInfoTests.cs @@ -1431,11 +1431,12 @@ public void CreateDelegate_ByName_IgnoreCase_SkipsStaticAbstract() } } - // A static abstract method in an abstract class or in a sealed interface, which C# can't declare, is rejected as one - // in an interface. The method comes from its token: Type.GetMethod would list the type's methods, and a Checked - // CoreLib asserts there that a non-virtual method of a class isn't abstract. + // A static abstract method in an abstract class, sealed or not, or in a sealed interface is rejected as one in an + // interface. The method comes from its token: Type.GetMethod would list the type's methods, and a Checked CoreLib + // asserts there that a non-virtual method of a class isn't abstract. [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsCoreCLR), nameof(PlatformDetection.IsReflectionEmitSupported))] [InlineData(TypeAttributes.Public | TypeAttributes.Abstract, (MethodAttributes)0)] + [InlineData(TypeAttributes.Public | TypeAttributes.Abstract | TypeAttributes.Sealed, (MethodAttributes)0)] [InlineData(TypeAttributes.Public | TypeAttributes.Interface | TypeAttributes.Abstract | TypeAttributes.Sealed, MethodAttributes.Virtual)] public void CreateDelegate_EmittedStaticAbstract_ThrowsMemberAccessException(TypeAttributes typeAttributes, MethodAttributes virtualAttribute) { diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Reflection/InvokeRefReturn.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Reflection/InvokeRefReturn.cs index 0f3d1dbfba810e..05b2447b5162fc 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Reflection/InvokeRefReturn.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Reflection/InvokeRefReturn.cs @@ -91,6 +91,23 @@ public static unsafe void TestByRefLikeRefReturn() } } + [Fact] + public static void TestByRefLikeRefReturn_AbstractMethod() + { + // An abstract method is rejected as one with a body is + ByRefLikeRefReturningClass target = new ByRefLikeRefReturningClass(); + MethodInfo[] methods = + { + typeof(ByRefLikeRefReturningBase).GetMethod(nameof(ByRefLikeRefReturningBase.AbstractMethod)), + typeof(IByRefLikeRefReturning).GetMethod(nameof(IByRefLikeRefReturning.InterfaceMethod)), + }; + + foreach (MethodInfo mi in methods) + { + Assert.Throws(() => mi.Invoke(target, null)); + } + } + public static IEnumerable RefReturnInvokeTestData { get @@ -160,6 +177,22 @@ private static void TestRefReturnInvokeNullable(T? nullable) where T : struct public ref struct ByRefLike { } + private interface IByRefLikeRefReturning + { + ref ByRefLike InterfaceMethod(); + } + + private abstract class ByRefLikeRefReturningBase + { + public abstract ref ByRefLike AbstractMethod(); + } + + private sealed class ByRefLikeRefReturningClass : ByRefLikeRefReturningBase, IByRefLikeRefReturning + { + public override ref ByRefLike AbstractMethod() => throw new InvalidOperationException(); + public ref ByRefLike InterfaceMethod() => throw new InvalidOperationException(); + } + private sealed class TestClass { private T _value;