MegaBites-AI/Windows-powershell
0372
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 