PowerShell / PowerShell/PowerShell
MshLog.GetLogContext throws InvalidCastException (crashes host) when InvocationInfo.MyCommand is a RemoteCommandInfo with CommandType Application/ExternalScript
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.ManageException → MshLog.LogCommandHealthEvent → GetLogContext) 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
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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