Team Ai
Datasetpublic

MegaBites-AI/Windows-powershell

sourceHugging Facemitupdated 6mo agoView on Hugging Face
0likes372downloads
CorePsAssemblyLoadContext.cs627 linesDownload Raw Back to CoreCLR
1// Copyright (c) Microsoft Corporation.2// Licensed under the MIT License.3 4using System.Collections.Concurrent;5using System.Collections.Generic;6using System.Globalization;7using System.IO;8using System.Linq;9using System.Runtime.InteropServices;10using System.Reflection;11using System.Runtime.Loader;12using Microsoft.PowerShell.Telemetry;13 14namespace System.Management.Automation15{16    /// <summary>17    /// The powershell custom AssemblyLoadContext implementation.18    /// </summary>19    internal sealed partial class PowerShellAssemblyLoadContext20    {21        #region Resource_Strings22 23        // We cannot use a satellite resources.dll to store resource strings for Microsoft.PowerShell.CoreCLR.AssemblyLoadContext.dll. This is because when retrieving resource strings, ResourceManager24        // tries to load the satellite resources.dll using a probing approach, which will cause an infinite loop to PowerShellAssemblyLoadContext.Load(AssemblyName).25        // Take the 'en-US' culture as an example. When retrieving resource string to construct an exception, ResourceManager calls Assembly.Load(..) in the following order to load the resource dll:26        //     1. Load assembly with culture 'en-US' (Microsoft.PowerShell.CoreCLR.AssemblyLoadContext.resources, Version=3.0.0.0, Culture=en-US, PublicKeyToken=31bf3856ad364e35)27        //     2. Load assembly with culture 'en'    (Microsoft.PowerShell.CoreCLR.AssemblyLoadContext.resources, Version=3.0.0.0, Culture=en, PublicKeyToken=31bf3856ad364e35)28        // When the first attempt fails, we again need to retrieve the resource string to construct another exception, which ends up with an infinite loop.29        private const string BaseFolderDoesNotExist = "The base directory '{0}' does not exist.";30        private const string ManifestDefinitionDoesNotMatch = "Could not load file or assembly '{0}' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference.";31        private const string SingletonAlreadyInitialized = "The singleton of PowerShellAssemblyLoadContext has already been initialized.";32 33        #endregion Resource_Strings34 35        #region Constructor36 37        /// <summary>38        /// Initialize a singleton of PowerShellAssemblyLoadContext.39        /// </summary>40        internal static PowerShellAssemblyLoadContext InitializeSingleton(string basePaths, bool throwOnReentry)41        {42            lock (s_syncObj)43            {44                if (Instance is null)45                {46                    Instance = new PowerShellAssemblyLoadContext(basePaths);47                }48                else if (throwOnReentry)49                {50                    throw new InvalidOperationException(SingletonAlreadyInitialized);51                }52 53                return Instance;54            }55        }56 57        /// <summary>58        /// Constructor.59        /// </summary>60        /// <param name="basePaths">61        /// Base directory paths that are separated by semicolon ';'. They will be the default paths to probe assemblies.62        /// The passed-in argument could be null or an empty string, in which case there is no default paths to probe assemblies.63        /// </param>64        private PowerShellAssemblyLoadContext(string basePaths)65        {66#if !UNIX67            // Set GAC related member variables to null68            _winDir = _gacPath32 = _gacPath64 = _gacPathMSIL = null;69#endif70 71            // FIRST: Validate and populate probing paths72            if (string.IsNullOrEmpty(basePaths))73            {74                _probingPaths = Array.Empty<string>();75            }76            else77            {78                _probingPaths = basePaths.Split(';', StringSplitOptions.RemoveEmptyEntries);79                for (int i = 0; i < _probingPaths.Length; i++)80                {81                    string basePath = _probingPaths[i];82                    if (!Directory.Exists(basePath))83                    {84                        string message = string.Format(CultureInfo.CurrentCulture, BaseFolderDoesNotExist, basePath);85                        throw new ArgumentException(message, nameof(basePaths));86                    }87 88                    _probingPaths[i] = basePath.Trim();89                }90            }91 92            // NEXT: Initialize the CoreCLR type catalog dictionary [OrdinalIgnoreCase]93            _coreClrTypeCatalog = InitializeTypeCatalog();94            _availableDotNetAssemblyNames = new Lazy<HashSet<string>>(95                    () => new HashSet<string>(_coreClrTypeCatalog.Values, StringComparer.Ordinal));96 97            // LAST: Register the 'Resolving' handler and 'ResolvingUnmanagedDll' handler on the default load context.98            AssemblyLoadContext.Default.Resolving += Resolve;99 100            // Add last resort native dll resolver.101            // Default order:102            //      1. System.Runtime.InteropServices.DllImportResolver callbacks103            //      2. AssemblyLoadContext.LoadUnmanagedDll()104            //      3. AssemblyLoadContext.Default.ResolvingUnmanagedDll handlers105            AssemblyLoadContext.Default.ResolvingUnmanagedDll += NativeDllHandler;106        }107 108        #endregion Constructor109 110        #region Fields111 112        private static readonly object s_syncObj = new();113        private readonly string[] _probingPaths;114        private readonly string[] _extensions = new string[] { ".ni.dll", ".dll" };115        // CoreCLR type catalog dictionary116        //  - Key: namespace qualified type name (FullName)117        //  - Value: strong name of the TPA that contains the type represented by Key.118        private readonly Dictionary<string, string> _coreClrTypeCatalog;119        private readonly Lazy<HashSet<string>> _availableDotNetAssemblyNames;120 121        private readonly HashSet<string> _denyListedAssemblies =122            new(StringComparer.OrdinalIgnoreCase) { "System.Windows.Forms" };123 124#if !UNIX125        private string _winDir;126        private string _gacPathMSIL;127        private string _gacPath32;128        private string _gacPath64;129#endif130 131        /// <summary>132        /// Assembly cache across the AppDomain.133        /// </summary>134        /// <remarks>135        /// We user the assembly short name (AssemblyName.Name) as the key.136        /// According to the Spec of AssemblyLoadContext, "in the context of a given instance of AssemblyLoadContext, only one assembly with137        /// a given name can be loaded. Attempt to load a second assembly with the same name and different MVID will result in an exception."138        ///139        /// MVID is Module Version Identifier, which is a guid. Its purpose is solely to be unique for each time the module is compiled, and140        /// it gets regenerated for every compilation. That means AssemblyLoadContext cannot handle loading two assemblies with the same name141        /// but different versions, not even two assemblies with the exactly same code and version but built by two separate compilations.142        ///143        /// Therefore, there is no need to use the full assembly name as the key. Short assembly name is sufficient.144        /// </remarks>145        private static readonly ConcurrentDictionary<string, Assembly> s_assemblyCache =146            new(StringComparer.OrdinalIgnoreCase);147 148        #endregion Fields149 150        #region Properties151 152        /// <summary>153        /// Singleton instance of PowerShellAssemblyLoadContext.154        /// </summary>155        internal static PowerShellAssemblyLoadContext Instance156        {157            get; private set;158        }159 160        /// <summary>161        /// Get the namespace-qualified type names of all available .NET Core types shipped with PowerShell.162        /// This is used for type name auto-completion in PS engine.163        /// </summary>164        internal IEnumerable<string> AvailableDotNetTypeNames165        {166            get { return _coreClrTypeCatalog.Keys; }167        }168 169        /// <summary>170        /// Get the assembly names of all available .NET Core assemblies shipped with PowerShell.171        /// This is used for type name auto-completion in PS engine.172        /// </summary>173        internal HashSet<string> AvailableDotNetAssemblyNames174        {175            get { return _availableDotNetAssemblyNames.Value; }176        }177 178        #endregion Properties179 180        #region Internal_Methods181 182        /// <summary>183        /// Get the current loaded assemblies.184        /// </summary>185        internal IEnumerable<Assembly> GetAssembly(string namespaceQualifiedTypeName)186        {187            // If 'namespaceQualifiedTypeName' is specified and it's a CoreCLR framework type,188            // then we only return that specific TPA assembly.189            if (!string.IsNullOrEmpty(namespaceQualifiedTypeName))190            {191                if (_coreClrTypeCatalog.TryGetValue(namespaceQualifiedTypeName, out string tpaStrongName))192                {193                    try194                    {195                        return new Assembly[] { GetTrustedPlatformAssembly(tpaStrongName) };196                    }197                    catch (FileNotFoundException) { }198                }199            }200 201            // Otherwise, we return null202            return null;203        }204 205        /// <summary>206        /// If a managed dll has native dependencies the handler will try to find these native dlls.207        ///     1. Gets the managed.dll location (folder)208        ///     2. Based on OS name and architecture name builds subfolder name where it is expected the native dll resides:209        ///     3. Loads the native dll210        ///211        ///     managed.dll folder212        ///                     |213        ///                     |--- 'win-x64' subfolder214        ///                     |       |--- native.dll215        ///                     |216        ///                     |--- 'win-x86' subfolder217        ///                     |       |--- native.dll218        ///                     |219        ///                     |--- 'win-arm' subfolder220        ///                     |       |--- native.dll221        ///                     |222        ///                     |--- 'win-arm64' subfolder223        ///                     |       |--- native.dll224        ///                     |225        ///                     |--- 'linux-x64' subfolder226        ///                     |       |--- native.so227        ///                     |228        ///                     |--- 'linux-x86' subfolder229        ///                     |       |--- native.so230        ///                     |231        ///                     |--- 'linux-arm' subfolder232        ///                     |       |--- native.so233        ///                     |234        ///                     |--- 'linux-arm64' subfolder235        ///                     |       |--- native.so236        ///                     |237        ///                     |--- 'osx-x64' subfolder238        ///                     |       |--- native.dylib239        ///                     |240        ///                     |--- 'osx-arm64' subfolder241        ///                     |       |--- native.dylib242        /// </summary>243        internal static IntPtr NativeDllHandler(Assembly assembly, string libraryName)244        {245            s_nativeDllSubFolder ??= GetNativeDllSubFolderName(out s_nativeDllExtension);246            string folder = Path.GetDirectoryName(assembly.Location);247            string fullName = Path.Combine(folder, s_nativeDllSubFolder, libraryName) + s_nativeDllExtension;248 249            return NativeLibrary.TryLoad(fullName, out IntPtr pointer) ? pointer : IntPtr.Zero;250        }251 252        #endregion Internal_Methods253 254        #region Private_Methods255 256        /// <summary>257        /// The handler for the Resolving event.258        /// </summary>259        private Assembly Resolve(AssemblyLoadContext loadContext, AssemblyName assemblyName)260        {261            // Probe the assembly cache262            Assembly asmLoaded;263            if (TryGetAssemblyFromCache(assemblyName, out asmLoaded))264                return asmLoaded;265 266            // Prepare to load the assembly267            lock (s_syncObj)268            {269                // Probe the cache again in case it's already loaded270                if (TryGetAssemblyFromCache(assemblyName, out asmLoaded))271                    return asmLoaded;272 273                // Search the specified assembly in probing paths, and load it through 'LoadFromAssemblyPath' if the file exists and matches the requested AssemblyName.274                // If the CultureName of the requested assembly is not NullOrEmpty, then it's a resources.dll and we need to search corresponding culture sub-folder.275                bool isAssemblyFileFound = false, isAssemblyFileMatching = false;276                string asmCultureName = assemblyName.CultureName ?? string.Empty;277                string asmFilePath = null;278 279                for (int i = 0; i < _probingPaths.Length; i++)280                {281                    string probingPath = _probingPaths[i];282                    string asmCulturePath = Path.Combine(probingPath, asmCultureName);283                    for (int k = 0; k < _extensions.Length; k++)284                    {285                        string asmFileName = assemblyName.Name + _extensions[k];286                        asmFilePath = Path.Combine(asmCulturePath, asmFileName);287 288                        if (File.Exists(asmFilePath))289                        {290                            isAssemblyFileFound = true;291                            AssemblyName asmNameFound = AssemblyLoadContext.GetAssemblyName(asmFilePath);292                            if (IsAssemblyMatching(assemblyName, asmNameFound))293                            {294                                isAssemblyFileMatching = true;295                                break;296                            }297                        }298                    }299 300                    if (isAssemblyFileFound && isAssemblyFileMatching)301                    {302                        break;303                    }304                }305 306                // We failed to find the assembly file; or we found the file, but the assembly file doesn't match the request.307                // In this case, return null so that other Resolving event handlers can kick in to resolve the request.308                if (!isAssemblyFileFound || !isAssemblyFileMatching)309                {310#if !UNIX311                    // Try loading from GAC312                    if (!TryFindInGAC(assemblyName, out asmFilePath))313                    {314                        return null;315                    }316#else317                    return null;318#endif319                }320 321                asmLoaded = asmFilePath.EndsWith(".ni.dll", StringComparison.OrdinalIgnoreCase)322                                ? loadContext.LoadFromNativeImagePath(asmFilePath, null)323                                : loadContext.LoadFromAssemblyPath(asmFilePath);324                if (asmLoaded != null)325                {326                    // Add the loaded assembly to the cache327                    s_assemblyCache.TryAdd(assemblyName.Name, asmLoaded);328                }329            }330 331            return asmLoaded;332        }333 334#if !UNIX335        // Try to find the assembly in GAC by looking up the directories in well know locations.336        // First try to find in GAC_MSIL, then depending on process bitness; GAC_64 or GAC32.337        // If there are multiple version of the assembly, load the latest.338        private bool TryFindInGAC(AssemblyName assemblyName, out string assemblyFilePath)339        {340            assemblyFilePath = null;341            if (_denyListedAssemblies.Contains(assemblyName.Name))342            {343                // DotNet catches and throws a new exception with no inner exception344                // We cannot change the message DotNet returns.345                return false;346            }347 348            if (Internal.InternalTestHooks.DisableGACLoading)349            {350                return false;351            }352 353            if (string.IsNullOrEmpty(_winDir))354            {355                // cache value of '_winDir' folder in member variable.356                _winDir = Environment.GetEnvironmentVariable("winDir");357            }358 359            if (string.IsNullOrEmpty(_gacPathMSIL))360            {361                // cache value of '_gacPathMSIL' folder in member variable.362                _gacPathMSIL = Path.Join(_winDir, "Microsoft.NET", "assembly", "GAC_MSIL");363            }364 365            bool assemblyFound = FindInGac(_gacPathMSIL, assemblyName, out assemblyFilePath);366 367            if (!assemblyFound)368            {369                string gacBitnessAwarePath;370 371                if (Environment.Is64BitProcess)372                {373                    if (string.IsNullOrEmpty(_gacPath64))374                    {375                       var gacName = RuntimeInformation.ProcessArchitecture == Architecture.Arm64 ? "GAC_Arm64" : "GAC_64";376                        _gacPath64 = Path.Join(_winDir, "Microsoft.NET", "assembly", gacName);377                    }378 379                    gacBitnessAwarePath = _gacPath64;380                }381                else382                {383                    if (string.IsNullOrEmpty(_gacPath32))384                    {385                        _gacPath32 = Path.Join(_winDir, "Microsoft.NET", "assembly", "GAC_32");386                    }387 388                    gacBitnessAwarePath = _gacPath32;389                }390 391                assemblyFound = FindInGac(gacBitnessAwarePath, assemblyName, out assemblyFilePath);392            }393 394            return assemblyFound;395        }396 397        // Find the assembly under 'gacRoot' and select the latest version.398        private static bool FindInGac(string gacRoot, AssemblyName assemblyName, out string assemblyPath)399        {400            bool assemblyFound = false;401            assemblyPath = null;402 403            string tempAssemblyDirPath = Path.Join(gacRoot, assemblyName.Name);404 405            if (Directory.Exists(tempAssemblyDirPath))406            {407                // Enumerate all directories, sort by name and select the last. This selects the latest version.408                var chosenVersionDirectory = Directory.EnumerateDirectories(tempAssemblyDirPath).Order().LastOrDefault();409 410                if (!string.IsNullOrEmpty(chosenVersionDirectory))411                {412                    // Select first or default as the directory will contain only one assembly. If nothing then default is null;413                    var foundAssemblyPath = Directory.EnumerateFiles(chosenVersionDirectory, $"{assemblyName.Name}*").FirstOrDefault();414 415                    if (!string.IsNullOrEmpty(foundAssemblyPath))416                    {417                        AssemblyName asmNameFound = AssemblyLoadContext.GetAssemblyName(foundAssemblyPath);418                        if (IsAssemblyMatching(assemblyName, asmNameFound))419                        {420                            assemblyPath = foundAssemblyPath;421                            assemblyFound = true;422                        }423                    }424                }425            }426 427            return assemblyFound;428        }429#endif430 431        /// <summary>432        /// Try to get the specified assembly from cache.433        /// </summary>434        private static bool TryGetAssemblyFromCache(AssemblyName assemblyName, out Assembly asmLoaded)435        {436            if (s_assemblyCache.TryGetValue(assemblyName.Name, out asmLoaded))437            {438                // Check if loaded assembly matches the request439                if (IsAssemblyMatching(assemblyName, asmLoaded.GetName()))440                    return true;441 442                // In the context of a given instance of AssemblyLoadContext, only one assembly with the443                // same name can be loaded. So we throw exception if assembly doesn't match the request.444                ThrowFileLoadException(445                    ManifestDefinitionDoesNotMatch,446                    assemblyName.FullName);447            }448 449            return false;450        }451 452        /// <summary>453        /// Check if the loaded assembly matches the request.454        /// </summary>455        /// <param name="requestedAssembly">AssemblyName of the requested assembly.</param>456        /// <param name="loadedAssembly">AssemblyName of the loaded assembly.</param>457        /// <returns></returns>458        private static bool IsAssemblyMatching(AssemblyName requestedAssembly, AssemblyName loadedAssembly)459        {460            //461            // We use the same rules as CoreCLR loader to compare the requested assembly and loaded assembly:462            //  1. If 'Version' of the requested assembly is specified, then the requested version should be less or equal to the loaded version;463            //  2. If 'CultureName' of the requested assembly is specified (not NullOrEmpty), then the CultureName of the loaded assembly should be the same;464            //  3. If 'PublicKeyToken' of the requested assembly is specified (not Null or EmptyArray), then the PublicKenToken of the loaded assembly should be the same.465            //466 467            // Version of the requested assembly should be the same or before the version of loaded assembly468            if (requestedAssembly.Version != null && requestedAssembly.Version.CompareTo(loadedAssembly.Version) > 0)469            {470                return false;471            }472 473            // CultureName of requested assembly and loaded assembly should be the same474            string requestedCultureName = requestedAssembly.CultureName;475            if (!string.IsNullOrEmpty(requestedCultureName) && !requestedCultureName.Equals(loadedAssembly.CultureName, StringComparison.OrdinalIgnoreCase))476            {477                return false;478            }479 480            // PublicKeyToken should be the same, unless it's not specified in the requested assembly481            byte[] requestedPublicKeyToken = requestedAssembly.GetPublicKeyToken();482            byte[] loadedPublicKeyToken = loadedAssembly.GetPublicKeyToken();483 484            if (requestedPublicKeyToken != null && requestedPublicKeyToken.Length > 0)485            {486                if (loadedPublicKeyToken == null || requestedPublicKeyToken.Length != loadedPublicKeyToken.Length)487                    return false;488 489                for (int i = 0; i < requestedPublicKeyToken.Length; i++)490                {491                    if (requestedPublicKeyToken[i] != loadedPublicKeyToken[i])492                        return false;493                }494            }495 496            return true;497        }498 499        /// <summary>500        /// Get the TPA that is represented by the specified assembly strong name.501        /// </summary>502        /// <param name="tpaStrongName">503        /// The assembly strong name of a CoreCLR Trusted_Platform_Assembly504        /// </param>505        private static Assembly GetTrustedPlatformAssembly(string tpaStrongName)506        {507            // We always depend on the default context to load the TPAs that are recorded in508            // the type catalog.509            //   - If the requested TPA is already loaded, then 'Assembly.Load' will just get510            //     it back from the cache of default context.511            //   - If the requested TPA is not loaded yet, then 'Assembly.Load' will make the512            //     default context to load it513            AssemblyName assemblyName = new(tpaStrongName);514            Assembly asmLoaded = Assembly.Load(assemblyName);515            return asmLoaded;516        }517 518        /// <summary>519        /// Throw FileLoadException.520        /// </summary>521        private static void ThrowFileLoadException(string errorTemplate, params object[] args)522        {523            string message = string.Format(CultureInfo.CurrentCulture, errorTemplate, args);524            throw new FileLoadException(message);525        }526 527        /// <summary>528        /// Throw FileNotFoundException.529        /// </summary>530        private static void ThrowFileNotFoundException(string errorTemplate, params object[] args)531        {532            string message = string.Format(CultureInfo.CurrentCulture, errorTemplate, args);533            throw new FileNotFoundException(message);534        }535 536        private static string s_nativeDllSubFolder;537        private static string s_nativeDllExtension;538 539        private static string GetNativeDllSubFolderName(out string ext)540        {541            string folderName = string.Empty;542            ext = string.Empty;543            var processArch = RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant();544 545            if (Platform.IsWindows)546            {547                folderName = "win-" + processArch;548                ext = ".dll";549            }550            else if (Platform.IsLinux)551            {552                folderName = "linux-" + processArch;553                ext = ".so";554            }555            else if (Platform.IsMacOS)556            {557                folderName = "osx-" + processArch;558                ext = ".dylib";559            }560 561            return folderName;562        }563 564        #endregion Private_Methods565    }566 567    /// <summary>568    /// This is the managed entry point for Microsoft.PowerShell.CoreCLR.AssemblyLoadContext.dll.569    /// </summary>570    public static class PowerShellAssemblyLoadContextInitializer571    {572        /// <summary>573        /// Create a singleton of PowerShellAssemblyLoadContext.574        /// Then register to the Resolving event of the load context that loads this assembly.575        /// </summary>576        /// <remarks>577        /// This method is to be used by native host whose TPA list doesn't include PS assemblies, such as the578        /// in-box Nano powershell, the PS remote WinRM plugin, in-box Nano DSC and in-box Nano SCOM agent.579        /// </remarks>580        /// <param name="basePaths">581        /// Base directory paths that are separated by semicolon ';'.582        /// They will be the default paths to probe assemblies.583        /// </param>584        public static void SetPowerShellAssemblyLoadContext([MarshalAs(UnmanagedType.LPWStr)] string basePaths)585        {586            ArgumentException.ThrowIfNullOrEmpty(basePaths);587 588            // Disallow calling this method from native code for more than once.589            PowerShellAssemblyLoadContext.InitializeSingleton(basePaths, throwOnReentry: true);590        }591    }592 593    /// <summary>594    /// Provides helper functions to facilitate calling managed code from a native PowerShell host.595    /// </summary>596    public static unsafe class PowerShellUnsafeAssemblyLoad597    {598        /// <summary>599        /// Load an assembly in memory from unmanaged code.600        /// </summary>601        /// <remarks>602        /// This API is covered by the experimental feature 'PSLoadAssemblyFromNativeCode',603        /// and it may be deprecated and removed in future.604        /// </remarks>605        /// <param name="data">Unmanaged pointer to assembly data buffer.</param>606        /// <param name="size">Size in bytes of the assembly data buffer.</param>607        /// <returns>Returns zero on success and non-zero on failure.</returns>608        [UnmanagedCallersOnly]609        public static int LoadAssemblyFromNativeMemory(IntPtr data, int size)610        {611            int result = 0;612            try613            {614                using var stream = new UnmanagedMemoryStream((byte*)data, size);615                AssemblyLoadContext.Default.LoadFromStream(stream);616            }617            catch618            {619                result = -1;620            }621 622            ApplicationInsightsTelemetry.SendUseTelemetry("PowerShellUnsafeAssemblyLoad", result == 0 ? "1" : "0");623            return result;624        }625    }626}627