From f1c70aee7ea0167e8022149c54ab6535c4f1ee91 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:33:58 +0000 Subject: [PATCH 01/21] Add Process.Start callback overload with ProcessStartArguments and tests Introduces a new Process.Start(ProcessStartInfo, Func) overload that allows users to create processes using their own system calls while leveraging .NET's Process infrastructure (pipe management, async I/O, WaitForExit). - Added ProcessStartArguments class with platform-specific pointer properties - Added Windows implementation using BuildCommandLine/GetEnvironmentVariablesBlock - Added Unix implementation using ResolvePath/ParseArgv/AllocArgvArray/AllocEnvpArray - Updated ref assembly with new public API surface - Added Windows test using CreateProcess inside the callback - Added Unix test using posix_spawn inside the callback Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../Interop.ForkAndExecProcess.cs | 4 +- .../ref/System.Diagnostics.Process.cs | 19 ++ .../ref/System.Diagnostics.Process.csproj | 1 + .../src/Resources/Strings.resx | 3 + .../src/System.Diagnostics.Process.csproj | 1 + .../src/System/Diagnostics/Process.Unix.cs | 37 ++++ .../src/System/Diagnostics/Process.Windows.cs | 37 ++++ .../src/System/Diagnostics/Process.cs | 174 ++++++++++++++++++ .../Diagnostics/ProcessStartArguments.cs | 75 ++++++++ .../tests/ProcessHandlesTests.Unix.cs | 97 ++++++++++ .../tests/ProcessHandlesTests.Windows.cs | 52 ++++++ 11 files changed, 498 insertions(+), 2 deletions(-) create mode 100644 src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.cs index 7f2622b6d49c19..9a95fa3074d57a 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.cs @@ -111,7 +111,7 @@ private static unsafe partial int ForkAndExecProcess( /// Allocates a single native memory block containing both a null-terminated pointer array /// and the UTF-8 encoded string data for the given array of strings. /// - private static unsafe void AllocArgvArray(string[] arr, ref byte** arrPtr) + internal static unsafe void AllocArgvArray(string[] arr, ref byte** arrPtr) { int count = arr.Length; @@ -150,7 +150,7 @@ private static unsafe void AllocArgvArray(string[] arr, ref byte** arrPtr) /// Allocates a single native memory block containing both a null-terminated pointer array /// and the UTF-8 encoded "key=value\0" data for all non-null entries in the environment dictionary. /// - private static unsafe void AllocEnvpArray(IDictionary env, ref byte** arrPtr) + internal static unsafe void AllocEnvpArray(IDictionary env, ref byte** arrPtr) { // First pass: count entries with non-null values and compute total buffer size. int count = 0; diff --git a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs index 1294bc9accf1c0..fbff81bb40e76d 100644 --- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs +++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs @@ -215,6 +215,11 @@ public void Refresh() { } [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")] [System.Runtime.Versioning.SupportedOSPlatformAttribute("maccatalyst")] // this needs to come after the ios attribute due to limitations in the platform analyzer public static System.Diagnostics.Process? Start(System.Diagnostics.ProcessStartInfo startInfo) { throw null; } + [System.CLSCompliantAttribute(false)] + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")] + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")] + [System.Runtime.Versioning.SupportedOSPlatformAttribute("maccatalyst")] + public static System.Diagnostics.Process Start(System.Diagnostics.ProcessStartInfo startInfo, System.Func callback) { throw null; } [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")] [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")] [System.Runtime.Versioning.SupportedOSPlatformAttribute("maccatalyst")] // this needs to come after the ios attribute due to limitations in the platform analyzer @@ -287,6 +292,20 @@ public readonly partial struct ProcessOutputLine public bool StandardError { get { throw null; } } public override string ToString() { throw null; } } + public sealed partial class ProcessStartArguments + { + internal ProcessStartArguments() { } + [System.CLSCompliantAttribute(false)] + public unsafe void* Arguments { get { throw null; } } + [System.CLSCompliantAttribute(false)] + public unsafe void* EnvironmentVariables { get { throw null; } } + public string? FileName { get { throw null; } } + public System.Diagnostics.ProcessStartInfo ProcessStartInfo { get { throw null; } } + public Microsoft.Win32.SafeHandles.SafeFileHandle StandardError { get { throw null; } } + public Microsoft.Win32.SafeHandles.SafeFileHandle StandardInput { get { throw null; } } + public Microsoft.Win32.SafeHandles.SafeFileHandle StandardOutput { get { throw null; } } + public string? WorkingDirectory { get { throw null; } } + } public enum ProcessPriorityClass { Normal = 32, diff --git a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.csproj b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.csproj index c94a5333818707..b31936a2e89d09 100644 --- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.csproj @@ -1,6 +1,7 @@ $(NetCoreAppCurrent) + true diff --git a/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx index 92a6dd8e10675e..da808cf0554d77 100644 --- a/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx +++ b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx @@ -372,4 +372,7 @@ On Unix, it's impossible to obtain the exit status of a non-child process. + + The callback must return a valid SafeProcessHandle for the newly created process. + 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 bc1e3b89bb10de..fcd34c1621f3d2 100644 --- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj @@ -24,6 +24,7 @@ + diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs index bd7a9df16cb809..256fb0ad78ff83 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs @@ -409,6 +409,43 @@ private static AnonymousPipeClientStream OpenStream(SafeFileHandle handle, FileA return new AnonymousPipeClientStream(direction, safePipeHandle); } + private static unsafe partial SafeProcessHandle InvokeStartCallback(ProcessStartInfo startInfo, SafeFileHandle childInput, SafeFileHandle childOutput, SafeFileHandle childError, Func callback) + { + string? resolvedFileName = ProcessUtils.ResolvePath(startInfo.FileName); + string[] argv = ProcessUtils.ParseArgv(startInfo); + + IDictionary env = startInfo.Environment; + string? workingDirectory = !string.IsNullOrWhiteSpace(startInfo.WorkingDirectory) ? startInfo.WorkingDirectory : null; + + byte** argvPtr = null; + byte** envpPtr = null; + + try + { + Interop.Sys.AllocArgvArray(argv, ref argvPtr); + Interop.Sys.AllocEnvpArray(env, ref envpPtr); + + ProcessStartArguments args = new() + { + FileName = resolvedFileName, + Arguments = argvPtr, + WorkingDirectory = workingDirectory, + EnvironmentVariables = envpPtr, + StandardInput = childInput, + StandardOutput = childOutput, + StandardError = childError, + ProcessStartInfo = startInfo, + }; + + return callback(args); + } + finally + { + NativeMemory.Free(envpPtr); + NativeMemory.Free(argvPtr); + } + } + private static Encoding GetStandardInputEncoding() => Encoding.Default; private static Encoding GetStandardOutputEncoding() => Encoding.Default; 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 cb4207390483fd..57507b4a2996c4 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 @@ -526,6 +526,43 @@ private bool StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, return true; } + private static unsafe partial SafeProcessHandle InvokeStartCallback(ProcessStartInfo startInfo, SafeFileHandle childInput, SafeFileHandle childOutput, SafeFileHandle childError, Func callback) + { + ValueStringBuilder commandLine = new(stackalloc char[256]); + ProcessUtils.BuildCommandLine(startInfo, ref commandLine); + commandLine.NullTerminate(); + + string? environmentBlock = null; + if (startInfo._environmentVariables != null) + { + environmentBlock = ProcessUtils.GetEnvironmentVariablesBlock(startInfo._environmentVariables!); + } + + string? workingDirectory = startInfo.WorkingDirectory; + if (workingDirectory is not null && workingDirectory.Length == 0) + { + workingDirectory = null; + } + + ProcessStartArguments args = new() + { + FileName = null, // On Windows, the file name is embedded in the command line + WorkingDirectory = workingDirectory, + StandardInput = childInput, + StandardOutput = childOutput, + StandardError = childError, + ProcessStartInfo = startInfo, + }; + + fixed (char* commandLinePtr = &commandLine.GetPinnableReference()) + fixed (char* environmentBlockPtr = environmentBlock) + { + args.Arguments = commandLinePtr; + args.EnvironmentVariables = environmentBlockPtr; + return callback(args); + } + } + private string GetMainWindowTitle() { IntPtr handle = MainWindowHandle; diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs index ef037f21ec64cf..e21abe7e555b79 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs @@ -1385,6 +1385,180 @@ public static Process Start(string fileName, IEnumerable arguments) null; } + /// + /// Starts a new process by preparing all necessary arguments (standard handles, command line, environment, working directory) + /// and then invoking the user-supplied to perform the actual process creation system call. + /// The callback receives a instance with the prepared data and must return a + /// representing the created process. + /// + /// The that contains the information used to start the process. + /// + /// A function that receives the prepared and creates the process using any system call of the user's choice. + /// The callback must return a valid for the newly created process. + /// The memory referenced by pointer properties in is only valid for the duration of the callback. + /// + /// A new instance associated with the started process. + /// or is . + /// The returned by the callback is invalid. + /// is set to . + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] + [CLSCompliant(false)] + public static Process Start(ProcessStartInfo startInfo, Func callback) + { + ArgumentNullException.ThrowIfNull(startInfo); + ArgumentNullException.ThrowIfNull(callback); + + if (startInfo.UseShellExecute) + { + throw new InvalidOperationException(SR.Format(SR.UseShellExecuteNotSupportedForScenario, nameof(Start))); + } + + if (!ProcessUtils.PlatformSupportsProcessStartAndKill) + { + throw new PlatformNotSupportedException(); + } + + SerializationGuard.ThrowIfDeserializationInProgress("AllowProcessCreation", ref ProcessUtils.s_cachedSerializationSwitch); + + startInfo.ThrowIfInvalid(out bool anyRedirection, out SafeHandle[]? inheritedHandles); + + SafeFileHandle? parentInputPipeHandle = null; + SafeFileHandle? parentOutputPipeHandle = null; + SafeFileHandle? parentErrorPipeHandle = null; + + SafeFileHandle? childInputHandle = null; + SafeFileHandle? childOutputHandle = null; + SafeFileHandle? childErrorHandle = null; + + try + { + bool requiresLock = anyRedirection && !ProcessUtils.SupportsAtomicNonInheritablePipeCreation; + + if (requiresLock) + { + ProcessUtils.s_processStartLock.EnterWriteLock(); + } + + try + { + if (startInfo.StandardInputHandle is not null) + { + childInputHandle = startInfo.StandardInputHandle; + } + else if (startInfo.RedirectStandardInput) + { + SafeFileHandle.CreateAnonymousPipe(out childInputHandle, out parentInputPipeHandle); + } + + if (startInfo.StandardOutputHandle is not null) + { + childOutputHandle = startInfo.StandardOutputHandle; + } + else if (startInfo.RedirectStandardOutput) + { + SafeFileHandle.CreateAnonymousPipe(out parentOutputPipeHandle, out childOutputHandle, asyncRead: OperatingSystem.IsWindows()); + } + + if (startInfo.StandardErrorHandle is not null) + { + childErrorHandle = startInfo.StandardErrorHandle; + } + else if (startInfo.RedirectStandardError) + { + SafeFileHandle.CreateAnonymousPipe(out parentErrorPipeHandle, out childErrorHandle, asyncRead: OperatingSystem.IsWindows()); + } + } + finally + { + if (requiresLock) + { + ProcessUtils.s_processStartLock.ExitWriteLock(); + } + } + + if (startInfo.StartDetached) + { + if (childInputHandle is null || childOutputHandle is null || childErrorHandle is null) + { + SafeFileHandle nullDeviceHandle = File.OpenNullHandle(); + childInputHandle ??= nullDeviceHandle; + childOutputHandle ??= nullDeviceHandle; + childErrorHandle ??= nullDeviceHandle; + } + } + else if (ProcessUtils.PlatformSupportsConsole) + { + childInputHandle ??= Console.OpenStandardInputHandle(); + childOutputHandle ??= Console.OpenStandardOutputHandle(); + childErrorHandle ??= Console.OpenStandardErrorHandle(); + } + + SafeProcessHandle processHandle = InvokeStartCallback(startInfo, childInputHandle!, childOutputHandle!, childErrorHandle!, callback); + + if (processHandle is null || processHandle.IsInvalid) + { + throw new ArgumentException(SR.Argument_InvalidHandle, nameof(callback)); + } + + Process process = new Process(); + process._startInfo = startInfo; + process.SetProcessHandle(processHandle); + process.SetProcessId(processHandle.ProcessId); + + if (startInfo.RedirectStandardInput) + { + process._standardInput = new StreamWriter(OpenStream(parentInputPipeHandle!, FileAccess.Write), + startInfo.StandardInputEncoding ?? GetStandardInputEncoding(), StreamBufferSize) + { + AutoFlush = true + }; + } + if (startInfo.RedirectStandardOutput) + { + process._standardOutput = new StreamReader(OpenStream(parentOutputPipeHandle!, FileAccess.Read), + startInfo.StandardOutputEncoding ?? GetStandardOutputEncoding(), true, StreamBufferSize); + } + if (startInfo.RedirectStandardError) + { + process._standardError = new StreamReader(OpenStream(parentErrorPipeHandle!, FileAccess.Read), + startInfo.StandardErrorEncoding ?? GetStandardOutputEncoding(), true, StreamBufferSize); + } + + return process; + } + catch + { + parentInputPipeHandle?.Dispose(); + parentOutputPipeHandle?.Dispose(); + parentErrorPipeHandle?.Dispose(); + + throw; + } + finally + { + if (startInfo.StandardInputHandle is null) + { + childInputHandle?.Dispose(); + } + if (startInfo.StandardOutputHandle is null) + { + childOutputHandle?.Dispose(); + } + if (startInfo.StandardErrorHandle is null) + { + childErrorHandle?.Dispose(); + } + } + } + + /// + /// Platform-specific method that prepares the command line / argv, environment block, and working directory, + /// then invokes the user callback with the populated . + /// + private static unsafe partial SafeProcessHandle InvokeStartCallback(ProcessStartInfo startInfo, SafeFileHandle childInput, SafeFileHandle childOutput, SafeFileHandle childError, Func callback); + /// /// Make sure we are not watching for process exit. /// diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs new file mode 100644 index 00000000000000..544b4e9892a9ea --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Win32.SafeHandles; + +namespace System.Diagnostics +{ + /// + /// Provides the prepared arguments for starting a process via a user-supplied callback. + /// This class is populated by the method + /// with the resolved file name, command-line arguments, working directory, environment variables, and standard I/O handles. + /// The user's callback receives this instance and is responsible for invoking the appropriate system call to create the process. + /// + public sealed class ProcessStartArguments + { + internal ProcessStartArguments() { } + + /// + /// Gets the resolved file name of the process to start. + /// On Windows, this is (the file name is embedded in ). + /// On Unix, this is the resolved absolute path to the executable. + /// + public string? FileName { get; internal set; } + + /// + /// Gets a pointer to the command-line arguments for the process. + /// On Windows, this is a pointer to a null-terminated string (the full command line including the executable). + /// On Unix, this is a pointer to a null-terminated array of pointers to null-terminated UTF-8 byte strings (argv). + /// + /// + /// The memory pointed to by this property is only valid for the duration of the callback invocation. + /// + [CLSCompliant(false)] + public unsafe void* Arguments { get; internal set; } + + /// + /// Gets the working directory for the new process, or if the current directory should be used. + /// + public string? WorkingDirectory { get; internal set; } + + /// + /// Gets a pointer to the environment variables block for the new process. + /// On Windows, this is a pointer to a null-terminated string in the format used by CreateProcess + /// (each variable is "name=value\0", terminated by an extra '\0'). + /// On Unix, this is a pointer to a null-terminated array of pointers to null-terminated UTF-8 byte strings ("name=value"). + /// When , the new process inherits the current process's environment. + /// + /// + /// The memory pointed to by this property is only valid for the duration of the callback invocation. + /// + [CLSCompliant(false)] + public unsafe void* EnvironmentVariables { get; internal set; } + + /// + /// Gets the to use as the standard input for the new process. + /// + public SafeFileHandle StandardInput { get; internal set; } = null!; + + /// + /// Gets the to use as the standard output for the new process. + /// + public SafeFileHandle StandardOutput { get; internal set; } = null!; + + /// + /// Gets the to use as the standard error for the new process. + /// + public SafeFileHandle StandardError { get; internal set; } = null!; + + /// + /// Gets the original provided by the user, + /// allowing the callback to inspect any additional configuration. + /// + public ProcessStartInfo ProcessStartInfo { get; internal set; } = null!; + } +} diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs index 70bc434b2b3c25..513e6754f33d21 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs @@ -2,12 +2,109 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel; +using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; +using Xunit; namespace System.Diagnostics.Tests { public partial class ProcessHandlesTests { + [Fact] + public unsafe void StartWithCallback_PosixSpawn_CanRedirectOutput() + { + ProcessStartInfo startInfo = new("/bin/sh") + { + ArgumentList = { "-c", "echo hello" }, + RedirectStandardOutput = true, + UseShellExecute = false + }; + + using Process process = Process.Start(startInfo, (ProcessStartArguments args) => + { + int result; + + // posix_spawn_file_actions_t is a platform-specific struct (80 bytes on glibc x64, different on macOS). + // We allocate enough space on the stack and pass a pointer. + byte* fileActionsBuffer = stackalloc byte[PosixSpawnFileActionsSize]; + NativeMemory.Clear(fileActionsBuffer, (nuint)PosixSpawnFileActionsSize); + + result = posix_spawn_file_actions_init(fileActionsBuffer); + if (result != 0) + { + throw new Win32Exception(result); + } + + try + { + // Redirect stdin + result = posix_spawn_file_actions_adddup2(fileActionsBuffer, (int)args.StandardInput.DangerousGetHandle(), 0); + if (result != 0) + { + throw new Win32Exception(result); + } + + // Redirect stdout to the pipe provided by Process + result = posix_spawn_file_actions_adddup2(fileActionsBuffer, (int)args.StandardOutput.DangerousGetHandle(), 1); + if (result != 0) + { + throw new Win32Exception(result); + } + + // Redirect stderr + result = posix_spawn_file_actions_adddup2(fileActionsBuffer, (int)args.StandardError.DangerousGetHandle(), 2); + if (result != 0) + { + throw new Win32Exception(result); + } + + int pid; + byte[] pathBytes = System.Text.Encoding.UTF8.GetBytes(args.FileName! + '\0'); + fixed (byte* pathPtr = pathBytes) + { + result = posix_spawn(&pid, pathPtr, fileActionsBuffer, null, (byte**)args.Arguments, (byte**)args.EnvironmentVariables); + } + + if (result != 0) + { + throw new Win32Exception(result); + } + + // Get SafeProcessHandle from the pid. + // In the future, SafeProcessHandle.Open will be used instead. + Process spawned = Process.GetProcessById(pid); + return spawned.SafeHandle; + } + finally + { + posix_spawn_file_actions_destroy(fileActionsBuffer); + } + }); + + string output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(WaitInMS); + + Assert.Equal("hello\n", output); + Assert.True(process.HasExited); + Assert.Equal(0, process.ExitCode); + } + + // posix_spawn_file_actions_t is 80 bytes on glibc x64, 104 bytes on macOS arm64. + // Use 128 bytes to be safe across platforms. + private const int PosixSpawnFileActionsSize = 128; + + [DllImport("libc", SetLastError = false)] + private static extern unsafe int posix_spawn(int* pid, byte* path, void* file_actions, void* attrp, byte** argv, byte** envp); + + [DllImport("libc", SetLastError = false)] + private static extern unsafe int posix_spawn_file_actions_init(void* file_actions); + + [DllImport("libc", SetLastError = false)] + private static extern unsafe int posix_spawn_file_actions_destroy(void* file_actions); + + [DllImport("libc", SetLastError = false)] + private static extern unsafe int posix_spawn_file_actions_adddup2(void* file_actions, int fd, int newfd); + private static string GetSafeFileHandleId(SafeFileHandle handle) { if (Interop.Sys.FStat(handle, out Interop.Sys.FileStatus status) != 0) diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs index c49b71627e0a0e..f5a40b576d8561 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs @@ -105,6 +105,58 @@ public void ProcessStartedWithInvalidHandles_CanRedirectOutput(bool restrictHand Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithInvalidHandles(process.StartInfo)); } + [Fact] + public unsafe void StartWithCallback_CreateProcess_CanRedirectOutput() + { + ProcessStartInfo startInfo = new("cmd") + { + ArgumentList = { "/c", "echo hello" }, + RedirectStandardOutput = true, + UseShellExecute = false + }; + + using Process process = Process.Start(startInfo, (ProcessStartArguments args) => + { + Interop.Kernel32.STARTUPINFOEX startupInfoEx = default; + Interop.Kernel32.PROCESS_INFORMATION processInfo = default; + Interop.Kernel32.SECURITY_ATTRIBUTES unused_SecAttrs = default; + + startupInfoEx.StartupInfo.cb = sizeof(Interop.Kernel32.STARTUPINFOEX); + startupInfoEx.StartupInfo.hStdInput = args.StandardInput.DangerousGetHandle(); + startupInfoEx.StartupInfo.hStdOutput = args.StandardOutput.DangerousGetHandle(); + startupInfoEx.StartupInfo.hStdError = args.StandardError.DangerousGetHandle(); + startupInfoEx.StartupInfo.dwFlags = Interop.Advapi32.StartupInfoOptions.STARTF_USESTDHANDLES; + + bool retVal = Interop.Kernel32.CreateProcess( + null, + (char*)args.Arguments, + ref unused_SecAttrs, + ref unused_SecAttrs, + bInheritHandles: true, + Interop.Kernel32.EXTENDED_STARTUPINFO_PRESENT, + args.EnvironmentVariables, + (char*)null, + &startupInfoEx, + &processInfo + ); + + if (!retVal) + { + throw new Win32Exception(); + } + + Interop.Kernel32.CloseHandle(processInfo.hThread); + + return new SafeProcessHandle(processInfo.hProcess, ownsHandle: true); + }); + + string output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(WaitInMS); + + Assert.Contains("hello", output); + Assert.Equal(0, process.ExitCode); + } + private unsafe int RunWithInvalidHandles(ProcessStartInfo startInfo) { const nint INVALID_HANDLE_VALUE = -1; From 59234fdcf1ef46f085633975587b4a2c1361f9ad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:35:30 +0000 Subject: [PATCH 02/21] Fix CreateProcess environment parameter cast in Windows test Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../tests/ProcessHandlesTests.Windows.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs index f5a40b576d8561..2e0923872d445b 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs @@ -134,8 +134,8 @@ public unsafe void StartWithCallback_CreateProcess_CanRedirectOutput() ref unused_SecAttrs, bInheritHandles: true, Interop.Kernel32.EXTENDED_STARTUPINFO_PRESENT, - args.EnvironmentVariables, - (char*)null, + (char*)args.EnvironmentVariables, + null, &startupInfoEx, &processInfo ); From 3163021f8ae9fb836ccd0a65132b43ccb240dc82 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 2 Jun 2026 11:30:10 +0200 Subject: [PATCH 03/21] address my own feedback: - reduce code duplication - acquire the locks! - fix Windows tests: enable inheritance - fix Unix implementation: handle "usesTerminal" --- .../ref/System.Diagnostics.Process.cs | 1 - .../SafeHandles/SafeProcessHandle.Unix.cs | 73 ++++++++ .../SafeHandles/SafeProcessHandle.Windows.cs | 78 +++++++++ .../src/System/Diagnostics/Process.Unix.cs | 52 ++---- .../src/System/Diagnostics/Process.Windows.cs | 43 +---- .../src/System/Diagnostics/Process.cs | 156 ++---------------- .../Diagnostics/ProcessStartArguments.cs | 5 - 7 files changed, 184 insertions(+), 224 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs index fbff81bb40e76d..f6c4fe6e0b6b17 100644 --- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs +++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs @@ -304,7 +304,6 @@ internal ProcessStartArguments() { } public Microsoft.Win32.SafeHandles.SafeFileHandle StandardError { get { throw null; } } public Microsoft.Win32.SafeHandles.SafeFileHandle StandardInput { get { throw null; } } public Microsoft.Win32.SafeHandles.SafeFileHandle StandardOutput { get { throw null; } } - public string? WorkingDirectory { get { throw null; } } } public enum ProcessPriorityClass { 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 580c921aa243ec..b33e5aaf15d125 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 @@ -218,6 +218,79 @@ internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFile out waitStateHolder); } + internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo startInfo, SafeFileHandle childInput, SafeFileHandle childOutput, SafeFileHandle childError, + Func callback, out ProcessWaitState.Holder? waitStateHolder) + { + waitStateHolder = null; + ProcessUtils.EnsureInitialized(); + + string? resolvedFileName = ProcessUtils.ResolvePath(startInfo.FileName); + string[] argv = ProcessUtils.ParseArgv(startInfo); + bool usesTerminal = UsesTerminal(childInput, childOutput, childError); + + byte** argvPtr = null; + byte** envpPtr = null; + + try + { + Interop.Sys.AllocArgvArray(argv, ref argvPtr); + Interop.Sys.AllocEnvpArray(startInfo.Environment, ref envpPtr); + + ProcessStartArguments args = new() + { + FileName = resolvedFileName, + Arguments = argvPtr, + EnvironmentVariables = envpPtr, + StandardInput = childInput, + StandardOutput = childOutput, + StandardError = childError, + ProcessStartInfo = startInfo, + }; + + // Lock to avoid races with OnSigChild + // By using a ReaderWriterLock we allow multiple processes to start concurrently. + ProcessUtils.s_processStartLock.EnterReadLock(); + + try + { + if (usesTerminal) + { + ProcessUtils.ConfigureTerminalForChildProcesses(1); + } + + SafeProcessHandle processHandle = callback(args); + if (processHandle is null || processHandle.IsInvalid) + { + throw new ArgumentException(SR.Argument_InvalidHandle, nameof(callback)); + } + + waitStateHolder = new ProcessWaitState.Holder(processHandle.ProcessId, isNewChild: true, usesTerminal); + return processHandle; + } + catch + { + if (usesTerminal) + { + // We failed to launch a child that could use the terminal. + ProcessUtils.s_processStartLock.EnterWriteLock(); + ProcessUtils.ConfigureTerminalForChildProcesses(-1); + ProcessUtils.s_processStartLock.ExitWriteLock(); + } + + throw; + } + finally + { + ProcessUtils.s_processStartLock.ExitReadLock(); + } + } + finally + { + NativeMemory.Free(envpPtr); + NativeMemory.Free(argvPtr); + } + } + private static SafeProcessHandle StartWithShellExecute(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, out ProcessWaitState.Holder? waitStateHolder) { 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 6410a78817e170..5cdc01caeeb319 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 @@ -356,6 +356,84 @@ internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, S return procSH; } + internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo startInfo, SafeFileHandle childInput, SafeFileHandle childOutput, SafeFileHandle childError, + Func callback) + { + ValueStringBuilder commandLine = new(stackalloc char[256]); + ProcessUtils.BuildCommandLine(startInfo, ref commandLine); + commandLine.NullTerminate(); + + string? environmentBlock = null; + if (startInfo._environmentVariables != null) + { + environmentBlock = ProcessUtils.GetEnvironmentVariablesBlock(startInfo._environmentVariables!); + } + + ProcessStartArguments args = new() + { + FileName = null, // On Windows, the file name is embedded in the command line + StandardInput = childInput, + StandardOutput = childOutput, + StandardError = childError, + ProcessStartInfo = startInfo, + }; + + // Acquire the process lock to avoid accidental handle inheritance issues. + ProcessUtils.s_processStartLock.EnterWriteLock(); + + try + { + EnableInheritance(childInput); + EnableInheritance(childOutput); + EnableInheritance(childError); + + fixed (char* commandLinePtr = &commandLine.GetPinnableReference()) + fixed (char* environmentBlockPtr = environmentBlock) + { + args.Arguments = commandLinePtr; + args.EnvironmentVariables = environmentBlockPtr; + + SafeProcessHandle startedProcess = callback(args); + if (startedProcess is null || startedProcess.IsInvalid) + { + throw new ArgumentException(SR.Argument_InvalidHandle, nameof(callback)); + } + + return startedProcess; + } + } + finally + { + ProcessUtils.s_processStartLock.ExitWriteLock(); + } + + static void EnableInheritance(SafeFileHandle safeHandle) + { + bool refAdded = false; + + try + { + safeHandle.DangerousAddRef(ref refAdded); + + // Enable inheritance on this handle so the child process can use it. + if (!Interop.Kernel32.SetHandleInformation( + safeHandle.DangerousGetHandle(), + Interop.Kernel32.HandleFlags.HANDLE_FLAG_INHERIT, + Interop.Kernel32.HandleFlags.HANDLE_FLAG_INHERIT)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } + finally + { + if (refAdded) + { + safeHandle.DangerousRelease(); + } + } + } + } + private static unsafe SafeProcessHandle StartWithShellExecute(ProcessStartInfo startInfo) { if (!string.IsNullOrEmpty(startInfo.UserName) || startInfo.Password != null) diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs index 256fb0ad78ff83..441490801bc4a4 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs @@ -358,13 +358,20 @@ private SafeProcessHandle GetProcessHandle() return new SafeProcessHandle(_waitStateHolder!.IncrementRefCount()); } - private bool StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, SafeHandle[]? inheritedHandles) + private bool StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, SafeHandle[]? inheritedHandles, Func? callback) { - SafeProcessHandle startedProcess = SafeProcessHandle.StartCore(startInfo, stdinHandle, stdoutHandle, stderrHandle, inheritedHandles, out ProcessWaitState.Holder? waitStateHolder); + ProcessWaitState.Holder? waitStateHolder = null; + + SafeProcessHandle startedProcess = callback is null + ? SafeProcessHandle.StartCore(startInfo, stdinHandle, stdoutHandle, stderrHandle, inheritedHandles, out waitStateHolder) + : SafeProcessHandle.StartWithCallback(startInfo, stdinHandle!, stdoutHandle!, stderrHandle!, callback, out waitStateHolder); + Debug.Assert(!startedProcess.IsInvalid); - // SafeProcessHandle has its own copy of the wait state holder, so we need to increment the ref count for our copy. - _waitStateHolder = waitStateHolder!.IncrementRefCount(); + _waitStateHolder = callback is null + ? waitStateHolder!.IncrementRefCount() // SafeProcessHandle has its own copy of the wait state holder, so we need to increment the ref count for our copy. + : waitStateHolder; // we created a dedicated holder + SetProcessHandle(startedProcess); SetProcessId(startedProcess.ProcessId); return true; @@ -409,43 +416,6 @@ private static AnonymousPipeClientStream OpenStream(SafeFileHandle handle, FileA return new AnonymousPipeClientStream(direction, safePipeHandle); } - private static unsafe partial SafeProcessHandle InvokeStartCallback(ProcessStartInfo startInfo, SafeFileHandle childInput, SafeFileHandle childOutput, SafeFileHandle childError, Func callback) - { - string? resolvedFileName = ProcessUtils.ResolvePath(startInfo.FileName); - string[] argv = ProcessUtils.ParseArgv(startInfo); - - IDictionary env = startInfo.Environment; - string? workingDirectory = !string.IsNullOrWhiteSpace(startInfo.WorkingDirectory) ? startInfo.WorkingDirectory : null; - - byte** argvPtr = null; - byte** envpPtr = null; - - try - { - Interop.Sys.AllocArgvArray(argv, ref argvPtr); - Interop.Sys.AllocEnvpArray(env, ref envpPtr); - - ProcessStartArguments args = new() - { - FileName = resolvedFileName, - Arguments = argvPtr, - WorkingDirectory = workingDirectory, - EnvironmentVariables = envpPtr, - StandardInput = childInput, - StandardOutput = childOutput, - StandardError = childError, - ProcessStartInfo = startInfo, - }; - - return callback(args); - } - finally - { - NativeMemory.Free(envpPtr); - NativeMemory.Free(argvPtr); - } - } - private static Encoding GetStandardInputEncoding() => Encoding.Default; private static Encoding GetStandardOutputEncoding() => Encoding.Default; 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 57507b4a2996c4..335178619fca7a 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 @@ -508,9 +508,11 @@ private SafeProcessHandle GetProcessHandle(int access, bool throwIfExited = true private static ConsoleEncoding GetStandardOutputEncoding() => GetEncoding((int)Interop.Kernel32.GetConsoleOutputCP()); - private bool StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, SafeHandle[]? inheritedHandles) + private bool StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, SafeHandle[]? inheritedHandles, Func? callback) { - SafeProcessHandle startedProcess = SafeProcessHandle.StartCore(startInfo, stdinHandle, stdoutHandle, stderrHandle, inheritedHandles); + SafeProcessHandle startedProcess = callback is null + ? SafeProcessHandle.StartCore(startInfo, stdinHandle, stdoutHandle, stderrHandle, inheritedHandles) + : SafeProcessHandle.StartWithCallback(startInfo, stdinHandle!, stdoutHandle!, stderrHandle!, callback); if (startedProcess.IsInvalid) { @@ -526,43 +528,6 @@ private bool StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, return true; } - private static unsafe partial SafeProcessHandle InvokeStartCallback(ProcessStartInfo startInfo, SafeFileHandle childInput, SafeFileHandle childOutput, SafeFileHandle childError, Func callback) - { - ValueStringBuilder commandLine = new(stackalloc char[256]); - ProcessUtils.BuildCommandLine(startInfo, ref commandLine); - commandLine.NullTerminate(); - - string? environmentBlock = null; - if (startInfo._environmentVariables != null) - { - environmentBlock = ProcessUtils.GetEnvironmentVariablesBlock(startInfo._environmentVariables!); - } - - string? workingDirectory = startInfo.WorkingDirectory; - if (workingDirectory is not null && workingDirectory.Length == 0) - { - workingDirectory = null; - } - - ProcessStartArguments args = new() - { - FileName = null, // On Windows, the file name is embedded in the command line - WorkingDirectory = workingDirectory, - StandardInput = childInput, - StandardOutput = childOutput, - StandardError = childError, - ProcessStartInfo = startInfo, - }; - - fixed (char* commandLinePtr = &commandLine.GetPinnableReference()) - fixed (char* environmentBlockPtr = environmentBlock) - { - args.Arguments = commandLinePtr; - args.EnvironmentVariables = environmentBlockPtr; - return callback(args); - } - } - private string GetMainWindowTitle() { IntPtr handle = MainWindowHandle; diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs index e21abe7e555b79..93bb066afdd904 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs @@ -1161,7 +1161,17 @@ public bool Start() { Close(); - ProcessStartInfo startInfo = StartInfo; + //Cannot start a new process and store its handle if the object has been disposed, since finalization has been suppressed. + CheckDisposed(); + + return StartCore(StartInfo, callback: null); + } + + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] + private bool StartCore(ProcessStartInfo startInfo, Func? callback) + { startInfo.ThrowIfInvalid(out bool anyRedirection, out SafeHandle[]? inheritedHandles); if (!ProcessUtils.PlatformSupportsProcessStartAndKill) @@ -1169,9 +1179,6 @@ public bool Start() throw new PlatformNotSupportedException(); } - //Cannot start a new process and store its handle if the object has been disposed, since finalization has been suppressed. - CheckDisposed(); - SerializationGuard.ThrowIfDeserializationInProgress("AllowProcessCreation", ref ProcessUtils.s_cachedSerializationSwitch); SafeFileHandle? parentInputPipeHandle = null; @@ -1258,7 +1265,7 @@ public bool Start() ProcessStartInfo.ValidateInheritedHandles(childInputHandle, childOutputHandle, childErrorHandle, inheritedHandles); } - if (!StartCore(startInfo, childInputHandle, childOutputHandle, childErrorHandle, inheritedHandles)) + if (!StartCore(startInfo, childInputHandle, childOutputHandle, childErrorHandle, inheritedHandles, callback)) { return false; } @@ -1415,149 +1422,22 @@ public static Process Start(ProcessStartInfo startInfo, Func - /// Platform-specific method that prepares the command line / argv, environment block, and working directory, - /// then invokes the user callback with the populated . - /// - private static unsafe partial SafeProcessHandle InvokeStartCallback(ProcessStartInfo startInfo, SafeFileHandle childInput, SafeFileHandle childOutput, SafeFileHandle childError, Func callback); + return process; + } /// /// Make sure we are not watching for process exit. diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs index 544b4e9892a9ea..c2d079364c4c50 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs @@ -33,11 +33,6 @@ internal ProcessStartArguments() { } [CLSCompliant(false)] public unsafe void* Arguments { get; internal set; } - /// - /// Gets the working directory for the new process, or if the current directory should be used. - /// - public string? WorkingDirectory { get; internal set; } - /// /// Gets a pointer to the environment variables block for the new process. /// On Windows, this is a pointer to a null-terminated string in the format used by CreateProcess From ba271660aa17ad59e128a7d4d5b5624ec3b444d9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 11:01:54 +0000 Subject: [PATCH 04/21] Address @adamsitnik test feedback: use ReadAllText, Redirect helper, local const, redirect stderr Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../tests/ProcessHandlesTests.Unix.cs | 41 ++++++++----------- .../tests/ProcessHandlesTests.Windows.cs | 5 ++- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs index 513e6754f33d21..3e6ca6cca46870 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs @@ -17,6 +17,7 @@ public unsafe void StartWithCallback_PosixSpawn_CanRedirectOutput() { ArgumentList = { "-c", "echo hello" }, RedirectStandardOutput = true, + RedirectStandardError = true, UseShellExecute = false }; @@ -26,8 +27,9 @@ public unsafe void StartWithCallback_PosixSpawn_CanRedirectOutput() // posix_spawn_file_actions_t is a platform-specific struct (80 bytes on glibc x64, different on macOS). // We allocate enough space on the stack and pass a pointer. + // Use 128 bytes to be safe across platforms. + const int PosixSpawnFileActionsSize = 128; byte* fileActionsBuffer = stackalloc byte[PosixSpawnFileActionsSize]; - NativeMemory.Clear(fileActionsBuffer, (nuint)PosixSpawnFileActionsSize); result = posix_spawn_file_actions_init(fileActionsBuffer); if (result != 0) @@ -37,26 +39,9 @@ public unsafe void StartWithCallback_PosixSpawn_CanRedirectOutput() try { - // Redirect stdin - result = posix_spawn_file_actions_adddup2(fileActionsBuffer, (int)args.StandardInput.DangerousGetHandle(), 0); - if (result != 0) - { - throw new Win32Exception(result); - } - - // Redirect stdout to the pipe provided by Process - result = posix_spawn_file_actions_adddup2(fileActionsBuffer, (int)args.StandardOutput.DangerousGetHandle(), 1); - if (result != 0) - { - throw new Win32Exception(result); - } - - // Redirect stderr - result = posix_spawn_file_actions_adddup2(fileActionsBuffer, (int)args.StandardError.DangerousGetHandle(), 2); - if (result != 0) - { - throw new Win32Exception(result); - } + Redirect(fileActionsBuffer, args.StandardInput, 0); + Redirect(fileActionsBuffer, args.StandardOutput, 1); + Redirect(fileActionsBuffer, args.StandardError, 2); int pid; byte[] pathBytes = System.Text.Encoding.UTF8.GetBytes(args.FileName! + '\0'); @@ -81,7 +66,7 @@ public unsafe void StartWithCallback_PosixSpawn_CanRedirectOutput() } }); - string output = process.StandardOutput.ReadToEnd(); + (string output, string error) = process.ReadAllText(); process.WaitForExit(WaitInMS); Assert.Equal("hello\n", output); @@ -89,10 +74,16 @@ public unsafe void StartWithCallback_PosixSpawn_CanRedirectOutput() Assert.Equal(0, process.ExitCode); } - // posix_spawn_file_actions_t is 80 bytes on glibc x64, 104 bytes on macOS arm64. - // Use 128 bytes to be safe across platforms. - private const int PosixSpawnFileActionsSize = 128; + private static unsafe void Redirect(void* fileActionsBuffer, SafeFileHandle handle, int fd) + { + int result = posix_spawn_file_actions_adddup2(fileActionsBuffer, (int)handle.DangerousGetHandle(), fd); + if (result != 0) + { + throw new Win32Exception(result); + } + } + // posix_spawn_file_actions_t is 80 bytes on glibc x64, 104 bytes on macOS arm64. [DllImport("libc", SetLastError = false)] private static extern unsafe int posix_spawn(int* pid, byte* path, void* file_actions, void* attrp, byte** argv, byte** envp); diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs index 2e0923872d445b..23c8cadeb546e1 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs @@ -112,6 +112,7 @@ public unsafe void StartWithCallback_CreateProcess_CanRedirectOutput() { ArgumentList = { "/c", "echo hello" }, RedirectStandardOutput = true, + RedirectStandardError = true, UseShellExecute = false }; @@ -150,10 +151,10 @@ public unsafe void StartWithCallback_CreateProcess_CanRedirectOutput() return new SafeProcessHandle(processInfo.hProcess, ownsHandle: true); }); - string output = process.StandardOutput.ReadToEnd(); + (string output, string error) = process.ReadAllText(); process.WaitForExit(WaitInMS); - Assert.Contains("hello", output); + Assert.Equal("hello\r\n", output); Assert.Equal(0, process.ExitCode); } From 9c12e8e9c01003f14650ec7f427b5a1c30f78f98 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 11:02:56 +0000 Subject: [PATCH 05/21] Preserve size rationale comment alongside local const Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../tests/ProcessHandlesTests.Unix.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs index 3e6ca6cca46870..0626eb9c05173c 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs @@ -25,7 +25,7 @@ public unsafe void StartWithCallback_PosixSpawn_CanRedirectOutput() { int result; - // posix_spawn_file_actions_t is a platform-specific struct (80 bytes on glibc x64, different on macOS). + // posix_spawn_file_actions_t is a platform-specific struct (80 bytes on glibc x64, 104 bytes on macOS arm64). // We allocate enough space on the stack and pass a pointer. // Use 128 bytes to be safe across platforms. const int PosixSpawnFileActionsSize = 128; @@ -83,7 +83,6 @@ private static unsafe void Redirect(void* fileActionsBuffer, SafeFileHandle hand } } - // posix_spawn_file_actions_t is 80 bytes on glibc x64, 104 bytes on macOS arm64. [DllImport("libc", SetLastError = false)] private static extern unsafe int posix_spawn(int* pid, byte* path, void* file_actions, void* attrp, byte** argv, byte** envp); From 8b593b3a4de2aefafd2016911987affb6344f73f Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 2 Jun 2026 16:43:09 +0200 Subject: [PATCH 06/21] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs | 8 +++++++- .../Win32/SafeHandles/SafeProcessHandle.Windows.cs | 7 ++++++- .../src/System/Diagnostics/ProcessStartArguments.cs | 2 +- 3 files changed, 14 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 b33e5aaf15d125..91492311b49d0f 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 @@ -224,10 +224,16 @@ internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo star waitStateHolder = null; ProcessUtils.EnsureInitialized(); + string? cwd = !string.IsNullOrWhiteSpace(startInfo.WorkingDirectory) ? startInfo.WorkingDirectory : null; string? resolvedFileName = ProcessUtils.ResolvePath(startInfo.FileName); + if (string.IsNullOrEmpty(resolvedFileName)) + { + Interop.ErrorInfo error = Interop.Error.ENOENT.Info(); + throw ProcessUtils.CreateExceptionForErrorStartingProcess(error.GetErrorMessage(), error.RawErrno, startInfo.FileName, cwd); + } + string[] argv = ProcessUtils.ParseArgv(startInfo); bool usesTerminal = UsesTerminal(childInput, childOutput, childError); - byte** argvPtr = null; byte** envpPtr = null; 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 5cdc01caeeb319..dc2ef790db45be 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 @@ -405,16 +405,21 @@ internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo star finally { ProcessUtils.s_processStartLock.ExitWriteLock(); + commandLine.Dispose(); } static void EnableInheritance(SafeFileHandle safeHandle) { + if (safeHandle.IsInvalid) + { + return; + } + bool refAdded = false; try { safeHandle.DangerousAddRef(ref refAdded); - // Enable inheritance on this handle so the child process can use it. if (!Interop.Kernel32.SetHandleInformation( safeHandle.DangerousGetHandle(), diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs index c2d079364c4c50..d9fa156db4ae2b 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs @@ -8,7 +8,7 @@ namespace System.Diagnostics /// /// Provides the prepared arguments for starting a process via a user-supplied callback. /// This class is populated by the method - /// with the resolved file name, command-line arguments, working directory, environment variables, and standard I/O handles. + /// with the resolved file name, command-line arguments, environment variables, and standard I/O handles. /// The user's callback receives this instance and is responsible for invoking the appropriate system call to create the process. /// public sealed class ProcessStartArguments From d01ae8e62cb640a2b5fa7a4e334fb5ab6332ead8 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 2 Jun 2026 16:44:14 +0200 Subject: [PATCH 07/21] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../src/System/Diagnostics/Process.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs index 93bb066afdd904..8892a9b3dde57e 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs @@ -1393,7 +1393,7 @@ public static Process Start(string fileName, IEnumerable arguments) } /// - /// Starts a new process by preparing all necessary arguments (standard handles, command line, environment, working directory) + /// Starts a new process by preparing all necessary arguments (standard handles, command line, environment) /// and then invoking the user-supplied to perform the actual process creation system call. /// The callback receives a instance with the prepared data and must return a /// representing the created process. From b29a7211fdfc01f591950221115cebffbaf68b89 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 2 Jun 2026 17:02:06 +0200 Subject: [PATCH 08/21] address feedback: - handle inheritance bug fix - make properties mutable - expose standard handles as raw nint --- .../ref/System.Diagnostics.Process.cs | 16 ++-- .../SafeHandles/SafeProcessHandle.Unix.cs | 62 ++++++++++------ .../SafeHandles/SafeProcessHandle.Windows.cs | 74 +++++++++---------- .../Diagnostics/ProcessStartArguments.cs | 22 +++--- .../tests/ProcessHandlesTests.Unix.cs | 4 +- .../tests/ProcessHandlesTests.Windows.cs | 6 +- 6 files changed, 98 insertions(+), 86 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs index f6c4fe6e0b6b17..f846ea1d498db1 100644 --- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs +++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs @@ -294,16 +294,16 @@ public readonly partial struct ProcessOutputLine } public sealed partial class ProcessStartArguments { - internal ProcessStartArguments() { } + public ProcessStartArguments() { } [System.CLSCompliantAttribute(false)] - public unsafe void* Arguments { get { throw null; } } + public unsafe void* Arguments { get { throw null; } set { } } [System.CLSCompliantAttribute(false)] - public unsafe void* EnvironmentVariables { get { throw null; } } - public string? FileName { get { throw null; } } - public System.Diagnostics.ProcessStartInfo ProcessStartInfo { get { throw null; } } - public Microsoft.Win32.SafeHandles.SafeFileHandle StandardError { get { throw null; } } - public Microsoft.Win32.SafeHandles.SafeFileHandle StandardInput { get { throw null; } } - public Microsoft.Win32.SafeHandles.SafeFileHandle StandardOutput { get { throw null; } } + public unsafe void* EnvironmentVariables { get { throw null; } set { } } + public string? FileName { get { throw null; } set { } } + public System.Diagnostics.ProcessStartInfo ProcessStartInfo { get { throw null; } set { } } + public System.IntPtr StandardError { get { throw null; } set { } } + public System.IntPtr StandardInput { get { throw null; } set { } } + public System.IntPtr StandardOutput { get { throw null; } set { } } } public enum ProcessPriorityClass { 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 91492311b49d0f..24b5a4e47e6e83 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 @@ -218,38 +218,57 @@ internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFile out waitStateHolder); } - internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo startInfo, SafeFileHandle childInput, SafeFileHandle childOutput, SafeFileHandle childError, + internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo startInfo, SafeFileHandle? stdinFd, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, Func callback, out ProcessWaitState.Holder? waitStateHolder) { waitStateHolder = null; ProcessUtils.EnsureInitialized(); - string? cwd = !string.IsNullOrWhiteSpace(startInfo.WorkingDirectory) ? startInfo.WorkingDirectory : null; string? resolvedFileName = ProcessUtils.ResolvePath(startInfo.FileName); if (string.IsNullOrEmpty(resolvedFileName)) { Interop.ErrorInfo error = Interop.Error.ENOENT.Info(); - throw ProcessUtils.CreateExceptionForErrorStartingProcess(error.GetErrorMessage(), error.RawErrno, startInfo.FileName, cwd); + throw ProcessUtils.CreateExceptionForErrorStartingProcess(error.GetErrorMessage(), error.RawErrno, startInfo.FileName, startInfo.WorkingDirectory); } string[] argv = ProcessUtils.ParseArgv(startInfo); - bool usesTerminal = UsesTerminal(childInput, childOutput, childError); - byte** argvPtr = null; - byte** envpPtr = null; + bool configuredTerminal = false, usesTerminal = UsesTerminal(stdinFd, stdoutHandle, stderrHandle); + byte** argvPtr = null, envpPtr = null; + bool stdinRefAdded = false, stdoutRefAdded = false, stderrRefAdded = false; try { Interop.Sys.AllocArgvArray(argv, ref argvPtr); Interop.Sys.AllocEnvpArray(startInfo.Environment, ref envpPtr); + int stdinRawFd = -1, stdoutRawFd = -1, stderrRawFd = -1; + + if (stdinFd is not null) + { + stdinFd.DangerousAddRef(ref stdinRefAdded); + stdinRawFd = stdinFd.DangerousGetHandle().ToInt32(); + } + + if (stdoutHandle is not null) + { + stdoutHandle.DangerousAddRef(ref stdoutRefAdded); + stdoutRawFd = stdoutHandle.DangerousGetHandle().ToInt32(); + } + + if (stderrHandle is not null) + { + stderrHandle.DangerousAddRef(ref stderrRefAdded); + stderrRawFd = stderrHandle.DangerousGetHandle().ToInt32(); + } + ProcessStartArguments args = new() { FileName = resolvedFileName, Arguments = argvPtr, EnvironmentVariables = envpPtr, - StandardInput = childInput, - StandardOutput = childOutput, - StandardError = childError, + StandardInput = stdinRawFd, + StandardOutput = stdoutRawFd, + StandardError = stderrRawFd, ProcessStartInfo = startInfo, }; @@ -262,6 +281,7 @@ internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo star if (usesTerminal) { ProcessUtils.ConfigureTerminalForChildProcesses(1); + configuredTerminal = true; } SafeProcessHandle processHandle = callback(args); @@ -273,23 +293,23 @@ internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo star waitStateHolder = new ProcessWaitState.Holder(processHandle.ProcessId, isNewChild: true, usesTerminal); return processHandle; } - catch - { - if (usesTerminal) - { - // We failed to launch a child that could use the terminal. - ProcessUtils.s_processStartLock.EnterWriteLock(); - ProcessUtils.ConfigureTerminalForChildProcesses(-1); - ProcessUtils.s_processStartLock.ExitWriteLock(); - } - - throw; - } finally { ProcessUtils.s_processStartLock.ExitReadLock(); } } + catch + { + if (configuredTerminal) + { + // We failed to launch a child that could use the terminal. + ProcessUtils.s_processStartLock.EnterWriteLock(); + ProcessUtils.ConfigureTerminalForChildProcesses(-1); + ProcessUtils.s_processStartLock.ExitWriteLock(); + } + + throw; + } finally { NativeMemory.Free(envpPtr); 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 dc2ef790db45be..76de13593e375d 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 @@ -356,7 +356,7 @@ internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, S return procSH; } - internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo startInfo, SafeFileHandle childInput, SafeFileHandle childOutput, SafeFileHandle childError, + internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo startInfo, SafeFileHandle stdinHandle, SafeFileHandle stdoutHandle, SafeFileHandle stderrHandle, Func callback) { ValueStringBuilder commandLine = new(stackalloc char[256]); @@ -369,23 +369,26 @@ internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo star environmentBlock = ProcessUtils.GetEnvironmentVariablesBlock(startInfo._environmentVariables!); } - ProcessStartArguments args = new() - { - FileName = null, // On Windows, the file name is embedded in the command line - StandardInput = childInput, - StandardOutput = childOutput, - StandardError = childError, - ProcessStartInfo = startInfo, - }; + nint stdin = -1, stdout = -1, stderr = -1; + bool stdinRefAdded = false, stdoutRefAdded = false, stderrRefAdded = false; // Acquire the process lock to avoid accidental handle inheritance issues. ProcessUtils.s_processStartLock.EnterWriteLock(); try { - EnableInheritance(childInput); - EnableInheritance(childOutput); - EnableInheritance(childError); + ProcessUtils.DuplicateAsInheritableIfNeeded(stdinHandle, ref stdin, ref stdinRefAdded); + ProcessUtils.DuplicateAsInheritableIfNeeded(stdoutHandle, ref stdout, ref stdoutRefAdded); + ProcessUtils.DuplicateAsInheritableIfNeeded(stderrHandle, ref stderr, ref stderrRefAdded); + + ProcessStartArguments args = new() + { + FileName = null, // On Windows, the file name is embedded in the command line + StandardInput = stdin, + StandardOutput = stdout, + StandardError = stderr, + ProcessStartInfo = startInfo, + }; fixed (char* commandLinePtr = &commandLine.GetPinnableReference()) fixed (char* environmentBlockPtr = environmentBlock) @@ -404,38 +407,27 @@ internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo star } finally { - ProcessUtils.s_processStartLock.ExitWriteLock(); - commandLine.Dispose(); - } + // If the provided handle was inheritable, just release the reference we added. + // Otherwise if we created a valid duplicate, close it. - static void EnableInheritance(SafeFileHandle safeHandle) - { - if (safeHandle.IsInvalid) - { - return; - } + if (stdinRefAdded) + stdinHandle.DangerousRelease(); + else if (!IsInvalidHandle(stdin)) + Interop.Kernel32.CloseHandle(stdin); - bool refAdded = false; + if (stdoutRefAdded) + stdoutHandle.DangerousRelease(); + else if (!IsInvalidHandle(stdout)) + Interop.Kernel32.CloseHandle(stdout); - try - { - safeHandle.DangerousAddRef(ref refAdded); - // Enable inheritance on this handle so the child process can use it. - if (!Interop.Kernel32.SetHandleInformation( - safeHandle.DangerousGetHandle(), - Interop.Kernel32.HandleFlags.HANDLE_FLAG_INHERIT, - Interop.Kernel32.HandleFlags.HANDLE_FLAG_INHERIT)) - { - throw new Win32Exception(Marshal.GetLastWin32Error()); - } - } - finally - { - if (refAdded) - { - safeHandle.DangerousRelease(); - } - } + if (stderrRefAdded) + stderrHandle.DangerousRelease(); + else if (!IsInvalidHandle(stderr)) + Interop.Kernel32.CloseHandle(stderr); + + ProcessUtils.s_processStartLock.ExitWriteLock(); + + commandLine.Dispose(); } } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs index d9fa156db4ae2b..85c63a44ead946 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs @@ -13,14 +13,14 @@ namespace System.Diagnostics /// public sealed class ProcessStartArguments { - internal ProcessStartArguments() { } + public ProcessStartArguments() { } /// /// Gets the resolved file name of the process to start. /// On Windows, this is (the file name is embedded in ). /// On Unix, this is the resolved absolute path to the executable. /// - public string? FileName { get; internal set; } + public string? FileName { get; set; } /// /// Gets a pointer to the command-line arguments for the process. @@ -31,7 +31,7 @@ internal ProcessStartArguments() { } /// The memory pointed to by this property is only valid for the duration of the callback invocation. /// [CLSCompliant(false)] - public unsafe void* Arguments { get; internal set; } + public unsafe void* Arguments { get; set; } /// /// Gets a pointer to the environment variables block for the new process. @@ -44,27 +44,27 @@ internal ProcessStartArguments() { } /// The memory pointed to by this property is only valid for the duration of the callback invocation. /// [CLSCompliant(false)] - public unsafe void* EnvironmentVariables { get; internal set; } + public unsafe void* EnvironmentVariables { get; set; } /// - /// Gets the to use as the standard input for the new process. + /// Gets the raw handle to use as the standard input for the new process. /// - public SafeFileHandle StandardInput { get; internal set; } = null!; + public nint StandardInput { get; set; } /// - /// Gets the to use as the standard output for the new process. + /// Gets the raw handle to use as the standard output for the new process. /// - public SafeFileHandle StandardOutput { get; internal set; } = null!; + public nint StandardOutput { get; set; } /// - /// Gets the to use as the standard error for the new process. + /// Gets the raw handle to use as the standard error for the new process. /// - public SafeFileHandle StandardError { get; internal set; } = null!; + public nint StandardError { get; set; } /// /// Gets the original provided by the user, /// allowing the callback to inspect any additional configuration. /// - public ProcessStartInfo ProcessStartInfo { get; internal set; } = null!; + public ProcessStartInfo ProcessStartInfo { get; set; } = null!; } } diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs index 0626eb9c05173c..e1310a7cd8dd00 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs @@ -74,9 +74,9 @@ public unsafe void StartWithCallback_PosixSpawn_CanRedirectOutput() Assert.Equal(0, process.ExitCode); } - private static unsafe void Redirect(void* fileActionsBuffer, SafeFileHandle handle, int fd) + private static unsafe void Redirect(void* fileActionsBuffer, nint handle, int fd) { - int result = posix_spawn_file_actions_adddup2(fileActionsBuffer, (int)handle.DangerousGetHandle(), fd); + int result = posix_spawn_file_actions_adddup2(fileActionsBuffer, (int)handle, fd); if (result != 0) { throw new Win32Exception(result); diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs index 23c8cadeb546e1..dff10c80403187 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs @@ -123,9 +123,9 @@ public unsafe void StartWithCallback_CreateProcess_CanRedirectOutput() Interop.Kernel32.SECURITY_ATTRIBUTES unused_SecAttrs = default; startupInfoEx.StartupInfo.cb = sizeof(Interop.Kernel32.STARTUPINFOEX); - startupInfoEx.StartupInfo.hStdInput = args.StandardInput.DangerousGetHandle(); - startupInfoEx.StartupInfo.hStdOutput = args.StandardOutput.DangerousGetHandle(); - startupInfoEx.StartupInfo.hStdError = args.StandardError.DangerousGetHandle(); + startupInfoEx.StartupInfo.hStdInput = args.StandardInput; + startupInfoEx.StartupInfo.hStdOutput = args.StandardOutput; + startupInfoEx.StartupInfo.hStdError = args.StandardError; startupInfoEx.StartupInfo.dwFlags = Interop.Advapi32.StartupInfoOptions.STARTF_USESTDHANDLES; bool retVal = Interop.Kernel32.CreateProcess( From 03cda3119e8705d21a806eff3ddfc7989dc77fb8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:42:00 +0000 Subject: [PATCH 09/21] Address Process callback test feedback Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../SafeHandles/SafeProcessHandle.Unix.cs | 13 ++++ .../tests/ProcessHandlesTests.Unix.cs | 63 ++++++++++++------- .../tests/ProcessHandlesTests.Windows.cs | 3 +- 3 files changed, 54 insertions(+), 25 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 24b5a4e47e6e83..0ec9a032768fe4 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 @@ -312,6 +312,19 @@ internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo star } finally { + if (stdinRefAdded) + { + stdinFd!.DangerousRelease(); + } + if (stdoutRefAdded) + { + stdoutHandle!.DangerousRelease(); + } + if (stderrRefAdded) + { + stderrHandle!.DangerousRelease(); + } + NativeMemory.Free(envpPtr); NativeMemory.Free(argvPtr); } diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs index e1310a7cd8dd00..3918497c98a375 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs @@ -3,6 +3,8 @@ using System.ComponentModel; using System.Runtime.InteropServices; +using Microsoft.DotNet.RemoteExecutor; +using Microsoft.DotNet.XUnitExtensions; using Microsoft.Win32.SafeHandles; using Xunit; @@ -10,12 +12,12 @@ namespace System.Diagnostics.Tests { public partial class ProcessHandlesTests { - [Fact] + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] public unsafe void StartWithCallback_PosixSpawn_CanRedirectOutput() { ProcessStartInfo startInfo = new("/bin/sh") { - ArgumentList = { "-c", "echo hello" }, + ArgumentList = { "-c", "echo hello && echo error >&2" }, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false @@ -27,42 +29,54 @@ public unsafe void StartWithCallback_PosixSpawn_CanRedirectOutput() // posix_spawn_file_actions_t is a platform-specific struct (80 bytes on glibc x64, 104 bytes on macOS arm64). // We allocate enough space on the stack and pass a pointer. - // Use 128 bytes to be safe across platforms. + // Use 128 bytes to be safe across platforms, and request explicit alignment for the native struct. const int PosixSpawnFileActionsSize = 128; - byte* fileActionsBuffer = stackalloc byte[PosixSpawnFileActionsSize]; - - result = posix_spawn_file_actions_init(fileActionsBuffer); - if (result != 0) + const nuint PosixSpawnFileActionsAlignment = 16; + void* fileActionsBuffer = NativeMemory.AlignedAlloc(PosixSpawnFileActionsSize, PosixSpawnFileActionsAlignment); + if (fileActionsBuffer is null) { - throw new Win32Exception(result); + throw new OutOfMemoryException(); } try { - Redirect(fileActionsBuffer, args.StandardInput, 0); - Redirect(fileActionsBuffer, args.StandardOutput, 1); - Redirect(fileActionsBuffer, args.StandardError, 2); - - int pid; - byte[] pathBytes = System.Text.Encoding.UTF8.GetBytes(args.FileName! + '\0'); - fixed (byte* pathPtr = pathBytes) - { - result = posix_spawn(&pid, pathPtr, fileActionsBuffer, null, (byte**)args.Arguments, (byte**)args.EnvironmentVariables); - } - + result = posix_spawn_file_actions_init(fileActionsBuffer); if (result != 0) { throw new Win32Exception(result); } - // Get SafeProcessHandle from the pid. - // In the future, SafeProcessHandle.Open will be used instead. - Process spawned = Process.GetProcessById(pid); - return spawned.SafeHandle; + try + { + Redirect(fileActionsBuffer, args.StandardInput, 0); + Redirect(fileActionsBuffer, args.StandardOutput, 1); + Redirect(fileActionsBuffer, args.StandardError, 2); + + int pid; + byte[] pathBytes = System.Text.Encoding.UTF8.GetBytes(args.FileName! + '\0'); + fixed (byte* pathPtr = pathBytes) + { + result = posix_spawn(&pid, pathPtr, fileActionsBuffer, null, (byte**)args.Arguments, (byte**)args.EnvironmentVariables); + } + + if (result != 0) + { + throw new Win32Exception(result); + } + + // Get SafeProcessHandle from the pid. + // In the future, SafeProcessHandle.Open will be used instead. + Process spawned = Process.GetProcessById(pid); + return spawned.SafeHandle; + } + finally + { + posix_spawn_file_actions_destroy(fileActionsBuffer); + } } finally { - posix_spawn_file_actions_destroy(fileActionsBuffer); + NativeMemory.AlignedFree(fileActionsBuffer); } }); @@ -70,6 +84,7 @@ public unsafe void StartWithCallback_PosixSpawn_CanRedirectOutput() process.WaitForExit(WaitInMS); Assert.Equal("hello\n", output); + Assert.Equal("error\n", error); Assert.True(process.HasExited); Assert.Equal(0, process.ExitCode); } diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs index dff10c80403187..5da5c96e8ac749 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs @@ -110,7 +110,7 @@ public unsafe void StartWithCallback_CreateProcess_CanRedirectOutput() { ProcessStartInfo startInfo = new("cmd") { - ArgumentList = { "/c", "echo hello" }, + ArgumentList = { "/c", "echo hello && echo error 1>&2" }, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false @@ -155,6 +155,7 @@ public unsafe void StartWithCallback_CreateProcess_CanRedirectOutput() process.WaitForExit(WaitInMS); Assert.Equal("hello\r\n", output); + Assert.Equal("error\r\n", error); Assert.Equal(0, process.ExitCode); } From 090d716f8b2f3b22840b5d7766442cfabc51e6a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 17:03:34 +0000 Subject: [PATCH 10/21] Use NativeMemory.Alloc in Unix process callback test Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- .../tests/ProcessHandlesTests.Unix.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs index 3918497c98a375..aca2ac5278318b 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs @@ -28,11 +28,9 @@ public unsafe void StartWithCallback_PosixSpawn_CanRedirectOutput() int result; // posix_spawn_file_actions_t is a platform-specific struct (80 bytes on glibc x64, 104 bytes on macOS arm64). - // We allocate enough space on the stack and pass a pointer. - // Use 128 bytes to be safe across platforms, and request explicit alignment for the native struct. + // Use 128 bytes to be safe across platforms; NativeMemory.Alloc provides sufficient native alignment. const int PosixSpawnFileActionsSize = 128; - const nuint PosixSpawnFileActionsAlignment = 16; - void* fileActionsBuffer = NativeMemory.AlignedAlloc(PosixSpawnFileActionsSize, PosixSpawnFileActionsAlignment); + void* fileActionsBuffer = NativeMemory.Alloc(PosixSpawnFileActionsSize); if (fileActionsBuffer is null) { throw new OutOfMemoryException(); @@ -76,7 +74,7 @@ public unsafe void StartWithCallback_PosixSpawn_CanRedirectOutput() } finally { - NativeMemory.AlignedFree(fileActionsBuffer); + NativeMemory.Free(fileActionsBuffer); } }); From cf52098b54a49b78a90be63a8e9034f0bcb3203f Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 2 Jun 2026 19:06:43 +0200 Subject: [PATCH 11/21] Apply suggestions from code review Co-authored-by: Adam Sitnik --- .../tests/ProcessHandlesTests.Unix.cs | 3 +-- .../tests/ProcessHandlesTests.Windows.cs | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs index aca2ac5278318b..56b1a7a23b0425 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs @@ -19,8 +19,7 @@ public unsafe void StartWithCallback_PosixSpawn_CanRedirectOutput() { ArgumentList = { "-c", "echo hello && echo error >&2" }, RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false + RedirectStandardError = true }; using Process process = Process.Start(startInfo, (ProcessStartArguments args) => diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs index 5da5c96e8ac749..70e8ebf9a3d23a 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs @@ -112,8 +112,7 @@ public unsafe void StartWithCallback_CreateProcess_CanRedirectOutput() { ArgumentList = { "/c", "echo hello && echo error 1>&2" }, RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false + RedirectStandardError = true }; using Process process = Process.Start(startInfo, (ProcessStartArguments args) => @@ -154,7 +153,7 @@ public unsafe void StartWithCallback_CreateProcess_CanRedirectOutput() (string output, string error) = process.ReadAllText(); process.WaitForExit(WaitInMS); - Assert.Equal("hello\r\n", output); + Assert.Equal("hello \r\n", output); Assert.Equal("error\r\n", error); Assert.Equal(0, process.ExitCode); } From 2245788f7abfc0e44af5e341fa7e46508a776ff0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 17:25:42 +0000 Subject: [PATCH 12/21] Update ProcessStartArguments docs for public setters Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../Diagnostics/ProcessStartArguments.cs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs index 85c63a44ead946..bb9b825f794c06 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs @@ -16,14 +16,14 @@ public sealed class ProcessStartArguments public ProcessStartArguments() { } /// - /// Gets the resolved file name of the process to start. + /// Gets or sets the resolved file name of the process to start. /// On Windows, this is (the file name is embedded in ). /// On Unix, this is the resolved absolute path to the executable. /// public string? FileName { get; set; } /// - /// Gets a pointer to the command-line arguments for the process. + /// Gets or sets a pointer to the command-line arguments for the process. /// On Windows, this is a pointer to a null-terminated string (the full command line including the executable). /// On Unix, this is a pointer to a null-terminated array of pointers to null-terminated UTF-8 byte strings (argv). /// @@ -34,7 +34,7 @@ public ProcessStartArguments() { } public unsafe void* Arguments { get; set; } /// - /// Gets a pointer to the environment variables block for the new process. + /// Gets or sets a pointer to the environment variables block for the new process. /// On Windows, this is a pointer to a null-terminated string in the format used by CreateProcess /// (each variable is "name=value\0", terminated by an extra '\0'). /// On Unix, this is a pointer to a null-terminated array of pointers to null-terminated UTF-8 byte strings ("name=value"). @@ -47,22 +47,28 @@ public ProcessStartArguments() { } public unsafe void* EnvironmentVariables { get; set; } /// - /// Gets the raw handle to use as the standard input for the new process. + /// Gets or sets the raw handle to use as the standard input for the new process. + /// On Unix, this is a file descriptor value. + /// On Windows, this is an OS handle value. /// public nint StandardInput { get; set; } /// - /// Gets the raw handle to use as the standard output for the new process. + /// Gets or sets the raw handle to use as the standard output for the new process. + /// On Unix, this is a file descriptor value. + /// On Windows, this is an OS handle value. /// public nint StandardOutput { get; set; } /// - /// Gets the raw handle to use as the standard error for the new process. + /// Gets or sets the raw handle to use as the standard error for the new process. + /// On Unix, this is a file descriptor value. + /// On Windows, this is an OS handle value. /// public nint StandardError { get; set; } /// - /// Gets the original provided by the user, + /// Gets or sets the original provided by the user, /// allowing the callback to inspect any additional configuration. /// public ProcessStartInfo ProcessStartInfo { get; set; } = null!; From d46d4bc987ed14b96a8c6273f2787e273d6eee85 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 17:59:33 +0000 Subject: [PATCH 13/21] Revert standard handle semantics wording in ProcessStartArguments docs Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../src/System/Diagnostics/ProcessStartArguments.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs index bb9b825f794c06..d17bebb364168c 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs @@ -48,22 +48,16 @@ public ProcessStartArguments() { } /// /// Gets or sets the raw handle to use as the standard input for the new process. - /// On Unix, this is a file descriptor value. - /// On Windows, this is an OS handle value. /// public nint StandardInput { get; set; } /// /// Gets or sets the raw handle to use as the standard output for the new process. - /// On Unix, this is a file descriptor value. - /// On Windows, this is an OS handle value. /// public nint StandardOutput { get; set; } /// /// Gets or sets the raw handle to use as the standard error for the new process. - /// On Unix, this is a file descriptor value. - /// On Windows, this is an OS handle value. /// public nint StandardError { get; set; } From 3079bddebb3276e6120f10ee03451390d1512543 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 18:43:01 +0000 Subject: [PATCH 14/21] Update callback path to expose Unix resolved executable pointer Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../ref/System.Diagnostics.Process.cs | 4 +- .../SafeHandles/SafeProcessHandle.Unix.cs | 76 +++++++++++-------- .../SafeHandles/SafeProcessHandle.Windows.cs | 1 - .../Diagnostics/ProcessStartArguments.cs | 19 +++-- .../tests/ProcessHandlesTests.Unix.cs | 6 +- 5 files changed, 64 insertions(+), 42 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs index f846ea1d498db1..b0114e49840607 100644 --- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs +++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs @@ -299,7 +299,9 @@ public ProcessStartArguments() { } public unsafe void* Arguments { get { throw null; } set { } } [System.CLSCompliantAttribute(false)] public unsafe void* EnvironmentVariables { get { throw null; } set { } } - public string? FileName { get { throw null; } set { } } + [System.CLSCompliantAttribute(false)] + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("windows")] + public unsafe byte* ResolvedPath { get { throw null; } set { } } public System.Diagnostics.ProcessStartInfo ProcessStartInfo { get { throw null; } set { } } public System.IntPtr StandardError { get { throw null; } set { } } public System.IntPtr StandardInput { get { throw null; } set { } } 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 0ec9a032768fe4..dbddd3e5ff9eeb 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 @@ -234,6 +234,7 @@ internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo star string[] argv = ProcessUtils.ParseArgv(startInfo); bool configuredTerminal = false, usesTerminal = UsesTerminal(stdinFd, stdoutHandle, stderrHandle); byte** argvPtr = null, envpPtr = null; + byte* resolvedPathBufferPtr = null; bool stdinRefAdded = false, stdoutRefAdded = false, stderrRefAdded = false; try @@ -241,6 +242,17 @@ internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo star Interop.Sys.AllocArgvArray(argv, ref argvPtr); Interop.Sys.AllocEnvpArray(startInfo.Environment, ref envpPtr); + int resolvedPathByteCount = Encoding.UTF8.GetByteCount(resolvedFileName); + // Keep small paths on the stack, while avoiding excessive stack usage for long executable paths. + const int ResolvedPathStackBufferSize = 256; + Span resolvedPathBuffer = resolvedPathByteCount + 1 <= ResolvedPathStackBufferSize + ? stackalloc byte[resolvedPathByteCount + 1] + : new Span(resolvedPathBufferPtr = (byte*)NativeMemory.Alloc((nuint)(resolvedPathByteCount + 1)), resolvedPathByteCount + 1); + + int resolvedPathBytesWritten = Encoding.UTF8.GetBytes(resolvedFileName, resolvedPathBuffer); + Debug.Assert(resolvedPathBytesWritten == resolvedPathByteCount); + resolvedPathBuffer[resolvedPathBytesWritten] = (byte)0; + int stdinRawFd = -1, stdoutRawFd = -1, stderrRawFd = -1; if (stdinFd is not null) @@ -261,41 +273,44 @@ internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo star stderrRawFd = stderrHandle.DangerousGetHandle().ToInt32(); } - ProcessStartArguments args = new() - { - FileName = resolvedFileName, - Arguments = argvPtr, - EnvironmentVariables = envpPtr, - StandardInput = stdinRawFd, - StandardOutput = stdoutRawFd, - StandardError = stderrRawFd, - ProcessStartInfo = startInfo, - }; - - // Lock to avoid races with OnSigChild - // By using a ReaderWriterLock we allow multiple processes to start concurrently. - ProcessUtils.s_processStartLock.EnterReadLock(); - - try + fixed (byte* resolvedPathPtr = resolvedPathBuffer) { - if (usesTerminal) + ProcessStartArguments args = new() { - ProcessUtils.ConfigureTerminalForChildProcesses(1); - configuredTerminal = true; + ResolvedPath = resolvedPathPtr, + Arguments = argvPtr, + EnvironmentVariables = envpPtr, + StandardInput = stdinRawFd, + StandardOutput = stdoutRawFd, + StandardError = stderrRawFd, + ProcessStartInfo = startInfo, + }; + + // Lock to avoid races with OnSigChild + // By using a ReaderWriterLock we allow multiple processes to start concurrently. + ProcessUtils.s_processStartLock.EnterReadLock(); + + try + { + if (usesTerminal) + { + ProcessUtils.ConfigureTerminalForChildProcesses(1); + configuredTerminal = true; + } + + SafeProcessHandle processHandle = callback(args); + if (processHandle is null || processHandle.IsInvalid) + { + throw new ArgumentException(SR.Argument_InvalidHandle, nameof(callback)); + } + + waitStateHolder = new ProcessWaitState.Holder(processHandle.ProcessId, isNewChild: true, usesTerminal); + return processHandle; } - - SafeProcessHandle processHandle = callback(args); - if (processHandle is null || processHandle.IsInvalid) + finally { - throw new ArgumentException(SR.Argument_InvalidHandle, nameof(callback)); + ProcessUtils.s_processStartLock.ExitReadLock(); } - - waitStateHolder = new ProcessWaitState.Holder(processHandle.ProcessId, isNewChild: true, usesTerminal); - return processHandle; - } - finally - { - ProcessUtils.s_processStartLock.ExitReadLock(); } } catch @@ -327,6 +342,7 @@ internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo star NativeMemory.Free(envpPtr); NativeMemory.Free(argvPtr); + NativeMemory.Free(resolvedPathBufferPtr); } } 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 76de13593e375d..bad329d41e6036 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 @@ -383,7 +383,6 @@ internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo star ProcessStartArguments args = new() { - FileName = null, // On Windows, the file name is embedded in the command line StandardInput = stdin, StandardOutput = stdout, StandardError = stderr, diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs index d17bebb364168c..c409ce80558355 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs @@ -2,13 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Win32.SafeHandles; +using System.Runtime.Versioning; namespace System.Diagnostics { /// /// Provides the prepared arguments for starting a process via a user-supplied callback. /// This class is populated by the method - /// with the resolved file name, command-line arguments, environment variables, and standard I/O handles. + /// with the resolved executable path, command-line arguments, environment variables, and standard I/O handles. /// The user's callback receives this instance and is responsible for invoking the appropriate system call to create the process. /// public sealed class ProcessStartArguments @@ -16,11 +17,19 @@ public sealed class ProcessStartArguments public ProcessStartArguments() { } /// - /// Gets or sets the resolved file name of the process to start. - /// On Windows, this is (the file name is embedded in ). - /// On Unix, this is the resolved absolute path to the executable. + /// Gets or sets a pointer to the resolved absolute executable path encoded as null-terminated UTF-8. /// - public string? FileName { get; set; } + /// + /// A pointer to a null-terminated UTF-8 encoded string representing the resolved absolute executable path. + /// + /// + /// The memory pointed to by this property is only valid for the duration of the callback invocation. + /// This property is writable to allow callback implementations to override the resolved path when needed. + /// Do not cache or use this pointer after the callback returns. + /// + [CLSCompliant(false)] + [UnsupportedOSPlatform("windows")] + public unsafe byte* ResolvedPath { get; set; } /// /// Gets or sets a pointer to the command-line arguments for the process. diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs index 56b1a7a23b0425..853d490beb248a 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs @@ -50,11 +50,7 @@ public unsafe void StartWithCallback_PosixSpawn_CanRedirectOutput() Redirect(fileActionsBuffer, args.StandardError, 2); int pid; - byte[] pathBytes = System.Text.Encoding.UTF8.GetBytes(args.FileName! + '\0'); - fixed (byte* pathPtr = pathBytes) - { - result = posix_spawn(&pid, pathPtr, fileActionsBuffer, null, (byte**)args.Arguments, (byte**)args.EnvironmentVariables); - } + result = posix_spawn(&pid, args.ResolvedPath, fileActionsBuffer, null, (byte**)args.Arguments, (byte**)args.EnvironmentVariables); if (result != 0) { From 26a38000a8bd5f4d77a0be7d483059f77cba2196 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:03:21 +0000 Subject: [PATCH 15/21] Adjust Unix resolved path buffer strategy Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../Win32/SafeHandles/SafeProcessHandle.Unix.cs | 10 ++++++++-- 1 file changed, 8 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 dbddd3e5ff9eeb..e0f92731db6a66 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 @@ -244,10 +244,16 @@ internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo star int resolvedPathByteCount = Encoding.UTF8.GetByteCount(resolvedFileName); // Keep small paths on the stack, while avoiding excessive stack usage for long executable paths. - const int ResolvedPathStackBufferSize = 256; + const int ResolvedPathStackBufferSize = +#if DEBUG + 2; // make sure we test both code paths +#else + 256; +#endif Span resolvedPathBuffer = resolvedPathByteCount + 1 <= ResolvedPathStackBufferSize - ? stackalloc byte[resolvedPathByteCount + 1] + ? stackalloc byte[ResolvedPathStackBufferSize] : new Span(resolvedPathBufferPtr = (byte*)NativeMemory.Alloc((nuint)(resolvedPathByteCount + 1)), resolvedPathByteCount + 1); + resolvedPathBuffer = resolvedPathBuffer[..(resolvedPathByteCount + 1)]; int resolvedPathBytesWritten = Encoding.UTF8.GetBytes(resolvedFileName, resolvedPathBuffer); Debug.Assert(resolvedPathBytesWritten == resolvedPathByteCount); From d30a6f62bb74465f8e561df0a904fbc61e74cf86 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:03:25 +0000 Subject: [PATCH 16/21] Make ProcessStartArguments a ref struct Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../ref/System.Diagnostics.Process.cs | 2 +- .../src/System/Diagnostics/ProcessStartArguments.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs index b0114e49840607..f8be1c4a2cfeae 100644 --- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs +++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs @@ -292,7 +292,7 @@ public readonly partial struct ProcessOutputLine public bool StandardError { get { throw null; } } public override string ToString() { throw null; } } - public sealed partial class ProcessStartArguments + public ref partial struct ProcessStartArguments { public ProcessStartArguments() { } [System.CLSCompliantAttribute(false)] diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs index c409ce80558355..11e50826f5c606 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs @@ -8,11 +8,11 @@ namespace System.Diagnostics { /// /// Provides the prepared arguments for starting a process via a user-supplied callback. - /// This class is populated by the method + /// This ref struct is populated by the method /// with the resolved executable path, command-line arguments, environment variables, and standard I/O handles. /// The user's callback receives this instance and is responsible for invoking the appropriate system call to create the process. /// - public sealed class ProcessStartArguments + public ref struct ProcessStartArguments { public ProcessStartArguments() { } From 39ef8d15ff6849973475416c465c038d0f7a1e0e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:11:15 +0000 Subject: [PATCH 17/21] Fix Windows callback stderr assertion spacing Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../tests/ProcessHandlesTests.Windows.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs index 70e8ebf9a3d23a..dcdca892aa3827 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs @@ -154,7 +154,7 @@ public unsafe void StartWithCallback_CreateProcess_CanRedirectOutput() process.WaitForExit(WaitInMS); Assert.Equal("hello \r\n", output); - Assert.Equal("error\r\n", error); + Assert.Equal("error \r\n", error); Assert.Equal(0, process.ExitCode); } From f93911acc787ac9d6bdc914f512c5b3ac93b862e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 10:31:34 +0000 Subject: [PATCH 18/21] Remove unnecessary ToInt32 conversions in Unix callback start path Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 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 e0f92731db6a66..318e844b1ee865 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 @@ -259,24 +259,24 @@ internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo star Debug.Assert(resolvedPathBytesWritten == resolvedPathByteCount); resolvedPathBuffer[resolvedPathBytesWritten] = (byte)0; - int stdinRawFd = -1, stdoutRawFd = -1, stderrRawFd = -1; + nint stdinRawFd = -1, stdoutRawFd = -1, stderrRawFd = -1; if (stdinFd is not null) { stdinFd.DangerousAddRef(ref stdinRefAdded); - stdinRawFd = stdinFd.DangerousGetHandle().ToInt32(); + stdinRawFd = stdinFd.DangerousGetHandle(); } if (stdoutHandle is not null) { stdoutHandle.DangerousAddRef(ref stdoutRefAdded); - stdoutRawFd = stdoutHandle.DangerousGetHandle().ToInt32(); + stdoutRawFd = stdoutHandle.DangerousGetHandle(); } if (stderrHandle is not null) { stderrHandle.DangerousAddRef(ref stderrRefAdded); - stderrRawFd = stderrHandle.DangerousGetHandle().ToInt32(); + stderrRawFd = stderrHandle.DangerousGetHandle(); } fixed (byte* resolvedPathPtr = resolvedPathBuffer) From 4d460ec3233dd1bf1f164bb37de702c3447ae018 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Mon, 8 Jun 2026 16:26:12 +0200 Subject: [PATCH 19/21] Apply suggestions from code review Co-authored-by: Jan Kotas --- .../src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 318e844b1ee865..210eb235f2935f 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 @@ -246,7 +246,7 @@ internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo star // Keep small paths on the stack, while avoiding excessive stack usage for long executable paths. const int ResolvedPathStackBufferSize = #if DEBUG - 2; // make sure we test both code paths + 2; // test the NativeMemory.Alloc code path in DEBUG builds #else 256; #endif From 7c654896a1645b0b682eeeebeffa841c45e5f922 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:07:36 +0000 Subject: [PATCH 20/21] Address Unix callback path and docs feedback Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../SafeHandles/SafeProcessHandle.Unix.cs | 102 +++++++----------- .../Diagnostics/ProcessStartArguments.cs | 10 +- .../System/Diagnostics/ProcessUtils.Unix.cs | 19 +++- 3 files changed, 60 insertions(+), 71 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 210eb235f2935f..8ba99cd1c6fce1 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 @@ -9,9 +9,9 @@ using System.IO; using System.IO.Pipes; using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; using System.Runtime.Versioning; using System.Security; -using System.Text; using System.Threading; using Microsoft.Win32.SafeHandles; @@ -186,7 +186,7 @@ internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFile return s_startWithShellExecute!(startInfo, stdinHandle, stdoutHandle, stderrHandle, out waitStateHolder); } - string? filename; + string filename; string[] argv; IDictionary env = startInfo.Environment; @@ -203,12 +203,8 @@ internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFile bool usesTerminal = UsesTerminal(stdinHandle, stdoutHandle, stderrHandle); - filename = ProcessUtils.ResolvePath(startInfo.FileName); + filename = ProcessUtils.ResolveValidPath(startInfo.FileName, cwd); argv = ProcessUtils.ParseArgv(startInfo); - if (Directory.Exists(filename)) - { - throw new Win32Exception(SR.DirectoryNotValidAsInput); - } return ForkAndExecProcess( startInfo, filename, argv, env, cwd, @@ -224,40 +220,21 @@ internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo star waitStateHolder = null; ProcessUtils.EnsureInitialized(); - string? resolvedFileName = ProcessUtils.ResolvePath(startInfo.FileName); - if (string.IsNullOrEmpty(resolvedFileName)) - { - Interop.ErrorInfo error = Interop.Error.ENOENT.Info(); - throw ProcessUtils.CreateExceptionForErrorStartingProcess(error.GetErrorMessage(), error.RawErrno, startInfo.FileName, startInfo.WorkingDirectory); - } + string resolvedFileName = ProcessUtils.ResolveValidPath(startInfo.FileName, startInfo.WorkingDirectory); string[] argv = ProcessUtils.ParseArgv(startInfo); bool configuredTerminal = false, usesTerminal = UsesTerminal(stdinFd, stdoutHandle, stderrHandle); byte** argvPtr = null, envpPtr = null; - byte* resolvedPathBufferPtr = null; bool stdinRefAdded = false, stdoutRefAdded = false, stderrRefAdded = false; + Utf8StringMarshaller.ManagedToUnmanagedIn resolvedPathMarshaller = default; try { Interop.Sys.AllocArgvArray(argv, ref argvPtr); Interop.Sys.AllocEnvpArray(startInfo.Environment, ref envpPtr); - int resolvedPathByteCount = Encoding.UTF8.GetByteCount(resolvedFileName); - // Keep small paths on the stack, while avoiding excessive stack usage for long executable paths. - const int ResolvedPathStackBufferSize = -#if DEBUG - 2; // test the NativeMemory.Alloc code path in DEBUG builds -#else - 256; -#endif - Span resolvedPathBuffer = resolvedPathByteCount + 1 <= ResolvedPathStackBufferSize - ? stackalloc byte[ResolvedPathStackBufferSize] - : new Span(resolvedPathBufferPtr = (byte*)NativeMemory.Alloc((nuint)(resolvedPathByteCount + 1)), resolvedPathByteCount + 1); - resolvedPathBuffer = resolvedPathBuffer[..(resolvedPathByteCount + 1)]; - - int resolvedPathBytesWritten = Encoding.UTF8.GetBytes(resolvedFileName, resolvedPathBuffer); - Debug.Assert(resolvedPathBytesWritten == resolvedPathByteCount); - resolvedPathBuffer[resolvedPathBytesWritten] = (byte)0; + Span resolvedPathBuffer = stackalloc byte[Utf8StringMarshaller.ManagedToUnmanagedIn.BufferSize]; + resolvedPathMarshaller.FromManaged(resolvedFileName, resolvedPathBuffer); nint stdinRawFd = -1, stdoutRawFd = -1, stderrRawFd = -1; @@ -279,44 +256,41 @@ internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo star stderrRawFd = stderrHandle.DangerousGetHandle(); } - fixed (byte* resolvedPathPtr = resolvedPathBuffer) + ProcessStartArguments args = new() { - ProcessStartArguments args = new() - { - ResolvedPath = resolvedPathPtr, - Arguments = argvPtr, - EnvironmentVariables = envpPtr, - StandardInput = stdinRawFd, - StandardOutput = stdoutRawFd, - StandardError = stderrRawFd, - ProcessStartInfo = startInfo, - }; - - // Lock to avoid races with OnSigChild - // By using a ReaderWriterLock we allow multiple processes to start concurrently. - ProcessUtils.s_processStartLock.EnterReadLock(); - - try + ResolvedPath = resolvedPathMarshaller.ToUnmanaged(), + Arguments = argvPtr, + EnvironmentVariables = envpPtr, + StandardInput = stdinRawFd, + StandardOutput = stdoutRawFd, + StandardError = stderrRawFd, + ProcessStartInfo = startInfo, + }; + + // Lock to avoid races with OnSigChild + // By using a ReaderWriterLock we allow multiple processes to start concurrently. + ProcessUtils.s_processStartLock.EnterReadLock(); + + try + { + if (usesTerminal) { - if (usesTerminal) - { - ProcessUtils.ConfigureTerminalForChildProcesses(1); - configuredTerminal = true; - } - - SafeProcessHandle processHandle = callback(args); - if (processHandle is null || processHandle.IsInvalid) - { - throw new ArgumentException(SR.Argument_InvalidHandle, nameof(callback)); - } - - waitStateHolder = new ProcessWaitState.Holder(processHandle.ProcessId, isNewChild: true, usesTerminal); - return processHandle; + ProcessUtils.ConfigureTerminalForChildProcesses(1); + configuredTerminal = true; } - finally + + SafeProcessHandle processHandle = callback(args); + if (processHandle is null || processHandle.IsInvalid) { - ProcessUtils.s_processStartLock.ExitReadLock(); + throw new ArgumentException(SR.Argument_InvalidHandle, nameof(callback)); } + + waitStateHolder = new ProcessWaitState.Holder(processHandle.ProcessId, isNewChild: true, usesTerminal); + return processHandle; + } + finally + { + ProcessUtils.s_processStartLock.ExitReadLock(); } } catch @@ -348,7 +322,7 @@ internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo star NativeMemory.Free(envpPtr); NativeMemory.Free(argvPtr); - NativeMemory.Free(resolvedPathBufferPtr); + resolvedPathMarshaller.Free(); } } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs index 11e50826f5c606..27821fbc2f3f50 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs @@ -7,20 +7,18 @@ namespace System.Diagnostics { /// - /// Provides the prepared arguments for starting a process via a user-supplied callback. - /// This ref struct is populated by the method - /// with the resolved executable path, command-line arguments, environment variables, and standard I/O handles. - /// The user's callback receives this instance and is responsible for invoking the appropriate system call to create the process. + /// Provides the prepared data required to start a process via a user-supplied callback. + /// This ref struct is populated by the method. /// public ref struct ProcessStartArguments { public ProcessStartArguments() { } /// - /// Gets or sets a pointer to the resolved absolute executable path encoded as null-terminated UTF-8. + /// Gets or sets a pointer to the resolved executable path encoded as null-terminated UTF-8. /// /// - /// A pointer to a null-terminated UTF-8 encoded string representing the resolved absolute executable path. + /// A pointer to a null-terminated UTF-8 encoded string representing the resolved executable path. /// /// /// The memory pointed to by this property is only valid for the duration of the callback invocation. diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Unix.cs index a70df8e902b8e5..8e6eb872a58656 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Unix.cs @@ -323,10 +323,27 @@ internal static string[] ParseArgv(ProcessStartInfo psi, string? resolvedExe = n return argvList.ToArray(); } + internal static string ResolveValidPath(string filename, string? workingDirectory) + { + string? resolvedPath = ResolvePath(filename); + if (string.IsNullOrEmpty(resolvedPath)) + { + Interop.ErrorInfo error = Interop.Error.ENOENT.Info(); + throw CreateExceptionForErrorStartingProcess(error.GetErrorMessage(), error.RawErrno, filename, workingDirectory); + } + + if (Directory.Exists(resolvedPath)) + { + throw new Win32Exception(SR.DirectoryNotValidAsInput); + } + + return resolvedPath; + } + /// Resolves a path to the filename passed to ProcessStartInfo. /// The filename. /// The resolved path. It can return null in case of URLs. - internal static string? ResolvePath(string filename) + private static string? ResolvePath(string filename) { // Follow the same resolution that Windows uses with CreateProcess: // 1. First try the exact path provided From 47c08c4dfbe1f89a4b3c1c8705483f50ca76e269 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 17:35:24 +0000 Subject: [PATCH 21/21] Fix Unix callback marshaller lifetime compile error Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 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 8ba99cd1c6fce1..bad6a7d3d8f27b 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 @@ -177,8 +177,6 @@ private static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileH internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, SafeHandle[]? inheritedHandles, out ProcessWaitState.Holder? waitStateHolder) { - waitStateHolder = null; - ProcessUtils.EnsureInitialized(); if (startInfo.UseShellExecute) @@ -226,14 +224,14 @@ internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo star bool configuredTerminal = false, usesTerminal = UsesTerminal(stdinFd, stdoutHandle, stderrHandle); byte** argvPtr = null, envpPtr = null; bool stdinRefAdded = false, stdoutRefAdded = false, stderrRefAdded = false; - Utf8StringMarshaller.ManagedToUnmanagedIn resolvedPathMarshaller = default; + scoped Utf8StringMarshaller.ManagedToUnmanagedIn resolvedPathMarshaller = default; + Span resolvedPathBuffer = stackalloc byte[Utf8StringMarshaller.ManagedToUnmanagedIn.BufferSize]; try { Interop.Sys.AllocArgvArray(argv, ref argvPtr); Interop.Sys.AllocEnvpArray(startInfo.Environment, ref envpPtr); - Span resolvedPathBuffer = stackalloc byte[Utf8StringMarshaller.ManagedToUnmanagedIn.BufferSize]; resolvedPathMarshaller.FromManaged(resolvedFileName, resolvedPathBuffer); nint stdinRawFd = -1, stdoutRawFd = -1, stderrRawFd = -1;