PowerShell / PowerShell/PowerShell

MshLog.GetLogContext throws InvalidCastException (crashes host) when InvocationInfo.MyCommand is a RemoteCommandInfo with CommandType Application/ExternalScript

Open Beginner friendly
#27,720 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C#
Stars
55.5k
Forks
8.5k
Avg merge
1d 2h
Merged PRs (30d)
88

Description

Summary

MshLog.GetLogContext unconditionally down-casts InvocationInfo.MyCommand to ApplicationInfo / ExternalScriptInfo based solely on its CommandType. A RemoteCommandInfo (the type the remoting deserializer produces via InvocationInfo.FromPSObjectForRemoting) can report CommandType.Application or CommandType.ExternalScript but is not an ApplicationInfo/ExternalScriptInfo, so the cast throws InvalidCastException.

Because this runs while a terminating error is being logged (MshCommandRuntime.ManageExceptionMshLog.LogCommandHealthEventGetLogContext) on the pipeline worker thread (LocalPipeline.InvokeThreadProc), and that thread has no catch-all, the exception is unhandled and terminates the entire process.

The offending code

https://github.com/PowerShell/PowerShell/blob/6633089f88c18574effae4a806eabf0e8345c003/src/System.Management.Automation/logging/MshLog.cs#L826-L840

if (invocationInfo.MyCommand != null)
{
    logContext.CommandName = invocationInfo.MyCommand.Name;
    logContext.CommandType = invocationInfo.MyCommand.CommandType.ToString();

    switch (invocationInfo.MyCommand.CommandType)
    {
        case CommandTypes.Application:
            logContext.CommandPath = ((ApplicationInfo)invocationInfo.MyCommand).Path;      // <-- throws for RemoteCommandInfo
            break;
        case CommandTypes.ExternalScript:
            logContext.CommandPath = ((ExternalScriptInfo)invocationInfo.MyCommand).Path;   // <-- same
            break;
    }
}

RemoteCommandInfo derives directly from CommandInfo and is constructed by InvocationInfo.FromPSObjectForRemoting, copying CommandType verbatim from the serialized CommandInfo_CommandType:

internal static RemoteCommandInfo FromPSObjectForRemoting(PSObject psObject)
{
    ...
    CommandTypes type = RemotingDecoder.GetPropertyValue<CommandTypes>(psObject, "CommandInfo_CommandType");
    string name = RemotingDecoder.GetPropertyValue<string>(psObject, "CommandInfo_Name");
    commandInfo = new RemoteCommandInfo(name, type);
    ...
}

So whenever an error record is rehydrated from a remote/serialized source (PowerShell remoting, Invoke-Command, or any host that uses the remoting serialization to surface errors) and the original failing command was a native application or an external script, MyCommand is a RemoteCommandInfo reporting that CommandType — and the cast is invalid.

Steps to reproduce

Minimal, deterministic reproduction of the invalid cast (this synthesizes exactly the object InvocationInfo.FromPSObjectForRemoting builds when deserializing a remote error for a native command):

using System;
using System.Management.Automation;
using System.Reflection;

var sma = typeof(PSObject).Assembly;
var remoteCommandInfoType = sma.GetType("System.Management.Automation.RemoteCommandInfo")!;

// Same shape the remoting deserializer produces: CommandType is copied from the wire.
var remoteCommandInfo = (CommandInfo)Activator.CreateInstance(
    remoteCommandInfoType,
    BindingFlags.Instance | BindingFlags.NonPublic, binder: null,
    args: new object[] { "some-native-tool.exe", CommandTypes.Application }, culture: null)!;

Console.WriteLine(remoteCommandInfo.CommandType);          // Application
Console.WriteLine(remoteCommandInfo is ApplicationInfo);   // False

// This is precisely what MshLog.GetLogContext does for CommandTypes.Application:
var path = ((ApplicationInfo)(object)remoteCommandInfo).Path;   // System.InvalidCastException

In the wild this is reached from a real terminating error being logged; the crash observed in a PowerShell-SDK host was:

Unhandled exception. System.InvalidCastException: Unable to cast object of type
'System.Management.Automation.RemoteCommandInfo' to type 'System.Management.Automation.ApplicationInfo'.
   at System.Management.Automation.MshLog.GetLogContext(ExecutionContext executionContext, InvocationInfo invocationInfo, Severity severity)
   at System.Management.Automation.MshLog.LogCommandHealthEvent(ExecutionContext executionContext, Exception exception, Severity severity)
   at System.Management.Automation.MshCommandRuntime.ManageException(Exception e)
   at System.Management.Automation.CommandProcessorBase.ManageInvocationException(Exception e)
   at System.Management.Automation.CommandProcessorBase.Complete()
   at System.Management.Automation.Internal.PipelineProcessor.DoCompleteCore(CommandProcessorBase commandRequestingUpstreamCommandsToStop)
   at System.Management.Automation.Internal.PipelineProcessor.SynchronousExecuteEnumerate(Object input)
   at System.Management.Automation.Runspaces.LocalPipeline.InvokeHelper()
   at System.Management.Automation.Runspaces.LocalPipeline.InvokeThreadProc()
   at System.Management.Automation.Runspaces.PipelineThread.WorkerProc()
   at System.Threading.Thread.StartHelper.Callback(Object state)

LocalPipeline.InvokeThreadProc only catches PipelineStoppedException, RuntimeException, ScriptCallDepthException, SecurityException, and HaltCommandException; an InvalidCastException escapes and the process dies.

Expected behavior

Building a log context for a health event must never throw / crash the host. A command whose CommandType is Application/ExternalScript but which is not the corresponding concrete type (e.g. a RemoteCommandInfo) should be handled gracefully (skip CommandPath, or use it if available), and logging should never be able to terminate the process.

Actual behavior

InvalidCastException is thrown from GetLogContext, propagates through ManageException on the pipeline worker thread, is unhandled, and terminates the process.

Proposed fix

Use pattern matching instead of unchecked casts, e.g.:

switch (invocationInfo.MyCommand)
{
    case ApplicationInfo applicationInfo:
        logContext.CommandPath = applicationInfo.Path;
        break;
    case ExternalScriptInfo externalScriptInfo:
        logContext.CommandPath = externalScriptInfo.Path;
        break;
}

(Optionally, the health-logging entry points could also be defensive so that a logging failure can never crash the host.)

Environment data

Reproduced against Microsoft.PowerShell.SDK / System.Management.Automation 7.5.4 (hosted in-process on Linux). The offending cast is unchanged on master (6633089), so it affects current builds as well.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in src/System.Management.Automation/logging/MshLog.cs around lines 826-840 and trace the health-logging path through MshCommandRuntime.ManageException. Reproduce the provided RemoteCommandInfo shape, then verify that Application and ExternalScript command types no longer cause logging to throw and terminate the host.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
cli, observability
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.