Repository navigation
ILVerify: accept custom modifiers at the start of a TypeSpec - #136143
Open
pcshrosbree wants to merge 7 commits into
Open
pcshrosbree wants to merge 7 commits into
pcshrosbree wants to merge 7 commits into
Conversation
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
|
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. |
This was referenced Oct 20, 2026
Contributor
There was a problem hiding this comment.
🟢 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
jkotas
reviewed
Oct 20, 2026
Say which augment allows a TypeSpec to start with custom modifiers: custom modifiers may precede any signature Type, including one encoded in a TypeSpec.
jkotas
reviewed
Oct 20, 2026
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.
jkotas
reviewed
Oct 20, 2026
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.
jkotas
reviewed
Oct 21, 2026
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.
jkotas
reviewed
Oct 21, 2026
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.
jkotas
approved these changes
Oct 21, 2026
jkotas
enabled auto-merge (squash)
October 21, 2026 18:29
jkotas
reviewed
Oct 21, 2026
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
force-pushed
the
fix/ilverify-typespec-custom-modifiers
branch
from
October 21, 2026 20:51
2d499b0 to
e8ff536
Compare
Contributor
Author
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.
The change
EcmaSignatureParser.ParseTypeSpecnow:ParseTypeCodeImpl, which skips any custom modifiers andPINNED.PTR,FNPTR,ARRAY,SZARRAY,GENERICINST,VAR,MVAR), with or without modifiers.CLASSorVALUETYPE(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
OBJECTis still rejected, soextends 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 XTypeSpec is redundant, becauseXcan be referenced directly with its TypeDef or TypeRef token. Augments section 5 gives this as the reason: "the indirection through aTypeSpecwould serve no purpose". With a custom modifier, that is no longer true.modreq(UnmanagedType) class System.ValueTypehas no TypeDef or TypeRef form, so the TypeSpec is the only way to write it. Section 3 of the augments addsType ::= CustomMod* Typeto theTypegrammar, and section 5 describes the TypeSpec grammar as a subset ofType.These are still rejected:
CLASSorVALUETYPEwithout modifiers (call void class [System.Runtime]System.GC::Collect()makes ilasm emit the TypeSpecCLASS System.GC). No test covered this case before.modopt(IsConst) int32, or byBYREF.ParseTypeCodeImplstill callsReportTypeSpecUsedAsCustomModifier.ParseTypeCodeImplskipsPINNEDwherever it occurs. The seven forms listed above are accepted with leadingPINNED. ForCLASSorVALUETYPE, the blob must still start with a custom modifier;PINNEDmay 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
ReportInvalidTypeSpecEncodingandReportTypeSpecUsedAsCustomModifier(inILVerification/EcmaSignatureParser.ILVerify.cs). ILCompiler.TypeSystem compilesEcmaSignatureParser.cswithout that file, so the calls are removed there. For any blob,ParseTypeSpecreads the same bytes in the same order and returns the same type as on main. The call toParseTypeCodeImplmoves from after theswitchto before it, with the same arguments. ILCompiler.TypeSystem.Tests pass at the current head (356/356, Release).Tests
ILTests/TypeSpecModifierTests.il(new):CallUnmanagedConstraintMethod_Validcalls a method constrainedwhere T : unmanaged, with the constraint encoded the way Roslyn does it (1F 0D 12 09).CallThroughClassTypeSpec_Invalid_BadTypeSpeccoversCLASSwithout modifiers.ILMetadataReferenceTester.AcceptsTypeSpecsWithCustomModifiersOnAClass(new) runsVerifyMetadataReferenceson that assembly. It checks that theunmanagedTypeSpec is not reported, and that theCLASS System.GCTypeSpec is, withBadTypeSpec.On main, method verification does not report
BadTypeSpecfor a generic method that only declares the constraint.CallUnmanagedConstraintMethod_Validtherefore 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):
CLASS/VALUETYPEnot acceptedCallUnmanagedConstraintMethod_Validand the new metadata-reference testCLASS/VALUETYPEaccepted without modifiersCallThroughClassTypeSpec_Invalid_BadTypeSpecand the new metadata-reference testNo 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:BadTypeSpecThe 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
BadTypeSpecnow continues past the TypeSpec. Of those methods, 378 now have no findings. The remaining methods account for all 959 new findings:InitLocalsUnmanagedPointerExpectedNumericTypeStackUnexpectedUnverifiableStackByRefReturnPtrToStackNo finding changed in any other method.
Points for review
modopt(X) int32rejected, because a bareint32TypeSpec is rejected and I know of no compiler that emits the modified form in a TypeSpec. Accepting anyTypeafter the modifiers would also follow from the augments; I chose the narrower rule.OBJECTandSTRINGafter modifiers are rejected for the same reason. This PR accepts a modifiedobjectwhen it usesCLASS System.Object.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.