Repository navigation
ILVerify: add a validation-only mode - #136169
Draft
pcshrosbree wants to merge 1 commit into
Draft
pcshrosbree wants to merge 1 commit into
pcshrosbree wants to merge 1 commit into
Conversation
Contributor
|
Tagging subscribers to this area: @JulieLeeMSFT, @dotnet/jit-contrib |
pcshrosbree
force-pushed
the
feat/ilverify-validation-mode
branch
from
October 20, 2026 18:55
2831173 to
004e000
Compare
This was referenced Oct 20, 2026
Add VerifierOptions.ValidationOnly and the ilverify --validation-only option. In this mode ILVerify reports IL that is not valid and does not report most IL that is valid but not verifiable (ECMA-335 III.1.8), such as unmanaged pointers, localloc, calli, pointer arithmetic, or methods without localsinit. Each reporting site is classified by the rule it enforces: a rule in an instruction's Correctness paragraph stays, and a rule found only in its Verifiability paragraph is generally not reported. Most checks of a value against a declared type compare only the stack types of I.12.3.2.1. Checks that compare two signatures, such as the delegate target check, still compare the types in those signatures, not only their stack types. A suppressed check does not abort the block or the method, so checking continues after it. With the option off, results are unchanged. Tests: every existing method test also runs in validation-only mode. A suffix on the test method name gives the errors expected in that mode when they differ, and a malformed suffix fails the tests. ValidationOnlyTests.il covers the suppressed checks and the checks that are kept. Fixes dotnet#37391
pcshrosbree
force-pushed
the
feat/ilverify-validation-mode
branch
from
October 21, 2026 07:26
004e000 to
49854e3
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#136133 has merged; this draft now contains one commit rebased onto
main.What changes
With
ValidationOnlyset, checks generally stay for correctness rules and are not reported for rules that only enforce verifiability (ECMA-335 III.1.8). A few verifiability checks remain (review point 7). ILVerify also approximates one correctness rule by keeping a stricter verification check (review point 5). These retained checks can report valid IL. The option is off by default. With it off, ILVerify keeps its existing verification checks.The classification rule. Each Partition III instruction has a Correctness paragraph and a Verifiability paragraph. I classified each reporting site, not each error code, because codes such as
StackUnexpectedare used for both kinds of rule.A check that is not reported does not abort the basic block or the method, and the importer keeps a stack model after it. For example,
ldind.refthrough an unmanaged pointer pushes an object reference of unknown class. Ifldelem.refthen uses that reference as its array operand, the importer pushes a null reference, as it does for a null array. The fix that keepsldelem.reffrom crashing on such an operand is #136159, which has merged, so this PR no longer contains it.Type checks. Most checks that compare a value on the stack with a declared type compare only the stack types of I.12.3.2.1 (review point 1), with the exceptions below. The following checks still use declared types, not only stack types:
Invokesignature (II.14.6);System.Objectand native int (II.14.6).A few checks of values also look past the stack type. The array instructions need a single-dimensional, zero-based array. A readonly
&cannot be used where a writable&is expected, for example as the destination ofinitobj(III.1.8.1.2.2). Neither mode enforces this rule for the destination ofcpobj; that existing gap remains. WithSanityChecksset, thrown and caught types must beSystem.Objector derive fromSystem.Exception.Public surface.
VerifierOptions.ValidationOnly(bool, default false), besideSanityChecks.ilverify --validation-only, also described in the README's usage section. In this mode the summary line and the--statisticslines say "validated" instead of "verified".Tests. Every existing method test now also runs in validation-only mode. By default, it expects the same errors as in verification mode. A name suffix beginning with
_Validation_overrides the expected errors for validation-only mode (the format is described in the README). Type tests and the other tests run once. 61 existing test methods carry the suffix. A malformed suffix fails the tests instead of silently dropping the method. The newValidationOnlyTests.ilhas 189 test methods: each reaches one check that is no longer reported, or one rule that is still reported.Evidence
After the rebase onto
main. With this PR rebased ontomaincommit b3c0988,ILVerification.Testspasses 1,448 of 1,448 in Release on linux-x64. TheParseExpectedErrorhelper from #136135 accepts numeric error codes. I therefore removed the two suffix-parser tests that expected those codes to be rejected, leaving 5 parser tests. The parser now accepts those two numeric codes, but a method test expecting either code would still fail: no check reports it. I have not re-run the scans since this rebase, which brought in #136159, #136033 and #136176.Earlier evidence (before the rebase onto
main). Built and run on linux-x64, Release and Debug.The scan figures below use the .NET 10.0.12 shared framework. They were measured before the rebase onto #136133's latest commit. That commit removed one
callicheck.I checked the effect of that earlier rebase onto #136133's latest commit separately on .NET 10.0.13. Results were identical before and after that rebase in both modes. With the option off, the version rebased onto #136133 also matched #136133's new head.
ILVerification.Tests: 1,407 of 1,407 pass in Release and in Debug (546 on ILVerify: continue checking methods that use calli, mkrefany, refanyval and refanytype instead of aborting them #136133's head). That is 665 method tests run in both modes, 66 type tests, 4 other tests, and 7 tests of the suffix parser.ilasmcannot reach: a zero-size body, two fall-through checks that need a filter without its finalendfilter, two handler entry-stack checks, and a by-ref catch type.System.Object; validation-only mode now checks that parameter directly (II.14.6).initobjthrough a readonly&, and readonly values at joins. All 7 failed at least one test.Shared framework, with the option on. 954 findings in 952 methods, against 84,235 findings in 35,248 methods in verification mode. Of the 954 findings, 691 are in System.Private.CoreLib. The 954 findings fall into four groups, all from existing ILVerify behavior. Validation-only mode can expose more of them, because it reaches blocks that verification skips (review point 6):
BadTypeSpec: ILVerify rejects the TypeSpec Roslyn emits forunmanagedconstraints (modreq(UnmanagedType) ValueType): #136142, which #136143 fixes. Verification mode reports it too.MethodAccesson constrained calls to protected static abstract interface members (INumberBase<T>.TryConvertTo*in the generic-mathCreate*methods).&returned where a writable&is expected (ImmutableArray<T>.ItemRef, frozen collections). ILVerify does not modelref readonlyreturns (#57444). Verification mode reports it too.AsyncHelpers.Await*methods.302 of the 954 are at offsets where verification mode reports nothing (review point 6).
Packages. 388,490 method bodies from hand-written IL packages, packages built with InlineIL.Fody, the 300 most-downloaded nuget.org packages and saved dynamic proxies. Verification mode reports 39,774 findings in 20,884 methods, and validation-only mode 2,099 in 2,033. The largest groups are the same
BadTypeSpec(1,473; #136142, fixed by #136143) and type or assembly load failures from my scan setup (388). The rest:&returns and 52MethodAccess, as above.Unsafe.As<T>(object). ILVerify gives every generic parameter the value-type stack kind.unboxof a struct-constrainedT(ValueTypeExpected).&and 9 native ints used as object references. These are stack-type mismatches in hand-written IL and Castle proxies, so they stay.TailByRefin one InlineIL method (review point 5).Points for review
Most checks of values on the stack use only stack types. The exceptions are under Type checks and in point 5. When it checks a value on the stack in validation-only mode, ILVerify uses the stack types of I.12.3.2.1 (int32, int64, native int, F, O, &, and value types). It also accepts a native int where a
&is expected (III.1.6). It does not check the class of an object reference, the type a pointer points to, an array's element type, or the exact type formkrefanyandconstrained.. In validation-only mode, the.refforms ofldind,stind,ldelemandstelemare treated as object-reference loads or stores regardless of the pointer's target type or the array's element type. I chose this reading because it follows [ILVerify] Add option for IL validation without IL verification #37391's words, "suppress all verification checks related to type safety". But the Correctness paragraphs ofcall,stind/stobj,mkrefanyandldflddo state class-level agreement, and I can switch to that literal reading. A scratch build using the literal reading adds:ldind.i/stind.ithrough aT*&, where ILVerify comparesT*withIntPtr, and 3 areconstrained.calls in System.Net.Quic.Unsafe.WriteUnaligned<T>(ref byte, T)in System.Runtime.CompilerServices.Unsafe, MonoMod.ILHelpers and DotNetCross.Memory.Unsafe.A
&passed where a native int orvoid*is expected. For arguments tocall,callvirt,newobjandcalli, a&is accepted where a native int is expected. This also applies to a delegate constructor's method pointer. This is the conversion thatconv.umakes explicitly. Table III.8 marks it valid but not verifiable (a shaded cell), because the GC no longer tracks a&once it is a native int. Validation-only mode also accepts the explicit conversions of a writable&or an object reference to int64 or native int. Roslyn passes a&directly to avoid*parameter foru8literals and static data:ldsflda <RVA field>; newobj ReadOnlySpan<byte>::.ctor(void*, int32). Accepting a&directly where a native int orvoid*argument is expected removes 333 framework findings and 1,353 package findings. A readonly&is still reported (III.1.8.1.2.2), and so is a&stored into a native int local, argument or field, or passed in a tail call (point 5).A native int and a writable
&that meet at a join merge to a native int. CoreLib'sOverlapped.set_OffsetLow,set_OffsetHighandset_EventHandleIntPtrdocond ? ref field : ref ptr->Field, andldfldathrough an unmanaged pointer pushes a native int (III.4.11). One limit: if ILVerify first imports the join block with a&on the stack, it reports an error if the block uses that value where only a native int is accepted. A later merge to native int does not remove the error, because ILVerify does not import a block again once it has an error.Merging a readonly
&with a native int would convert the readonly&to a native int, which III.1.8.1.2.2 does not allow. Both modes report an error at such a join. When two&values to different types meet, the result is readonly if either one is. ILVerify checks the join block again unless it already has an error; in that case, the method is already reported.One older defect remains in both modes: when a writable
T&reaches a join before a readonlyT&of the same type, the merged value is not readonly. A later write through that merged pointer is usually reported in verification mode for another reason; validation-only mode may miss it.A native int
thisforconstrained. callvirt. The augments' Description and Verifiability paragraphs say "the type of ptr must be a managed pointer (&) to thisType", but the Correctness paragraph does not. I accept a native int there, as for a value type'sthisincall(III.1.6, III.1.1.5.1). The threeNativeCallbackmethods in System.Net.Quic do this.Tail calls with managed pointers are still reported. III.2.4 Correctness says "no managed pointers can be passed to the method being called if they point into the stack frame that is about to be removed". ILVerify does not track which frame a pointer points into, so validation-only mode keeps the verification rule, which rejects any
&or byref-like argument. It checks the value passed as well as the declared parameter, so a&passed where a native int is declared is rejected too. The retained tail-call rule also rejects valid code: the 3 package findings come from one InlineIL method that passes two of its own¶meters and an unmanaged pointer to atail. calli. A precise rule would have to follow addresses of locals and arguments through every instruction that derives one address from another, and through merges; I can try it if you want.Validation reaches blocks that verification skips. ILVerify does not import a block that is reached only through a block with an error. Validation-only mode reports fewer errors, so it imports more blocks. In
Decimal.CreateChecked, for example, verification stops at anInitLocalserror before a later block that validation-only mode reaches.Other judgement calls. Checks are kept unless noted.
thisinitialization in constructors (III.1.8.1.4), access checks andCallAbstractforcall(III.3.19 lists them under Verifiability, but the runtime enforces them),CallVirtOnValueType,DelegateCtorfor a target whose signature does not match the delegate's,NewobjAbstractClass,CallCtor,LdftnCtor, theswitchoperand type,InitOnlyforstsfld(II.16.1.2), and value-type agreement forbox.BackwardBranch(the augments delete the rule),ConstrainedTypeNoInterfaceImpl(augmented III.2.1 Verifiability),EndfilterStack(only III.3.34 Verifiability states it), and merging&values that point to different types.Fixes #37391
Note
This PR was prepared with AI assistance (Anthropic Claude and OpenAI Codex), under my direction. I reviewed it before posting.