Skip to content

Run a function added as a CommandInfo in its own module scope - #28187

Open
Nova-Logic wants to merge 2 commits into
PowerShell:masterfrom
Nova-Logic:fix-addcommand-commandinfo-module-scope
Open

Nova-Logic wants to merge 2 commits into
PowerShell:masterfrom
Nova-Logic:fix-addcommand-commandinfo-module-scope

Conversation

@Nova-Logic

Copy link
Copy Markdown

A script function passed to the pipeline as a CommandInfo (PowerShell.AddCommand(CommandInfo)) runs in the caller's session state instead of its module's. LocalPipeline.CreateCommandProcessBase creates the CommandProcessor with LocalRunspace.ExecutionContext.EngineSessionState, while command discovery (CommandDiscovery.CreateCommandProcessorForScript / GetScriptAsCmdletProcessor) uses functionInfo.ScriptBlock.SessionStateInternal. As a result, a module function invoked this way cannot see its $script: variables or the module's private functions, whereas the same function invoked by name works.

This PR makes CreateCommandProcessBase use the function's own session state, falling back to the engine session state as before.

Repro (same result on Windows PowerShell 5.1 and PowerShell 7.6):

New-Module -Name ReproMod -ScriptBlock {
    $script:Target = { "called with $args" }
    function Get-PrivateValue { 'private' }
    function Invoke-Repro { & $script:Target (Get-PrivateValue) }
    Export-ModuleMember -Function Invoke-Repro
} | Import-Module

$ps = [powershell]::Create('CurrentRunspace')
$ps.AddCommand((Get-Command ReproMod\Invoke-Repro)).Invoke()   # nothing
$ps.Streams.Error   # The term 'Get-PrivateValue' is not recognized ...

Invoke-Repro                                                   # called with private

PR Context

The Hyper-V module hits this on PowerShell 7 for every change to a clustered VM. After Add-VMHardDiskDrive, Remove-VMHardDiskDrive and similar cmdlets, Microsoft.HyperV.PowerShell.ClusterUtilities.UpdateClusterVMConfiguration looks up FailoverClusters\Update-ClusterVirtualMachineConfiguration and runs it with PowerShell.Create(RunspaceMode.CurrentRunspace).AddCommand(commandInfo). On PowerShell 7, FailoverClusters is loaded through the Windows PowerShell compatibility session, so the command is an implicit-remoting proxy function. Its body calls & $script:InvokeCommand ..., and because it runs outside its module, $script:InvokeCommand is $null:

Microsoft.HyperV.PowerShell.VirtualizationException: The Update-ClusterVirtualMachineConfiguration command could not be completed.
 ---> System.Management.Automation.RuntimeException: The expression after '&' in a pipeline element produced an object that was not valid.
      It must result in a command name, a script block, or a CommandInfo object.
   at Microsoft.HyperV.PowerShell.ClusterUtilities.UpdateClusterVMConfiguration(...)

The VM change is applied, but the cluster's VM configuration is not refreshed. Invoking the same proxy function by name, or with AddCommand("FailoverClusters\Update-ClusterVirtualMachineConfiguration"), succeeds. That is the behaviour this PR gives the CommandInfo path.

Related, but not changed here: for a module-qualified name, CommandInvocationIntrinsics.GetCommand(name, CommandTypes.Cmdlet) also returns functions. The module auto-loading branch (CommandDiscovery.TryModuleAutoLoading) does not apply the CommandTypes filter, which is why the Hyper-V module receives the proxy function in the first place. This has been the behaviour since Windows PowerShell, and callers such as the Hyper-V module now depend on it: with the filter applied they would skip the cluster refresh with only a warning.

PR Checklist

Tests added to test/powershell/engine/Api/PSCommand.Tests.ps1:

  • a module function added as a CommandInfo sees its $script: variables and private functions. It fails on 7.5.4 with "The term 'Get-PrivateValue' is not recognized" and passes with this change;
  • a global function added as a CommandInfo keeps working (no change in behaviour).

Copilot AI balanced review requested due to automatic review settings October 12, 2026 21:04
@Nova-Logic
Nova-Logic requested a review from a team as a code owner October 12, 2026 21:04
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The defining session state must not be used when the CommandInfo belongs to another runspace.

1 open finding
What changed in this PR

Updates AddCommand(CommandInfo) so module functions execute in their defining module scope.

Changes:

  • Selects the function’s defining session state.
  • Adds module-scoped and global-function regression tests.
File Description
LocalPipeline.cs Uses the function’s defining session state.
PSCommand.Tests.ps1 Tests module and global function invocation.

🧠 Review effort: Balanced


💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/System.Management.Automation/engine/hostifaces/LocalPipeline.cs Outdated
@Nova-Logic

Copy link
Copy Markdown
Author

Nova-Logic please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.

@microsoft-github-policy-service agree [company="{your company}"]

Options:

  • (default - no company specified) I have sole ownership of intellectual property rights to my Submissions and I am not making Submissions in the course of work for my employer.
@microsoft-github-policy-service agree
  • (when company given) I am making Submissions in the course of work for my employer (or my employer has intellectual property rights in my Submissions by contract or applicable law). I have permission from my employer to make Submissions and enter into this Agreement on behalf of my employer. By signing below, the defined term “You” includes me and my employer.
@microsoft-github-policy-service agree company="Microsoft"

Contributor License Agreement

@microsoft-github-policy-service agree

`LocalPipeline.CreateCommandProcessBase` created the processor for an
`IScriptCommandInfo` with the caller's session state. A module function
passed to `PowerShell.AddCommand(CommandInfo)` therefore ran outside its
module: `$script:` variables were empty and private helper functions
were not found. Command discovery already uses the function's own
session state (`ScriptBlock.SessionStateInternal`), so invoking the same
function by name worked.

Use the function's session state, falling back to the engine session
state as before.
A CommandInfo captured in another runspace (for example with
[powershell]::Create()) must not run against that runspace's session
state: a session state belongs to one ExecutionContext. Use the defining
session state only when it belongs to the local runspace, otherwise keep
the previous engine session state fallback, and add a cross-runspace test.
@Nova-Logic
Nova-Logic force-pushed the fix-addcommand-commandinfo-module-scope branch from 487e9b4 to 83d722a Compare October 13, 2026 09:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants