Skip to content

ILVerify: continue checking methods that use calli, mkrefany, refanyval and refanytype instead of aborting them - #136133

Open
pcshrosbree wants to merge 3 commits into
dotnet:mainfrom
pcshrosbree:fix/ilverify-calli-typedreference
Open

pcshrosbree wants to merge 3 commits into
dotnet:mainfrom
pcshrosbree:fix/ilverify-calli-typedreference

Conversation

@pcshrosbree

@pcshrosbree pcshrosbree commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR: ILVerify currently stops checking a method when it reaches calli, mkrefany, refanyval or refanytype. This PR adds checks for these instructions so that checking continues, and it still reports every calli as unverifiable. In the .NET 10.0.12 shared framework, 31 methods are no longer aborted, and no other method's results change. I chose defaults for three open questions, which are listed at the end for review.

This follows the reply in #136111:

#136111 still asks for arglist, so this PR contributes to it rather than fixing it. If #136111 is narrowed to the four instructions this PR covers, this PR completes it.

Rules

Section numbers are from ECMA-335, 6th edition.

calli (III.3.20):

  • The operand must be a stand-alone signature token (ExpectedSignatureToken).
  • The method pointer must be a native int (StackMethod).
  • The arguments must be assignable to the call-site signature. For an instance site without explicit, the signature does not give the type of this, so it is only popped. An uninitialized this cannot be passed.
  • tail. is checked as for call. The return checks that were in ImportCall are now in CheckTailCallReturn, which call and calli share.
  • The return type is pushed.

mkrefany (III.4.19):

  • The pointer must be a managed pointer (StackByRef) that is not readonly (ReadOnlyInMkrefany).
  • The type it points to must exactly match the type named by the class token (StackUnexpected).
  • The class must be accessible (TypeAccess). It may be byref-like (see review point 1).
  • TypedReference is pushed.

refanyval (III.4.23): the operand must be a TypedReference. A managed pointer to the class is pushed. Because it may point to a caller's local, returning it is reported (ReturnPtrToStack).

refanytype (III.4.22): the operand must be a TypedReference. RuntimeTypeHandle is pushed.

The two new codes are appended at the end of VerifierError, each with a Strings.resx entry.

Two limits remain:

  • ILVerify does not verify a block that is reached only through a block that reported an error, and Unverifiable counts as an error. An error after a calli in the same block is reported, but a later block may not be checked.
  • Vararg call sites that pass extra arguments still fail in the shared type system, for both calli and call. This PR does not change that.

Tests

  • CalliTests.il has 17 tests and TypedReferenceTests.il has 16, with one rule per test where possible.
  • ILVerification.Tests goes from 513 tests on main to 546, all passing.
  • I made 27 mutations. In 26 of them, I disabled, inverted or changed one new check or stack effect. The other one adds back the byref-like rejection described in review point 1. Every mutant built and failed at least one test.

Measured impact

I compared builds of main (677ab53) with and without this change, running on linux-x64 under .NET 11 RC 1, with the .NET 10.0.12 shared framework as the reference set.

The figures below were measured before commit 8463836 removed the check comparing a calli signature with the method loaded by ldftn or ldvirtftn. In a separate comparison, builds from before and after that commit gave identical results for every method in the .NET 10.0.13 shared framework and the package sets below.

Shared framework (172 assemblies, 130,146 method bodies). 55 methods contain calli and 3 contain refanytype. None contains mkrefany or refanyval.

main This PR
Methods aborted at one of the four instructions 31 0
Methods whose results changed, among the other 130,115 0
New findings in the 31, besides Unverifiable 37

All 37 new findings come after the calli: InitLocals 33, UnmanagedPointer 2 and StackUnexpected 2. The same methods already report these kinds of errors before the calli. Neither of the two new codes fired.

Published packages (388,443 method bodies from hand-written IL packages, packages built with InlineIL.Fody, the 300 most-downloaded packages on nuget.org, and saved dynamic proxies). 149 methods were aborted at one of the four instructions, and ILVerify now continues checking them. They gained six findings, all at or after the instruction. One is a real bug: ImpromptuNinjas.ZStd 1.4.5.5's ZStdDCtx::Copy pushes two arguments for a calli whose signature takes three, and ILVerify now reports a stack underflow. No other method changed, and neither of the two new codes fired.

C# compiler output (SDK 10.0.401). Function-pointer calls now report only Unverifiable at the calli. Methods that use __makeref, __refvalue and __reftype verify with no errors. A method that uses __arglist still fails as before.

Roslyn. Four Roslyn tests quote the old messages: ExtensionTests.AddressOf_TypeReceiver, LocalStateTracingTests.RefArguments_FunctionPointerInvocation, FunctionPointerTests.FunctionPointerInference_ExactInferenceThroughArray_RefKindMatch (in a comment) and CodeGenTests.Issue51228. They would need updating when Roslyn takes a new ILVerify.

Points for review

These questions from #136111 are still open, and I chose a default for each:

  1. mkrefany and byref-like classes. This PR follows the augmented spec and accepts them. The 2012 edition of ECMA-335 said a managed pointer cannot point to a typed reference (I.8.6.1.3). The augments replace that with "a managed pointer can point to a byref-like type" (I.8.6.1.2). The old native verifier rejected TypedReference, ArgIterator and RuntimeArgumentHandle (VER_E_RA_PTR_TO_STACK), and the old JIT verifier rejected every byref-like class. I can add a rejection of all byref-like classes if you prefer it.
  2. mkrefany type match. The type the pointer points to must exactly match the type named by the class token, so int32& with uint32 is reported. Comparing verification types instead would accept it.
  3. Error codes. I added one code per new message. I can reuse existing codes such as TokenResolve or ReadOnlyIllegalWrite instead, or rename these.

