Skip to content

ILVerify: continue past verifiability-only errors - #136178

Draft
pcshrosbree wants to merge 2 commits into
dotnet:mainfrom
pcshrosbree:feat/ilverify-propagate-past-unverifiable
Draft

pcshrosbree wants to merge 2 commits into
dotnet:mainfrom
pcshrosbree:feat/ilverify-propagate-past-unverifiable

Conversation

@pcshrosbree

@pcshrosbree pcshrosbree commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR: This PR lets ILVerify keep checking the blocks after an error that only makes the IL unverifiable, such as localloc, and keeps stopping after every other error. In my scans the same methods pass and fail as before, but more errors are reported overall. Should this be the default? Should InitLocals, which makes up most of the new errors, be reported only once per method, as a separate change?

Fixes #136177

This is a draft that builds on #136169, which is now rebased onto main after #136133 merged. Only the last commit is new. It reuses #136169's split of reports into rules for valid IL and rules that only verifiable IL must follow.

Change

  • BasicBlock gets an UnverifiableErrorCount. UnverifiableError increments it after reporting, in verification mode only, and CheckVerifiable now calls UnverifiableError.
  • PropagateControlFlow stops when ErrorCount > UnverifiableErrorCount, that is, when the block reported at least one error that is not verifiability-only. ErrorCount still counts every report, so a block that has reported is still never imported twice.
  • CheckIsObjRef(TypeDesc) reports through VerificationError again, so it still stops propagation. After a CheckIsObjRef failure in verification mode, ldind.ref and ldelem.ref push the pointee or element type (an int32, say) instead of an object reference. Passing that type on would produce a wrong error in the next block. Validation-only mode still suppresses the CheckIsObjRef error.

After every other verifiability-only report, the importer still pushes the instruction's usual result, which does not depend on what made it unverifiable: localloc pushes a native int, calli pushes its return type, ldelema pushes a & to its token type, and so on. So the stack passed on is the one the importer would build for valid IL. FatalCheck and AbortBasicBlockVerification end the method, as before (#37392).

Validation-only mode is unchanged: there UnverifiableError reports nothing and counts nothing.

Results

These figures were measured before the rebase onto main, which brought in #136159, #136033 and #136176. I have not re-run the scans.

Verification mode over the .NET 10.0.12 shared framework (172 assemblies, 130,146 method bodies, linux-x64), before and after this commit:

Before After
Methods with errors 35,248 35,248 (the same methods)
Errors 84,235 305,635
Errors other than InitLocals 5,989 8,585
System.Private.CoreLib errors 21,541 66,244
  • No method changes between passing and failing. Validation-only mode gives the same results before and after, method for method.
  • InitLocals accounts for a net increase of 218,804 (219,078 new, 274 no longer reported). It is reported at every read of a local in a method without localsinit. Continuing past verifiability-only errors reaches more of those reads.
  • 236 newly reached errors are diagnostics that classify the IL as invalid: BadTypeSpec 149 (ILVerify reports BadTypeSpec for every unmanaged-constrained generic since #129118 #136142), MethodAccess 57 and StackUnexpected 30. Validation-only mode already reports these errors in the same methods, with the same error codes and offsets where present. The BadTypeSpec errors have no offset. The 57 MethodAccess errors, all in CreateChecked, CreateSaturating and CreateTruncating of the numeric types, are false positives. ILVerify's family access check does not consider implemented interfaces, while the ECMA-335 augments and the runtime let Int32 call INumberBase<TOther>'s protected members. That is a separate fix, ILVerify: allow family access through implemented interfaces #136176, which has since merged.
  • 425 errors in 136 methods are no longer reported. In 126 of these methods, verification ends when a newly reached block aborts ([ILVerify] Fix AbortBasicBlockVerification aborting the entire method #37392). In the other 10, verification ends on a BadTypeSpec error. The new paths reach that error for the first time in 6 methods and earlier than before in 4. As a result, some blocks checked before this change are now skipped. All 136 methods still fail.

Four package sets (IL SDK packages, the top 10 InlineIL users, the nuget.org top 300, saved dynamic proxies; 388,490 method bodies) give the same picture: no method changes between passing and failing, errors go from 39,774 to 100,188, and 114 errors in 25 methods are lost the same way.

Alternative not taken

I also considered treating a CheckIsAssignable failure as verifiability-only when validation-only mode accepts the IL. This would be a net increase of 1,371 framework errors (1,455 new, 84 lost) across 115 methods. None of these additional errors is also reported by validation-only mode. It would also need a special case for ldflda through a native int, where verification mode pushes a & and validation-only mode a native int. I left it out.

Tests

Nine new methods in BranchingTests.il, each run in both modes:

  • Propagate.After*: in each test, a block with one verifiability-only error branches to a block whose ret requires a return value but finds an empty stack. The ReturnMissing there is now reported. The five cases are localloc, calli through a method pointer argument, InitLocals, ldelema of a different element type, and ldind.i4 through an int64&. Together they reach every UnverifiableError overload and CheckVerifiable.
  • Propagate.StopAfterLoadIndirectRef and StopAfterLoadElementRef: the ldind.ref and ldelem.ref case still stops.
  • Propagate.StopAfterInvalid and StopAfterUnverifiableAndInvalid: propagation still stops after a correctness error, including when the same block also reports a verifiability-only error.

Without the change, the five After* tests fail in verification mode and the other new tests pass. Removing each part of the change in turn fails the test written for it. The one exception is the change to CheckVerifiable(bool, VerifierError, StackValue), which has no caller; it only keeps the overloads consistent.

After the rebase, ILVerification.Tests passes 1,461 of 1,461 in Release. The count is five lower because the five tests for #136169's test-name suffix parser were removed.

Questions

  1. This is the default, not an option, because the same methods pass and fail in my scans. Is that right?
  2. InitLocals at every read of a local makes the framework's error count about 3.6 times larger. Would you rather report it once per method, as a separate change?

Note

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

@github-actions github-actions Bot added the area-Tools-ILVerification Issues related to ilverify tool and IL verification in general label Oct 20, 2026
@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: @JulieLeeMSFT, @dotnet/jit-contrib
See info in area-owners.md if you want to be subscribed.

@pcshrosbree
pcshrosbree force-pushed the feat/ilverify-propagate-past-unverifiable branch from 8fa8e74 to e98857d Compare October 21, 2026 07:26
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
PropagateControlFlow returns early when the current block has reported
any error, and every report counts, including Unverifiable() for
localloc and calli and InitLocals for a skipped-init local. Code that
is reachable only through such a block was never imported, so
verification mode missed errors that validation-only mode reports.

A block now passes its stack on if it has reported only errors that
validation-only mode suppresses (UnverifiableError and CheckVerifiable).
These errors make the IL unverifiable but not invalid. The importer
builds the same stack after these reports as it does without them. Any
other error still stops propagation. A fatal check or an aborted block
ends the method, as before.

CheckIsObjRef(TypeDesc) still stops propagation in verification mode:
ldind.ref and ldelem.ref then push the pointee or element type, which
is not an object reference.

A block with errors is still not re-imported when its entry stack
changes, so no error is reported twice. Validation-only mode is
unchanged.
@pcshrosbree pcshrosbree changed the title ILVerify: keep checking code after a verifiability-only error ILVerify: continue past verifiability-only errors Oct 21, 2026
@pcshrosbree
pcshrosbree force-pushed the feat/ilverify-propagate-past-unverifiable branch from e98857d to 1d94656 Compare October 21, 2026 09:18

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-Tools-ILVerification Issues related to ilverify tool and IL verification in general 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.

ILVerify does not check code that follows an unverifiable instruction

1 participant