diff options
author | Jackson Schuster <36744439+jtschuster@users.noreply.github.com> | 2022-08-15 22:07:47 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-08-15 22:07:47 +0300 |
commit | c710e8af4224a3d0d2975ba5c2b09f0398ee2383 (patch) | |
tree | 70a4f2229c7e129fec32e259b4e84f64c45f1381 | |
parent | 33c3b2c60d0a2006162a6326db853fe5415439bd (diff) |
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.
-rw-r--r-- | eng/Versions.props | 1 | ||||
-rw-r--r-- | test/Mono.Linker.Tests/Mono.Linker.Tests.csproj | 3 | ||||
-rw-r--r-- | test/Mono.Linker.Tests/TestCasesRunner/ILVerifier.cs | 132 | ||||
-rw-r--r-- | test/Mono.Linker.Tests/TestCasesRunner/ResultChecker.cs | 62 |
4 files changed, 194 insertions, 4 deletions
diff --git a/eng/Versions.props b/eng/Versions.props index ea29c2eae..cef1af03c 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -25,6 +25,7 @@ <MicrosoftNetCompilersToolsetVersion>$(MicrosoftCodeAnalysisVersion)</MicrosoftNetCompilersToolsetVersion> <MicrosoftCodeAnalysisCSharpAnalyzerTestingXunitVersion>1.0.1-beta1.*</MicrosoftCodeAnalysisCSharpAnalyzerTestingXunitVersion> <MicrosoftCodeAnalysisBannedApiAnalyzersVersion>3.3.2</MicrosoftCodeAnalysisBannedApiAnalyzersVersion> + <MicrosoftILVerificationVersion>7.0.0-preview.7.22375.6</MicrosoftILVerificationVersion> <!-- This controls the version of the cecil package, or the version of cecil in the project graph when we build the cecil submodule. The reference assembly package will depend on this version of cecil. Keep this in sync with ProjectInfo.cs in the submodule. --> diff --git a/test/Mono.Linker.Tests/Mono.Linker.Tests.csproj b/test/Mono.Linker.Tests/Mono.Linker.Tests.csproj index 4a9ea7e7f..448601a8a 100644 --- a/test/Mono.Linker.Tests/Mono.Linker.Tests.csproj +++ b/test/Mono.Linker.Tests/Mono.Linker.Tests.csproj @@ -1,4 +1,4 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <AllowUnsafeBlocks>true</AllowUnsafeBlocks> @@ -32,6 +32,7 @@ <ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="$(MicrosoftCodeAnalysisVersion)" /> + <PackageReference Include="Microsoft.ILVerification" Version="$(MicrosoftILVerificationVersion)" /> <PackageReference Include="nunit" Version="3.12.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <!-- This reference is purely so that the linker can resolve this diff --git a/test/Mono.Linker.Tests/TestCasesRunner/ILVerifier.cs b/test/Mono.Linker.Tests/TestCasesRunner/ILVerifier.cs new file mode 100644 index 000000000..bf935cc22 --- /dev/null +++ b/test/Mono.Linker.Tests/TestCasesRunner/ILVerifier.cs @@ -0,0 +1,132 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Reflection.PortableExecutable; +using System.Runtime.Loader; +using ILVerify; +using Mono.Linker.Tests.Extensions; + +#nullable enable +namespace Mono.Linker.Tests.TestCasesRunner +{ + class ILVerifier : ILVerify.IResolver + { + Verifier _verifier; + NPath _assemblyFolder; + NPath _frameworkFolder; + Dictionary<string, PEReader> _assemblyCache; + AssemblyLoadContext _alc; + + public IEnumerable<VerificationResult> Results { get; private set; } + + public ILVerifier (NPath assemblyPath) + { + var assemblyName = assemblyPath.FileNameWithoutExtension; + _assemblyFolder = assemblyPath.Parent; + _assemblyCache = new Dictionary<string, PEReader> (); + _frameworkFolder = typeof (object).Assembly.Location.ToNPath ().Parent; + _alc = new AssemblyLoadContext (_assemblyFolder.FileName); + LoadAssembly ("mscorlib"); + LoadAssembly ("System.Private.CoreLib"); + LoadAssemblyFromPath (assemblyName, assemblyPath); + + _verifier = new ILVerify.Verifier ( + this, + new ILVerify.VerifierOptions { + SanityChecks = true, + IncludeMetadataTokensInErrorMessages = true + }); + _verifier.SetSystemModuleName (new AssemblyName ("mscorlib")); + + var allResults = _verifier.Verify (Resolve (assemblyName)) + ?? Enumerable.Empty<VerificationResult> (); + + Results = allResults.Where (r => r.Code switch { + ILVerify.VerifierError.None + // Static interface methods cause this warning + or ILVerify.VerifierError.CallAbstract + // "Missing callVirt after constrained prefix - static interface methods cause this warning + or ILVerify.VerifierError.Constrained + // ex. localloc cannot be statically verified by ILVerify + or ILVerify.VerifierError.Unverifiable + // ref returning a ref local causes this warning but is okay + or VerifierError.ReturnPtrToStack + // Span indexing with indexer (ex. span[^4]) causes this warning + or VerifierError.InitOnly + => false, + _ => true + }); + } + + PEReader LoadAssembly (string assemblyName) + { + if (_assemblyCache.TryGetValue (assemblyName, out PEReader? reader)) + return reader; + var assembly = _alc.LoadFromAssemblyName (new AssemblyName (assemblyName)); + reader = new PEReader (File.OpenRead (assembly.Location)); + _assemblyCache.Add (assemblyName, reader); + return reader; + } + + PEReader LoadAssemblyFromPath (string assemblyName, NPath pathToAssembly) + { + if (_assemblyCache.TryGetValue (assemblyName, out PEReader? reader)) + return reader; + var assembly = _alc.LoadFromAssemblyPath (pathToAssembly); + reader = new PEReader (File.OpenRead (assembly.Location)); + _assemblyCache.Add (assemblyName, reader); + return reader; + } + + bool TryLoadAssemblyFromFolder (string assemblyName, NPath folder, [NotNullWhen (true)] out PEReader? peReader) + { + Assembly? assembly = null; + string assemblyPath = Path.Join (folder.ToString (), assemblyName); + if (File.Exists (assemblyPath + ".dll")) + assembly = _alc.LoadFromAssemblyPath (assemblyPath + ".dll"); + else if (File.Exists (assemblyPath + ".exe")) + assembly = _alc.LoadFromAssemblyPath (assemblyPath + ".exe"); + + if (assembly is not null) { + peReader = new PEReader (File.OpenRead (assembly.Location)); + _assemblyCache.Add (assemblyName, peReader); + return true; + } + peReader = null; + return false; + } + + PEReader? Resolve (string assemblyName) + { + PEReader? reader; + if (_assemblyCache.TryGetValue (assemblyName, out reader)) { + return reader; + } + + if (TryLoadAssemblyFromFolder (assemblyName, _frameworkFolder, out reader)) + return reader; + + if (TryLoadAssemblyFromFolder (assemblyName, _assemblyFolder, out reader)) + return reader; + + return null; + } + + PEReader? ILVerify.IResolver.ResolveAssembly (AssemblyName assemblyName) + => Resolve (assemblyName.Name ?? assemblyName.FullName); + + PEReader? ILVerify.IResolver.ResolveModule (AssemblyName referencingModule, string fileName) + => Resolve (Path.GetFileNameWithoutExtension (fileName)); + + public string GetErrorMessage (VerificationResult result) + { + return $"IL Verification error:\n{result.Message}"; + } + } +} +#nullable restore diff --git a/test/Mono.Linker.Tests/TestCasesRunner/ResultChecker.cs b/test/Mono.Linker.Tests/TestCasesRunner/ResultChecker.cs index bd611721c..ccb76575f 100644 --- a/test/Mono.Linker.Tests/TestCasesRunner/ResultChecker.cs +++ b/test/Mono.Linker.Tests/TestCasesRunner/ResultChecker.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text.RegularExpressions; @@ -13,6 +14,7 @@ using Mono.Linker.Tests.Cases.Expectations.Assertions; using Mono.Linker.Tests.Cases.Expectations.Metadata; using Mono.Linker.Tests.Extensions; using NUnit.Framework; +using WellKnownType = ILLink.Shared.TypeSystemProxy.WellKnownType; namespace Mono.Linker.Tests.TestCasesRunner { @@ -47,6 +49,32 @@ namespace Mono.Linker.Tests.TestCasesRunner _linkedReaderParameters = linkedReaderParameters; } + static void VerifyIL (NPath pathToAssembly) + { + var verifier = new ILVerifier (pathToAssembly); + foreach (var result in verifier.Results) { + if (result.Code == ILVerify.VerifierError.None) + continue; + Assert.Fail (verifier.GetErrorMessage (result)); + } + } + + static bool ShouldValidateIL (AssemblyDefinition inputAssembly) + { + if (HasAttribute (inputAssembly, nameof (SkipPeVerifyAttribute))) + return false; + + var caaIsUnsafeFlag = (CustomAttributeArgument caa) => + caa.Type.IsTypeOf (WellKnownType.System_String) + && (string) caa.Value == "/unsafe"; + var customAttributeHasUnsafeFlag = (CustomAttribute ca) => ca.ConstructorArguments.Any (caaIsUnsafeFlag); + if (GetCustomAttributes (inputAssembly, nameof (SetupCompileArgumentAttribute)) + .Any (customAttributeHasUnsafeFlag)) + return false; + + return true; + } + public virtual void Check (LinkedTestCaseResult linkResult) { InitializeResolvers (linkResult); @@ -57,6 +85,9 @@ namespace Mono.Linker.Tests.TestCasesRunner Assert.IsTrue (linkResult.OutputAssemblyPath.FileExists (), $"The linked output assembly was not found. Expected at {linkResult.OutputAssemblyPath}"); var linked = ResolveLinkedAssembly (linkResult.OutputAssemblyPath.FileNameWithoutExtension); + if (ShouldValidateIL (original)) + VerifyIL (linkResult.OutputAssemblyPath); + InitialChecking (linkResult, original, linked); PerformOutputAssemblyChecks (original, linkResult.OutputAssemblyPath.Parent); @@ -1070,14 +1101,39 @@ namespace Mono.Linker.Tests.TestCasesRunner static bool HasAttribute (ICustomAttributeProvider caProvider, string attributeName) { + return TryGetCustomAttribute (caProvider, attributeName, out var _); + } + +#nullable enable + static bool TryGetCustomAttribute (ICustomAttributeProvider caProvider, string attributeName, [NotNullWhen (true)] out CustomAttribute? customAttribute) + { + if (caProvider is AssemblyDefinition assembly && assembly.EntryPoint != null) { + customAttribute = assembly.EntryPoint.DeclaringType.CustomAttributes + .FirstOrDefault (attr => attr!.AttributeType.Name == attributeName, null); + return customAttribute is not null; + } + + if (caProvider is TypeDefinition type) { + customAttribute = type.CustomAttributes + .FirstOrDefault (attr => attr!.AttributeType.Name == attributeName, null); + return customAttribute is not null; + } + customAttribute = null; + return false; + } + + static IEnumerable<CustomAttribute> GetCustomAttributes (ICustomAttributeProvider caProvider, string attributeName ) + { if (caProvider is AssemblyDefinition assembly && assembly.EntryPoint != null) return assembly.EntryPoint.DeclaringType.CustomAttributes - .Any (attr => attr.AttributeType.Name == attributeName); + .Where (attr => attr!.AttributeType.Name == attributeName); if (caProvider is TypeDefinition type) - return type.CustomAttributes.Any (attr => attr.AttributeType.Name == attributeName); + return type.CustomAttributes + .Where (attr => attr!.AttributeType.Name == attributeName); - return false; + return Enumerable.Empty<CustomAttribute> (); } +#nullable restore } } |