Contributes to #136111

Note

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

ILVerify stopped verifying a method at calli, mkrefany, refanyval or
refanytype, so errors after the instruction were not reported.

calli is now always reported as Unverifiable, and its call site is still
checked: the signature token, the method pointer, the arguments, tail.
and the return value. When the pointer comes straight from ldftn or
ldvirtftn, the call-site signature is also compared with that method's
signature. The tail. return checks move from ImportCall into
CheckTailCallReturn, which call and calli both use.

mkrefany, refanyval and refanytype follow ECMA-335 III.4.19, III.4.23
and III.4.22. mkrefany also rejects readonly pointers. It accepts a
byref-like class, because the ECMA-335 augments allow a managed pointer
to a byref-like type.

arglist is unchanged and is still not supported.

New VerifierError codes: ExpectedSignatureToken, CalliSignatureMismatch
and ReadOnlyInMkrefany.

Contributes to dotnet#136111
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Oct 20, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @agocke
See info in area-owners.md if you want to be subscribed.

if (implicitThis && tailCall)
Check(!IsByRefLike(args[0]), VerifierError.TailByRef, args[0]);

// A method pointer pushed by ldftn or ldvirtftn carries its method, so the call site can be checked against it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not useful check. ldftn immediately followed by calli is just a compilated way to do a call. It is going to be extremely rare in anything real.

It may be more interesting to track the function pointer types on IL stack and validate that the function pointer type matches the calli signature, and assignment compatibility for function pointers. (Separate PR.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TL;DR: I would like to keep the ldftn check, but I will remove it if you still prefer. Should ILVerify follow the runtime's safety limits where ECMA-335 says calli is verifiable? If a method uses ldftn then calli without storing the pointer, is the target guaranteed to stay loaded?

The mkrefany comment is fixed in 3287d03: it now sits with the III.4.19 check it qualifies.

ILVerify is meant for checking IL before it ships: the output of rewriters, weavers, profilers and new emitters. Released packages cannot show how often this shape appears there, and the ones I scanned have none: of 1,388 calli sites in 424 assemblies, none is immediately preceded by ldftn or ldvirtftn. The compilers I tested do not produce it either. The Roslyn version I tested stores the pointer in a local before pushing the arguments, and the F# output I scanned contains no calli. Hand-written or generated IL can push the arguments and then ldftn M; calli sig. The check reports a signature mismatch there, which III.3.20 makes a correctness error.

I agree that tracking function pointer types on the IL stack is the better general mechanism, and that it would subsume this check. I would be glad to do that in a separate PR. It is also how III.3.20 defines calli verification: subject to the correctness rules, calli verifies when the pointer's tracked method signature matches the this pointer and the arguments. The augments do not amend that rule.

On #136111 you explained that calli cannot really be verifiable, because the runtime does not track function pointer lifetimes. Is ILVerify's rule that soundness on the current runtime takes precedence over ECMA-335 where the two differ? Your answer would help me decide similar cases in future PRs.

In the CoreCLR paths I read, the target of a pointer from ldftn seems to stay loaded if the same method uses the pointer with calli without storing it. The paths I read appear to keep such a target loaded while the method's own LoaderAllocator is alive (EnsureReference, vm/appdomain.cpp:4053), and the GC reports that allocator while the method is on the stack (vm/gcenv.ee.common.cpp:350-380). Is that right, or can the target still be unloaded in this case? I inferred some of the paths rather than running them.

If runtime soundness takes precedence and the target stays loaded in this case, I could propose a separate change. It would add an augment and update ILVerify to make this case verifiable.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"the same method uses the pointer with calli without storing" is not an interesting scenario. It never happens in real code as your own analysis found. There is no point in building a verifier rule that kicks in only for this case.

The common scenario for function pointers is that the function pointer is created in one method, passed around and invoked in completely different method.

The function pointer verification was only ever sketched in the ECMA spec. It was never actually implemented. The old PEVerify tool treated function pointers and calli as unconditionally unverifiable, same as ILVerify. So trying to implement it is likely going to uncover number of interesting cases without prior art.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 8463836. You also suggested a separate PR to track function pointer types on the IL stack; given your warning about the lack of prior art, do you still want that PR?

Thank you for the background on PEVerify and on how function pointers are used in practice. More generally, thank you for the careful reviews and explanations across these ILVerify issues and PRs; they have shaped how I approach this work.

Comment thread src/coreclr/tools/ILVerification/ILImporter.Verify.cs Outdated
The comment saying that mkrefany accepts a byref-like class sat above
the type access check, which it does not describe. It now follows the
ECMA-335 III.4.19 comment on the managed-pointer check, which is the
rule it qualifies. There is no change in behaviour.
When the function pointer came directly from ldftn or ldvirtftn, the
calli call-site signature was also compared with the target method's
signature. Following review feedback, that check is removed, because
real code does not pass the pointer to calli this way.
CalliSignatureMismatch and the tests for the check go with it; the
other calli call-site checks are unchanged.

This branch has not been deployed

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

Labels

area-VM-coreclr community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants