Skip to content

Commit c710e8a

Browse files
authored
Add IL Verification to tests (#2960)
Adds an ILVerifier to check that the IL produced by the linker is valid. Unsafe C# produced unverifiable code, so we skip verification when we pass that flag to the compiler. Also, there are a few warnings that are produced by valid C# with new features like static abstract interface methods and ref fields and ref returns. In the future, it may be nice to add better error messages with the type, method name, and IL offset that produced the error, and perhaps an [ExpectedILVerifyError] attribute instead of filtering all of a type of error, but those are non-trivial to implement and don't occur in many tests (<10), so I haven't done that yet.
1 parent 33c3b2c commit c710e8a

File tree

4 files changed

+194
-4
lines changed

4 files changed

+194
-4
lines changed

eng/Versions.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<MicrosoftNetCompilersToolsetVersion>$(MicrosoftCodeAnalysisVersion)</MicrosoftNetCompilersToolsetVersion>
2626
<MicrosoftCodeAnalysisCSharpAnalyzerTestingXunitVersion>1.0.1-beta1.*</MicrosoftCodeAnalysisCSharpAnalyzerTestingXunitVersion>
2727
<MicrosoftCodeAnalysisBannedApiAnalyzersVersion>3.3.2</MicrosoftCodeAnalysisBannedApiAnalyzersVersion>
28+
<MicrosoftILVerificationVersion>7.0.0-preview.7.22375.6</MicrosoftILVerificationVersion>
2829
<!-- This controls the version of the cecil package, or the version of cecil in the project graph
2930
when we build the cecil submodule. The reference assembly package will depend on this version of cecil.
3031
Keep this in sync with ProjectInfo.cs in the submodule. -->

test/Mono.Linker.Tests/Mono.Linker.Tests.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@@ -32,6 +32,7 @@
3232

3333
<ItemGroup>
3434
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="$(MicrosoftCodeAnalysisVersion)" />
35+
<PackageReference Include="Microsoft.ILVerification" Version="$(MicrosoftILVerificationVersion)" />
3536
<PackageReference Include="nunit" Version="3.12.0" />
3637
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
3738
<!-- This reference is purely so that the linker can resolve this
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System.Collections.Generic;
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Reflection;
9+
using System.Reflection.PortableExecutable;
10+
using System.Runtime.Loader;
11+
using ILVerify;
12+
using Mono.Linker.Tests.Extensions;
13+
14+
#nullable enable
15+
namespace Mono.Linker.Tests.TestCasesRunner
16+
{
17+
class ILVerifier : ILVerify.IResolver
18+
{
19+
Verifier _verifier;
20+
NPath _assemblyFolder;
21+
NPath _frameworkFolder;
22+
Dictionary<string, PEReader> _assemblyCache;
23+
AssemblyLoadContext _alc;
24+
25+
public IEnumerable<VerificationResult> Results { get; private set; }
26+
27+
public ILVerifier (NPath assemblyPath)
28+
{
29+
var assemblyName = assemblyPath.FileNameWithoutExtension;
30+
_assemblyFolder = assemblyPath.Parent;
31+
_assemblyCache = new Dictionary<string, PEReader> ();
32+
_frameworkFolder = typeof (object).Assembly.Location.ToNPath ().Parent;
33+
_alc = new AssemblyLoadContext (_assemblyFolder.FileName);
34+
LoadAssembly ("mscorlib");
35+
LoadAssembly ("System.Private.CoreLib");
36+
LoadAssemblyFromPath (assemblyName, assemblyPath);
37+
38+
_verifier = new ILVerify.Verifier (
39+
this,
40+
new ILVerify.VerifierOptions {
41+
SanityChecks = true,
42+
IncludeMetadataTokensInErrorMessages = true
43+
});
44+
_verifier.SetSystemModuleName (new AssemblyName ("mscorlib"));
45+
46+
var allResults = _verifier.Verify (Resolve (assemblyName))
47+
?? Enumerable.Empty<VerificationResult> ();
48+
49+
Results = allResults.Where (r => r.Code switch {
50+
ILVerify.VerifierError.None
51+
// Static interface methods cause this warning
52+
or ILVerify.VerifierError.CallAbstract
53+
// "Missing callVirt after constrained prefix - static interface methods cause this warning
54+
or ILVerify.VerifierError.Constrained
55+
// ex. localloc cannot be statically verified by ILVerify
56+
or ILVerify.VerifierError.Unverifiable
57+
// ref returning a ref local causes this warning but is okay
58+
or VerifierError.ReturnPtrToStack
59+
// Span indexing with indexer (ex. span[^4]) causes this warning
60+
or VerifierError.InitOnly
61+
=> false,
62+
_ => true
63+
});
64+
}
65+
66+
PEReader LoadAssembly (string assemblyName)
67+
{
68+
if (_assemblyCache.TryGetValue (assemblyName, out PEReader? reader))
69+
return reader;
70+
var assembly = _alc.LoadFromAssemblyName (new AssemblyName (assemblyName));
71+
reader = new PEReader (File.OpenRead (assembly.Location));
72+
_assemblyCache.Add (assemblyName, reader);
73+
return reader;
74+
}
75+
76+
PEReader LoadAssemblyFromPath (string assemblyName, NPath pathToAssembly)
77+
{
78+
if (_assemblyCache.TryGetValue (assemblyName, out PEReader? reader))
79+
return reader;
80+
var assembly = _alc.LoadFromAssemblyPath (pathToAssembly);
81+
reader = new PEReader (File.OpenRead (assembly.Location));
82+
_assemblyCache.Add (assemblyName, reader);
83+
return reader;
84+
}
85+
86+
bool TryLoadAssemblyFromFolder (string assemblyName, NPath folder, [NotNullWhen (true)] out PEReader? peReader)
87+
{
88+
Assembly? assembly = null;
89+
string assemblyPath = Path.Join (folder.ToString (), assemblyName);
90+
if (File.Exists (assemblyPath + ".dll"))
91+
assembly = _alc.LoadFromAssemblyPath (assemblyPath + ".dll");
92+
else if (File.Exists (assemblyPath + ".exe"))
93+
assembly = _alc.LoadFromAssemblyPath (assemblyPath + ".exe");
94+
95+
if (assembly is not null) {
96+
peReader = new PEReader (File.OpenRead (assembly.Location));
97+
_assemblyCache.Add (assemblyName, peReader);
98+
return true;
99+
}
100+
peReader = null;
101+
return false;
102+
}
103+
104+
PEReader? Resolve (string assemblyName)
105+
{
106+
PEReader? reader;
107+
if (_assemblyCache.TryGetValue (assemblyName, out reader)) {
108+
return reader;
109+
}
110+
111+
if (TryLoadAssemblyFromFolder (assemblyName, _frameworkFolder, out reader))
112+
return reader;
113+
114+
if (TryLoadAssemblyFromFolder (assemblyName, _assemblyFolder, out reader))
115+
return reader;
116+
117+
return null;
118+
}
119+
120+
PEReader? ILVerify.IResolver.ResolveAssembly (AssemblyName assemblyName)
121+
=> Resolve (assemblyName.Name ?? assemblyName.FullName);
122+
123+
PEReader? ILVerify.IResolver.ResolveModule (AssemblyName referencingModule, string fileName)
124+
=> Resolve (Path.GetFileNameWithoutExtension (fileName));
125+
126+
public string GetErrorMessage (VerificationResult result)
127+
{
128+
return $"IL Verification error:\n{result.Message}";
129+
}
130+
}
131+
}
132+
#nullable restore

test/Mono.Linker.Tests/TestCasesRunner/ResultChecker.cs

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Diagnostics;
7+
using System.Diagnostics.CodeAnalysis;
78
using System.IO;
89
using System.Linq;
910
using System.Text.RegularExpressions;
@@ -13,6 +14,7 @@
1314
using Mono.Linker.Tests.Cases.Expectations.Metadata;
1415
using Mono.Linker.Tests.Extensions;
1516
using NUnit.Framework;
17+
using WellKnownType = ILLink.Shared.TypeSystemProxy.WellKnownType;
1618

1719
namespace Mono.Linker.Tests.TestCasesRunner
1820
{
@@ -47,6 +49,32 @@ public ResultChecker (BaseAssemblyResolver originalsResolver, BaseAssemblyResolv
4749
_linkedReaderParameters = linkedReaderParameters;
4850
}
4951

52+
static void VerifyIL (NPath pathToAssembly)
53+
{
54+
var verifier = new ILVerifier (pathToAssembly);
55+
foreach (var result in verifier.Results) {
56+
if (result.Code == ILVerify.VerifierError.None)
57+
continue;
58+
Assert.Fail (verifier.GetErrorMessage (result));
59+
}
60+
}
61+
62+
static bool ShouldValidateIL (AssemblyDefinition inputAssembly)
63+
{
64+
if (HasAttribute (inputAssembly, nameof (SkipPeVerifyAttribute)))
65+
return false;
66+
67+
var caaIsUnsafeFlag = (CustomAttributeArgument caa) =>
68+
caa.Type.IsTypeOf (WellKnownType.System_String)
69+
&& (string) caa.Value == "/unsafe";
70+
var customAttributeHasUnsafeFlag = (CustomAttribute ca) => ca.ConstructorArguments.Any (caaIsUnsafeFlag);
71+
if (GetCustomAttributes (inputAssembly, nameof (SetupCompileArgumentAttribute))
72+
.Any (customAttributeHasUnsafeFlag))
73+
return false;
74+
75+
return true;
76+
}
77+
5078
public virtual void Check (LinkedTestCaseResult linkResult)
5179
{
5280
InitializeResolvers (linkResult);
@@ -57,6 +85,9 @@ public virtual void Check (LinkedTestCaseResult linkResult)
5785
Assert.IsTrue (linkResult.OutputAssemblyPath.FileExists (), $"The linked output assembly was not found. Expected at {linkResult.OutputAssemblyPath}");
5886
var linked = ResolveLinkedAssembly (linkResult.OutputAssemblyPath.FileNameWithoutExtension);
5987

88+
if (ShouldValidateIL (original))
89+
VerifyIL (linkResult.OutputAssemblyPath);
90+
6091
InitialChecking (linkResult, original, linked);
6192

6293
PerformOutputAssemblyChecks (original, linkResult.OutputAssemblyPath.Parent);
@@ -1069,15 +1100,40 @@ bool IsTypeInOtherAssemblyAssertion (CustomAttribute attr)
10691100
}
10701101

10711102
static bool HasAttribute (ICustomAttributeProvider caProvider, string attributeName)
1103+
{
1104+
return TryGetCustomAttribute (caProvider, attributeName, out var _);
1105+
}
1106+
1107+
#nullable enable
1108+
static bool TryGetCustomAttribute (ICustomAttributeProvider caProvider, string attributeName, [NotNullWhen (true)] out CustomAttribute? customAttribute)
1109+
{
1110+
if (caProvider is AssemblyDefinition assembly && assembly.EntryPoint != null) {
1111+
customAttribute = assembly.EntryPoint.DeclaringType.CustomAttributes
1112+
.FirstOrDefault (attr => attr!.AttributeType.Name == attributeName, null);
1113+
return customAttribute is not null;
1114+
}
1115+
1116+
if (caProvider is TypeDefinition type) {
1117+
customAttribute = type.CustomAttributes
1118+
.FirstOrDefault (attr => attr!.AttributeType.Name == attributeName, null);
1119+
return customAttribute is not null;
1120+
}
1121+
customAttribute = null;
1122+
return false;
1123+
}
1124+
1125+
static IEnumerable<CustomAttribute> GetCustomAttributes (ICustomAttributeProvider caProvider, string attributeName )
10721126
{
10731127
if (caProvider is AssemblyDefinition assembly && assembly.EntryPoint != null)
10741128
return assembly.EntryPoint.DeclaringType.CustomAttributes
1075-
.Any (attr => attr.AttributeType.Name == attributeName);
1129+
.Where (attr => attr!.AttributeType.Name == attributeName);
10761130

10771131
if (caProvider is TypeDefinition type)
1078-
return type.CustomAttributes.Any (attr => attr.AttributeType.Name == attributeName);
1132+
return type.CustomAttributes
1133+
.Where (attr => attr!.AttributeType.Name == attributeName);
10791134

1080-
return false;
1135+
return Enumerable.Empty<CustomAttribute> ();
10811136
}
1137+
#nullable restore
10821138
}
10831139
}

0 commit comments

Comments
 (0)