diff --git a/Lib/Utils/AssemblyInfo.cs b/Lib/Utils/AssemblyInfo.cs index 763048f7..cbdde62b 100644 --- a/Lib/Utils/AssemblyInfo.cs +++ b/Lib/Utils/AssemblyInfo.cs @@ -1,7 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License. using System.Resources; +using System.Runtime.CompilerServices; [assembly: NeutralResourcesLanguage("en")] +[assembly: InternalsVisibleTo("AsaTests")] namespace Microsoft.CST.AttackSurfaceAnalyzer.Utils { diff --git a/Lib/Utils/MonoPosixAssemblyResolver.cs b/Lib/Utils/MonoPosixAssemblyResolver.cs new file mode 100644 index 00000000..8016b713 --- /dev/null +++ b/Lib/Utils/MonoPosixAssemblyResolver.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License. +using System; +using System.IO; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Loader; + +namespace Microsoft.CST.AttackSurfaceAnalyzer.Utils +{ + /// + /// Fallback resolver for the Mono.Posix.NETStandard assembly. + /// + /// Mono.Posix.NETStandard 1.0.0 only ships its managed assembly under + /// runtimes/{rid}/lib/netstandard2.0/ and does not provide a win-arm64 runtime asset + /// (only win-x64, win-x86, linux-* and osx). Because the default host resolves that + /// assembly per-RID, on Windows ARM64 it cannot be found and a FileNotFoundException is + /// thrown whenever code that references Mono.Unix is JIT compiled (for example during + /// file system collection or monitoring). + /// + /// The managed assembly is platform independent IL, so we resolve it from one of the + /// runtime folders that actually ships with the application. This only runs when the + /// default resolution fails, so it has no effect on platforms where the assembly already + /// resolves normally. + /// + internal static class MonoPosixAssemblyResolver + { + internal const string MonoPosixAssemblyName = "Mono.Posix.NETStandard"; + + // RID-specific subfolders to probe, ordered by preference. win-arm64 is listed first so a + // matching asset is preferred should one ever ship (the 1.0.0 package does not provide one); + // the remaining RIDs all ship a managed Mono.Posix.NETStandard.dll that is identical IL. + internal static readonly string[] CandidateRuntimeIdentifiers = new[] + { + "win-arm64", + "win-x64", + "win-x86", + "linux-x64", + "linux-arm64", + "osx" + }; + + [ModuleInitializer] + internal static void Initialize() + { + AssemblyLoadContext.Default.Resolving += ResolveMonoPosix; + } + + internal static Assembly? ResolveMonoPosix(AssemblyLoadContext context, AssemblyName assemblyName) + { + var path = FindAssemblyPath(AppContext.BaseDirectory, assemblyName?.Name); + if (path is null) + { + return null; + } + + return context.LoadFromAssemblyPath(path); + } + + /// + /// Returns the path to a shipped managed Mono.Posix.NETStandard.dll under the given base + /// directory, or null if the requested assembly is not Mono.Posix.NETStandard or no + /// shipped copy could be found. + /// + internal static string? FindAssemblyPath(string baseDirectory, string? assemblyName) + { + if (string.IsNullOrEmpty(baseDirectory) || !string.Equals(assemblyName, MonoPosixAssemblyName, StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + foreach (var rid in CandidateRuntimeIdentifiers) + { + var candidate = Path.Combine(baseDirectory, "runtimes", rid, "lib", "netstandard2.0", $"{MonoPosixAssemblyName}.dll"); + if (File.Exists(candidate)) + { + return candidate; + } + } + + return null; + } + } +} diff --git a/Tests/MonoPosixAssemblyResolverTests.cs b/Tests/MonoPosixAssemblyResolverTests.cs new file mode 100644 index 00000000..7077168f --- /dev/null +++ b/Tests/MonoPosixAssemblyResolverTests.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License. +using Microsoft.CST.AttackSurfaceAnalyzer.Utils; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.IO; +using System.Runtime.Loader; + +namespace Microsoft.CST.AttackSurfaceAnalyzer.Tests +{ + /// + /// Tests for , which provides the fallback used to + /// load Mono.Posix.NETStandard on RIDs (such as win-arm64) for which the package ships no + /// runtime asset. + /// + [TestClass] + public class MonoPosixAssemblyResolverTests + { + [TestMethod] + public void FindAssemblyPathReturnsNullForUnrelatedAssembly() + { + var dir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())).FullName; + try + { + Assert.IsNull(MonoPosixAssemblyResolver.FindAssemblyPath(dir, "System.Text.Json")); + } + finally + { + Directory.Delete(dir, true); + } + } + + [TestMethod] + public void FindAssemblyPathReturnsNullWhenNoRuntimeAssetShipped() + { + var dir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())).FullName; + try + { + Assert.IsNull(MonoPosixAssemblyResolver.FindAssemblyPath(dir, MonoPosixAssemblyResolver.MonoPosixAssemblyName)); + } + finally + { + Directory.Delete(dir, true); + } + } + + [TestMethod] + public void FindAssemblyPathLocatesShippedRuntimeAsset() + { + var baseDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())).FullName; + try + { + // Simulate the shipped layout for an RID other than the (missing) win-arm64. + var ridDir = Path.Combine(baseDir, "runtimes", "win-x64", "lib", "netstandard2.0"); + Directory.CreateDirectory(ridDir); + var expected = Path.Combine(ridDir, $"{MonoPosixAssemblyResolver.MonoPosixAssemblyName}.dll"); + File.WriteAllText(expected, string.Empty); + + var found = MonoPosixAssemblyResolver.FindAssemblyPath(baseDir, MonoPosixAssemblyResolver.MonoPosixAssemblyName); + + Assert.AreEqual(expected, found); + } + finally + { + Directory.Delete(baseDir, true); + } + } + + [TestMethod] + public void ResolverIsRegisteredAndMonoPosixLoads() + { + // The module initializer should have registered the resolver, and Mono.Posix.NETStandard + // should be loadable (either via the default host on supported RIDs or via the fallback). + var assembly = AssemblyLoadContext.Default.LoadFromAssemblyName( + new System.Reflection.AssemblyName(MonoPosixAssemblyResolver.MonoPosixAssemblyName)); + + Assert.IsNotNull(assembly); + Assert.IsNotNull(assembly.GetType("Mono.Unix.UnixFileInfo")); + } + } +}