From 3be80c93aaf63dd40c084372ca7149011f44dfd9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 19:13:19 +0000 Subject: [PATCH 1/4] Initial plan From 4cadb0697040ca9777c769978e1c8f26d5f55103 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 19:21:48 +0000 Subject: [PATCH 2/4] Add Mono.Posix.NETStandard fallback resolver for Windows ARM64 --- Lib/Utils/MonoPosixAssemblyResolver.cs | 65 ++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 Lib/Utils/MonoPosixAssemblyResolver.cs diff --git a/Lib/Utils/MonoPosixAssemblyResolver.cs b/Lib/Utils/MonoPosixAssemblyResolver.cs new file mode 100644 index 00000000..49d182c5 --- /dev/null +++ b/Lib/Utils/MonoPosixAssemblyResolver.cs @@ -0,0 +1,65 @@ +// 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 + { + private const string MonoPosixAssemblyName = "Mono.Posix.NETStandard"; + + // RID-specific subfolders that ship a managed Mono.Posix.NETStandard.dll, ordered by preference. + private 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) + { + if (context is null || !string.Equals(assemblyName?.Name, MonoPosixAssemblyName, StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + foreach (var rid in CandidateRuntimeIdentifiers) + { + var candidate = Path.Combine(AppContext.BaseDirectory, "runtimes", rid, "lib", "netstandard2.0", $"{MonoPosixAssemblyName}.dll"); + if (File.Exists(candidate)) + { + return context.LoadFromAssemblyPath(candidate); + } + } + + return null; + } + } +} From 5f86ddf302752e75a07dd990a37bd1fe464752c8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 19:25:37 +0000 Subject: [PATCH 3/4] Add focused tests and make resolver testable for win-arm64 Mono.Posix fix --- Lib/Utils/AssemblyInfo.cs | 2 + Lib/Utils/MonoPosixAssemblyResolver.cs | 31 ++++++++-- Tests/MonoPosixAssemblyResolverTests.cs | 79 +++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 Tests/MonoPosixAssemblyResolverTests.cs 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 index 49d182c5..beead6c8 100644 --- a/Lib/Utils/MonoPosixAssemblyResolver.cs +++ b/Lib/Utils/MonoPosixAssemblyResolver.cs @@ -24,10 +24,10 @@ namespace Microsoft.CST.AttackSurfaceAnalyzer.Utils /// internal static class MonoPosixAssemblyResolver { - private const string MonoPosixAssemblyName = "Mono.Posix.NETStandard"; + internal const string MonoPosixAssemblyName = "Mono.Posix.NETStandard"; // RID-specific subfolders that ship a managed Mono.Posix.NETStandard.dll, ordered by preference. - private static readonly string[] CandidateRuntimeIdentifiers = new[] + internal static readonly string[] CandidateRuntimeIdentifiers = new[] { "win-arm64", "win-x64", @@ -45,17 +45,38 @@ internal static void Initialize() internal static Assembly? ResolveMonoPosix(AssemblyLoadContext context, AssemblyName assemblyName) { - if (context is null || !string.Equals(assemblyName?.Name, MonoPosixAssemblyName, StringComparison.OrdinalIgnoreCase)) + if (context is null) + { + return null; + } + + 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(AppContext.BaseDirectory, "runtimes", rid, "lib", "netstandard2.0", $"{MonoPosixAssemblyName}.dll"); + var candidate = Path.Combine(baseDirectory, "runtimes", rid, "lib", "netstandard2.0", $"{MonoPosixAssemblyName}.dll"); if (File.Exists(candidate)) { - return context.LoadFromAssemblyPath(candidate); + return candidate; } } 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")); + } + } +} From 123133577c83df043fe7dcf27655be87c390fbc6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 19:28:53 +0000 Subject: [PATCH 4/4] Address review feedback: clarify RID comment and drop redundant null check --- Lib/Utils/MonoPosixAssemblyResolver.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Lib/Utils/MonoPosixAssemblyResolver.cs b/Lib/Utils/MonoPosixAssemblyResolver.cs index beead6c8..8016b713 100644 --- a/Lib/Utils/MonoPosixAssemblyResolver.cs +++ b/Lib/Utils/MonoPosixAssemblyResolver.cs @@ -26,7 +26,9 @@ internal static class MonoPosixAssemblyResolver { internal const string MonoPosixAssemblyName = "Mono.Posix.NETStandard"; - // RID-specific subfolders that ship a managed Mono.Posix.NETStandard.dll, ordered by preference. + // 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", @@ -45,11 +47,6 @@ internal static void Initialize() internal static Assembly? ResolveMonoPosix(AssemblyLoadContext context, AssemblyName assemblyName) { - if (context is null) - { - return null; - } - var path = FindAssemblyPath(AppContext.BaseDirectory, assemblyName?.Name); if (path is null) {