Repository navigation
Process: SafeChildProcessHandle and option bag #123380
Description
Activity
- addedapi-ready-for-reviewAPI is ready for review, it is NOT ready for implementationAPI is ready for review, it is NOT ready for implementation
on Jan 20, 2026 - addeduntriagedNew issue has not been triaged by the area ownerNew issue has not been triaged by the area owner
on Jan 20, 2026 - removeduntriagedNew issue has not been triaged by the area ownerNew issue has not been triaged by the area owner
on Jan 20, 2026 dotnet-policy-service commented
on Jan 20, 2026 ContributorMore actionsTagging subscribers to this area: @dotnet/area-system-diagnostics-process
See info in area-owners.md if you want to be subscribed.WaitForExit and WaitForExitAsync do not depend on reading STD OUT/ERR until EOF (this is very error prone, see #51277).
When a process produces more output than the size of the pipe, wait for exit depends on reading that output because the writing process will block when the pipe is full (which causes it not to exit).
SafeFileHandle? input, SafeFileHandle? output, SafeFileHandle? error, IList? inheritedHandles = null
On Linux/Unix, sometimes applications use conventions about the file descriptors that are passed. For example, systemd starts passing additional file descriptors at 3 (see https://www.freedesktop.org/software/systemd/man/latest/sd_listen_fds.html). This API doesn't support this.
I want to make explicit it doesn't. I don't mean to say it must.
Task WaitForExitAsync(CancellationToken cancellationToken = default)
Combining waiting for the process exit, returning the exit code and not depending it on EOF make a good API.
I've implemented the same behavior in a RemoteProces.GetExitCodeAsync for Tmds.Ssh. That method also actively discards the output so that the process may complete. In this case
SafeChildProcessHandledoesn't concern itself with the output streams, so killing the child when theCancellationTokentriggers is a good way to go about it.so killing the child when the
CancellationTokentriggers is a good way to go about it.Or you might leave it to the user when the method throws
OperationCanceledException. The user can then callKilland may callWaitForExitAsynconce more.Task WaitForExitAsync(CancellationToken cancellationToken = default)
Adding a separate
timeoutarg can help encourage users to set a timeout (esp when they'd need to create a linkedCancellationToken).This API doesn't support this.
Yes, the handles are inherited with their current values. The reason why I don't allow to provide new value like with
dup2on Linux is that Windows does not support it.The DuplicateHandle sys-call just returns the new value.
The exact code that I am using:
The reason why I don't allow to provide new value like with dup2 on Linux is that Windows does not support it.
The typical pattern of passing the extra handles on Windows vs. Unix seems to be different. On Windows, one cannot depend on fixed handle value and the current handle value must be passed to the child process via some channel (e.g. via the command line). On Unix, it is common to depend on the fixed handle value, we have seen multiple instances of that.
Emulating the Windows pattern on Unix does not make the API super useful on Unix. Should we have Unix-specific API to support the typical Unix pattern?
On Windows, the Universal C Runtime Library (UCRT) has a convention for mapping file descriptor numbers to handles, via the
WORD cbReserved2andLPBYTE lpReserved2members of STARTUPINFOW. This is implemented inexec\spawnv.cppandlowio\ioinit.cpp. So if the .NET Runtime implemented the same convention, then it would be possible for a .NET program to start a C program as a child process and pass a specific handle to it as file descriptor 3, for example. However, further API design would be needed if you wanted .NET programs to be able to receive this kind of information from their parent processes.UCRT is interesting thing, but this doesnt make any sense. Valid Windows program may never links to UCRT. E.g. it may have sense only if whole windows kernel say "hey, now additionally to file handles i know about file descriptors and there is documented way how to pass them". But file descriptors currently on Windows is completely C library thing, and all current Windows programs do openfdhandle and get "dynamic" fd when they really need it, or they skip use of C library completely in favor of native calls, because they already have handle in the hands.
14 remaining items
- addedapi-approvedAPI was approved in API review, it can be implementedAPI was approved in API review, it can be implemented
on Feb 10, 2026 SIGKILL = -11,
I'm so confused 😄 https://www.youtube.com/watch?v=Fow7iUaKrq4
Some late additional feedback:
public ProcessExitStatus WaitForExitOrKillOnTimeout(TimeSpan timeout);
public Task WaitForExitOrKillOnCancellationAsync(CancellationToken cancellationToken);Perhaps these could have been left out, and leave the killing to the user based on how the other methods return?
Does this kill the entire process group when
CreateNewProcessGroup = true.public bool Kill();
public bool KillProcessGroup();Perhaps a single
Killmethod might be enough ifCreateNewProcessGroup = trueimpliesKillis for the process group.Perhaps these could have been left out, and leave the killing to the user based on how the other methods return?
The reason I added them is that I've seen most of the code bases implementing it on their own. So I just want to provide in box (and ensure correctness in edge cases like
timeout reached -> attempt to kill -> process already exited).Does this kill the entire process group when
CreateNewProcessGroup = true.Killmaps tokill(pid, SIGKILL),KillProcessGroupmaps tokill(-pid, SIGKILL).Reacted by Alastair LundyKill maps to kill(pid, SIGKILL), KillProcessGroup maps to `kill(-pid, SIGKILL).
Users might end up accidentally calling
Killwhen they mean to callKillProcessGroup.
It could have been an option to leave outKillProcessGroupso that they can only callKill.If a user would really like to only kill the direct child, they could still call
Signal(PosixSignal.SIGKILL).public bool Kill();
Why does this return
bool?I assume the use of this method to be:
process.Kill(); process.WaitForExit();
And if you specifically like to know if the process was terminated by the kill:
process.Kill(); var status = process.WaitForExit(); bool wasKilled = status.Signal == PosixSignal.SIGKILL;
public void Signal(PosixSignal signal);
public void SignalProcessGroup(PosixSignal signal);Perhaps these should return a
boolso that they can returnfalsewhen the process/process group no longer exist.Users might end up accidentally calling
Killwhen they mean to callKillProcessGroup. It could have been an option to leave outKillProcessGroupso that they can only callKill.If a user would really like to only kill the direct child, they could still call
Signal(PosixSignal.SIGKILL).Historically in .NET, we had
Process.Killthat kills the direct child. Then we introducedProcess.Kill(bool entireProcessTree)that walks entire process tree and kills each process individually (it's bad for many reasons). The idea ofKillProcessGroupis to kill the entire process group with a single sys-call.Why does this return
bool?To let users identify scenarios where the process has already exited without throwing an exception. Imagine:
if (!handle.TryWaitForExit(TimeSpan.FromSeconds(3)) { // the process exits here handle.Kill(); // there is nothing to kill }
On Unix, we check
errnoand returnfalseonESRCHAnd if you specifically like to know if the process was terminated by the kill:
It works, but only on Unix. On Windows, there is no way to detect if the process was killed or not (the sys-call for termination requires the caller to provide exit code in explicit way)
Perhaps these should return a
boolso that they can returnfalsewhen the process/process group no longer exist.If everything goes right, this API will be shipped as part of preview 3. If it's clear that it would benefit our users and can be implemented on Windows too, I am more than happy to make it return
bool.The idea of KillProcessGroup is to kill the entire process group with a single sys-call.
My question is whether it would make sense to remove
KillProcessGroupand haveKillkill the process group whenCreateNewProcessGroup = true.This avoids accidentally calling
Killwhen you meant to callKillProcessGroup.To let users identify scenarios where the process has already exited without throwing an exception.
Process.Killhasvoidreturn and when the process has already terminated it doesn't throw.This API could do the same. It avoids returning a value the user thinks he should act on.
It works, but only on Unix. On Windows, there is no way to detect if the process was killed or not (the sys-call for termination requires the caller to provide exit code in explicit way)
Also on Windows the exit code will indicate the application terminated in an unexpected fashion.
If it's clear that it would benefit our users and can be implemented on Windows too, I am more than happy to make it return bool.
The user code that calls
Signal/SignalProcessGroupshould handle the case when the process (group) is no longer there.It's not intuitive this is done through an exception which should be caught by the user.
Reacted by Jan KotasMy question is whether it would make sense to remove
KillProcessGroupand haveKillkill the process group whenCreateNewProcessGroup = true.Thanks, now I get it. Can we guarantee there are no valid scenarios where users would like to kill the single process instead of a group when
CreateNewProcessGroup = true?And there is one more scenario: we are going to allow the users to open existing processes by pid. So for example, when for some reason they decided to spawn the process on their own, they can use our APIs to work with it. In such scenario, we don't know whether the process is a leader of it's own group (for example, on Windows, we can't fetch dwCreationFlags of an exiting process).
SafeProcessHandle handle = SafeProcessHandle.Open(myPid); handle.Kill(); // is it a leader?
This API could do the same. It avoids returning a value the user thinks he should act on.
It returns this value on purpose, so the users can differentiate a process that exited before it got killed. Others are free to ignore it, they same way they can ignore
HashSet.Addresult.It's not intuitive this is done through an exception which should be caught by the user.
I am not against changing
Signalto return bool, based on #59746 I simply had not enough data to identify this requirement.@adamsitnik can this be closed? or does it still track something?
@adamsitnik can this be closed? or does it still track something?
There is one API I still need to implement:
public static SafeProcessHandle Open(int processId);
I am most likely going to do it in one PR with #101582
- added a commit that references this issue
on Jun 9, 2026 - locked and limited conversation to collaborators
on Jul 10, 2026 - added a commit that references this issue
on Jul 15, 2026 - added a commit that references this issue
on Jul 22, 2026
Background and motivation
System.Diagnostics.Processhas plenty of design, usability, performance and reliablity issues. The API can not be 100% fixed without breaking changes, so I would like to introduce a set of new types related to process execution.The foundation of the new process APIs is a low-level API that represents
SafeHandlefor child processes. It allow users to start processes with fine-grained control over STD input/output/error, environment, working directory, and other parameters that can be implemented on every platform supported by .NET.API Proposal
The first part of the proposal is an option bag type that represents process start options. It's used by the low-level process creation API. It will be also used by higher-level APIs (proposed in #123959) that provide more user-friendly abstractions over typical process scenarios (execute, discard output, get output lines etc). Those high level APIs are currently WIP and will be proposed later.
The second part of the proposal is a low-level API that allows starting a child process with the given options and STD input/output/error handles.
The most important aspects:
WaitForExitandWaitForExitAsyncdo not depend on reading STD OUT/ERR until EOF (this is very error prone, see #51277).We need an ability to send SIGKILL and SIGSTOP which so far were not part of the
PosixSignalenum:namespace System.Runtime.InteropServices; public enum PosixSignal { SIGTSTP = -10, // polite request to stop + SIGKILL = -11, // force termination + SIGSTOP = -12 // unconditional stop }And exit status to express processes that were signaled and/or terminated due to timeout or cancellation:
API Usage
This example demonstrates piping output from one process to another using anonymous pipes, providing empty input, redirecting to file and discarding error:
Alternative Designs
Moving
InheritedHandlesout of the option bagInheritedHandlesprovides a list of additional handles to be inherited by the child process. Each of these handles implementsIDisposable, so it affects the lifetime management of the option bag. Since providing such handles is a niche scenario that we need for completness, we could move it out of the option bag and into theStartmethod:public sealed class ProcessStartOptions { - public IList<SafeHandle> InheritedHandles { get; } } public class SafeChildProcessHandle : SafeHandle { - public static SafeChildProcessHandle Start(ProcessStartOptions options, SafeFileHandle? input, SafeFileHandle? output, SafeFileHandle? error); + public static SafeChildProcessHandle Start(ProcessStartOptions options, SafeFileHandle? input, SafeFileHandle? output, SafeFileHandle? error, IList<SafeHandle>? inheritedHandles = null); }It would allow the users to cache the option bag (with a resolved exe path for example) without worrying about the lifetime of the inherited handles.
Risks
To be 100% safe from accidental handle inheritance, both old and new APIs need to share the global reader-writer lock on Windows. See the details for more:
Details
From https://learn.microsoft.com/windows/win32/api/processthreadsapi/nf-processthreadsapi-updateprocthreadattribute:
So even if we want to provide an explicit list of handles to inherit, we still need to create them as inheritable. It's fine as long as everyone uses
PROC_THREAD_ATTRIBUTE_HANDLE_LISTto spawn processes, but if some code spawns a process without it, the inheritable handles may be inherited by accident. And if we introduce the new Process API, there is nothing that would stop the users from using the old API to spawn processes withoutPROC_THREAD_ATTRIBUTE_HANDLE_LIST.To be 100% safe without breaking changes, the new and old API need to share the global reader-writer lock.
Due to explicit executable path resolution, if CreateProcess sys-call is one day changed to handle path resolution in a different way for security reasons, the managed layer will need to be updated as well.