Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can't start a Process if StartInfo.EnvironmentVariables is called or evaluated on Windows systems #94338

Closed
AptiviCEO opened this issue Nov 3, 2023 · 2 comments

Comments

@AptiviCEO
Copy link

AptiviCEO commented Nov 3, 2023

Description

The Process class contains a property called StartInfo that allows you to customize how the process will start, like UseShellExecute to determine whether the process is created by the operating system or by the application itself.

When UseShellExecute is turned on by setting it to true, the process can't be started with Start() because of an InvalidOperationException. This happens after evaluation of either the Environment or the EnvironmentVariables property from the StartInfo of the Process.

This is the message that is emitted as soon as Start() is called after evaluation:

System.InvalidOperationException: The Process object must have the UseShellExecute property set to false in order to use environment variables.

What's worse is that this issue can be either triggered by hovering over the Process variable on Visual Studio and opening the StartInfo property to see its values while debugging your process start code, or by directly invoking the EnvironmentVariables property from your code as we can see in the POC.

Furthermore, it appears to only affect Windows systems as Linux users don't get this error.

Reproduction Steps

  1. Create a new project with the following code in Program.cs:
namespace poc;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("POC of UseShellExecute pitfall");
        var proc = new Process()
        {
            StartInfo = new("C:/WINDOWS/System32/cmd.exe")
            {
                UseShellExecute = true,
                Verb = "runas"
            }
        };
        Console.WriteLine($"{proc.StartInfo.EnvironmentVariables.Count}");
        proc.Start();
    }
}
  1. Build and run this application on Windows
  2. Observe the InvalidOperationException

Expected behavior

The process will start the Command Prompt on Windows and will not emit an InvalidOperationException

Actual behavior

An error of InvalidOperationException occurs and the process will not launch.

System.InvalidOperationException: The Process object must have the UseShellExecute property set to false in order to use environment variables.

ioex

This error comes from StartWithShellExecuteEx(), which is called by StartCore().

Regression?

This is not a regression, because this issue occurred on .NET Framework, .NET 6.0, and .NET 7.0.

Known Workarounds

One can use a dirty reflection-based workaround to get the StartInfo back to how it was before calling the EnvironmentVariables property:

internal static ProcessStartInfo StripEnvironmentVariables(ProcessStartInfo processStartInfo)
{
    var privateReflection = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.SetField;
    var startInfoType = processStartInfo.GetType();
    var envVarsField = startInfoType.GetField("_environmentVariables", privateReflection);
    envVarsField.SetValue(processStartInfo, null);
    return processStartInfo;
}

Then, one can call this function to re-assign the StartInfo property on the afflicted Process variable:

Console.WriteLine($"{proc.StartInfo.EnvironmentVariables.Count}");
proc.StartInfo = StripEnvironmentVariables(proc.StartInfo)
proc.Start();

This workaround is not needed in cases where UseShellExecute is not enabled.

However, this workaround only lasts until the next evaluation of EnvironmentVariables or Environment.

Configuration

  • .NET 7.0, the latest version
  • Windows 10 22H2 64-bit (19045.3570)

This issue only occurs when running on Windows.

Other information

I've reviewed the source code for the .NET runtime and found that when starting processes with UseShellExecute enabled, StartCore() calls StartWithShellExecuteEx(), which checks the state of the Process instance before starting the process.

It looks like that one of these checks is for the startInfo._environmentVariables variable on line 47.

if (startInfo._environmentVariables != null)
    throw new InvalidOperationException(SR.CantUseEnvVars);

This variable is populated when any attempt to evaluate EnvironmentVariables or Environment is done either by the Visual Studio debugger or by the user code. This is on line 91.

public IDictionary<string, string?> Environment
{
    get
    {
        if (_environmentVariables == null)
        {
            IDictionary envVars = System.Environment.GetEnvironmentVariables();

            _environmentVariables = new DictionaryWrapper(new Dictionary<string, string?>(
                envVars.Count,
                OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal));

            // Manual use of IDictionaryEnumerator instead of foreach to avoid DictionaryEntry box allocations.
            IDictionaryEnumerator e = envVars.GetEnumerator();
            Debug.Assert(!(e is IDisposable), "Environment.GetEnvironmentVariables should not be IDisposable.");
            while (e.MoveNext())
            {
                DictionaryEntry entry = e.Entry;
                _environmentVariables.Add((string)entry.Key, (string?)entry.Value);
            }
        }
        return _environmentVariables;
    }
}
@ghost ghost added the untriaged New issue has not been triaged by the area owner label Nov 3, 2023
@ghost
Copy link

ghost commented Nov 3, 2023

Tagging subscribers to this area: @dotnet/area-system-diagnostics-process
See info in area-owners.md if you want to be subscribed.

Issue Details

Description

