Added setting to disable implicit WinPS module loading (#11332)

This commit is contained in:
Andrew
2020-01-03 10:32:00 -08:00
committed by Aditya Patwardhan
parent ee71081432
commit ea48e07c48
5 changed files with 131 additions and 14 deletions
@@ -103,6 +103,12 @@ namespace System.Management.Automation
var moduleManifestProperties = PsUtils.GetModuleManifestProperties(modulePath, PsUtils.FastModuleManifestAnalysisPropertyNames);
if (moduleManifestProperties != null)
{
if (!Configuration.PowerShellConfig.Instance.IsImplicitWinCompatEnabled() && ModuleIsEditionIncompatible(modulePath, moduleManifestProperties))
{
ModuleIntrinsics.Tracer.WriteLine($"Module lies on the Windows System32 legacy module path and is incompatible with current PowerShell edition, skipping module: {modulePath}");
return null;
}
Version version;
if (ModuleUtils.IsModuleInVersionSubdirectory(modulePath, out version))
{
@@ -485,6 +491,14 @@ namespace System.Management.Automation
{
ModuleIntrinsics.Tracer.WriteLine("Requested caching for {0}", module.Name);
// Don't cache incompatible modules on the system32 module path even if loaded with
// -SkipEditionCheck, since it will break subsequent sessions
if (!Configuration.PowerShellConfig.Instance.IsImplicitWinCompatEnabled() && !module.IsConsideredEditionCompatible)
{
ModuleIntrinsics.Tracer.WriteLine($"Module '{module.Name}' not edition compatible and not cached.");
return;
}
DateTime lastWriteTime;
ModuleCacheEntry moduleCacheEntry;
GetModuleEntryFromCache(module.Path, out lastWriteTime, out moduleCacheEntry);
@@ -2363,18 +2363,58 @@ namespace Microsoft.PowerShell.Commands
bool isConsideredCompatible = ModuleUtils.IsPSEditionCompatible(moduleManifestPath, inferredCompatiblePSEditions);
if (!BaseSkipEditionCheck && !isConsideredCompatible)
{
if (importingModule)
if (PowerShellConfig.Instance.IsImplicitWinCompatEnabled())
{
IList<PSModuleInfo> moduleProxies = ImportModulesUsingWinCompat(new string [] {moduleManifestPath}, null, new ImportModuleOptions());
if (importingModule)
{
IList<PSModuleInfo> moduleProxies = ImportModulesUsingWinCompat(new string [] {moduleManifestPath}, null, new ImportModuleOptions());
// we are loading by a single ManifestPath so expect max of 1
if(moduleProxies.Count > 0)
{
return moduleProxies[0];
// we are loading by a single ManifestPath so expect max of 1
if(moduleProxies.Count > 0)
{
return moduleProxies[0];
}
else
{
return null;
}
}
else
}
else
{
containedErrors = true;
if (writingErrors)
{
return null;
message = StringUtil.Format(
Modules.ImplicitWinCompatDisabled,
moduleManifestPath,
string.Join(',', inferredCompatiblePSEditions));
ErrorRecord er = new ErrorRecord(
new InvalidOperationException(message),
nameof(Modules) + "_" + nameof(Modules.ImplicitWinCompatDisabled),
ErrorCategory.ResourceUnavailable,
moduleManifestPath);
WriteError(er);
}
if (bailOnFirstError)
{
// If we're trying to load the module, return null so that caches
// are not polluted
if (importingModule)
{
return null;
}
// If we return null with Get-Module, a fake module info will be created. Since
// we want to suppress output of the module, we need to do that here.
return new PSModuleInfo(moduleManifestPath, context: null, sessionState: null)
{
HadErrorsLoading = true,
IsConsideredEditionCompatible = false,
};
}
}
}
@@ -49,6 +49,7 @@ namespace System.Management.Automation.Configuration
{
private const string ConfigFileName = "powershell.config.json";
private const string ExecutionPolicyDefaultShellKey = "Microsoft.PowerShell:ExecutionPolicy";
private const string DisableImplicitWinCompatKey = "DisableImplicitWinCompat";
// Provide a singleton
internal static readonly PowerShellConfig Instance = new PowerShellConfig();
@@ -214,6 +215,18 @@ namespace System.Management.Automation.Configuration
}
}
internal bool IsImplicitWinCompatEnabled()
{
bool? settingValue = ReadValueFromFile<bool?>(ConfigScope.CurrentUser, DisableImplicitWinCompatKey);
if (!settingValue.HasValue)
{
// if the setting is not mentioned in configuration files, then the default DisableImplicitWinCompat value is False
settingValue = ReadValueFromFile<bool?>(ConfigScope.AllUsers, DisableImplicitWinCompatKey, defaultValue: false);
}
return !settingValue.Value;
}
/// <summary>
/// Corresponding settings of the original Group Policies.
/// </summary>
@@ -594,6 +594,9 @@
<data name="PSEditionNotSupported" xml:space="preserve">
<value>Module '{0}' does not support current PowerShell edition '{1}'. Its supported editions are '{2}'. Use 'Import-Module -SkipEditionCheck' to ignore the compatibility of this module.</value>
</data>
<data name="ImplicitWinCompatDisabled" xml:space="preserve">
<value>Module '{0}' supports PowerShell edition '{1}' and cannot be loaded implicitly using the Windows Compatibility feature because it is disabled in the settings file. Use 'Import-Module -UseWindowsPowerShell' to load this module with Windows PowerShell or 'Import-Module -SkipEditionCheck' to try to load the module with the current PowerShell.</value>
</data>
<data name="ExperimentalFeatureNameMissingOrEmpty" xml:space="preserve">
<value>A non-empty string value should be specified for an experimental feature declared in the module manifest.</value>
</data>
@@ -390,13 +390,26 @@ Describe "Import-Module from CompatiblePSEditions-checked paths" -Tag "CI" {
}
}
Describe "Additional tests for Import-Module with WinCompat" -Tag "CI" {
Describe "Additional tests for Import-Module with WinCompat" -Tag "Feature" {
BeforeAll {
$originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
if ( ! $IsWindows ) {
$PSDefaultParameterValues["it:skip"] = $true
}
$ModuleName = "DesktopModule"
$ModuleName2 = "DesktopModule2"
$basePath = Join-Path $TestDrive "WinCompatModules"
Remove-Item -Path $basePath -Recurse -ErrorAction SilentlyContinue
# create an incompatible module that generates an error on import
New-EditionCompatibleModule -ModuleName $ModuleName -CompatiblePSEditions "Desktop" -Dir $basePath -ErrorGenerationCode '1/0;'
# create an incompatible module
New-EditionCompatibleModule -ModuleName $ModuleName2 -CompatiblePSEditions "Desktop" -Dir $basePath
}
AfterAll {
$global:PSDefaultParameterValues = $originalDefaultParameterValues
}
Context "Tests that ErrorAction/WarningAction have effect when Import-Module with WinCompat is used" {
@@ -408,34 +421,68 @@ Describe "Additional tests for Import-Module with WinCompat" -Tag "CI" {
Restore-ModulePath
}
It "Verify that Error is generated with default ErrorAction" -Skip:(-not $IsWindows) {
It "Verify that Error is generated with default ErrorAction" {
$LogPath = Join-Path $TestDrive (New-Guid).ToString()
pwsh -NoProfile -NonInteractive -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`');Import-Module $ModuleName" *> $LogPath
$LogPath | Should -FileContentMatch 'divide by zero'
}
It "Verify that Warning is generated with default WarningAction" -Skip:(-not $IsWindows) {
It "Verify that Warning is generated with default WarningAction" {
$LogPath = Join-Path $TestDrive (New-Guid).ToString()
pwsh -NoProfile -NonInteractive -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`');Import-Module $ModuleName" *> $LogPath
$LogPath | Should -FileContentMatch 'loaded in Windows PowerShell'
}
It "Verify that Error is Not generated with -ErrorAction Ignore" -Skip:(-not $IsWindows) {
It "Verify that Error is Not generated with -ErrorAction Ignore" {
$LogPath = Join-Path $TestDrive (New-Guid).ToString()
pwsh -NoProfile -NonInteractive -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`');Import-Module $ModuleName -ErrorAction Ignore" *> $LogPath
$LogPath | Should -Not -FileContentMatch 'divide by zero'
}
It "Verify that Warning is Not generated with -WarningAction Ignore" -Skip:(-not $IsWindows) {
It "Verify that Warning is Not generated with -WarningAction Ignore" {
$LogPath = Join-Path $TestDrive (New-Guid).ToString()
pwsh -NoProfile -NonInteractive -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`');Import-Module $ModuleName -WarningAction Ignore" *> $LogPath
$LogPath | Should -Not -FileContentMatch 'loaded in Windows PowerShell'
}
It "Fails to import incompatible module if implicit WinCompat is disabled in config" {
$LogPath = Join-Path $TestDrive (New-Guid).ToString()
$ConfigPath = Join-Path $TestDrive 'powershell.config.json'
'{"DisableImplicitWinCompat" : "True"}' | Out-File -Force $ConfigPath
pwsh -NoProfile -NonInteractive -settingsFile $ConfigPath -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`');Import-Module $ModuleName2" *> $LogPath
$LogPath | Should -FileContentMatch 'cannot be loaded implicitly using the Windows Compatibility'
}
It "Fails to auto-import incompatible module during CommandDiscovery\ModuleAutoload if implicit WinCompat is Disabled in config" {
$LogPath = Join-Path $TestDrive (New-Guid).ToString()
$ConfigPath = Join-Path $TestDrive 'powershell.config.json'
'{"DisableImplicitWinCompat" : "True"}' | Out-File -Force $ConfigPath
pwsh -NoProfile -NonInteractive -settingsFile $ConfigPath -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`'); Test-$ModuleName2" *> $LogPath
$LogPath | Should -FileContentMatch 'not recognized as the name of a cmdlet'
}
It "Successfully auto-imports incompatible module during CommandDiscovery\ModuleAutoload if implicit WinCompat is Enabled in config" {
$LogPath = Join-Path $TestDrive (New-Guid).ToString()
$ConfigPath = Join-Path $TestDrive 'powershell.config.json'
'{"DisableImplicitWinCompat" : "False"}' | Out-File -Force $ConfigPath
pwsh -NoProfile -NonInteractive -settingsFile $ConfigPath -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`'); Test-$ModuleName2" *> $LogPath
$LogPath | Should -FileContentMatch 'True'
}
}
}
Describe "PSModulePath changes interacting with other PowerShell processes" -Tag "Feature" {
$PSDefaultParameterValues = @{ 'It:Skip' = (-not $IsWindows) }
BeforeAll {
$originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
if ( ! $IsWindows ) {
$PSDefaultParameterValues["it:skip"] = $true
}
}
AfterAll {
$global:PSDefaultParameterValues = $originalDefaultParameterValues
}
Context "System32 module path prepended to PSModulePath" {
BeforeAll {