Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
87dc394
Implement base type verification and add related tests
pkuyo Jun 7, 2026
0657062
Add type specification validation and related tests
pkuyo Jun 9, 2026
a7ece37
Gate TypeSpec validation under ILVERIFICATION
pkuyo Jun 10, 2026
828661d
Refine base type validation and update ILVerify expectations
pkuyo Jun 30, 2026
74f03d2
Fix formatting in IL test files
pkuyo Jun 30, 2026
60febc6
Avoid base type resolution in interface checks
pkuyo Jun 30, 2026
6265b35
Validate TypeSpec parsing in EcmaSignatureParser
pkuyo Jul 1, 2026
e1ea8a3
Add ILVerify test for cyclic TypeSpec
pkuyo Jul 1, 2026
7ddbf4b
Simplify cyclic TypeSpec ILVerify test
pkuyo Jul 1, 2026
64d7e01
Reject TypeSpec custom modifiers
pkuyo Jul 2, 2026
22f2d56
Fix TypeSpec parsing with modifiers
pkuyo Jul 2, 2026
873f126
Updated Ecma-335-Augments.md
pkuyo Jul 3, 2026
fcba18c
Reformat TypeSpec custom modifier note
pkuyo Jul 3, 2026
5210c29
Reject invalid top-level type codes in TypeSpec signatures
pkuyo Jul 15, 2026
5fd0e41
Remove redundant TypeSpec base type check
pkuyo Jul 15, 2026
6a6a92c
Remove extra blank line
pkuyo Jul 15, 2026
a0416ad
Initialize error arguments for BadTypeSpec verification results
pkuyo Jul 16, 2026
21a8172
Update Ecma-335-Augments.md
pkuyo Jul 18, 2026
69a8cf8
Improve BadTypeSpec verification diagnostics
pkuyo Jul 18, 2026
bcf96c9
Add ILVerify enum base type regression test
pkuyo Jul 19, 2026
fb71e6b
Merge branch 'main' into fix-ilverify-119536
jkotas Aug 9, 2026
927ddb0
Clarify BadTypeSpec verification messages
pkuyo Aug 10, 2026
d1da679
Split TypeSpec verification error callbacks
pkuyo Aug 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 11 additions & 34 deletions docs/design/specs/Ecma-335-Augments.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,33 +146,21 @@ TypeSpecBlob ::=

3. PEVerify, the CLR runtime and C# compiler prior to VS 2015 report an error when encountering an encoded TypeSpec in the positions described above.

### 2. `(CMOD_OPT | CMOD_REQ) <TypeSpec>` is permitted in practice
### 2. `(CMOD_OPT | CMOD_REQ) <TypeSpec>` is invalid

In II.23.2.7, it is noted that CMOD_OPT or CMOD_REQD is followed
by a TypeRef or TypeDef metadata token, but TypeSpec tokens are
also allowed by ilasm, csc, peverify, and the CLR.
by a TypeRef or TypeDef metadata token. TypeSpec tokens in this
position should be treated as invalid metadata.