The Process class contains a property called StartInfo that allows you to customize how the process will start, like UseShellExecute to determine whether the process is created by the operating system or by the application itself.

When UseShellExecute is turned on by setting it to true, the process can't be started with Start() because of an InvalidOperationException. This happens after evaluation of either the Environment or the EnvironmentVariables property from the StartInfo of the Process.

This is the message that is emitted as soon as Start() is called after evaluation:

System.InvalidOperationException: The Process object must have the UseShellExecute property set to false in order to use environment variables.

What's worse is that this issue can be either triggered by hovering over the Process variable on Visual Studio and opening the StartInfo property to see its values while debugging your process start code, or by directly invoking the EnvironmentVariables property from your code as we can see in the POC.

Furthermore, it appears to only affect Windows systems as Linux users don't get this error.

Reproduction Steps

  1. Create a new project with the following code in Program.cs:
namespace poc;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("POC of UseShellExecute pitfall");
        var proc = new Process()
        {
            StartInfo = new("C:/WINDOWS/System32/cmd.exe")
            {
                UseShellExecute = true,
                Verb = "runas"
            }
        };
        Console.WriteLine($"{proc.StartInfo.EnvironmentVariables.Count}");
        proc.Start();
    }
}
  1. Build and run this application on Windows
  2. Observe the InvalidOperationException

Expected behavior

The process will start the Command Prompt on Windows and will not emit an InvalidOperationException

Actual behavior

An error of InvalidOperationException occurs and the process will not launch.

System.InvalidOperationException: The Process object must have the UseShellExecute property set to false in order to use environment variables.

ioex

This error comes from StartWithShellExecuteEx(), which is called by StartCore().

Regression?

This is not a regression, because this issue occurred on .NET Framework, .NET 6.0, and .NET 7.0.

Known Workarounds

One can use a dirty reflection-based workaround to get the StartInfo back to how it was before calling the EnvironmentVariables property:

internal static ProcessStartInfo StripEnvironmentVariables(ProcessStartInfo processStartInfo)
{
    var privateReflection = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.SetField;
    var startInfoType = processStartInfo.GetType();
    var envVarsField = startInfoType.GetField("_environmentVariables", privateReflection);
    envVarsField.SetValue(processStartInfo, null);
    return processStartInfo;
}

Then, one can call this function to re-assign the StartInfo property on the afflicted Process variable:

Console.WriteLine($"{proc.StartInfo.EnvironmentVariables.Count}");
proc.StartInfo = StripEnvironmentVariables(proc.StartInfo)
proc.Start();

This workaround is not needed in cases where UseShellExecute is not enabled.

However, this workaround only lasts until the next evaluation of EnvironmentVariables or Environment.

Configuration

  • .NET 7.0, the latest version
  • Windows 10 22H2 64-bit (19045.3570)

This issue only occurs when running on Windows.

Other information

I've reviewed the source code for the .NET runtime and found that when starting processes with UseShellExecute enabled, StartCore() calls StartWithShellExecuteEx(), which checks the state of the Process instance before starting the process.

It looks like that one of these checks is for the startInfo._environmentVariables variable on line 47.

if (startInfo._environmentVariables != null)
    throw new InvalidOperationException(SR.CantUseEnvVars);

This variable is populated when any attempt to evaluate EnvironmentVariables or Environment is done either by the Visual Studio debugger or by the user code. This is on line 91.

public IDictionary<string, string?> Environment
{
    get
    {
        if (_environmentVariables == null)
        {
            IDictionary envVars = System.Environment.GetEnvironmentVariables();

            _environmentVariables = new DictionaryWrapper(new Dictionary<string, string?>(
                envVars.Count,
                OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal));

            // Manual use of IDictionaryEnumerator instead of foreach to avoid DictionaryEntry box allocations.
            IDictionaryEnumerator e = envVars.GetEnumerator();
            Debug.Assert(!(e is IDisposable), "Environment.GetEnvironmentVariables should not be IDisposable.");
            while (e.MoveNext())
            {
                DictionaryEntry entry = e.Entry;
                _environmentVariables.Add((string)entry.Key, (string?)entry.Value);
            }
        }
        return _environmentVariables;
    }
}
Author: AptiviCEO
Assignees: -
Labels:

area-System.Diagnostics.Process, untriaged

Milestone: -

@jozkee
Copy link
Member

jozkee commented Nov 6, 2023

Duplicate of #58069.

As suggested there, this could be fixed by attempting to detect mutation of the IDictionary. @AptiviCEO would you be interested in sending a PR?

@jozkee jozkee closed this as not planned Won't fix, can't repro, duplicate, stale Nov 6, 2023
@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Nov 6, 2023
@ghost ghost locked as resolved and limited conversation to collaborators Dec 7, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

2 participants