Skip to content

ILVerify: accept custom modifiers at the start of a TypeSpec - #136143

Open
pcshrosbree wants to merge 7 commits into
dotnet:mainfrom
pcshrosbree:fix/ilverify-typespec-custom-modifiers
Open

pcshrosbree wants to merge 7 commits into
dotnet:mainfrom
pcshrosbree:fix/ilverify-typespec-custom-modifiers

Conversation

@pcshrosbree

@pcshrosbree pcshrosbree commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR: Since #129118, ILVerify rejects the way C# encodes unmanaged constraints. This PR accepts those constraints. In the 10.0.12 shared framework, all 658 BadTypeSpec errors disappear and verification continues past them. The change is 11 lines in the parser, plus tests.

The change

EcmaSignatureParser.ParseTypeSpec now:

  1. Notes whether the blob starts with a custom modifier, then reads the type code with ParseTypeCodeImpl, which skips any custom modifiers and PINNED.
  2. Accepts the seven forms it accepted before (PTR, FNPTR, ARRAY, SZARRAY, GENERICINST, VAR, MVAR), with or without modifiers.
  3. Accepts CLASS or VALUETYPE (SignatureTypeCode.TypeHandle) only if modifiers came first.

Everything else still calls ReportInvalidTypeSpecEncoding.

As in #129118, a TypeSpec is accepted or rejected in the same way wherever it is used. The TypeSpec OBJECT is still rejected, so extends object (#119536) is still reported. extends class Base modreq(UnmanagedType) is accepted, because the augments allow that TypeSpec, even though the runtime does not load it as a base type.

Why this keeps the intent of #129118

#129118 rejects a TypeSpec if its encoding is not allowed or a shorter encoding is available. A bare CLASS X TypeSpec is redundant, because X can be referenced directly with its TypeDef or TypeRef token. Augments section 5 gives this as the reason: "the indirection through a TypeSpec would serve no purpose". With a custom modifier, that is no longer true. modreq(UnmanagedType) class System.ValueType has no TypeDef or TypeRef form, so the TypeSpec is the only way to write it. Section 3 of the augments adds Type ::= CustomMod* Type to the Type grammar, and section 5 describes the TypeSpec grammar as a subset of Type.

These are still rejected:

  • CLASS or VALUETYPE without modifiers (call void class [System.Runtime]System.GC::Collect() makes ilasm emit the TypeSpec CLASS System.GC). No test covered this case before.
  • Modifiers followed by a primitive type, such as modopt(IsConst) int32, or by BYREF.
  • A TypeSpec used as a modifier type, as before: ParseTypeCodeImpl still calls ReportTypeSpecUsedAsCustomModifier.

ParseTypeCodeImpl skips PINNED wherever it occurs. The seven forms listed above are accepted with leading PINNED. For CLASS or VALUETYPE, the blob must still start with a custom modifier; PINNED may appear between the modifiers and the type code.

crossgen2 and NativeAOT

The parser is shared with ILCompiler.TypeSystem, but only ILVerify implements the partial methods ReportInvalidTypeSpecEncoding and ReportTypeSpecUsedAsCustomModifier (in ILVerification/EcmaSignatureParser.ILVerify.cs). ILCompiler.TypeSystem compiles EcmaSignatureParser.cs without that file, so the calls are removed there. For any blob, ParseTypeSpec reads the same bytes in the same order and returns the same type as on main. The call to ParseTypeCodeImpl moves from after the switch to before it, with the same arguments. ILCompiler.TypeSystem.Tests pass at the current head (356/356, Release).

Tests

  • ILTests/TypeSpecModifierTests.il (new): CallUnmanagedConstraintMethod_Valid calls a method constrained where T : unmanaged, with the constraint encoded the way Roslyn does it (1F 0D 12 09). CallThroughClassTypeSpec_Invalid_BadTypeSpec covers CLASS without modifiers.
  • ILMetadataReferenceTester.AcceptsTypeSpecsWithCustomModifiersOnAClass (new) runs VerifyMetadataReferences on that assembly. It checks that the unmanaged TypeSpec is not reported, and that the CLASS System.GC TypeSpec is, with BadTypeSpec.

On main, method verification does not report BadTypeSpec for a generic method that only declares the constraint. CallUnmanagedConstraintMethod_Valid therefore calls a method with that constraint. On main, metadata-reference verification reports the constraint's TypeSpec as invalid. The new metadata-reference test checks that this PR removes that report.

ILVerification.Tests: 522/522 in Release and in Debug (main: 519 in Release).

Each guard was checked by mutation (one change per mutant, Release, full suite):

Mutant Tests that fail
Modified CLASS/VALUETYPE not accepted CallUnmanagedConstraintMethod_Valid and the new metadata-reference test
CLASS/VALUETYPE accepted without modifiers CallThroughClassTypeSpec_Invalid_BadTypeSpec and the new metadata-reference test

No other test failed under either mutant.

Framework before and after

ILVerify CLI from this branch and from main (8ab789d9e8a0), over all 172 assemblies of Microsoft.NETCore.App 10.0.12, CLI defaults plus --tokens:

main this PR
BadTypeSpec 658 (11 from metadata-reference verification, 647 from method verification) 0
All errors 84,245 84,546

The totals are the CLI's error counts. They include 31 errors without an error code, such as ImportCalli not implemented, which are the same in both runs. At the current head, the results are the same, assembly by assembly, as at this PR's first commit.

Verification of the 647 methods that previously stopped at BadTypeSpec now continues past the TypeSpec. Of those methods, 378 now have no findings. The remaining methods account for all 959 new findings:

Finding Count
InitLocals 862
UnmanagedPointer 36
ExpectedNumericType 26
StackUnexpected 11
Unverifiable 11
StackByRef 8
ReturnPtrToStack 5

No finding changed in any other method.

Points for review

  • Primitives after modifiers. I kept modopt(X) int32 rejected, because a bare int32 TypeSpec is rejected and I know of no compiler that emits the modified form in a TypeSpec. Accepting any Type after the modifiers would also follow from the augments; I chose the narrower rule.
  • OBJECT and STRING after modifiers are rejected for the same reason. This PR accepts a modified object when it uses CLASS System.Object.
  • Interface kind. Neither main nor this PR checks the rule in II.22.23 that a type listed as an implemented interface is an interface. I can fix that in a separate PR.
  • No change to Ecma-335-Augments.md. Sections 3 and 5 together already allow the modified form.

Fixes #136142

Note

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

C# encodes the unmanaged constraint as a TypeSpec that starts with a
custom modifier: modreq(UnmanagedType) class System.ValueType. Since
dotnet#129118, ParseTypeSpec accepts a TypeSpec only if it starts with PTR,
FNPTR, ARRAY, SZARRAY, GENERICINST, VAR or MVAR. ILVerify therefore
reports BadTypeSpec for every use of a generic type or method with an
unmanaged constraint: 658 errors in the 10.0.12 shared framework.

ParseTypeSpec now skips leading custom modifiers and checks the type
that follows. It accepts the seven forms above, and also CLASS or
VALUETYPE when modifiers are present, because a class or value type
with custom modifiers cannot be written as a TypeDef or TypeRef
token. Everything else is still rejected: a CLASS or VALUETYPE
TypeSpec without modifiers, and modifiers followed by a primitive
type, BYREF or PINNED.

The parser reads the same bytes and returns the same type as before.
Only the ILVerify implementation of ReportInvalidTypeSpecEncoding
sees the difference, so crossgen2 and NativeAOT are not affected.

Fixes dotnet#136142
@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
@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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The narrow parser change preserves validation guards and includes targeted regression coverage, with no unresolved findings.

No open issues 🎉

What changed in this PR

Fixes ILVerify’s rejection of C# unmanaged constraints by accepting leading custom modifiers in TypeSpecs while preserving existing validation guards.

Changes:

  • Accepts modified class/value-type TypeSpecs.
  • Adds positive and negative method-verification tests.
  • Checks metadata-reference diagnostics for invalid encodings.

Builds and tests were not independently run.

File Description
src/​coreclr/​tools/​ILVerification.Tests/​ILTests/​TypeSpecModifierTests.ilproj Registers the new IL test assembly.
src/​coreclr/​tools/​ILVerification.Tests/​ILTests/​TypeSpecModifierTests.il Covers accepted constraints and rejected TypeSpecs.
src/​coreclr/​tools/​ILVerification.Tests/​ILMetadataReferenceTester.cs Checks TypeSpec metadata diagnostics.
src/​coreclr/​tools/​Common/​TypeSystem/​Ecma/​EcmaSignatureParser.cs Handles leading modifiers before validating TypeSpecs.

