Repository navigation
ILVerify: continue checking methods that use calli, mkrefany, refanyval and refanytype instead of aborting them - #136133
pcshrosbree wants to merge 3 commits into
Conversation
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: 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. |
|
Tagging subscribers to this area: @agocke |
| 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. |
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
TL;DR: I would like to keep the
ldftncheck, but I will remove it if you still prefer. Should ILVerify follow the runtime's safety limits where ECMA-335 sayscalliis verifiable? If a method usesldftnthencalliwithout 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.
There was a problem hiding this comment.
"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.
There was a problem hiding this comment.
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.
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 follows the reply in #136111:
callicannot be verifiable, because the runtime does not track the lifetime of function pointers, and it belongs under [ILVerify] Add option for IL validation without IL verification #37391. So everycalliis reported asUnverifiable, and verification continues. Call-site errors such as a wrong number of arguments are still reported. This PR does not add the validation-only mode that [ILVerify] Add option for IL validation without IL verification #37391 asks for.arglistunchanged:ImportArgListstill throws "RuntimeArgumentHandle not supported in .NET Core".#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):ExpectedSignatureToken).StackMethod).explicit, the signature does not give the type ofthis, so it is only popped. An uninitializedthiscannot be passed.tail.is checked as forcall. The return checks that were inImportCallare now inCheckTailCallReturn, whichcallandcallishare.mkrefany(III.4.19):StackByRef) that is not readonly (ReadOnlyInMkrefany).StackUnexpected).TypeAccess). It may be byref-like (see review point 1).TypedReferenceis pushed.refanyval(III.4.23): the operand must be aTypedReference. 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 aTypedReference.RuntimeTypeHandleis pushed.The two new codes are appended at the end of
VerifierError, each with aStrings.resxentry.Two limits remain:
Unverifiablecounts as an error. An error after acalliin the same block is reported, but a later block may not be checked.calliandcall. This PR does not change that.Tests
CalliTests.ilhas 17 tests andTypedReferenceTests.ilhas 16, with one rule per test where possible.ILVerification.Testsgoes from 513 tests onmainto 546, all passing.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
callisignature with the method loaded byldftnorldvirtftn. 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
calliand 3 containrefanytype. None containsmkrefanyorrefanyval.mainUnverifiableAll 37 new findings come after the
calli:InitLocals33,UnmanagedPointer2 andStackUnexpected2. The same methods already report these kinds of errors before thecalli. 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::Copypushes two arguments for acalliwhose 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
Unverifiableat thecalli. Methods that use__makeref,__refvalueand__reftypeverify with no errors. A method that uses__argliststill fails as before.Roslyn. Four Roslyn tests quote the old messages:
ExtensionTests.AddressOf_TypeReceiver,LocalStateTracingTests.RefArguments_FunctionPointerInvocation,FunctionPointerTests.FunctionPointerInference_ExactInferenceThroughArray_RefKindMatch(in a comment) andCodeGenTests.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:
mkrefanyand 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 rejectedTypedReference,ArgIteratorandRuntimeArgumentHandle(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.mkrefanytype match. The type the pointer points to must exactly match the type named by the class token, soint32&withuint32is reported. Comparing verification types instead would accept it.TokenResolveorReadOnlyIllegalWriteinstead, 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.