Skip to content

Validate invalid base type declarations in ILVerify#129118

Open
pkuyo wants to merge 9 commits into
dotnet:mainfrom
pkuyo:fix-ilverify-119536
Open

Validate invalid base type declarations in ILVerify#129118
pkuyo wants to merge 9 commits into
dotnet:mainfrom
pkuyo:fix-ilverify-119536

Conversation

@pkuyo

@pkuyo pkuyo commented Jun 8, 2026

Copy link
Copy Markdown

Fixes #119536

This adds ILVerify validation for invalid class base type declarations.

The original issue reports a case where ILAsm accepts a type declared with
extends object, but the resulting type cannot be loaded by the runtime.
ILVerify previously accepted that assembly without reporting an error.

This change reports InvalidBaseType for invalid base type declarations rather
than special-casing only the exact extends object spelling.

Tests added in BaseTypeTests.il:

  • ObjectTypeSpecBase_InvalidType_InvalidBaseType

    • Covers the original repro: a class declared with extends object.
    • Expected result: InvalidBaseType.
  • NilBaseInterface_ValidType_Valid

    • Ensures interfaces with no class base continue to verify successfully.
  • GenericClassTypeSpecBase_ValidType_Valid

    • Ensures valid generic class TypeSpec bases are still accepted.
  • ValueTypeBase_InvalidType_InvalidBaseType

    • Ensures a non-generic value type cannot be used as a class base.
  • GenericValueTypeSpecBase_InvalidType_InvalidBaseType

    • Ensures a generic value type instantiation cannot be used as a class base.

@github-actions github-actions Bot added the area-Tools-ILVerification Issues related to ilverify tool and IL verification in general label Jun 8, 2026
@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Jun 8, 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.

@pkuyo

pkuyo commented Jun 8, 2026

Copy link
Copy Markdown
Author

@dotnet-policy-service agree

Comment on lines +81 to +100
TypeSpecification typeSpecification = _module.MetadataReader.GetTypeSpecification(typeSpecificationHandle);
BlobReader signatureReader = _module.MetadataReader.GetBlobReader(typeSpecification.Signature);

if (signatureReader.ReadSignatureTypeCode() != SignatureTypeCode.GenericTypeInstance)
{
return false;
}

int genericTypeKind = signatureReader.ReadCompressedInteger();
if (genericTypeKind != (int)SignatureTypeKind.Class)
{
return false;
}

