Skip to content

Harden PromptForChoiceMultipleSelection host call - #28129

Open
Jordan Borean (jborean93) wants to merge 1 commit into
PowerShell:masterfrom
jborean93:prompt-choices-casting
Open

Jordan Borean (jborean93) wants to merge 1 commit into
PowerShell:masterfrom
jborean93:prompt-choices-casting

Conversation

@jborean93

Copy link
Copy Markdown
Collaborator

PR Summary

Fixes IHostUISupportsMultipleChoiceSelection.PromptForChoice over PowerShell remoting when defaultChoices is anything other than a Collection<int> or $null. There are two fixes in this commit.

The first is to make sure that PromptForChoice serializes the defaultChoices value as Collection<int>. This ensures that unpatched clients connecting to a patched server can deserialize the expected value for this argument.

The second is to support decoding a host method array value when the expected type is IEnumerable<int>. This ensures that patches clients connecting to an unpatched server can deserialize the array serialized value.

PR Context

Calling the multiple-choice PromptForChoice overload directly in a local session when using [int[]] for defaultChoices works out of the box:

$choices = [System.Management.Automation.Host.ChoiceDescription[]]@('&Apple', '&Banana', '&Cherry')
$Host.UI.PromptForChoice('Fruit', 'Pick one or more', $choices, [int[]]@(0, 2))
image

Calling the same overload in a remote PSSession with an int[] for defaultChoices fails. The script below reproduces it without any WinRM or SSH setup by opening a named-pipe session back to the current process:

$connInfo = [System.Management.Automation.Runspaces.NamedPipeConnectionInfo]::new($PID)
$runspace = [runspacefactory]::CreateRunspace($Host, $connInfo)
$runspace.Open()
$session = [System.Management.Automation.Runspaces.PSSession]::Create($runspace, 'NamedPipe', $null)

Invoke-Command -Session $session -ScriptBlock {
    $choices = [System.Management.Automation.Host.ChoiceDescription[]]@('&Apple', '&Banana', '&Cherry')
    $Host.UI.PromptForChoice('Fruit', 'Pick one or more', $choices, [int[]]@(0, 2))
}

$session | Remove-PSSession

Before this change:

image

After this change:

image

RemoteHostEncoder chooses the wire format from the value's runtime type when encoding, but the client decodes using the method's declared parameter type. The defaultChoices parameter is declared as IEnumerable<int>, and the client always decoded it as a Collection<int> (an ArrayList on the wire). An int[] is encoded with the array format (mae/mal properties), so the server serializes and sends the call without error and the client then fails to decode it. Other types such as List<int> fail earlier, on the server, with "Remote host method data encoding is not supported for type ...".

Passing an array is the usual case due to the simplicity in declaring [int[]] over [Collections.ObjectModel.Collection[int]]. So in practice this API only worked remotely when defaultChoices was $null or an explicit Collection<int>.

This behaviour dates back to Windows PowerShell 5.1 so is present in all of 7.x.

PR Checklist

Fixes `IHostUISupportsMultipleChoiceSelection.PromptForChoice` over
PowerShell remoting when `defaultChoices` is anything other than a
`Collection<int>` or `$null`. There are two fixes in this commit.

The first is to make sure that `PromptForChoice` serializes the
`defaultChoices` value as `Collection<int>`. This ensures that unpatched
clients connecting to a patched server can deserialize the expected
value for this argument.

The second is to support decoding a host method array value when the
expected type is `IEnumerable<int>`. This ensures that patches clients
connecting to an unpatched server can deserialize the array serialized
value.
Copilot AI balanced review requested due to automatic review settings September 29, 2026 19:12
@azure-pipelines

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is a unit test, with the patched PowerShell it can no longer emit such objects. The test is here to ensure that the decode logic does not regress and PowerShell can continue to receive the serialized array value from older PowerShell versions.

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.

Copilot review overview

🟢 Approval recommended

The compatibility fix is targeted, handles null and empty inputs, and has focused unit and integration coverage.

Review effort: Balanced
Findings: None

What changed in this PR

Hardens multiple-choice host prompts across mixed-version PowerShell remoting endpoints.

Changes:

  • Normalizes default choices to Collection<int> before transmission.
  • Decodes both array and collection wire formats.
  • Adds unit and end-to-end remoting coverage.
File Description
test/​xUnit/​csharp/​test_RemoteHostEncoder.cs Tests encoder round trips and invalid formats.
test/​powershell/​engine/​Remoting/​RemoteHostCalls.Tests.ps1 Tests remote prompts with supported collection forms.
src/​System.Management.Automation/​engine/​remoting/​server/​ServerRemoteHostUserInterface.cs Normalizes default choices before encoding.
src/​System.Management.Automation/​engine/​remoting/​common/​WireDataFormat/​RemoteHostEncoder.cs Supports array and collection decoding.

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

@iSazonov Ilya (iSazonov) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM with one minor comment.

Comment on lines +86 to +90
defaultChoicesCollection = new Collection<int>();
foreach (int choice in defaultChoices)
{
defaultChoicesCollection.Add(choice);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
defaultChoicesCollection = new Collection<int>();
foreach (int choice in defaultChoices)
{
defaultChoicesCollection.Add(choice);
}
defaultChoicesCollection = new Collection<int>(new List<int>(defaultChoices));

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Is that not going to add the items to a list then go through it again to add to the collection? I don't know much about the internal details here but with the current implementation it only enumerates and adds once. Most likely a moot point because this enumerable is going to be so small.

@iSazonov Ilya (iSazonov) added the CL-General Indicates that a PR should be marked as a general cmdlet change in the Change Log label Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CL-General Indicates that a PR should be marked as a general cmdlet change in the Change Log

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants