Prevent StackOverflows when assemblies have recursive references (dotnet/aspnetcore-tooling#901)

* Prevent StackOverflows when assemblies have recursive references

https://github.com/aspnet/AspNetCore/issues/12693

\n\nCommit migrated from 43776c1864
This commit is contained in:
Pranav K 2019-08-05 09:19:26 -07:00 committed by GitHub
parent 7312e7cc38
commit 2699268bc3
4 changed files with 144 additions and 105 deletions

View File

@ -7,7 +7,7 @@ namespace Microsoft.AspNetCore.Razor.Tasks
{
public string Path { get; set; }
public bool IsSystemReference { get; set; }
public bool IsFrameworkReference { get; set; }
public string AssemblyName { get; set; }
}

View File

@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Razor.Tasks
referenceItems.Add(new AssemblyItem
{
AssemblyName = assemblyName,
IsSystemReference = item.GetMetadata("IsSystemReference") == "true",
IsFrameworkReference = item.GetMetadata("IsFrameworkReference") == "true",
Path = item.ItemSpec,
});
}

View File

@ -3,7 +3,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
@ -15,155 +17,141 @@ namespace Microsoft.AspNetCore.Razor.Tasks
public class ReferenceResolver
{
private readonly HashSet<string> _mvcAssemblies;
private readonly Dictionary<string, ClassifiedAssemblyItem> _lookup = new Dictionary<string, ClassifiedAssemblyItem>(StringComparer.Ordinal);
private readonly IReadOnlyList<AssemblyItem> _assemblyItems;
private readonly Dictionary<AssemblyItem, Classification> _classifications;
public ReferenceResolver(IReadOnlyList<string> targetAssemblies, IReadOnlyList<AssemblyItem> assemblyItems)
{
_mvcAssemblies = new HashSet<string>(targetAssemblies, StringComparer.Ordinal);
_assemblyItems = assemblyItems;
_classifications = new Dictionary<AssemblyItem, Classification>();
Lookup = new Dictionary<string, AssemblyItem>(StringComparer.Ordinal);
foreach (var item in assemblyItems)
{
var classifiedItem = new ClassifiedAssemblyItem(item);
_lookup[item.AssemblyName] = classifiedItem;
Lookup[item.AssemblyName] = item;
}
}
protected Dictionary<string, AssemblyItem> Lookup { get; }
public IReadOnlyList<string> ResolveAssemblies()
{
var applicationParts = new List<string>();
foreach (var item in _lookup)
{
var classification = Resolve(item.Value);
if (classification == DependencyClassification.ReferencesMvc)
{
applicationParts.Add(item.Key);
}
// It's not interesting for us to know if a dependency has a classification of MvcReference.
// All applications targeting the Microsoft.AspNetCore.App will have have a reference to Mvc.
foreach (var item in _assemblyItems)
{
var classification = Resolve(item);
if (classification == Classification.ReferencesMvc)
{
applicationParts.Add(item.AssemblyName);
}
}
return applicationParts;
}
private DependencyClassification Resolve(ClassifiedAssemblyItem classifiedItem)
private Classification Resolve(AssemblyItem assemblyItem)
{
if (classifiedItem.DependencyClassification != DependencyClassification.Unknown)
if (_classifications.TryGetValue(assemblyItem, out var classification))
{
return classifiedItem.DependencyClassification;
return classification;
}
if (classifiedItem.AssemblyItem == null)
// Initialize the dictionary with a value to short-circuit recursive references.
classification = Classification.Unknown;
_classifications[assemblyItem] = classification;
if (assemblyItem.Path == null)
{
// We encountered a dependency that isn't part of this assembly's dependency set. We'll see if it happens to be an MVC assembly.
// This might be useful in scenarios where the app does not have a framework reference at the entry point,
// but the transitive dependency does.
classifiedItem.DependencyClassification = _mvcAssemblies.Contains(classifiedItem.Name) ?
DependencyClassification.MvcReference :
DependencyClassification.DoesNotReferenceMvc;
return classifiedItem.DependencyClassification;
// We encountered a dependency that isn't part of this assembly's dependency set. We'll see if it happens to be an MVC assembly
// since that's the only useful determination we can make given the assembly name.
classification = _mvcAssemblies.Contains(assemblyItem.AssemblyName) ?
Classification.IsMvc :
Classification.DoesNotReferenceMvc;
}
if (classifiedItem.AssemblyItem.IsSystemReference)
else if (assemblyItem.IsFrameworkReference)
{
// We do not allow transitive references to MVC via a framework reference to count.
// e.g. depending on Microsoft.AspNetCore.SomeThingNewThatDependsOnMvc would not result in an assembly being treated as
// referencing MVC.
classifiedItem.DependencyClassification = _mvcAssemblies.Contains(classifiedItem.Name) ?
DependencyClassification.MvcReference :
DependencyClassification.DoesNotReferenceMvc;
return classifiedItem.DependencyClassification;
classification = _mvcAssemblies.Contains(assemblyItem.AssemblyName) ?
Classification.IsMvc :
Classification.DoesNotReferenceMvc;
}
if (_mvcAssemblies.Contains(classifiedItem.Name))
else if (_mvcAssemblies.Contains(assemblyItem.AssemblyName))
{
classifiedItem.DependencyClassification = DependencyClassification.MvcReference;
return classifiedItem.DependencyClassification;
classification = Classification.IsMvc;
}
var dependencyClassification = DependencyClassification.DoesNotReferenceMvc;
foreach (var assemblyItem in GetReferences(classifiedItem.AssemblyItem.Path))
else
{
var classification = Resolve(assemblyItem);
if (classification == DependencyClassification.MvcReference || classification == DependencyClassification.ReferencesMvc)
classification = Classification.DoesNotReferenceMvc;
foreach (var reference in GetReferences(assemblyItem.Path))
{
dependencyClassification = DependencyClassification.ReferencesMvc;
break;
var referenceClassification = Resolve(reference);
if (referenceClassification == Classification.IsMvc || referenceClassification == Classification.ReferencesMvc)
{
classification = Classification.ReferencesMvc;
break;
}
}
}
classifiedItem.DependencyClassification = dependencyClassification;
return dependencyClassification;
Debug.Assert(classification != Classification.Unknown);
_classifications[assemblyItem] = classification;
return classification;
}
protected virtual IReadOnlyList<ClassifiedAssemblyItem> GetReferences(string file)
protected virtual IReadOnlyList<AssemblyItem> GetReferences(string file)
{
try
{
using var peReader = new PEReader(File.OpenRead(file));
if (!peReader.HasMetadata)
{
return Array.Empty<ClassifiedAssemblyItem>(); // not a managed assembly
return Array.Empty<AssemblyItem>(); // not a managed assembly
}
var metadataReader = peReader.GetMetadataReader();
var assemblyItems = new List<ClassifiedAssemblyItem>();
var references = new List<AssemblyItem>();
foreach (var handle in metadataReader.AssemblyReferences)
{
var reference = metadataReader.GetAssemblyReference(handle);
var referenceName = metadataReader.GetString(reference.Name);
if (_lookup.TryGetValue(referenceName, out var classifiedItem))
{
assemblyItems.Add(classifiedItem);
}
else
if (!Lookup.TryGetValue(referenceName, out var assemblyItem))
{
// A dependency references an item that isn't referenced by this project.
// We'll construct an item for so that we can calculate the classification based on it's name.
assemblyItems.Add(new ClassifiedAssemblyItem(referenceName));
assemblyItem = new AssemblyItem
{
AssemblyName = referenceName,
};
Lookup[referenceName] = assemblyItem;
}
references.Add(assemblyItem);
}
return assemblyItems;
return references;
}
catch (BadImageFormatException)
{
// not a PE file, or invalid metadata
}
return Array.Empty<ClassifiedAssemblyItem>(); // not a managed assembly
return Array.Empty<AssemblyItem>(); // not a managed assembly
}
protected enum DependencyClassification
protected enum Classification
{
Unknown,
DoesNotReferenceMvc,
ReferencesMvc,
MvcReference,
}
protected class ClassifiedAssemblyItem
{
public ClassifiedAssemblyItem(AssemblyItem classifiedItem)
: this(classifiedItem.AssemblyName)
{
AssemblyItem = classifiedItem;
}
public ClassifiedAssemblyItem(string name)
{
Name = name;
}
public string Name { get; }
public AssemblyItem AssemblyItem { get; }
public DependencyClassification DependencyClassification { get; set; }
IsMvc,
}
}
}

View File

@ -59,13 +59,13 @@ namespace Microsoft.AspNetCore.Razor.Tasks
var resolver = new TestReferencesToMvcResolver(new[]
{
CreateAssemblyItem("MyApp.Models"),
CreateAssemblyItem("Microsoft.AspNetCore.Mvc", isSystemReference: true),
CreateAssemblyItem("Microsoft.AspNetCore.Hosting", isSystemReference: true),
CreateAssemblyItem("Microsoft.AspNetCore.HttpAbstractions", isSystemReference: true),
CreateAssemblyItem("Microsoft.AspNetCore.KestrelHttpServer", isSystemReference: true),
CreateAssemblyItem("Microsoft.AspNetCore.StaticFiles", isSystemReference: true),
CreateAssemblyItem("Microsoft.Extensions.Primitives", isSystemReference: true),
CreateAssemblyItem("System.Net.Http", isSystemReference: true),
CreateAssemblyItem("Microsoft.AspNetCore.Mvc", isFrameworkReference: true),
CreateAssemblyItem("Microsoft.AspNetCore.Hosting", isFrameworkReference: true),
CreateAssemblyItem("Microsoft.AspNetCore.HttpAbstractions", isFrameworkReference: true),
CreateAssemblyItem("Microsoft.AspNetCore.KestrelHttpServer", isFrameworkReference: true),
CreateAssemblyItem("Microsoft.AspNetCore.StaticFiles", isFrameworkReference: true),
CreateAssemblyItem("Microsoft.Extensions.Primitives", isFrameworkReference: true),
CreateAssemblyItem("System.Net.Http", isFrameworkReference: true),
CreateAssemblyItem("Microsoft.EntityFrameworkCore"),
});
@ -89,16 +89,16 @@ namespace Microsoft.AspNetCore.Razor.Tasks
// Arrange
var resolver = new TestReferencesToMvcResolver(new[]
{
CreateAssemblyItem("Microsoft.AspNetCore.Mvc", isSystemReference: true),
CreateAssemblyItem("Microsoft.AspNetCore.Mvc.TagHelpers", isSystemReference: true),
CreateAssemblyItem("Microsoft.AspNetCore.Mvc", isFrameworkReference: true),
CreateAssemblyItem("Microsoft.AspNetCore.Mvc.TagHelpers", isFrameworkReference: true),
CreateAssemblyItem("MyTagHelpers"),
CreateAssemblyItem("MyControllers"),
CreateAssemblyItem("MyApp.Models"),
CreateAssemblyItem("Microsoft.AspNetCore.Hosting", isSystemReference: true),
CreateAssemblyItem("Microsoft.AspNetCore.HttpAbstractions", isSystemReference: true),
CreateAssemblyItem("Microsoft.AspNetCore.KestrelHttpServer", isSystemReference: true),
CreateAssemblyItem("Microsoft.AspNetCore.StaticFiles", isSystemReference: true),
CreateAssemblyItem("Microsoft.Extensions.Primitives", isSystemReference: true),
CreateAssemblyItem("Microsoft.AspNetCore.Hosting", isFrameworkReference: true),
CreateAssemblyItem("Microsoft.AspNetCore.HttpAbstractions", isFrameworkReference: true),
CreateAssemblyItem("Microsoft.AspNetCore.KestrelHttpServer", isFrameworkReference: true),
CreateAssemblyItem("Microsoft.AspNetCore.StaticFiles", isFrameworkReference: true),
CreateAssemblyItem("Microsoft.Extensions.Primitives", isFrameworkReference: true),
CreateAssemblyItem("Microsoft.EntityFrameworkCore"),
});
@ -126,13 +126,12 @@ namespace Microsoft.AspNetCore.Razor.Tasks
{
CreateAssemblyItem("MyCMS"),
CreateAssemblyItem("MyCMS.Core"),
CreateAssemblyItem("Microsoft.AspNetCore.Mvc.ViewFeatures", isSystemReference: true),
CreateAssemblyItem("Microsoft.AspNetCore.Mvc.ViewFeatures", isFrameworkReference: true),
});
resolver.Add("MyCMS", "MyCMS.Core");
resolver.Add("MyCMS.Core", "Microsoft.AspNetCore.Mvc.ViewFeatures");
// Act
var assemblies = resolver.ResolveAssemblies();
@ -140,41 +139,93 @@ namespace Microsoft.AspNetCore.Razor.Tasks
Assert.Equal(new[] { "MyCMS", "MyCMS.Core" }, assemblies.OrderBy(a => a));
}
public AssemblyItem CreateAssemblyItem(string name, bool isSystemReference = false)
[Fact]
public void Resolve_Works_WhenAssemblyReferencesAreRecursive()
{
// Test for https://github.com/aspnet/AspNetCore/issues/12693
// Arrange
var resolver = new TestReferencesToMvcResolver(new[]
{
CreateAssemblyItem("PresentationFramework"),
CreateAssemblyItem("ReachFramework"),
CreateAssemblyItem("MyCMS"),
CreateAssemblyItem("MyCMS.Core"),
CreateAssemblyItem("Microsoft.AspNetCore.Mvc.ViewFeatures", isFrameworkReference: true),
});
resolver.Add("PresentationFramework", "ReachFramework");
resolver.Add("ReachFramework", "PresentationFramework");
resolver.Add("MyCMS", "MyCMS.Core");
resolver.Add("MyCMS.Core", "Microsoft.AspNetCore.Mvc.ViewFeatures");
// Act
var assemblies = resolver.ResolveAssemblies();
// Assert
Assert.Equal(new[] { "MyCMS", "MyCMS.Core" }, assemblies.OrderBy(a => a));
}
[Fact]
public void Resolve_Works_WhenAssemblyReferencesAreRecursive_ButAlsoReferencesMvc()
{
// Arrange
var resolver = new TestReferencesToMvcResolver(new[]
{
CreateAssemblyItem("MyCoolLibrary"),
CreateAssemblyItem("PresentationFramework"),
CreateAssemblyItem("ReachFramework"),
CreateAssemblyItem("MyCMS"),
CreateAssemblyItem("MyCMS.Core"),
CreateAssemblyItem("Microsoft.AspNetCore.Mvc.ViewFeatures", isFrameworkReference: true),
});
resolver.Add("MyCoolLibrary", "PresentationFramework");
resolver.Add("PresentationFramework", "ReachFramework");
resolver.Add("ReachFramework", "PresentationFramework", "MyCMS");
resolver.Add("MyCMS", "MyCMS.Core");
resolver.Add("MyCMS.Core", "Microsoft.AspNetCore.Mvc.ViewFeatures");
// Act
var assemblies = resolver.ResolveAssemblies();
// Assert
Assert.Equal(new[] { "MyCMS", "MyCMS.Core", "MyCoolLibrary", "PresentationFramework", "ReachFramework" }, assemblies.OrderBy(a => a));
}
public AssemblyItem CreateAssemblyItem(string name, bool isFrameworkReference = false)
{
return new AssemblyItem
{
AssemblyName = name,
IsSystemReference = isSystemReference,
IsFrameworkReference = isFrameworkReference,
Path = name,
};
}
private class TestReferencesToMvcResolver : ReferenceResolver
{
private readonly Dictionary<string, List<ClassifiedAssemblyItem>> _references = new Dictionary<string, List<ClassifiedAssemblyItem>>();
private readonly Dictionary<string, ClassifiedAssemblyItem> _lookup;
private readonly Dictionary<string, string[]> _references = new Dictionary<string, string[]>();
public TestReferencesToMvcResolver(AssemblyItem[] referenceItems)
: base(MvcAssemblies, referenceItems)
{
_lookup = referenceItems.ToDictionary(r => r.AssemblyName, r => new ClassifiedAssemblyItem(r));
}
public void Add(string assembly, params string[] references)
{
var assemblyItems = references.Select(r => _lookup[r]).ToList();
_references[assembly] = assemblyItems;
_references.Add(assembly, references);
}
protected override IReadOnlyList<ClassifiedAssemblyItem> GetReferences(string file)
protected override IReadOnlyList<AssemblyItem> GetReferences(string file)
{
if (_references.TryGetValue(file, out var result))
{
return result;
return result.Select(r => Lookup[r]).ToArray();
}
return Array.Empty<ClassifiedAssemblyItem>();
return Array.Empty<AssemblyItem>();
}
}
}