Note, in particular, that TypeSpecs are used there by C++/CLI to
represent strongly-typed boxing in C++/CLI. e.g. `Nullable<int>^`
in C++/CLI becomes
``[mscorlib]System.ValueType modopt([mscorlib]System.Nullable`1<int>) modopt([mscorlib]System.Runtime.CompilerServices.IsBoxed)``
in IL.

This tolerance adds a loophole to the rule above whereby cyclical
signatures are in fact possible, e.g.:

* `TypeSpec #1: PTR CMOD_OPT <TypeSpec #1> I4`

Such signatures can currently cause crashes in the runtime and various
tools, so if the spec is amended to permit TypeSpecs as modifiers,
then there should be a clarification that cycles are nonetheless not
permitted, and ideally readers would detect such cycles and handle the
error with a suitable message rather than a stack overflow.
Allowing TypeSpec custom modifier types creates a path for
Comment thread
jkotas marked this conversation as resolved.
recursive TypeSpec encodings that are complicated to detect.
Such metadata should be rejected rather than cycle-detected.

Related issues:

* https://github.com/dotnet/roslyn/issues/7971
* https://github.com/dotnet/runtime/issues/4945
* https://github.com/dotnet/runtime/pull/123819#discussion_r2810397721

#### Proposed specification change

Expand All @@ -185,9 +173,9 @@ for details
with

> The CMOD_OPT or CMOD_REQD is followed by a metadata token that indexes a row in the TypeDef
table, TypeRef table, or TypeSpec table. However, these tokens are encoded and compressed –
see §II.23.2.8 for details. Furthermore, if a row in the TypeSpec table is indicated,
it must not create cycle.
table or TypeRef table. However, these tokens are encoded and compressed –
Comment thread
jkotas marked this conversation as resolved.
see §II.23.2.8 for details. TypeSpec tokens are not valid in this position; this
restriction prevents custom modifiers from forming recursive TypeSpec encodings.

### 3. Custom modifiers can go in more places than specified

Expand Down Expand Up @@ -323,17 +311,6 @@ sense in `TypeSpec` are `(CLASS | VALUETYPE) TypeDefOrRef` since
`TypeDefOrRef` tokens can be used directly and the indirection through
a `TypeSpec` would serve no purpose.

In the same way as `constrained.`, (assuming #2 is a spec bug and not
an ilasm/peverify/CLR quirk), custom modifiers can beget `TypeSpec`s
beyond what is allowed by II.23.2.14, e.g. `modopt(int32)` creates a
typespec with signature I4.

Even more obscurely, this gives us a way to use `VOID`, `TYPEDBYREF`,
`CMOD_OPT`, and `CMOD_REQ` at the root of a `TypeSpec`, which are not even
specified as valid at the root of a `Type`: `modopt(int32 modopt(int32))`,
`modopt(void)`, and `modopt(typedref)` all work in
practice. `CMOD_OPT` and `CMOD_REQ` at the root can also be obtained by putting
a modifier on the type used with `constrained.`.

## Heap sizes

Expand Down Expand Up @@ -1212,4 +1189,4 @@ In section II.23.1.15, the following row is added to the table:

## Implict argument coercion rules

Implicit argument coercion as defined in section III.1.6 does not match with existing practice in CLR runtimes. Notably, implicit argument coercion of an `int32` on the IL evaluation stack to a `native unsigned int` is a sign extending operation, not a zero-extending operation.
Implicit argument coercion as defined in section III.1.6 does not match with existing practice in CLR runtimes. Notably, implicit argument coercion of an `int32` on the IL evaluation stack to a `native unsigned int` is a sign extending operation, not a zero-extending operation.
3 changes: 2 additions & 1 deletion src/coreclr/tools/Common/TypeSystem/Ecma/EcmaModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -572,9 +572,10 @@ private object ResolveTypeSpecification(TypeSpecificationHandle handle)
TypeSpecification typeSpecification = _metadataReader.GetTypeSpecification(handle);

BlobReader signatureReader = _metadataReader.GetBlobReader(typeSpecification.Signature);

EcmaSignatureParser parser = new EcmaSignatureParser(this, signatureReader, NotFoundBehavior.ReturnResolutionFailure);

TypeDesc parsedType = parser.ParseType();
TypeDesc parsedType = parser.ParseTypeSpec(handle);
if (parsedType == null)
return parser.ResolutionFailure;
else
Expand Down
43 changes: 39 additions & 4 deletions src/coreclr/tools/Common/TypeSystem/Ecma/EcmaSignatureParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

namespace Internal.TypeSystem.Ecma
{
public struct EcmaSignatureParser
public partial struct EcmaSignatureParser
{
private TypeSystemContext _tsc;
private Func<EntityHandle, NotFoundBehavior, TypeDesc> _typeResolver;
Expand Down Expand Up @@ -53,6 +53,10 @@ private void SetResolutionFailure(ResolutionFailure failure)

public ResolutionFailure ResolutionFailure => _resolutionFailure;

partial void ReportInvalidTypeSpecEncoding(EntityHandle typeSpecHandle);

partial void ReportTypeSpecUsedAsCustomModifier(EntityHandle typeSpecHandle);

private TypeDesc ResolveHandle(EntityHandle handle)
{
object resolvedValue;
Expand Down Expand Up @@ -273,20 +277,33 @@ private SignatureTypeCode ParseTypeCode(bool skipPinned = true)

private SignatureTypeCode ParseTypeCodeImpl(bool skipPinned = true)
{
for (; ; )
{
SignatureTypeCode typeCode = _reader.ReadSignatureTypeCode();
return ParseTypeCodeImpl(_reader.ReadSignatureTypeCode(), skipPinned);
}

private SignatureTypeCode ParseTypeCodeImpl(SignatureTypeCode typeCode, bool skipPinned = true)
{
for (; ; typeCode = _reader.ReadSignatureTypeCode())
{
if (typeCode == SignatureTypeCode.RequiredModifier)
{
EntityHandle typeHandle = _reader.ReadTypeHandle();
if (typeHandle.Kind == HandleKind.TypeSpecification)
{
ReportTypeSpecUsedAsCustomModifier(typeHandle);
}

_embeddedSignatureDataList?.Add(new EmbeddedSignatureData { index = string.Join(".", _indexStack), kind = EmbeddedSignatureDataKind.RequiredCustomModifier, type = ResolveHandle(typeHandle) });
continue;
}

if (typeCode == SignatureTypeCode.OptionalModifier)
{
EntityHandle typeHandle = _reader.ReadTypeHandle();
if (typeHandle.Kind == HandleKind.TypeSpecification)
{
ReportTypeSpecUsedAsCustomModifier(typeHandle);
}

_embeddedSignatureDataList?.Add(new EmbeddedSignatureData { index = string.Join(".", _indexStack), kind = EmbeddedSignatureDataKind.OptionalCustomModifier, type = ResolveHandle(typeHandle) });
continue;
}
Expand Down Expand Up @@ -320,6 +337,24 @@ private TypeDesc ParseTypeImpl()
return ParseType(ParseTypeCode());
}

public TypeDesc ParseTypeSpec(TypeSpecificationHandle typeSpecHandle)
{
SignatureTypeCode typeCode = _reader.ReadSignatureTypeCode();
switch (typeCode)
{
case SignatureTypeCode.Pointer:
case SignatureTypeCode.FunctionPointer:
case SignatureTypeCode.Array:
case SignatureTypeCode.SZArray:
case SignatureTypeCode.GenericTypeInstance:
case SignatureTypeCode.GenericTypeParameter:
case SignatureTypeCode.GenericMethodParameter:
return ParseType(ParseTypeCodeImpl(typeCode));
}
ReportInvalidTypeSpecEncoding(typeSpecHandle);
return ParseType(ParseTypeCodeImpl(typeCode));
}

public bool IsFieldSignature
{
get
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@

.method public hidebysig instance void Call.GenericMethodWithPrivateNestedClass_Invalid_MethodAccess() cil managed
{
call void class SimpleClass::GenericMethod<class SimpleClass/PrivateNestedClass>()
call void SimpleClass::GenericMethod<class SimpleClass/PrivateNestedClass>()
ret
}

Expand Down
78 changes: 78 additions & 0 deletions src/coreclr/tools/ILVerification.Tests/ILTests/BaseTypeTests.il
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

.assembly extern System.Runtime
{
}

.assembly BaseTypeTests
{
}

.class public auto ansi beforefieldinit ObjectTypeSpecBase_InvalidType_BadTypeSpec
extends object
Comment thread
jkotas marked this conversation as resolved.
{
.method public hidebysig specialname rtspecialname instance void .ctor() cil managed
{
.maxstack 8

ldarg.0
call instance void [System.Runtime]System.Object::.ctor()
ret
}
}

.class public auto ansi beforefieldinit ArrayTypeSpecBase_InvalidType_InvalidBaseType
extends int32[]
{
}

.class public auto ansi beforefieldinit PointerTypeSpecBase_InvalidType_InvalidBaseType
extends int32*
{
}

.class interface public auto ansi abstract NilBaseInterface_ValidType_Valid
{
}

.class public auto ansi beforefieldinit GenericBase`1<T>
extends [System.Runtime]System.Object
{
}

.class public auto ansi beforefieldinit GenericClassTypeSpecBase_ValidType_Valid
extends class GenericBase`1<int32>
{
}

.class public auto ansi beforefieldinit GenericOpenClassTypeSpecBase_ValidType_Valid`1<T>
extends class GenericBase`1<!0>
{
}

.class public sequential ansi sealed beforefieldinit ValueTypeBase
extends [System.Runtime]System.ValueType
{
}

.class public auto ansi beforefieldinit ValueTypeBase_InvalidType_InvalidBaseType
extends ValueTypeBase
{
}

.class public sequential ansi sealed beforefieldinit GenericValueTypeBase`1<T>
extends [System.Runtime]System.ValueType
{
}

.class public auto ansi sealed EnumBase_ValidType_Valid
extends [System.Runtime]System.Enum
Comment thread
pkuyo marked this conversation as resolved.
{
.field public specialname rtspecialname int32 value__
}

.class public auto ansi beforefieldinit GenericValueTypeSpecBase_InvalidType_InvalidBaseType
extends valuetype GenericValueTypeBase`1<int32>
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.IL">
<PropertyGroup>
<AssemblyName>$(MSBuildProjectName)</AssemblyName>
</PropertyGroup>

<ItemGroup>
<Compile Include="$(MSBuildProjectName).il" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -343,10 +343,11 @@
ret
}

.method public static void Casting.BoxByRefInt_Invalid_BoxByRef.ExpectedValClassObjRefVariable(int32& i) cil managed
.method public static void Casting.BoxPointer_Invalid_ExpectedValClassObjRefVariable() cil managed
{
ldarg.0
box int32&
ldc.i4.0
conv.u
box int32*
pop
ret
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@

ldloca.s V_0
ldstr "Hello"
stobj string
stobj [System.Runtime]System.String
ret
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

.assembly extern System.Runtime
{
}

.assembly MalformedTypeSpecTests
{
}

.class public auto ansi beforefieldinit GenericBase`1<T>
extends [System.Runtime]System.Object
{
}

.class public auto ansi beforefieldinit CustomModifierTypeSpecBase_InvalidType_BadTypeSpec
extends class GenericBase`1<int32 modopt(int32)>
{
}

.class public auto ansi beforefieldinit MethodSignatureTests
extends [System.Runtime]System.Object
{
.method public static hidebysig int32 modopt(int32) CustomModifierTypeSpecReturn_Invalid_BadTypeSpec() cil managed
{
ldc.i4.0
ret
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.IL">
<PropertyGroup>
<AssemblyName>$(MSBuildProjectName)</AssemblyName>
</PropertyGroup>

<ItemGroup>
<Compile Include="$(MSBuildProjectName).il" />
</ItemGroup>
</Project>
26 changes: 26 additions & 0 deletions src/coreclr/tools/ILVerification/EcmaSignatureParser.ILVerify.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using ILVerify;
using Internal.IL;

namespace Internal.TypeSystem.Ecma
{
public partial struct EcmaSignatureParser
{
partial void ReportInvalidTypeSpecEncoding(EntityHandle typeSpecHandle) =>
ThrowBadTypeSpec(typeSpecHandle, "The TypeSpec signature encoding is invalid.");

partial void ReportTypeSpecUsedAsCustomModifier(EntityHandle typeSpecHandle) =>
ThrowBadTypeSpec(typeSpecHandle, "A TypeSpec token is not valid as a custom modifier type.");

private void ThrowBadTypeSpec(EntityHandle typeSpecHandle, string message)
{
throw new VerifierException(
VerifierError.BadTypeSpec,
$"{message} ([{_ecmaModule}]0x{MetadataTokens.GetToken(typeSpecHandle):X8})");
}
}
}
11 changes: 10 additions & 1 deletion src/coreclr/tools/ILVerification/ILImporter.Verify.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,18 @@ public LocalVerificationException()

class VerifierException : Exception
{
internal VerifierException(string message) : base(message)
internal VerifierException(string message)
: this(VerifierError.None, message)
{
}

internal VerifierException(VerifierError code, string message)
: base(message)
{
Code = code;
}

internal VerifierError Code { get; }
}

partial class ILImporter
Expand Down
1 change: 1 addition & 0 deletions src/coreclr/tools/ILVerification/ILVerification.projitems
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<Project>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)AccessVerificationHelpers.cs" />
<Compile Include="$(MSBuildThisFileDirectory)EcmaSignatureParser.ILVerify.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ILImporter.StackValue.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ILImporter.Verify.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ILVerifyTypeSystemContext.cs" />
Expand Down
Loading
Loading