Skip to content

ILVerify reports MethodAccess when a type calls a protected member of an interface it implements #136175

Closed

Description

@pcshrosbree

TL;DR: ILVerify reports MethodAccess when a type calls a protected member of an interface that it implements. The runtime allows these calls, and they occur in CoreLib's generic math and in NuGet packages. ILVerify's family-access check looks only at base classes, while the runtime also looks at implemented interfaces. I have a small fix with tests and will open a PR.

Repro

using System;

public interface IProbe<TSelf> where TSelf : IProbe<TSelf>
{
    protected static abstract string Probe();
}

public struct A : IProbe<A>
{
    static string IProbe<A>.Probe() => "A";

    public static string Call<T>() where T : IProbe<T> => T.Probe();
}

public struct B : IProbe<B>
{
    static string IProbe<B>.Probe() => "B";
}

public static class Program
{
    public static void Main() => Console.WriteLine(A.Call<B>());
}

The program compiles and prints B on .NET 10.0.12. ILVerify built from main reports:

[IL]: Error [MethodAccess]: [Repro.dll : A::Call()][offset 0x00000006] Method is not visible.

A.Call<B> compiles to constrained. !!T call string IProbe<!!T>::Probe(). A implements IProbe<A>, which is a different instantiation of the same interface. CoreLib has the same shape: Int32.CreateChecked<TOther> calls TOther.TryConvertToChecked, a protected static abstract member of INumberBase<TSelf>.

Cause

  • ILVerify. CanAccessFamily grants family access only when the target's type definition is one of the current type's base types (lines 219-227). An interface is never a base type, so an implementing type cannot reach its protected members.
  • Runtime. ClassLoader::CanAccessFamily has a separate branch for interfaces (lines 4554-4567). It grants access when an entry in the caller's interface map has the same type definition as the target, with the comment "We only loosely check if they are of the same generic type". It then repeats the check for enclosing types. The JIT runs this check against the method named by the call token, which here is the interface method (jitinterface.cpp).
  • Spec. The ECMA-335 augments for default interface methods extend family access to "all of the types that inherit from it, or implement it as an interface". Their examples are a derived class, a nested type and a derived interface.

I tested the following caller shapes on .NET 10.0.12 and 10.0.13. The runtime allows the call from:

  • a type that implements any instantiation of the interface;
  • a class whose base class implements it;
  • a type nested in an implementing class;
  • a derived interface.

It throws MethodAccessException when the caller implements no instantiation of the interface, or implements only some other interface. ILVerify reports MethodAccess for all four allowed shapes as well as both rejected ones.

Impact

  • NuGet packages. DotNext 6.10.0 has 51 of these false errors in 41 methods. They are calls to protected static members of IFunctional, IInterpolatedStringHandler, IGrowableBuffer<T>, IBufferedDecoder<T>, IBufferedEncoder<T> and INumberBase<T>, made by types that implement those interfaces or by types nested in such a type.
  • Shared framework. There are 57 such calls, in CreateChecked, CreateSaturating and CreateTruncating of 19 numeric types: 51 in System.Private.CoreLib and 6 in System.Runtime.Numerics. ILVerify on main does not report them today: each of these methods has an earlier InitLocals error, which stops ILVerify before it reaches the call. The validation-only mode in ILVerify: add a validation-only mode #136169 reports all 57.

Proposal

In CanAccessFamily, when the target type is an interface, grant access if one of the current type's runtime interfaces has the same type definition. Runtime interfaces include those inherited from base classes and from other interfaces. The existing loop over enclosing types then covers nested types. This follows what the runtime implements, for a shape that occurs in shipped code.

The rule that a protected instance member must be accessed through an instance of the current type stays as it is. The runtime does not check that rule, but ILVerify already applies it to protected members of classes.

Note

This issue was prepared with AI assistance (Anthropic Claude and OpenAI Codex), under my direction. I reviewed it before posting.

Activity

  1. added a commit that references this issue on Oct 21, 2026
    b3c0988
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions