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; } }