Review effort: Balanced

Comment thread src/coreclr/tools/Common/TypeSystem/Ecma/EcmaSignatureParser.cs Outdated
Say which augment allows a TypeSpec to start with custom modifiers:
custom modifiers may precede any signature Type, including one
encoded in a TypeSpec.
Comment thread src/coreclr/tools/ILVerification.Tests/ILTests/TypeSpecModifierTests.il Outdated
The custom modifier tests also checked that a primitive type, BYREF or
PINNED after the modifiers is reported as BadTypeSpec. ILVerify
reported all three before the parser change, so the change does not
alter their outcome. The primitive case was also wrong: the augments
allow primitive TypeSpecs ("TypeSpecs can encode more than
specified"). Keep the 'unmanaged' constraint case and the class
TypeSpec without modifiers.
Comment thread src/coreclr/tools/Common/TypeSystem/Ecma/EcmaSignatureParser.cs
Skip the leading custom modifiers once, with the default skipPinned,
and parse the type from the code that call returns, instead of
skipping a second time before parsing. Only ILVerify implements
ReportInvalidTypeSpecEncoding, so for crossgen2 and NativeAOT the
parser still reads the same bytes and returns the same type as on
main.

Compared with the previous commit, PINNED is now skipped together with
the leading custom modifiers, and the type code that follows decides
whether the TypeSpec is accepted. For example, PINNED PTR I4 and
CMOD_REQD T PINNED CLASS C are no longer reported as BadTypeSpec.
Comment thread src/coreclr/tools/Common/TypeSystem/Ecma/EcmaSignatureParser.cs
For a TypeSpec used as a base type or an implemented interface, the
runtime skips custom modifiers and then requires a generic class
instantiation, GENERICINST CLASS (ClassLoader::LoadApproxTypeThrowing).
A modified class there, such as
'extends class Base modreq(UnmanagedType)', fails to load with a
TypeLoadException, as do a leading PINNED and a modified generic value
type. ParseTypeSpec accepts all three. Accepting leading custom
modifiers supports C#'s 'unmanaged' constraint, where the runtime loads
the modified class.

TypeVerifier now checks base type and interface TypeSpecs that start
with custom modifiers or PINNED. It reports BadTypeSpec unless the type
after any custom modifiers is a generic class instantiation. This
restores the errors ILVerify reported before ParseTypeSpec began
accepting these forms. A modified generic class instantiation is still
accepted in both positions, because the runtime loads it.
Comment thread src/coreclr/tools/ILVerification/TypeVerifier.cs Outdated
This reverts commit 3e9716f. That check reported a base type or
interface TypeSpec that starts with custom modifiers or PINNED unless
it is a generic class instantiation. It followed what the runtime
loads instead of ECMA-335 and its augments. Under those rules, a
TypeSpec's validity does not depend on where it is used.

ParseTypeSpec still rejects the TypeSpec OBJECT, so 'extends object'
(dotnet#119536) is still reported.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The focused parser change has targeted acceptance and rejection coverage, with no unresolved findings.

No open issues 🎉

Review effort: Balanced

@jkotas
jkotas enabled auto-merge (squash) October 21, 2026 18:29
Comment thread src/coreclr/tools/ILVerification.Tests/ILMetadataReferenceTester.cs Outdated
@jkotas
jkotas self-requested a review October 21, 2026 19:17
auto-merge was automatically disabled October 21, 2026 19:34

Head branch was pushed to by a user without write access

Move AcceptsTypeSpecsWithCustomModifiersOnAClass and its two helpers
below AssertResult. VerifyMetadataReferences and AssertResult again
follow ReportsUnusedMetadataReferences and
ReportsInvalidMetadataReferenceKinds, the tests that use them, as on
main. The code itself is unchanged.
@pcshrosbree
pcshrosbree force-pushed the fix/ilverify-typespec-custom-modifiers branch from 2d499b0 to e8ff536 Compare October 21, 2026 20:51
@pcshrosbree

Copy link
Copy Markdown
Contributor Author

@jkotas, the test move you asked for is in e8ff536, the only commit since your approval. That push turned off auto-merge, and the PR needs your approval again.

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-TypeSystem-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.

ILVerify reports BadTypeSpec for every unmanaged-constrained generic since #129118

4 participants