Fix for JEA session leaking functions (#19024)

This commit is contained in:
Travis Plunk
2023-02-01 16:19:51 -08:00
committed by GitHub
parent c87f2a48fd
commit 137b4d788b
3 changed files with 157 additions and 83 deletions
@@ -2966,13 +2966,20 @@ namespace System.Management.Automation.Runspaces
HashSet<string> unresolvedCmdsToExpose)
{
RunspaceOpenModuleLoadException exceptionToReturn = null;
List<PSModuleInfo> processedModules = new List<PSModuleInfo>();
foreach (object module in moduleList)
{
string moduleName = module as string;
if (moduleName != null)
{
exceptionToReturn = ProcessOneModule(initializedRunspace, moduleName, null, path, publicCommands);
exceptionToReturn = ProcessOneModule(
initializedRunspace: initializedRunspace,
name: moduleName,
moduleInfoToLoad: null,
path: path,
publicCommands: publicCommands,
processedModules: processedModules);
}
else
{
@@ -2983,7 +2990,13 @@ namespace System.Management.Automation.Runspaces
{
// if only name is specified in the module spec, just try import the module
// ie., don't take the performance overhead of calling GetModule.
exceptionToReturn = ProcessOneModule(initializedRunspace, moduleSpecification.Name, null, path, publicCommands);
exceptionToReturn = ProcessOneModule(
initializedRunspace: initializedRunspace,
name: moduleSpecification.Name,
moduleInfoToLoad: null,
path: path,
publicCommands: publicCommands,
processedModules: processedModules);
}
else
{
@@ -2991,7 +3004,13 @@ namespace System.Management.Automation.Runspaces
if (moduleInfos != null && moduleInfos.Count > 0)
{
exceptionToReturn = ProcessOneModule(initializedRunspace, moduleSpecification.Name, moduleInfos[0], path, publicCommands);
exceptionToReturn = ProcessOneModule(
initializedRunspace: initializedRunspace,
name: moduleSpecification.Name,
moduleInfoToLoad: moduleInfos[0],
path: path,
publicCommands: publicCommands,
processedModules: processedModules);
}
else
{
@@ -3040,7 +3059,11 @@ namespace System.Management.Automation.Runspaces
string commandToMakeVisible = Utils.ParseCommandName(unresolvedCommand, out moduleName);
bool found = false;
foreach (CommandInfo cmd in LookupCommands(commandToMakeVisible, moduleName, initializedRunspace.ExecutionContext))
foreach (CommandInfo cmd in LookupCommands(
commandPattern: commandToMakeVisible,
moduleName: moduleName,
context: initializedRunspace.ExecutionContext,
processedModules: processedModules))
{
if (!found)
{
@@ -3091,11 +3114,13 @@ namespace System.Management.Automation.Runspaces
/// <param name="commandPattern"></param>
/// <param name="moduleName"></param>
/// <param name="context"></param>
/// <param name="processedModules"></param>
/// <returns></returns>
private static IEnumerable<CommandInfo> LookupCommands(
string commandPattern,
string moduleName,
ExecutionContext context)
ExecutionContext context,
List<PSModuleInfo> processedModules)
{
bool isWildCardPattern = WildcardPattern.ContainsWildcardCharacters(commandPattern);
var searchOptions = isWildCardPattern ?
@@ -3109,7 +3134,11 @@ namespace System.Management.Automation.Runspaces
CommandOrigin cmdOrigin = CommandOrigin.Runspace;
while (true)
{
foreach (CommandInfo commandInfo in context.SessionState.InvokeCommand.GetCommands(commandPattern, CommandTypes.All, searchOptions, cmdOrigin))
foreach (CommandInfo commandInfo in context.SessionState.InvokeCommand.GetCommands(
name: commandPattern,
commandTypes: CommandTypes.All,
options: searchOptions,
commandOrigin: cmdOrigin))
{
// If module name is provided then use it to restrict returned results.
if (haveModuleName && !moduleName.Equals(commandInfo.ModuleName, StringComparison.OrdinalIgnoreCase))
@@ -3139,13 +3168,43 @@ namespace System.Management.Automation.Runspaces
// Next try internal search.
cmdOrigin = CommandOrigin.Internal;
}
// If the command is associated with a module, try finding the command in the imported module list.
// The SessionState function table holds only one command name, and if two or more modules contain
// a command with the same name, only one of them will appear in the function table search above.
if (!found && haveModuleName)
{
var pattern = new WildcardPattern(commandPattern);
foreach (PSModuleInfo moduleInfo in processedModules)
{
if (moduleInfo.Name.Equals(moduleName, StringComparison.OrdinalIgnoreCase))
{
foreach (var cmd in moduleInfo.ExportedCommands.Values)
{
if (pattern.IsMatch(cmd.Name))
{
yield return cmd;
}
}
break;
}
}
}
}
/// <summary>
/// If <paramref name="moduleInfoToLoad"/> is null, import module using <paramref name="name"/>. Otherwise,
/// import module using <paramref name="moduleInfoToLoad"/>
/// </summary>
private RunspaceOpenModuleLoadException ProcessOneModule(Runspace initializedRunspace, string name, PSModuleInfo moduleInfoToLoad, string path, HashSet<CommandInfo> publicCommands)
private RunspaceOpenModuleLoadException ProcessOneModule(
Runspace initializedRunspace,
string name,
PSModuleInfo moduleInfoToLoad,
string path,
HashSet<CommandInfo> publicCommands,
List<PSModuleInfo> processedModules)
{
using (PowerShell pse = PowerShell.Create())
{
@@ -3182,6 +3241,11 @@ namespace System.Management.Automation.Runspaces
c = new CmdletInfo("Out-Default", typeof(OutDefaultCommand), null, null, initializedRunspace.ExecutionContext);
pse.AddCommand(new Command(c));
}
else
{
// For runspace init module processing, pass back the PSModuleInfo to the output pipeline.
cmd.Parameters.Add("PassThru");
}
pse.Runspace = initializedRunspace;
// Module import should be run in FullLanguage mode since it is running in
@@ -3190,7 +3254,10 @@ namespace System.Management.Automation.Runspaces
pse.Runspace.ExecutionContext.LanguageMode = PSLanguageMode.FullLanguage;
try
{
pse.Invoke();
// For runspace init module processing, collect the imported PSModuleInfo returned in the output pipeline.
// In other cases, this collection will be empty.
Collection<PSModuleInfo> moduleInfos = pse.Invoke<PSModuleInfo>();
processedModules.AddRange(moduleInfos);
}
finally
{
@@ -7192,6 +7192,9 @@ namespace Microsoft.PowerShell.Commands
CommandOrigin.Internal,
targetSessionState.ExecutionContext);
// Note that the module 'func' and the function table 'functionInfo' instances are now linked
// together (see 'CopiedCommand' in CommandInfo class), so setting visibility on one also
// sets it on the other.
SetCommandVisibility(isImportModulePrivate, functionInfo);
functionInfo.Module = sourceModule;
@@ -1245,11 +1245,18 @@ namespace System.Management.Automation
name != null,
"The caller should verify the name");
var functionInfos = GetFunctions();
FunctionInfo existingValue;
Dictionary<string, FunctionInfo> functionInfos = GetFunctions();
FunctionInfo result;
if (!functionInfos.TryGetValue(name, out existingValue))
// Functions are equal only if they have the same name and if they come from the same module (if any).
// If the function is not associated with a module then the info 'ModuleName' property is set to empty string.
// If the new function has the same name of an existing function, but different module names, then the
// existing table function is replaced with the new function.
if (!functionInfos.TryGetValue(name, out FunctionInfo existingValue) ||
(originalFunction != null &&
!existingValue.ModuleName.Equals(originalFunction.ModuleName, StringComparison.OrdinalIgnoreCase)))
{
// Add new function info to function table and return.
result = functionFactory(name, function, originalFunction, options, context, helpFile);
functionInfos[name] = result;
@@ -1257,81 +1264,78 @@ namespace System.Management.Automation
{
GetAllScopeFunctions()[name] = result;
}
return result;
}
// Update the existing function.
// Make sure the function isn't constant or readonly.
SessionState.ThrowIfNotVisible(origin, existingValue);
if (IsFunctionOptionSet(existingValue, ScopedItemOptions.Constant) ||
(!force && IsFunctionOptionSet(existingValue, ScopedItemOptions.ReadOnly)))
{
SessionStateUnauthorizedAccessException e =
new SessionStateUnauthorizedAccessException(
name,
SessionStateCategory.Function,
"FunctionNotWritable",
SessionStateStrings.FunctionNotWritable);
throw e;
}
// Ensure we are not trying to set the function to constant as this can only be
// done at creation time.
if ((options & ScopedItemOptions.Constant) != 0)
{
SessionStateUnauthorizedAccessException e =
new SessionStateUnauthorizedAccessException(
name,
SessionStateCategory.Function,
"FunctionCannotBeMadeConstant",
SessionStateStrings.FunctionCannotBeMadeConstant);
throw e;
}
// Ensure we are not trying to remove the AllScope option.
if ((options & ScopedItemOptions.AllScope) == 0 &&
IsFunctionOptionSet(existingValue, ScopedItemOptions.AllScope))
{
SessionStateUnauthorizedAccessException e =
new SessionStateUnauthorizedAccessException(
name,
SessionStateCategory.Function,
"FunctionAllScopeOptionCannotBeRemoved",
SessionStateStrings.FunctionAllScopeOptionCannotBeRemoved);
throw e;
}
FunctionInfo existingFunction = existingValue;
// If the function type changes (i.e.: function to workflow or back)
// then we need to replace what was there.
FunctionInfo newValue = functionFactory(name, function, originalFunction, options, context, helpFile);
bool changesFunctionType = existingFunction.GetType() != newValue.GetType();
// Since the options are set after the script block, we have to
// forcefully apply the script block if the options will be
// set to not being ReadOnly.
if (changesFunctionType ||
((existingFunction.Options & ScopedItemOptions.ReadOnly) != 0 && force))
{
result = newValue;
functionInfos[name] = newValue;
}
else
{
// Make sure the function isn't constant or readonly
SessionState.ThrowIfNotVisible(origin, existingValue);
if (IsFunctionOptionSet(existingValue, ScopedItemOptions.Constant) ||
(!force && IsFunctionOptionSet(existingValue, ScopedItemOptions.ReadOnly)))
{
SessionStateUnauthorizedAccessException e =
new SessionStateUnauthorizedAccessException(
name,
SessionStateCategory.Function,
"FunctionNotWritable",
SessionStateStrings.FunctionNotWritable);
throw e;
}
// Ensure we are not trying to set the function to constant as this can only be
// done at creation time.
if ((options & ScopedItemOptions.Constant) != 0)
{
SessionStateUnauthorizedAccessException e =
new SessionStateUnauthorizedAccessException(
name,
SessionStateCategory.Function,
"FunctionCannotBeMadeConstant",
SessionStateStrings.FunctionCannotBeMadeConstant);
throw e;
}
// Ensure we are not trying to remove the AllScope option
if ((options & ScopedItemOptions.AllScope) == 0 &&
IsFunctionOptionSet(existingValue, ScopedItemOptions.AllScope))
{
SessionStateUnauthorizedAccessException e =
new SessionStateUnauthorizedAccessException(
name,
SessionStateCategory.Function,
"FunctionAllScopeOptionCannotBeRemoved",
SessionStateStrings.FunctionAllScopeOptionCannotBeRemoved);
throw e;
}
FunctionInfo existingFunction = existingValue;
FunctionInfo newValue = null;
// If the function type changes (i.e.: function to workflow or back)
// then we need to blast what was there
newValue = functionFactory(name, function, originalFunction, options, context, helpFile);
bool changesFunctionType = existingFunction.GetType() != newValue.GetType();
// Since the options are set after the script block, we have to
// forcefully apply the script block if the options will be
// set to not being ReadOnly
if (changesFunctionType ||
((existingFunction.Options & ScopedItemOptions.ReadOnly) != 0 && force))
{
result = newValue;
functionInfos[name] = newValue;
}
else
{
bool applyForce = force || (options & ScopedItemOptions.ReadOnly) == 0;
existingFunction.Update(newValue, applyForce, options, helpFile);
result = existingFunction;
}
bool applyForce = force || (options & ScopedItemOptions.ReadOnly) == 0;
existingFunction.Update(newValue, applyForce, options, helpFile);
result = existingFunction;
}
return result;