EntityHandle genericTypeHandle = signatureReader.ReadTypeHandle();
if (genericTypeHandle.Kind != HandleKind.TypeDefinition &&
genericTypeHandle.Kind != HandleKind.TypeReference)
{
return false;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this any better than just calling _module.GetType(typeHandle)?

@pkuyo pkuyo Jun 8, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I tried that locally again, using only _module.GetType(typeHandle) does not catch the #119536 repro. (ObjectTypeSpecBase_InvalidType_InvalidBaseType)

In the extends object case, _module.GetType(typeHandle) resolves the TypeSpec to System.Object, which loses the issue that the base type was encoded as an invalid TypeSpec in the extends clause.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If you would like to check for invalid type specs, it should be done in the central location where typespecs are decoded so that it applies to all of them. The problem is not specific to base type.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks, I tried moving this into EcmaModule.GetType, but I don't think this check can be global.

EcmaModule.GetType is context-independent. A top-level built-in TypeSpec like ELEMENT_TYPE_OBJECT is invalid in a TypeDef extends clause, but it is not invalid in every context. For example, CoreCLR accepts stobj with a TypeSpec object token, and ILVerification already has a similar existing test case using stobj string ( [LoadStoreIndirectTests] StoreObject.ValidTypeToken).

Rejecting these TypeSpecs in EcmaModule.GetType would therefore make ILVerify reject IL that is accepted outside of base type declarations. I think this validation should stay in the base type verification path, where we know the token is being used as a class base type.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

TypeSpec with object token is invalid in every context according to the spec. Look for TypeSpecBlob grammar in ECMA-335.

ILVerify is meant to check for speced behavior. It is not meant to check for what CoreCLR happens to accept. The runtime often accepts more than what's allowed by the spec.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for clarifying. I understand now.

I updated the change so malformed TypeSpecs are validated in EcmaModule.GetType, and added related test coverage.

After making that change, I found a few existing ILVerification tests that were also using invalid TypeSpecs, so I updated them as well:

  • In LoadStoreIndirectTests.il, changed stobj string to stobj [System.Runtime]System.String.
  • In CastingTests.il, replaced BoxByRefInt_Invalid_BoxByRef with BoxIntPointer_Invalid_UnmanagedPointer, so the test no longer depends on an invalid TypeSpec.
  • For .override method instance string class A.T1::Func1()-style cases, removed the unnecessary class modifier. When class is present, ILAsm emits a TypeSpec as the MemberRef.Parent for a non-generic class, instead of using a TypeDef or TypeRef. This is invalid according to the TypeSpecBlob grammar.

Please let me know if you would prefer any of these test updates to be split out or adjusted differently.

Comment thread src/coreclr/tools/Common/TypeSystem/Ecma/EcmaModule.cs Outdated
Comment thread src/coreclr/tools/ILVerification/TypeVerifier.cs Outdated
@pkuyo

pkuyo commented Jun 17, 2026

Copy link
Copy Markdown
Author

Hi @jkotas, just checking in on this PR. I’ve addressed the feedback and made the requested updates. Please let me know if there’s anything else I should adjust. Thanks!

#if ILVERIFICATION
private static void ValidateTypeSpecificationSignature(BlobReader signatureReader)
{
switch (signatureReader.ReadSignatureTypeCode())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This check may be too restrictive after looking into this in more detail.

We have ECMA-335 spec augment that suggests that the only TypeSpec should only disallow CLASS and VALUETYPE since the other forms may show up in different contexts: https://github.com/dotnet/runtime/blob/main/docs/design/specs/Ecma-335-Augments.md#5-typespecs-can-encode-more-than-specified

I think we should implement the behavior from ECMA spec augment here, and also link to it so that future readers understand where it came from.

{
ldarg.0
box int32&
box int32*

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These changes should be unnecessary with the other feedback

Comment on lines +87 to +88
if (resolvedBaseType.IsValueType ||
(baseType.Kind == HandleKind.TypeSpecification && resolvedBaseType is not InstantiatedType))

@jkotas jkotas Jun 29, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
if (resolvedBaseType.IsValueType ||
(baseType.Kind == HandleKind.TypeSpecification && resolvedBaseType is not InstantiatedType))
if (!resolvedBaseType.IsDefType || resolvedBaseType.IsInterface || resolvedBaseType.IsPrimitive)

I think this would be more precise.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks, I updated this to use a more precise check.

I used resolvedBaseType.IsValueType || resolvedBaseType.IsInterface || !resolvedBaseType.IsDefType because base types should reject value types in general, not just primitive types. IsValueType also covers the primitive case.

Comment on lines +42 to +47
// Once the base type metadata is invalid, later checks can force the type system to
// resolve the same bad base token again and produce a duplicate token resolution error.
if (!VerifyBaseType())
{
return;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
// Once the base type metadata is invalid, later checks can force the type system to
// resolve the same bad base token again and produce a duplicate token resolution error.
if (!VerifyBaseType())
{
return;
}
VerifyBaseType();

We may want to stop on errors thrown by the type system only (throw an exception that will be caught by the caller of Verify method) ,but keep going for non-fatal errors to produce as many of them as possible. It is how the existing VerifyInterfaces method seems to work.

@pkuyo

pkuyo commented Jun 30, 2026

Copy link
Copy Markdown
Author

Thanks for the review. I updated the PR to address the latest feedback, The TypeSpec validation now follows the ECMA-335 augment behavior.

Please let me know if there is anything else I should adjust.

@MichalStrehovsky

MichalStrehovsky commented Jul 1, 2026

Copy link
Copy Markdown
Member

(Edited to fix issue number)

I wonder if we should approach this from an angle that also allows to address #4945

  • Add ParseTypeSpec to EcmaSignatureParser.
  • Do the validation in EcmaSignatureParser (this allows us getting rid of the ILVERIFICATION ifdef, because this could be a ReportInvalidTypeSpec partial method on EcmaSignatureParser: this matches more closely how invalid things are reported (e.g. in ILImporter.cs).

@jkotas

jkotas commented Jul 1, 2026

Copy link
Copy Markdown
Member
  • Add ParseTypeSpec to EcmaSignatureParser.
  • Do the validation in EcmaSignatureParser (this allows us getting rid of the ILVERIFICATION ifdef, because this could be a ReportInvalidTypeSpec partial method on EcmaSignatureParser: this matches more closely how invalid things are reported (e.g. in ILImporter.cs).

Yes, I think it would look better. @pkuyo Could you please implement it?

}
else
{
if (baseType.Kind == HandleKind.TypeSpecification &&

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should not be needed. It is covered by the resolvedBaseType.IsValueType || resolvedBaseType.IsInterface || !resolvedBaseType.IsDefType check below.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks. I simplified this to a smaller TypeSpec-specific check.

The resolvedBaseType.IsValueType || resolvedBaseType.IsInterface || !resolvedBaseType.IsDefType check alone does not catch the #119536 case, because a TypeSpec that resolves to System.Object passes those resolved-type checks. The extra TypeSpec check is only for that context-specific extends validation.

{
resolvedBaseType = _module.GetType(baseType);
}
catch (BadImageFormatException)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it would be better to let the errors from the core type system to propagate. Catching the exception and reporting it as something else loses information.

@pkuyo

pkuyo commented Jul 1, 2026

Copy link
Copy Markdown
Author
  • Add ParseTypeSpec to EcmaSignatureParser.
  • Do the validation in EcmaSignatureParser (this allows us getting rid of the ILVERIFICATION ifdef, because this could be a ReportInvalidTypeSpec partial method on EcmaSignatureParser: this matches more closely how invalid things are reported (e.g. in ILImporter.cs).

Yes, I think it would look better. @pkuyo Could you please implement it?

Sure, thanks. I’ll implement it this way and update the PR.

@pkuyo

pkuyo commented Jul 1, 2026

Copy link
Copy Markdown
Author

(Edited to fix issue number)

I wonder if we should approach this from an angle that also allows to address #4945

  • Add ParseTypeSpec to EcmaSignatureParser.
  • Do the validation in EcmaSignatureParser (this allows us getting rid of the ILVERIFICATION ifdef, because this could be a ReportInvalidTypeSpec partial method on EcmaSignatureParser: this matches more closely how invalid things are reported (e.g. in ILImporter.cs).

Thanks, I reworked this to use the EcmaSignatureParser approach.

The TypeSpec validation is now done through ParseTypeSpec / ReportInvalidTypeSpec, and I addedCoverage-relatedto #4945.

For CMOD_OPT/CMOD_REQ, I kept the check simple for now: it reports a TypeSpec reference while parsing another TypeSpec, but does not try to distinguish whether it actually forms a cycle. Full cycle detection would need extra tracking for the active TypeSpec resolution stack, since the current TypeSpec is not in the cache yet.

I wasn't sure that extra state was necessary for this PR, so I wanted to check before adding it.

@MichalStrehovsky

Copy link
Copy Markdown
Member

For CMOD_OPT/CMOD_REQ, I kept the check simple for now: it reports a TypeSpec reference while parsing another TypeSpec, but does not try to distinguish whether it actually forms a cycle. Full cycle detection would need extra tracking for the active TypeSpec resolution stack, since the current TypeSpec is not in the cache yet.

We shouldn't allow TypeSpec as a modifier. It only causes problems. If the augment says we allow it, we should update the augment. We can capture the reason so that somebody doesn't try to relax it without thinking about the consequences (@jkotas do you agree?). I think this recently came up in #123819 and we decided not to handle TypeSpec either, but GitHub makes it really hard to find the relevant comment.

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 fails to warn about invalid class declaration

3 participants