From 716f641e46750393777d6060c8481ee459f1d0e6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 30 Mar 2026 13:10:37 +0000
Subject: [PATCH 01/18] Address review comments: fix "OS handle" doc nit,
convert ShellExecuteHelper to private static methods
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/6cd5d18a-42f4-41d2-9ed4-a230ebb3c71a
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../SafeHandles/SafeProcessHandle.Unix.cs | 2 +-
.../SafeHandles/SafeProcessHandle.Windows.cs | 69 +++++++++++++-
.../src/System.Diagnostics.Process.csproj | 1 -
.../System/Diagnostics/ShellExecuteHelper.cs | 94 -------------------
4 files changed, 66 insertions(+), 100 deletions(-)
delete mode 100644 src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ShellExecuteHelper.cs
diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs
index f227eeaf4dd18d..3734880c8417cd 100644
--- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs
+++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs
@@ -64,7 +64,7 @@ private static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileH
SafeProcessHandle startedProcess = StartCore(startInfo, stdinHandle, stdoutHandle, stderrHandle, out ProcessWaitState.Holder? waitStateHolder);
// For standalone SafeProcessHandle.Start, we dispose the wait state holder immediately.
- // The DangerousAddRef on the SafeWaitHandle (Unix) keeps the OS handle alive.
+ // The DangerousAddRef on the SafeWaitHandle (Unix) keeps the handle alive.
waitStateHolder?.Dispose();
return startedProcess;
diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
index 416ec91ae11317..b2cffb7dbc1fa8 100644
--- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
+++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
@@ -6,6 +6,7 @@
using System.Runtime.InteropServices;
using System.Security;
using System.Text;
+using System.Threading;
namespace Microsoft.Win32.SafeHandles
{
@@ -63,13 +64,11 @@ private static unsafe SafeProcessHandle StartWithShellExecuteEx(ProcessStartInfo
shellExecuteInfo.fMask |= Interop.Shell32.SEE_MASK_FLAG_NO_UI;
shellExecuteInfo.nShow = ProcessUtils.GetShowWindowFromWindowStyle(startInfo.WindowStyle);
- ShellExecuteHelper executeHelper = new ShellExecuteHelper(&shellExecuteInfo);
- if (!executeHelper.ShellExecuteOnSTAThread())
+ if (!ShellExecuteOnSTAThread(&shellExecuteInfo, out int errorCode))
{
- int errorCode = executeHelper.ErrorCode;
if (errorCode == 0)
{
- errorCode = ShellExecuteHelper.GetShellError(shellExecuteInfo.hInstApp);
+ errorCode = GetShellError(shellExecuteInfo.hInstApp);
}
switch (errorCode)
@@ -282,5 +281,67 @@ ref processInfo // pointer to PROCESS_INFORMATION
}
private int GetProcessIdCore() => Interop.Kernel32.GetProcessId(this);
+
+ private static unsafe bool ShellExecuteOnSTAThread(Interop.Shell32.SHELLEXECUTEINFO* executeInfo, out int errorCode)
+ {
+ bool succeeded = false;
+ bool notPresent = false;
+ int lastError = 0;
+ nuint executeInfoAddress = (nuint)executeInfo; // cast to nuint to allow delegate capture; safe because Join() keeps the caller's stack frame alive for the thread's lifetime
+
+ unsafe void ShellExecuteFunction()
+ {
+ try
+ {
+ if (!(succeeded = Interop.Shell32.ShellExecuteExW((Interop.Shell32.SHELLEXECUTEINFO*)executeInfoAddress)))
+ lastError = Marshal.GetLastWin32Error();
+ }
+ catch (EntryPointNotFoundException)
+ {
+ notPresent = true;
+ }
+ }
+
+ // ShellExecute() requires STA in order to work correctly.
+ if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA)
+ {
+ Thread executionThread = new Thread(ShellExecuteFunction)
+ {
+ IsBackground = true,
+ Name = ".NET Process STA"
+ };
+ executionThread.SetApartmentState(ApartmentState.STA);
+ executionThread.Start();
+ executionThread.Join();
+ }
+ else
+ {
+ ShellExecuteFunction();
+ }
+
+ if (notPresent)
+ throw new PlatformNotSupportedException(SR.UseShellExecuteNotSupported);
+
+ errorCode = lastError;
+ return succeeded;
+ }
+
+ private static int GetShellError(IntPtr error)
+ {
+ return (long)error switch
+ {
+ Interop.Shell32.SE_ERR_FNF => Interop.Errors.ERROR_FILE_NOT_FOUND,
+ Interop.Shell32.SE_ERR_PNF => Interop.Errors.ERROR_PATH_NOT_FOUND,
+ Interop.Shell32.SE_ERR_ACCESSDENIED => Interop.Errors.ERROR_ACCESS_DENIED,
+ Interop.Shell32.SE_ERR_OOM => Interop.Errors.ERROR_NOT_ENOUGH_MEMORY,
+ Interop.Shell32.SE_ERR_DDEFAIL or
+ Interop.Shell32.SE_ERR_DDEBUSY or
+ Interop.Shell32.SE_ERR_DDETIMEOUT => Interop.Errors.ERROR_DDE_FAIL,
+ Interop.Shell32.SE_ERR_SHARE => Interop.Errors.ERROR_SHARING_VIOLATION,
+ Interop.Shell32.SE_ERR_NOASSOC => Interop.Errors.ERROR_NO_ASSOCIATION,
+ Interop.Shell32.SE_ERR_DLLNOTFOUND => Interop.Errors.ERROR_DLL_NOT_FOUND,
+ _ => (int)(long)error,
+ };
+ }
}
}
diff --git a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj
index 10ef7fd5f0bfa2..c01a815d851e91 100644
--- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj
+++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj
@@ -226,7 +226,6 @@
-
diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ShellExecuteHelper.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ShellExecuteHelper.cs
deleted file mode 100644
index e31eb4fd6684ac..00000000000000
--- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ShellExecuteHelper.cs
+++ /dev/null
@@ -1,94 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.IO;
-using System.Runtime.InteropServices;
-using System.Text;
-using System.Threading;
-using Microsoft.Win32.SafeHandles;
-
-namespace System.Diagnostics
-{
- internal sealed unsafe class ShellExecuteHelper
- {
- private readonly Interop.Shell32.SHELLEXECUTEINFO* _executeInfo;
- private bool _succeeded;
- private bool _notpresent;
-
- public ShellExecuteHelper(Interop.Shell32.SHELLEXECUTEINFO* executeInfo)
- {
- _executeInfo = executeInfo;
- }
-
- private void ShellExecuteFunction()
- {
- try
- {
- if (!(_succeeded = Interop.Shell32.ShellExecuteExW(_executeInfo)))
- ErrorCode = Marshal.GetLastWin32Error();
- }
- catch (EntryPointNotFoundException)
- {
- _notpresent = true;
- }
- }
-
- public bool ShellExecuteOnSTAThread()
- {
- // ShellExecute() requires STA in order to work correctly.
-
- if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA)
- {
- ThreadStart threadStart = new ThreadStart(ShellExecuteFunction);
- Thread executionThread = new Thread(threadStart)
- {
- IsBackground = true,
- Name = ".NET Process STA"
- };
- executionThread.SetApartmentState(ApartmentState.STA);
- executionThread.Start();
- executionThread.Join();
- }
- else
- {
- ShellExecuteFunction();
- }
-
- if (_notpresent)
- throw new PlatformNotSupportedException(SR.UseShellExecuteNotSupported);
-
- return _succeeded;
- }
-
- internal static int GetShellError(IntPtr error)
- {
- switch ((long)error)
- {
- case Interop.Shell32.SE_ERR_FNF:
- return Interop.Errors.ERROR_FILE_NOT_FOUND;
- case Interop.Shell32.SE_ERR_PNF:
- return Interop.Errors.ERROR_PATH_NOT_FOUND;
- case Interop.Shell32.SE_ERR_ACCESSDENIED:
- return Interop.Errors.ERROR_ACCESS_DENIED;
- case Interop.Shell32.SE_ERR_OOM:
- return Interop.Errors.ERROR_NOT_ENOUGH_MEMORY;
- case Interop.Shell32.SE_ERR_DDEFAIL:
- case Interop.Shell32.SE_ERR_DDEBUSY:
- case Interop.Shell32.SE_ERR_DDETIMEOUT:
- return Interop.Errors.ERROR_DDE_FAIL;
- case Interop.Shell32.SE_ERR_SHARE:
- return Interop.Errors.ERROR_SHARING_VIOLATION;
- case Interop.Shell32.SE_ERR_NOASSOC:
- return Interop.Errors.ERROR_NO_ASSOCIATION;
- case Interop.Shell32.SE_ERR_DLLNOTFOUND:
- return Interop.Errors.ERROR_DLL_NOT_FOUND;
- default:
- return (int)(long)error;
- }
- }
-
- public int ErrorCode { get; private set; }
- }
-}
From 7b2fad9a1bc4ffd4f6f60c877b3d610373643f85 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 30 Mar 2026 13:24:27 +0000
Subject: [PATCH 02/18] Use field keyword for ProcessId; add test for invalid
handle validation
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/e7d88dfb-ddec-4d6c-b371-7097a11d6d45
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../Microsoft/Win32/SafeHandles/SafeProcessHandle.cs | 12 +++++-------
.../tests/SafeProcessHandleTests.cs | 7 +++++++
2 files changed, 12 insertions(+), 7 deletions(-)
diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs
index 0351614413591f..7cfb01c028d030 100644
--- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs
+++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs
@@ -20,7 +20,6 @@ namespace Microsoft.Win32.SafeHandles
public sealed partial class SafeProcessHandle : SafeHandleZeroOrMinusOneIsInvalid
{
internal static readonly SafeProcessHandle InvalidHandle = new SafeProcessHandle();
- private int _processId = -1;
///
/// Gets the process ID.
@@ -31,16 +30,15 @@ public int ProcessId
{
Validate();
- if (_processId == -1)
+ if (field == -1)
{
- _processId = GetProcessIdCore();
+ field = GetProcessIdCore();
}
- return _processId;
-
+ return field;
}
- private set => _processId = value;
- }
+ private set;
+ } = -1;
///
/// Creates a .
diff --git a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs
index 332abe1cdde2f0..b49235cf04cb25 100644
--- a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs
+++ b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs
@@ -63,6 +63,13 @@ public void CanStartProcess()
}
}
+ [Fact]
+ public void ProcessId_InvalidHandle_ThrowsInvalidOperationException()
+ {
+ using SafeProcessHandle invalidHandle = new SafeProcessHandle();
+ Assert.Throws(() => invalidHandle.ProcessId);
+ }
+
[Fact]
[PlatformSpecific(TestPlatforms.Windows)] // We don't use pidfd on Unix yet
public void CanGetProcessIdForCopyOfTheHandle()
From 2a8a77987318c671ed639b0d8f4c61a320213213 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 30 Mar 2026 13:51:24 +0000
Subject: [PATCH 03/18] Pass SHELLEXECUTEINFO by value; return
hProcess/hInstApp via out params
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/faf61926-cc6a-42c0-a8c6-f0fec98e0b93
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../Win32/SafeHandles/SafeProcessHandle.Windows.cs | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
index b2cffb7dbc1fa8..23c8c1d04d73bc 100644
--- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
+++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
@@ -64,11 +64,11 @@ private static unsafe SafeProcessHandle StartWithShellExecuteEx(ProcessStartInfo
shellExecuteInfo.fMask |= Interop.Shell32.SEE_MASK_FLAG_NO_UI;
shellExecuteInfo.nShow = ProcessUtils.GetShowWindowFromWindowStyle(startInfo.WindowStyle);
- if (!ShellExecuteOnSTAThread(&shellExecuteInfo, out int errorCode))
+ if (!ShellExecuteOnSTAThread(shellExecuteInfo, out int errorCode, out IntPtr hProcess, out IntPtr hInstApp))
{
if (errorCode == 0)
{
- errorCode = GetShellError(shellExecuteInfo.hInstApp);
+ errorCode = GetShellError(hInstApp);
}
switch (errorCode)
@@ -88,7 +88,7 @@ private static unsafe SafeProcessHandle StartWithShellExecuteEx(ProcessStartInfo
// From https://learn.microsoft.com/windows/win32/api/shellapi/ns-shellapi-shellexecuteinfow:
// "In some cases, such as when execution is satisfied through a DDE conversation, no handle will be returned."
// Process.Start will return false if the handle is invalid.
- return new SafeProcessHandle(shellExecuteInfo.hProcess);
+ return new SafeProcessHandle(hProcess);
}
}
@@ -282,14 +282,14 @@ ref processInfo // pointer to PROCESS_INFORMATION
private int GetProcessIdCore() => Interop.Kernel32.GetProcessId(this);
- private static unsafe bool ShellExecuteOnSTAThread(Interop.Shell32.SHELLEXECUTEINFO* executeInfo, out int errorCode)
+ private static unsafe bool ShellExecuteOnSTAThread(Interop.Shell32.SHELLEXECUTEINFO executeInfo, out int errorCode, out IntPtr hProcess, out IntPtr hInstApp)
{
bool succeeded = false;
bool notPresent = false;
int lastError = 0;
- nuint executeInfoAddress = (nuint)executeInfo; // cast to nuint to allow delegate capture; safe because Join() keeps the caller's stack frame alive for the thread's lifetime
+ nuint executeInfoAddress = (nuint)(&executeInfo); // cast to nuint to allow delegate capture; safe because Join() keeps this stack frame alive for the thread's lifetime
- unsafe void ShellExecuteFunction()
+ void ShellExecuteFunction()
{
try
{
@@ -322,6 +322,8 @@ unsafe void ShellExecuteFunction()
if (notPresent)
throw new PlatformNotSupportedException(SR.UseShellExecuteNotSupported);
+ hProcess = executeInfo.hProcess;
+ hInstApp = executeInfo.hInstApp;
errorCode = lastError;
return succeeded;
}
From 6658eef426839a244aadea94f1b40221fab90269 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 30 Mar 2026 14:02:14 +0000
Subject: [PATCH 04/18] Replace notPresent bool with ERROR_CALL_NOT_IMPLEMENTED
in catch block
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/9134d130-f2dc-4ad8-800b-2dbb7a35a509
Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com>
---
.../Win32/SafeHandles/SafeProcessHandle.Windows.cs | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
index 23c8c1d04d73bc..02a63ec0d9efdd 100644
--- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
+++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
@@ -285,7 +285,6 @@ ref processInfo // pointer to PROCESS_INFORMATION
private static unsafe bool ShellExecuteOnSTAThread(Interop.Shell32.SHELLEXECUTEINFO executeInfo, out int errorCode, out IntPtr hProcess, out IntPtr hInstApp)
{
bool succeeded = false;
- bool notPresent = false;
int lastError = 0;
nuint executeInfoAddress = (nuint)(&executeInfo); // cast to nuint to allow delegate capture; safe because Join() keeps this stack frame alive for the thread's lifetime
@@ -298,7 +297,7 @@ void ShellExecuteFunction()
}
catch (EntryPointNotFoundException)
{
- notPresent = true;
+ lastError = Interop.Errors.ERROR_CALL_NOT_IMPLEMENTED;
}
}
@@ -319,9 +318,6 @@ void ShellExecuteFunction()
ShellExecuteFunction();
}
- if (notPresent)
- throw new PlatformNotSupportedException(SR.UseShellExecuteNotSupported);
-
hProcess = executeInfo.hProcess;
hInstApp = executeInfo.hInstApp;
errorCode = lastError;
From 79f436262c603f249b7e166e9fe268d11f7122fb Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 30 Mar 2026 14:12:16 +0000
Subject: [PATCH 05/18] Rename StartWithShellExecuteEx to StartWithShellExecute
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/bd35b541-ebf3-4f48-af2a-9e7b588d5567
Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com>
---
.../Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
index 02a63ec0d9efdd..a2a164e12341c4 100644
--- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
+++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
@@ -20,11 +20,11 @@ protected override bool ReleaseHandle()
internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle)
{
return startInfo.UseShellExecute
- ? StartWithShellExecuteEx(startInfo)
+ ? StartWithShellExecute(startInfo)
: StartWithCreateProcess(startInfo, stdinHandle, stdoutHandle, stderrHandle);
}
- private static unsafe SafeProcessHandle StartWithShellExecuteEx(ProcessStartInfo startInfo)
+ private static unsafe SafeProcessHandle StartWithShellExecute(ProcessStartInfo startInfo)
{
if (!string.IsNullOrEmpty(startInfo.UserName) || startInfo.Password != null)
throw new InvalidOperationException(SR.CantStartAsUser);
From ebb852416e4afe2f5b4b4838b18ef6dc0238b185 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 30 Mar 2026 14:47:20 +0000
Subject: [PATCH 06/18] Fold ShellExecuteOnSTAThread into
StartWithShellExecute; add static delegate for UseShellExecute trimming
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/83dbdfce-7f91-4beb-bb72-804badd92fc3
Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com>
---
.../SafeHandles/SafeProcessHandle.Unix.cs | 126 +++++++++++-------
.../SafeHandles/SafeProcessHandle.Windows.cs | 92 ++++++-------
.../Win32/SafeHandles/SafeProcessHandle.cs | 4 +
.../src/System/Diagnostics/Process.Windows.cs | 2 -
.../Diagnostics/ProcessStartInfo.Unix.cs | 11 +-
.../Diagnostics/ProcessStartInfo.Win32.cs | 11 +-
6 files changed, 148 insertions(+), 98 deletions(-)
diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs
index 3734880c8417cd..c4194f0da51ce5 100644
--- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs
+++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs
@@ -74,6 +74,11 @@ internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFile
{
waitStateHolder = null;
+ if (startInfo.UseShellExecute)
+ {
+ return s_startWithShellExecute!(startInfo, stdinHandle, stdoutHandle, stderrHandle);
+ }
+
if (ProcessUtils.PlatformDoesNotSupportProcessStartAndKill)
{
throw new PlatformNotSupportedException();
@@ -105,63 +110,92 @@ internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFile
|| (stdoutHandle is not null && Interop.Sys.IsATty(stdoutHandle))
|| (stderrHandle is not null && Interop.Sys.IsATty(stderrHandle));
- if (startInfo.UseShellExecute)
+ filename = ProcessUtils.ResolvePath(startInfo.FileName);
+ argv = ProcessUtils.ParseArgv(startInfo);
+ if (Directory.Exists(filename))
{
- string verb = startInfo.Verb;
- if (verb != string.Empty &&
- !string.Equals(verb, "open", StringComparison.OrdinalIgnoreCase))
- {
- throw new Win32Exception(Interop.Errors.ERROR_NO_ASSOCIATION);
- }
+ throw new Win32Exception(SR.DirectoryNotValidAsInput);
+ }
- // On Windows, UseShellExecute of executables and scripts causes those files to be executed.
- // To achieve this on Unix, we check if the file is executable (x-bit).
- // Some files may have the x-bit set even when they are not executable. This happens for example
- // when a Windows filesystem is mounted on Linux. To handle that, treat it as a regular file
- // when exec returns ENOEXEC (file format cannot be executed).
- filename = ProcessUtils.ResolveExecutableForShellExecute(startInfo.FileName, cwd);
- if (filename != null)
- {
- argv = ProcessUtils.ParseArgv(startInfo);
-
- SafeProcessHandle processHandle = ForkAndExecProcess(
- startInfo, filename, argv, env, cwd,
- setCredentials, userId, groupId, groups,
- stdinHandle, stdoutHandle, stderrHandle, usesTerminal,
- out waitStateHolder,
- throwOnNoExec: false); // return invalid handle instead of throwing on ENOEXEC
-
- if (!processHandle.IsInvalid)
- {
- return processHandle;
- }
- }
+ return ForkAndExecProcess(
+ startInfo, filename, argv, env, cwd,
+ setCredentials, userId, groupId, groups,
+ stdinHandle, stdoutHandle, stderrHandle, usesTerminal,
+ out waitStateHolder);
+ }
- // use default program to open file/url
- filename = Process.GetPathToOpenFile();
- argv = ProcessUtils.ParseArgv(startInfo, filename, ignoreArguments: true);
+ internal static void EnsureShellExecuteFunc() =>
+ s_startWithShellExecute ??= StartWithShellExecute;
- return ForkAndExecProcess(
- startInfo, filename, argv, env, cwd,
- setCredentials, userId, groupId, groups,
- stdinHandle, stdoutHandle, stderrHandle, usesTerminal,
- out waitStateHolder);
+ private static SafeProcessHandle StartWithShellExecute(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle)
+ {
+ if (ProcessUtils.PlatformDoesNotSupportProcessStartAndKill)
+ {
+ throw new PlatformNotSupportedException();
}
- else
+
+ ProcessUtils.EnsureInitialized();
+
+ IDictionary env = startInfo.Environment;
+ string? cwd = !string.IsNullOrWhiteSpace(startInfo.WorkingDirectory) ? startInfo.WorkingDirectory : null;
+
+ bool setCredentials = !string.IsNullOrEmpty(startInfo.UserName);
+ uint userId = 0;
+ uint groupId = 0;
+ uint[]? groups = null;
+ if (setCredentials)
{
- filename = ProcessUtils.ResolvePath(startInfo.FileName);
- argv = ProcessUtils.ParseArgv(startInfo);
- if (Directory.Exists(filename))
- {
- throw new Win32Exception(SR.DirectoryNotValidAsInput);
- }
+ (userId, groupId, groups) = ProcessUtils.GetUserAndGroupIds(startInfo);
+ }
+
+ bool usesTerminal = (stdinHandle is not null && Interop.Sys.IsATty(stdinHandle))
+ || (stdoutHandle is not null && Interop.Sys.IsATty(stdoutHandle))
+ || (stderrHandle is not null && Interop.Sys.IsATty(stderrHandle));
- return ForkAndExecProcess(
+ string verb = startInfo.Verb;
+ if (verb != string.Empty &&
+ !string.Equals(verb, "open", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new Win32Exception(Interop.Errors.ERROR_NO_ASSOCIATION);
+ }
+
+ // On Windows, UseShellExecute of executables and scripts causes those files to be executed.
+ // To achieve this on Unix, we check if the file is executable (x-bit).
+ // Some files may have the x-bit set even when they are not executable. This happens for example
+ // when a Windows filesystem is mounted on Linux. To handle that, treat it as a regular file
+ // when exec returns ENOEXEC (file format cannot be executed).
+ string? filename = ProcessUtils.ResolveExecutableForShellExecute(startInfo.FileName, cwd);
+ if (filename != null)
+ {
+ string[] argv = ProcessUtils.ParseArgv(startInfo);
+
+ SafeProcessHandle processHandle = ForkAndExecProcess(
startInfo, filename, argv, env, cwd,
setCredentials, userId, groupId, groups,
stdinHandle, stdoutHandle, stderrHandle, usesTerminal,
- out waitStateHolder);
+ out ProcessWaitState.Holder? waitStateHolder,
+ throwOnNoExec: false); // return invalid handle instead of throwing on ENOEXEC
+
+ waitStateHolder?.Dispose();
+
+ if (!processHandle.IsInvalid)
+ {
+ return processHandle;
+ }
}
+
+ // use default program to open file/url
+ filename = Process.GetPathToOpenFile();
+ string[] openFileArgv = ProcessUtils.ParseArgv(startInfo, filename, ignoreArguments: true);
+
+ SafeProcessHandle result = ForkAndExecProcess(
+ startInfo, filename, openFileArgv, env, cwd,
+ setCredentials, userId, groupId, groups,
+ stdinHandle, stdoutHandle, stderrHandle, usesTerminal,
+ out ProcessWaitState.Holder? waitStateHolder2);
+
+ waitStateHolder2?.Dispose();
+ return result;
}
private static SafeProcessHandle ForkAndExecProcess(
diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
index a2a164e12341c4..917a0e7020e32a 100644
--- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
+++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
@@ -20,11 +20,14 @@ protected override bool ReleaseHandle()
internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle)
{
return startInfo.UseShellExecute
- ? StartWithShellExecute(startInfo)
+ ? s_startWithShellExecute!(startInfo, stdinHandle, stdoutHandle, stderrHandle)
: StartWithCreateProcess(startInfo, stdinHandle, stdoutHandle, stderrHandle);
}
- private static unsafe SafeProcessHandle StartWithShellExecute(ProcessStartInfo startInfo)
+ internal static void EnsureShellExecuteFunc() =>
+ s_startWithShellExecute ??= StartWithShellExecute;
+
+ private static unsafe SafeProcessHandle StartWithShellExecute(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle)
{
if (!string.IsNullOrEmpty(startInfo.UserName) || startInfo.Password != null)
throw new InvalidOperationException(SR.CantStartAsUser);
@@ -64,11 +67,47 @@ private static unsafe SafeProcessHandle StartWithShellExecute(ProcessStartInfo s
shellExecuteInfo.fMask |= Interop.Shell32.SEE_MASK_FLAG_NO_UI;
shellExecuteInfo.nShow = ProcessUtils.GetShowWindowFromWindowStyle(startInfo.WindowStyle);
- if (!ShellExecuteOnSTAThread(shellExecuteInfo, out int errorCode, out IntPtr hProcess, out IntPtr hInstApp))
+
+ bool succeeded = false;
+ int lastError = 0;
+ nuint executeInfoAddress = (nuint)(&shellExecuteInfo); // cast to nuint to allow delegate capture; safe because Join() keeps this stack frame alive for the thread's lifetime
+
+ void ShellExecuteFunction()
+ {
+ try
+ {
+ if (!(succeeded = Interop.Shell32.ShellExecuteExW((Interop.Shell32.SHELLEXECUTEINFO*)executeInfoAddress)))
+ lastError = Marshal.GetLastWin32Error();
+ }
+ catch (EntryPointNotFoundException)
+ {
+ lastError = Interop.Errors.ERROR_CALL_NOT_IMPLEMENTED;
+ }
+ }
+
+ // ShellExecute() requires STA in order to work correctly.
+ if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA)
+ {
+ Thread executionThread = new Thread(ShellExecuteFunction)
+ {
+ IsBackground = true,
+ Name = ".NET Process STA"
+ };
+ executionThread.SetApartmentState(ApartmentState.STA);
+ executionThread.Start();
+ executionThread.Join();
+ }
+ else
{
+ ShellExecuteFunction();
+ }
+
+ if (!succeeded)
+ {
+ int errorCode = lastError;
if (errorCode == 0)
{
- errorCode = GetShellError(hInstApp);
+ errorCode = GetShellError(shellExecuteInfo.hInstApp);
}
switch (errorCode)
@@ -88,7 +127,7 @@ private static unsafe SafeProcessHandle StartWithShellExecute(ProcessStartInfo s
// From https://learn.microsoft.com/windows/win32/api/shellapi/ns-shellapi-shellexecuteinfow:
// "In some cases, such as when execution is satisfied through a DDE conversation, no handle will be returned."
// Process.Start will return false if the handle is invalid.
- return new SafeProcessHandle(hProcess);
+ return new SafeProcessHandle(shellExecuteInfo.hProcess);
}
}
@@ -281,49 +320,6 @@ ref processInfo // pointer to PROCESS_INFORMATION
}
private int GetProcessIdCore() => Interop.Kernel32.GetProcessId(this);
-
- private static unsafe bool ShellExecuteOnSTAThread(Interop.Shell32.SHELLEXECUTEINFO executeInfo, out int errorCode, out IntPtr hProcess, out IntPtr hInstApp)
- {
- bool succeeded = false;
- int lastError = 0;
- nuint executeInfoAddress = (nuint)(&executeInfo); // cast to nuint to allow delegate capture; safe because Join() keeps this stack frame alive for the thread's lifetime
-
- void ShellExecuteFunction()
- {
- try
- {
- if (!(succeeded = Interop.Shell32.ShellExecuteExW((Interop.Shell32.SHELLEXECUTEINFO*)executeInfoAddress)))
- lastError = Marshal.GetLastWin32Error();
- }
- catch (EntryPointNotFoundException)
- {
- lastError = Interop.Errors.ERROR_CALL_NOT_IMPLEMENTED;
- }
- }
-
- // ShellExecute() requires STA in order to work correctly.
- if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA)
- {
- Thread executionThread = new Thread(ShellExecuteFunction)
- {
- IsBackground = true,
- Name = ".NET Process STA"
- };
- executionThread.SetApartmentState(ApartmentState.STA);
- executionThread.Start();
- executionThread.Join();
- }
- else
- {
- ShellExecuteFunction();
- }
-
- hProcess = executeInfo.hProcess;
- hInstApp = executeInfo.hInstApp;
- errorCode = lastError;
- return succeeded;
- }
-
private static int GetShellError(IntPtr error)
{
return (long)error switch
diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs
index 7cfb01c028d030..8f913fcd64ca4a 100644
--- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs
+++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs
@@ -21,6 +21,10 @@ public sealed partial class SafeProcessHandle : SafeHandleZeroOrMinusOneIsInvali
{
internal static readonly SafeProcessHandle InvalidHandle = new SafeProcessHandle();
+ // Allows for StartWithShellExecute (and its dependencies) to be trimmed when
+ // UseShellExecute is not being used.
+ internal static Func? s_startWithShellExecute;
+
///
/// Gets the process ID.
///
diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs
index 27701f20649280..aa2b3e7fcb7ee8 100644
--- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs
+++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs
@@ -49,7 +49,6 @@ public static Process[] GetProcessesByName(string? processName, string machineNa
startInfo.UserName = userName;
startInfo.Password = password;
startInfo.Domain = domain;
- startInfo.UseShellExecute = false;
return Start(startInfo);
}
@@ -61,7 +60,6 @@ public static Process[] GetProcessesByName(string? processName, string machineNa
startInfo.UserName = userName;
startInfo.Password = password;
startInfo.Domain = domain;
- startInfo.UseShellExecute = false;
return Start(startInfo);
}
diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Unix.cs
index 196d57bf0b36b9..ac1d6e1049cae5 100644
--- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Unix.cs
+++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Unix.cs
@@ -4,6 +4,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.Versioning;
using System.Security;
+using Microsoft.Win32.SafeHandles;
namespace System.Diagnostics
{
@@ -41,7 +42,15 @@ public bool UseCredentialsForNetworkingOnly
set { throw new PlatformNotSupportedException(SR.Format(SR.ProcessStartSingleFeatureNotSupported, nameof(UseCredentialsForNetworkingOnly))); }
}
- public bool UseShellExecute { get; set; }
+ public bool UseShellExecute
+ {
+ get;
+ set
+ {
+ SafeProcessHandle.EnsureShellExecuteFunc();
+ field = value;
+ }
+ }
public string[] Verbs => Array.Empty();
diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Win32.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Win32.cs
index eaf3bd617c463e..e06939eace4309 100644
--- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Win32.cs
+++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Win32.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.IO;
using Microsoft.Win32;
+using Microsoft.Win32.SafeHandles;
namespace System.Diagnostics
{
@@ -46,6 +47,14 @@ public string[] Verbs
}
}
- public bool UseShellExecute { get; set; }
+ public bool UseShellExecute
+ {
+ get;
+ set
+ {
+ SafeProcessHandle.EnsureShellExecuteFunc();
+ field = value;
+ }
+ }
}
}
From 8b4d138c617d03559c6da90a6ab022a9bd8effe2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 30 Mar 2026 16:01:16 +0000
Subject: [PATCH 07/18] Move UseShellExecute property to shared
ProcessStartInfo.cs
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/b469dc47-ec06-42d3-81ce-0b2d6edc9922
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../src/System/Diagnostics/ProcessStartInfo.Unix.cs | 11 -----------
.../src/System/Diagnostics/ProcessStartInfo.Win32.cs | 10 ----------
.../src/System/Diagnostics/ProcessStartInfo.cs | 10 ++++++++++
3 files changed, 10 insertions(+), 21 deletions(-)
diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Unix.cs
index ac1d6e1049cae5..494d79c4a6d1e3 100644
--- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Unix.cs
+++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Unix.cs
@@ -4,7 +4,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.Versioning;
using System.Security;
-using Microsoft.Win32.SafeHandles;
namespace System.Diagnostics
{
@@ -42,16 +41,6 @@ public bool UseCredentialsForNetworkingOnly
set { throw new PlatformNotSupportedException(SR.Format(SR.ProcessStartSingleFeatureNotSupported, nameof(UseCredentialsForNetworkingOnly))); }
}
- public bool UseShellExecute
- {
- get;
- set
- {
- SafeProcessHandle.EnsureShellExecuteFunc();
- field = value;
- }
- }
-
public string[] Verbs => Array.Empty();
[CLSCompliant(false)]
diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Win32.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Win32.cs
index e06939eace4309..77f3c396a13962 100644
--- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Win32.cs
+++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Win32.cs
@@ -4,7 +4,6 @@
using System.Collections.Generic;
using System.IO;
using Microsoft.Win32;
-using Microsoft.Win32.SafeHandles;
namespace System.Diagnostics
{
@@ -47,14 +46,5 @@ public string[] Verbs
}
}
- public bool UseShellExecute
- {
- get;
- set
- {
- SafeProcessHandle.EnsureShellExecuteFunc();
- field = value;
- }
- }
}
}
diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs
index e564f6201faa72..d0b8c3ae9da979 100644
--- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs
+++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs
@@ -232,6 +232,16 @@ public string WorkingDirectory
public bool ErrorDialog { get; set; }
public IntPtr ErrorDialogParentHandle { get; set; }
+ public bool UseShellExecute
+ {
+ get;
+ set
+ {
+ SafeProcessHandle.EnsureShellExecuteFunc();
+ field = value;
+ }
+ }
+
[AllowNull]
public string UserName
{
From 1b6b78fd6739d19a31ff6c8c907ebf680d71628a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 30 Mar 2026 16:13:27 +0000
Subject: [PATCH 08/18] Fold StartWithCreateProcess into StartCore on Windows
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/bce9a651-9be1-419f-930f-2e03b6aa6266
Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com>
---
.../SafeHandles/SafeProcessHandle.Windows.cs | 225 +++++++++---------
1 file changed, 110 insertions(+), 115 deletions(-)
diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
index 917a0e7020e32a..037a441fea8b5e 100644
--- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
+++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
@@ -17,123 +17,11 @@ protected override bool ReleaseHandle()
return Interop.Kernel32.CloseHandle(handle);
}
- internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle)
+ internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle)
{
- return startInfo.UseShellExecute
- ? s_startWithShellExecute!(startInfo, stdinHandle, stdoutHandle, stderrHandle)
- : StartWithCreateProcess(startInfo, stdinHandle, stdoutHandle, stderrHandle);
- }
-
- internal static void EnsureShellExecuteFunc() =>
- s_startWithShellExecute ??= StartWithShellExecute;
-
- private static unsafe SafeProcessHandle StartWithShellExecute(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle)
- {
- if (!string.IsNullOrEmpty(startInfo.UserName) || startInfo.Password != null)
- throw new InvalidOperationException(SR.CantStartAsUser);
-
- if (startInfo.StandardInputEncoding != null)
- throw new InvalidOperationException(SR.StandardInputEncodingNotAllowed);
-
- if (startInfo.StandardErrorEncoding != null)
- throw new InvalidOperationException(SR.StandardErrorEncodingNotAllowed);
-
- if (startInfo.StandardOutputEncoding != null)
- throw new InvalidOperationException(SR.StandardOutputEncodingNotAllowed);
-
- if (startInfo._environmentVariables != null)
- throw new InvalidOperationException(SR.CantUseEnvVars);
-
- string arguments = startInfo.BuildArguments();
-
- fixed (char* fileName = startInfo.FileName.Length > 0 ? startInfo.FileName : null)
- fixed (char* verb = startInfo.Verb.Length > 0 ? startInfo.Verb : null)
- fixed (char* parameters = arguments.Length > 0 ? arguments : null)
- fixed (char* directory = startInfo.WorkingDirectory.Length > 0 ? startInfo.WorkingDirectory : null)
- {
- Interop.Shell32.SHELLEXECUTEINFO shellExecuteInfo = new Interop.Shell32.SHELLEXECUTEINFO()
- {
- cbSize = (uint)sizeof(Interop.Shell32.SHELLEXECUTEINFO),
- lpFile = fileName,
- lpVerb = verb,
- lpParameters = parameters,
- lpDirectory = directory,
- fMask = Interop.Shell32.SEE_MASK_NOCLOSEPROCESS | Interop.Shell32.SEE_MASK_FLAG_DDEWAIT
- };
-
- if (startInfo.ErrorDialog)
- shellExecuteInfo.hwnd = startInfo.ErrorDialogParentHandle;
- else
- shellExecuteInfo.fMask |= Interop.Shell32.SEE_MASK_FLAG_NO_UI;
-
- shellExecuteInfo.nShow = ProcessUtils.GetShowWindowFromWindowStyle(startInfo.WindowStyle);
-
- bool succeeded = false;
- int lastError = 0;
- nuint executeInfoAddress = (nuint)(&shellExecuteInfo); // cast to nuint to allow delegate capture; safe because Join() keeps this stack frame alive for the thread's lifetime
-
- void ShellExecuteFunction()
- {
- try
- {
- if (!(succeeded = Interop.Shell32.ShellExecuteExW((Interop.Shell32.SHELLEXECUTEINFO*)executeInfoAddress)))
- lastError = Marshal.GetLastWin32Error();
- }
- catch (EntryPointNotFoundException)
- {
- lastError = Interop.Errors.ERROR_CALL_NOT_IMPLEMENTED;
- }
- }
-
- // ShellExecute() requires STA in order to work correctly.
- if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA)
- {
- Thread executionThread = new Thread(ShellExecuteFunction)
- {
- IsBackground = true,
- Name = ".NET Process STA"
- };
- executionThread.SetApartmentState(ApartmentState.STA);
- executionThread.Start();
- executionThread.Join();
- }
- else
- {
- ShellExecuteFunction();
- }
-
- if (!succeeded)
- {
- int errorCode = lastError;
- if (errorCode == 0)
- {
- errorCode = GetShellError(shellExecuteInfo.hInstApp);
- }
-
- switch (errorCode)
- {
- case Interop.Errors.ERROR_CALL_NOT_IMPLEMENTED:
- // This happens on Windows Nano
- throw new PlatformNotSupportedException(SR.UseShellExecuteNotSupported);
- default:
- string nativeErrorMessage = errorCode == Interop.Errors.ERROR_BAD_EXE_FORMAT || errorCode == Interop.Errors.ERROR_EXE_MACHINE_TYPE_MISMATCH
- ? SR.InvalidApplication
- : Interop.Kernel32.GetMessage(errorCode);
-
- throw ProcessUtils.CreateExceptionForErrorStartingProcess(nativeErrorMessage, errorCode, startInfo.FileName, startInfo.WorkingDirectory);
- }
- }
-
- // From https://learn.microsoft.com/windows/win32/api/shellapi/ns-shellapi-shellexecuteinfow:
- // "In some cases, such as when execution is satisfied through a DDE conversation, no handle will be returned."
- // Process.Start will return false if the handle is invalid.
- return new SafeProcessHandle(shellExecuteInfo.hProcess);
- }
- }
+ if (startInfo.UseShellExecute)
+ return s_startWithShellExecute!(startInfo, stdinHandle, stdoutHandle, stderrHandle);
- /// Starts the process using the supplied start info.
- private static unsafe SafeProcessHandle StartWithCreateProcess(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle)
- {
// See knowledge base article Q190351 for an explanation of the following code. Noteworthy tricky points:
// * The handles are duplicated as inheritable before they are passed to CreateProcess so
// that the child process can use them
@@ -319,6 +207,113 @@ ref processInfo // pointer to PROCESS_INFORMATION
return procSH;
}
+ internal static void EnsureShellExecuteFunc() =>
+ s_startWithShellExecute ??= StartWithShellExecute;
+
+ private static unsafe SafeProcessHandle StartWithShellExecute(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle)
+ {
+ if (!string.IsNullOrEmpty(startInfo.UserName) || startInfo.Password != null)
+ throw new InvalidOperationException(SR.CantStartAsUser);
+
+ if (startInfo.StandardInputEncoding != null)
+ throw new InvalidOperationException(SR.StandardInputEncodingNotAllowed);
+
+ if (startInfo.StandardErrorEncoding != null)
+ throw new InvalidOperationException(SR.StandardErrorEncodingNotAllowed);
+
+ if (startInfo.StandardOutputEncoding != null)
+ throw new InvalidOperationException(SR.StandardOutputEncodingNotAllowed);
+
+ if (startInfo._environmentVariables != null)
+ throw new InvalidOperationException(SR.CantUseEnvVars);
+
+ string arguments = startInfo.BuildArguments();
+
+ fixed (char* fileName = startInfo.FileName.Length > 0 ? startInfo.FileName : null)
+ fixed (char* verb = startInfo.Verb.Length > 0 ? startInfo.Verb : null)
+ fixed (char* parameters = arguments.Length > 0 ? arguments : null)
+ fixed (char* directory = startInfo.WorkingDirectory.Length > 0 ? startInfo.WorkingDirectory : null)
+ {
+ Interop.Shell32.SHELLEXECUTEINFO shellExecuteInfo = new Interop.Shell32.SHELLEXECUTEINFO()
+ {
+ cbSize = (uint)sizeof(Interop.Shell32.SHELLEXECUTEINFO),
+ lpFile = fileName,
+ lpVerb = verb,
+ lpParameters = parameters,
+ lpDirectory = directory,
+ fMask = Interop.Shell32.SEE_MASK_NOCLOSEPROCESS | Interop.Shell32.SEE_MASK_FLAG_DDEWAIT
+ };
+
+ if (startInfo.ErrorDialog)
+ shellExecuteInfo.hwnd = startInfo.ErrorDialogParentHandle;
+ else
+ shellExecuteInfo.fMask |= Interop.Shell32.SEE_MASK_FLAG_NO_UI;
+
+ shellExecuteInfo.nShow = ProcessUtils.GetShowWindowFromWindowStyle(startInfo.WindowStyle);
+
+ bool succeeded = false;
+ int lastError = 0;
+ nuint executeInfoAddress = (nuint)(&shellExecuteInfo); // cast to nuint to allow delegate capture; safe because Join() keeps this stack frame alive for the thread's lifetime
+
+ void ShellExecuteFunction()
+ {
+ try
+ {
+ if (!(succeeded = Interop.Shell32.ShellExecuteExW((Interop.Shell32.SHELLEXECUTEINFO*)executeInfoAddress)))
+ lastError = Marshal.GetLastWin32Error();
+ }
+ catch (EntryPointNotFoundException)
+ {
+ lastError = Interop.Errors.ERROR_CALL_NOT_IMPLEMENTED;
+ }
+ }
+
+ // ShellExecute() requires STA in order to work correctly.
+ if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA)
+ {
+ Thread executionThread = new Thread(ShellExecuteFunction)
+ {
+ IsBackground = true,
+ Name = ".NET Process STA"
+ };
+ executionThread.SetApartmentState(ApartmentState.STA);
+ executionThread.Start();
+ executionThread.Join();
+ }
+ else
+ {
+ ShellExecuteFunction();
+ }
+
+ if (!succeeded)
+ {
+ int errorCode = lastError;
+ if (errorCode == 0)
+ {
+ errorCode = GetShellError(shellExecuteInfo.hInstApp);
+ }
+
+ switch (errorCode)
+ {
+ case Interop.Errors.ERROR_CALL_NOT_IMPLEMENTED:
+ // This happens on Windows Nano
+ throw new PlatformNotSupportedException(SR.UseShellExecuteNotSupported);
+ default:
+ string nativeErrorMessage = errorCode == Interop.Errors.ERROR_BAD_EXE_FORMAT || errorCode == Interop.Errors.ERROR_EXE_MACHINE_TYPE_MISMATCH
+ ? SR.InvalidApplication
+ : Interop.Kernel32.GetMessage(errorCode);
+
+ throw ProcessUtils.CreateExceptionForErrorStartingProcess(nativeErrorMessage, errorCode, startInfo.FileName, startInfo.WorkingDirectory);
+ }
+ }
+
+ // From https://learn.microsoft.com/windows/win32/api/shellapi/ns-shellapi-shellexecuteinfow:
+ // "In some cases, such as when execution is satisfied through a DDE conversation, no handle will be returned."
+ // Process.Start will return false if the handle is invalid.
+ return new SafeProcessHandle(shellExecuteInfo.hProcess);
+ }
+ }
+
private int GetProcessIdCore() => Interop.Kernel32.GetProcessId(this);
private static int GetShellError(IntPtr error)
{
From a774a3705889f5bc218e67e74ba5392876c56525 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 30 Mar 2026 16:26:42 +0000
Subject: [PATCH 09/18] Move s_startWithShellExecute to platform files,
EnsureShellExecuteFunc to shared, GetShellError to local function
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/d1c93e3a-0df8-4457-a74e-bd488e00b909
Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com>
---
.../SafeHandles/SafeProcessHandle.Unix.cs | 7 +--
.../SafeHandles/SafeProcessHandle.Windows.cs | 44 +++++++++----------
.../Win32/SafeHandles/SafeProcessHandle.cs | 7 +--
3 files changed, 30 insertions(+), 28 deletions(-)
diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs
index c4194f0da51ce5..18c871aeb81629 100644
--- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs
+++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs
@@ -59,6 +59,10 @@ protected override bool ReleaseHandle()
// On Unix, we don't use process descriptors yet, so we can't get PID.
private static int GetProcessIdCore() => throw new PlatformNotSupportedException();
+ // Allows for StartWithShellExecute (and its dependencies) to be trimmed when UseShellExecute is not being used.
+ // On Unix, standard I/O handles are passed through to the shell process.
+ internal static Func? s_startWithShellExecute;
+
private static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle)
{
SafeProcessHandle startedProcess = StartCore(startInfo, stdinHandle, stdoutHandle, stderrHandle, out ProcessWaitState.Holder? waitStateHolder);
@@ -124,9 +128,6 @@ internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFile
out waitStateHolder);
}
- internal static void EnsureShellExecuteFunc() =>
- s_startWithShellExecute ??= StartWithShellExecute;
-
private static SafeProcessHandle StartWithShellExecute(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle)
{
if (ProcessUtils.PlatformDoesNotSupportProcessStartAndKill)
diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
index 037a441fea8b5e..d156680ade1646 100644
--- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
+++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
@@ -17,10 +17,14 @@ protected override bool ReleaseHandle()
return Interop.Kernel32.CloseHandle(handle);
}
+ // Allows for StartWithShellExecute (and its dependencies) to be trimmed when UseShellExecute is not being used.
+ // On Windows, StartWithShellExecute does not use standard I/O handles.
+ internal static Func? s_startWithShellExecute;
+
internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle)
{
if (startInfo.UseShellExecute)
- return s_startWithShellExecute!(startInfo, stdinHandle, stdoutHandle, stderrHandle);
+ return s_startWithShellExecute!(startInfo);
// See knowledge base article Q190351 for an explanation of the following code. Noteworthy tricky points:
// * The handles are duplicated as inheritable before they are passed to CreateProcess so
@@ -207,10 +211,7 @@ ref processInfo // pointer to PROCESS_INFORMATION
return procSH;
}
- internal static void EnsureShellExecuteFunc() =>
- s_startWithShellExecute ??= StartWithShellExecute;
-
- private static unsafe SafeProcessHandle StartWithShellExecute(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle)
+ private static unsafe SafeProcessHandle StartWithShellExecute(ProcessStartInfo startInfo)
{
if (!string.IsNullOrEmpty(startInfo.UserName) || startInfo.Password != null)
throw new InvalidOperationException(SR.CantStartAsUser);
@@ -311,26 +312,25 @@ void ShellExecuteFunction()
// "In some cases, such as when execution is satisfied through a DDE conversation, no handle will be returned."
// Process.Start will return false if the handle is invalid.
return new SafeProcessHandle(shellExecuteInfo.hProcess);
+
+ static int GetShellError(IntPtr error) =>
+ (long)error switch
+ {
+ Interop.Shell32.SE_ERR_FNF => Interop.Errors.ERROR_FILE_NOT_FOUND,
+ Interop.Shell32.SE_ERR_PNF => Interop.Errors.ERROR_PATH_NOT_FOUND,
+ Interop.Shell32.SE_ERR_ACCESSDENIED => Interop.Errors.ERROR_ACCESS_DENIED,
+ Interop.Shell32.SE_ERR_OOM => Interop.Errors.ERROR_NOT_ENOUGH_MEMORY,
+ Interop.Shell32.SE_ERR_DDEFAIL or
+ Interop.Shell32.SE_ERR_DDEBUSY or
+ Interop.Shell32.SE_ERR_DDETIMEOUT => Interop.Errors.ERROR_DDE_FAIL,
+ Interop.Shell32.SE_ERR_SHARE => Interop.Errors.ERROR_SHARING_VIOLATION,
+ Interop.Shell32.SE_ERR_NOASSOC => Interop.Errors.ERROR_NO_ASSOCIATION,
+ Interop.Shell32.SE_ERR_DLLNOTFOUND => Interop.Errors.ERROR_DLL_NOT_FOUND,
+ _ => (int)(long)error,
+ };
}
}
private int GetProcessIdCore() => Interop.Kernel32.GetProcessId(this);
- private static int GetShellError(IntPtr error)
- {
- return (long)error switch
- {
- Interop.Shell32.SE_ERR_FNF => Interop.Errors.ERROR_FILE_NOT_FOUND,
- Interop.Shell32.SE_ERR_PNF => Interop.Errors.ERROR_PATH_NOT_FOUND,
- Interop.Shell32.SE_ERR_ACCESSDENIED => Interop.Errors.ERROR_ACCESS_DENIED,
- Interop.Shell32.SE_ERR_OOM => Interop.Errors.ERROR_NOT_ENOUGH_MEMORY,
- Interop.Shell32.SE_ERR_DDEFAIL or
- Interop.Shell32.SE_ERR_DDEBUSY or
- Interop.Shell32.SE_ERR_DDETIMEOUT => Interop.Errors.ERROR_DDE_FAIL,
- Interop.Shell32.SE_ERR_SHARE => Interop.Errors.ERROR_SHARING_VIOLATION,
- Interop.Shell32.SE_ERR_NOASSOC => Interop.Errors.ERROR_NO_ASSOCIATION,
- Interop.Shell32.SE_ERR_DLLNOTFOUND => Interop.Errors.ERROR_DLL_NOT_FOUND,
- _ => (int)(long)error,
- };
- }
}
}
diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs
index 8f913fcd64ca4a..64ddd87b03c070 100644
--- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs
+++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs
@@ -21,9 +21,10 @@ public sealed partial class SafeProcessHandle : SafeHandleZeroOrMinusOneIsInvali
{
internal static readonly SafeProcessHandle InvalidHandle = new SafeProcessHandle();
- // Allows for StartWithShellExecute (and its dependencies) to be trimmed when
- // UseShellExecute is not being used.
- internal static Func? s_startWithShellExecute;
+ // Allows for StartWithShellExecute (and its dependencies) to be trimmed when UseShellExecute is not being used.
+ // s_startWithShellExecute is defined in platform-specific partial files with OS-appropriate delegate signatures.
+ internal static void EnsureShellExecuteFunc() =>
+ s_startWithShellExecute ??= StartWithShellExecute;
///
/// Gets the process ID.
From 22abeaca3ec943fcd311264aac716a8585e746a2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 30 Mar 2026 16:40:01 +0000
Subject: [PATCH 10/18] Unix StartCore: move UseShellExecute delegate call
after shared platform checks
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/548ef13b-9ba4-43dd-8c76-1ab0b33bd259
Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com>
---
.../Win32/SafeHandles/SafeProcessHandle.Unix.cs | 17 +++++------------
1 file changed, 5 insertions(+), 12 deletions(-)
diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs
index 18c871aeb81629..8f38701447680a 100644
--- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs
+++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs
@@ -78,11 +78,6 @@ internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFile
{
waitStateHolder = null;
- if (startInfo.UseShellExecute)
- {
- return s_startWithShellExecute!(startInfo, stdinHandle, stdoutHandle, stderrHandle);
- }
-
if (ProcessUtils.PlatformDoesNotSupportProcessStartAndKill)
{
throw new PlatformNotSupportedException();
@@ -90,6 +85,11 @@ internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFile
ProcessUtils.EnsureInitialized();
+ if (startInfo.UseShellExecute)
+ {
+ return s_startWithShellExecute!(startInfo, stdinHandle, stdoutHandle, stderrHandle);
+ }
+
string? filename;
string[] argv;
@@ -130,13 +130,6 @@ internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFile
private static SafeProcessHandle StartWithShellExecute(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle)
{
- if (ProcessUtils.PlatformDoesNotSupportProcessStartAndKill)
- {
- throw new PlatformNotSupportedException();
- }
-
- ProcessUtils.EnsureInitialized();
-
IDictionary env = startInfo.Environment;
string? cwd = !string.IsNullOrWhiteSpace(startInfo.WorkingDirectory) ? startInfo.WorkingDirectory : null;
From 82c81f525f8c256db4021b8fc18c8f2c21f175c9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 30 Mar 2026 16:51:46 +0000
Subject: [PATCH 11/18] Make s_startWithShellExecute private in both platform
files
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/4652feac-4f97-49cf-a9f1-7f4f3bf445a5
Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com>
---
.../src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs | 2 +-
.../Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs
index 8f38701447680a..bed79a49c7109a 100644
--- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs
+++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs
@@ -61,7 +61,7 @@ protected override bool ReleaseHandle()
// Allows for StartWithShellExecute (and its dependencies) to be trimmed when UseShellExecute is not being used.
// On Unix, standard I/O handles are passed through to the shell process.
- internal static Func? s_startWithShellExecute;
+ private static Func? s_startWithShellExecute;
private static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle)
{
diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
index d156680ade1646..2c381c78025c3a 100644
--- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
+++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
@@ -19,7 +19,7 @@ protected override bool ReleaseHandle()
// Allows for StartWithShellExecute (and its dependencies) to be trimmed when UseShellExecute is not being used.
// On Windows, StartWithShellExecute does not use standard I/O handles.
- internal static Func? s_startWithShellExecute;
+ private static Func? s_startWithShellExecute;
internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle)
{
From bb7af8060c79a0ac6e33583776b5713d64557956 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 31 Mar 2026 00:33:12 +0000
Subject: [PATCH 12/18] Fix waitStateHolder bug in Unix UseShellExecute path;
add test for UseShellExecute delegate initialization
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/842a9987-1668-4933-baf7-2c38be354e11
Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com>
---
.../SafeHandles/SafeProcessHandle.Unix.cs | 18 ++++++++++--------
.../tests/SafeProcessHandleTests.cs | 18 ++++++++++++++++++
2 files changed, 28 insertions(+), 8 deletions(-)
diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs
index bed79a49c7109a..888595c15ec0a3 100644
--- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs
+++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs
@@ -61,7 +61,8 @@ protected override bool ReleaseHandle()
// Allows for StartWithShellExecute (and its dependencies) to be trimmed when UseShellExecute is not being used.
// On Unix, standard I/O handles are passed through to the shell process.
- private static Func? s_startWithShellExecute;
+ private delegate SafeProcessHandle StartWithShellExecuteDelegate(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, out ProcessWaitState.Holder? waitStateHolder);
+ private static StartWithShellExecuteDelegate? s_startWithShellExecute;
private static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle)
{
@@ -87,7 +88,7 @@ internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFile
if (startInfo.UseShellExecute)
{
- return s_startWithShellExecute!(startInfo, stdinHandle, stdoutHandle, stderrHandle);
+ return s_startWithShellExecute!(startInfo, stdinHandle, stdoutHandle, stderrHandle, out waitStateHolder);
}
string? filename;
@@ -128,7 +129,7 @@ internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFile
out waitStateHolder);
}
- private static SafeProcessHandle StartWithShellExecute(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle)
+ private static SafeProcessHandle StartWithShellExecute(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, out ProcessWaitState.Holder? waitStateHolder)
{
IDictionary env = startInfo.Environment;
string? cwd = !string.IsNullOrWhiteSpace(startInfo.WorkingDirectory) ? startInfo.WorkingDirectory : null;
@@ -167,15 +168,17 @@ private static SafeProcessHandle StartWithShellExecute(ProcessStartInfo startInf
startInfo, filename, argv, env, cwd,
setCredentials, userId, groupId, groups,
stdinHandle, stdoutHandle, stderrHandle, usesTerminal,
- out ProcessWaitState.Holder? waitStateHolder,
+ out ProcessWaitState.Holder? firstHolder,
throwOnNoExec: false); // return invalid handle instead of throwing on ENOEXEC
- waitStateHolder?.Dispose();
-
if (!processHandle.IsInvalid)
{
+ waitStateHolder = firstHolder;
return processHandle;
}
+
+ // ENOEXEC: the process was not started on this path; dispose the holder and try the fallback.
+ firstHolder?.Dispose();
}
// use default program to open file/url
@@ -186,9 +189,8 @@ private static SafeProcessHandle StartWithShellExecute(ProcessStartInfo startInf
startInfo, filename, openFileArgv, env, cwd,
setCredentials, userId, groupId, groups,
stdinHandle, stdoutHandle, stderrHandle, usesTerminal,
- out ProcessWaitState.Holder? waitStateHolder2);
+ out waitStateHolder);
- waitStateHolder2?.Dispose();
return result;
}
diff --git a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs
index b49235cf04cb25..fc19984543c68e 100644
--- a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs
+++ b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs
@@ -101,5 +101,23 @@ public void Start_WithRedirectedStreams_ThrowsInvalidOperationException(
Assert.Throws(() => SafeProcessHandle.Start(startInfo));
}
+
+ [Fact]
+ [PlatformSpecific(TestPlatforms.AnyUnix | TestPlatforms.Windows)] // Covers platforms where UseShellExecute is supported
+ public void Start_UseShellExecuteTrue_InitializesDelegate()
+ {
+ // Setting UseShellExecute = true should call EnsureShellExecuteFunc(), which initializes
+ // the shell execute delegate. If the delegate were not set, Start would throw NullReferenceException.
+ // This test verifies the delegate is set by confirming Start throws a meaningful exception
+ // (e.g., Win32Exception, PlatformNotSupportedException) rather than NullReferenceException.
+ ProcessStartInfo startInfo = new("nonexistent_file_xyz_12345_copilot_test")
+ {
+ UseShellExecute = true,
+ };
+
+ Exception? ex = Record.Exception(() => SafeProcessHandle.Start(startInfo)?.Dispose());
+ Assert.NotNull(ex);
+ Assert.IsNotType(ex);
+ }
}
}
From 45697c415746795ce90f707eca45c20a72a8d506 Mon Sep 17 00:00:00 2001
From: Jan Kotas
Date: Mon, 30 Mar 2026 17:39:25 -0700
Subject: [PATCH 13/18] Apply suggestion from @jkotas
---
.../tests/SafeProcessHandleTests.cs | 18 ------------------
1 file changed, 18 deletions(-)
diff --git a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs
index fc19984543c68e..b49235cf04cb25 100644
--- a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs
+++ b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs
@@ -101,23 +101,5 @@ public void Start_WithRedirectedStreams_ThrowsInvalidOperationException(
Assert.Throws(() => SafeProcessHandle.Start(startInfo));
}
-
- [Fact]
- [PlatformSpecific(TestPlatforms.AnyUnix | TestPlatforms.Windows)] // Covers platforms where UseShellExecute is supported
- public void Start_UseShellExecuteTrue_InitializesDelegate()
- {
- // Setting UseShellExecute = true should call EnsureShellExecuteFunc(), which initializes
- // the shell execute delegate. If the delegate were not set, Start would throw NullReferenceException.
- // This test verifies the delegate is set by confirming Start throws a meaningful exception
- // (e.g., Win32Exception, PlatformNotSupportedException) rather than NullReferenceException.
- ProcessStartInfo startInfo = new("nonexistent_file_xyz_12345_copilot_test")
- {
- UseShellExecute = true,
- };
-
- Exception? ex = Record.Exception(() => SafeProcessHandle.Start(startInfo)?.Dispose());
- Assert.NotNull(ex);
- Assert.IsNotType(ex);
- }
}
}
From 989c73f461669526204b0c84cabfb629aaaeecb4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 31 Mar 2026 00:49:02 +0000
Subject: [PATCH 14/18] Simplify StartWithShellExecute: reuse waitStateHolder
instead of firstHolder
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/bfd2bdde-ccb1-4a59-8768-45b4d935e7c4
Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com>
---
.../Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs
index 888595c15ec0a3..8c429cf3eddf4b 100644
--- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs
+++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs
@@ -168,17 +168,16 @@ private static SafeProcessHandle StartWithShellExecute(ProcessStartInfo startInf
startInfo, filename, argv, env, cwd,
setCredentials, userId, groupId, groups,
stdinHandle, stdoutHandle, stderrHandle, usesTerminal,
- out ProcessWaitState.Holder? firstHolder,
+ out waitStateHolder,
throwOnNoExec: false); // return invalid handle instead of throwing on ENOEXEC
if (!processHandle.IsInvalid)
{
- waitStateHolder = firstHolder;
return processHandle;
}
// ENOEXEC: the process was not started on this path; dispose the holder and try the fallback.
- firstHolder?.Dispose();
+ waitStateHolder?.Dispose();
}
// use default program to open file/url
From 787aaeb251396685d457c9f71f86345870327c8a Mon Sep 17 00:00:00 2001
From: Jan Kotas
Date: Mon, 30 Mar 2026 17:52:17 -0700
Subject: [PATCH 15/18] Update
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
---
.../Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs | 2 --
1 file changed, 2 deletions(-)
diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
index 2c381c78025c3a..dadecbaaf191a2 100644
--- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
+++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs
@@ -17,8 +17,6 @@ protected override bool ReleaseHandle()
return Interop.Kernel32.CloseHandle(handle);
}
- // Allows for StartWithShellExecute (and its dependencies) to be trimmed when UseShellExecute is not being used.
- // On Windows, StartWithShellExecute does not use standard I/O handles.
private static Func? s_startWithShellExecute;
internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle)
From 4879a6d43c269d915658dbca55fb26c3a606ae6f Mon Sep 17 00:00:00 2001
From: Jan Kotas
Date: Mon, 30 Mar 2026 17:52:28 -0700
Subject: [PATCH 16/18] Update
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs
---
.../src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs | 2 --
1 file changed, 2 deletions(-)
diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs
index 8c429cf3eddf4b..5d5d11283be0d0 100644
--- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs
+++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs
@@ -59,8 +59,6 @@ protected override bool ReleaseHandle()
// On Unix, we don't use process descriptors yet, so we can't get PID.
private static int GetProcessIdCore() => throw new PlatformNotSupportedException();
- // Allows for StartWithShellExecute (and its dependencies) to be trimmed when UseShellExecute is not being used.
- // On Unix, standard I/O handles are passed through to the shell process.
private delegate SafeProcessHandle StartWithShellExecuteDelegate(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, out ProcessWaitState.Holder? waitStateHolder);
private static StartWithShellExecuteDelegate? s_startWithShellExecute;
From 5fa8793327cc4a4427d945cec3529aab7aadafd9 Mon Sep 17 00:00:00 2001
From: Jan Kotas
Date: Mon, 30 Mar 2026 17:59:21 -0700
Subject: [PATCH 17/18] Apply suggestion from @jkotas
---
.../src/System/Diagnostics/ProcessStartInfo.Win32.cs | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Win32.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Win32.cs
index 77f3c396a13962..b81c1041a498d8 100644
--- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Win32.cs
+++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Win32.cs
@@ -45,6 +45,5 @@ public string[] Verbs
}
}
}
-
}
}
From 280f757f9b5e9fab07d033b66d154883c39cba93 Mon Sep 17 00:00:00 2001
From: Jan Kotas
Date: Mon, 30 Mar 2026 21:22:42 -0700
Subject: [PATCH 18/18] Apply suggestion from @jkotas
---
.../src/System/Diagnostics/ProcessStartInfo.cs | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs
index d0b8c3ae9da979..264fdd27d7dcdf 100644
--- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs
+++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs
@@ -237,7 +237,10 @@ public bool UseShellExecute
get;
set
{
- SafeProcessHandle.EnsureShellExecuteFunc();
+ if (value)
+ {
+ SafeProcessHandle.EnsureShellExecuteFunc();
+ }
field = value;
}
}