Skip to content

[Process]: Start detached process #124334

Description

@adamsitnik

Background and motivation

There is currently no easy, cross-platform way to start a detached process in .NET. A detached process is one that:

  • Does not belong to the parent process's process group
  • Continues running after the parent process exits
  • Has its standard input/output/error disconnected from the parent

This is a common requirement for scenarios such as:

  • Starting long-running server applications or daemons
  • Launching GUI applications from console tools
  • Creating background services that outlive the parent process

Currently, achieving this requires platform-specific workarounds like invoking shell commands like nohup <command> & through /bin/sh, which requires concatenating arguments as strings and loses the ability to obtain the child process ID (see #104210)

These workarounds are error-prone, not portable, and don't provide consistent behavior across platforms.

API Proposal

The proposal is to extend ProcessStartOptions with a new StartDetached property that indicates the process should be started in a detached manner. The proposal also includes moving StartSuspended method to the option bag and making it a property as well (so both can be composed).

namespace System.Diagnostics;
{
    public sealed class ProcessStartOption
    {
        // Starts a new detached process with standard input, output, and error redirected to NUL.
        // On Windows, the process is started with DETACHED_PROCESS and CREATE_NEW_PROCESS_GROUP flags.
        // On macOS, the process is started with POSIX_SPAWN_SETSID.
        // On other Unix systems, the process calls setsid() after fork and before exec.
+       public bool StartDetached { get; set; }

        // Starts the process in a suspended state.
+       public bool StartSuspended { get; set; }
    }
}

namespace Microsoft.Win32.SafeHandles
{
    public partial class SafeProcessHandle
    {
        public static SafeProcessHandle Start(ProcessStartOptions options, SafeFileHandle? input, SafeFileHandle? output, SafeFileHandle? error);
-       public static SafeProcessHandle StartSuspended(ProcessStartOptions options, SafeFileHandle? input, SafeFileHandle? output, SafeFileHandle? error);
        public void Resume();
    }
}

Usage Example

The following example demonstrates how to start a detached process:

using Microsoft.Win32.SafeHandles;
using System.Diagnostics;

ProcessStartOptions options = new("myserver")
{ 
    Arguments = { "--port", "8080" }, 
    StartDetached = true
};

SafeFileHandle nullHandle = File.OpenNullHandle();
SafeProcessHandle handle = SafeProcessHandle.Start(options, input: nullHandle, output: nullHandle, error: nullHandle);

Alternative Designs

My main goals for ProcessStartOptions is to keep it simple and consistent across different operating systems.

But some of the features of process creation are specific to certain platforms. For example, Windows has the DETACHED_PROCESS flag, while Unix-like systems use setsid(). And at the same time, I really don't want this type to become a mess like ProcessStartInfo with a lot of properties that only apply to certain platforms.

So please consider following alternative designs for exposing more advanced OS-specific options for process creation, while keeping the main API simple and cross-platform.

Platform-specific derived option bags

The idea is following:

  • keep all cross-platform options in ProcessStartOptions (like Arguments, WorkingDirectory, etc.)
  • provide two separate types for advanced OS-specific options. One for Unix and another for Windows.
  • the Unix option bag has to expose all the features that can be configured only from the calling process (for example setsid) so we can call them after fork and before exec.
namespace System.Diagnostics;

public class ProcessStartOptions
{
    // Cross-platform options for process creation
    public string FileName { get; }
    public IList<string> Arguments { get; set; }
    public IDictionary<string, string?> Environment { get; }
    public string? WorkingDirectory { get; set; }
    public bool KillOnParentExit { get; set; }
    public IList<SafeHandle> InheritedHandles { get; set; }
    public bool StartSuspended { get; set; }

    public ProcessStartOptions(string fileName);
}

[UnsupportedOSPlatform("windows")]
public sealed class UnixProcessStartOptions : ProcessStartOptions
{
    public bool CreateNewProcessGroup { get; set; } // setpgid
    public bool StartNewSession { get; set; } // setsid

    // In the future, we could add more Unix-specific options here, such as:
    public int? UserId { get; set; } // setuid
    public int? GroupId { get; set; } // setgid

    // Linux-specific options, could be added here as well.
    [SupportedOSPlatform("linux")]]
    public string? RootDirectory { get; set; } // chroot

    public UnixProcessStartOptions(string fileName);
    public UnixProcessStartOptions(ProcessStartOptions source);
}

[SupportedOSPlatform("windows")]
public sealed class WindowsProcessStartOptions : ProcessStartOptions
{
    public WindowsProcessCreationFlags CreationFlags { get; set; }

    // In the future, we could add more Windows-specific options here, such as:
    public IList<SafeHandle> JobHandles { get; set; }
    
    // And provide input for UpdateProcThreadAttribute 
    public List<(WindowsProcessAttribute attribute, GCHandle value, nint byteSize)> ProcessAttributes { get; set; }

    public WindowsProcessStartOptions(string fileName);
    public WindowsProcessStartOptions(ProcessStartOptions source);
}

[Flags]
public enum WindowsProcessCreationFlags // #71515
{
    CREATE_BREAKAWAY_FROM_JOB,
    CREATE_DEFAULT_ERROR_MODE,
    CREATE_NEW_CONSOLE, // #71515
    CREATE_NEW_PROCESS_GROUP, // ProcessStartInfo.CreateNewProcessGroup
    CREATE_NO_WINDOW, // ProcessStartInfo.CreateNoWindow
    CREATE_PROTECTED_PROCESS,
    CREATE_PRESERVE_CODE_AUTHZ_LEVEL,
    CREATE_SECURE_PROCESS,
    CREATE_SEPARATE_WOW_VDM,
    CREATE_SUSPENDED, // #94127
    CREATE_UNICODE_ENVIRONMENT,
    DEBUG_ONLY_THIS_PROCESS, // #71515
    DEBUG_PROCESS, // #71515
    DETACHED_PROCESS, // this issue
    EXTENDED_STARTUPINFO_PRESENT,
    INHERIT_PARENT_AFFINITY
}

public enum WindowsProcessAttribute
{
    PROC_THREAD_ATTRIBUTE_GROUP_AFFINITY,
    // PROC_THREAD_ATTRIBUTE_HANDLE_LIST, exposed via ProcessStartOptions.InheritedHandles
    PROC_THREAD_ATTRIBUTE_IDEAL_PROCESSOR,
    PROC_THREAD_ATTRIBUTE_MACHINE_TYPE,
    PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY,
    PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
    PROC_THREAD_ATTRIBUTE_PREFERRED_NODE,
    PROC_THREAD_ATTRIBUTE_UMS_THREAD,
    PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES,
    PROC_THREAD_ATTRIBUTE_PROTECTION_LEVEL,
    PROC_THREAD_ATTRIBUTE_CHILD_PROCESS_POLICY,
    PROC_THREAD_ATTRIBUTE_DESKTOP_APP_POLICY,
    // PROC_THREAD_ATTRIBUTE_JOB_LIST, exposed via WindowsProcessStartOptions.JobHandles
    PROC_THREAD_ATTRIBUTE_ENABLE_OPTIONAL_XSTATE_FEATURES
}

Sample usage:

ProcessStartOptions options = new("myserver") { Arguments = { "--port", "8080" } };
options = OperatingSystem.IsWindows() 
    ? new WindowsProcessStartOptions(options)
    {
        CreationFlags = WindowsProcessCreationFlags.DETACHED_PROCESS
    }
    : new UnixProcessStartOptions(options)
    { 
        StartNewSession = true
    };

SafeFileHandle nullHandle = File.OpenNullHandle();
SafeProcessHandle handle = SafeProcessHandle.Start(options, input: nullHandle, output: nullHandle, error: nullHandle);

This design is more complex and less discoverable than the previous one, but it has the advantage of keeping the main ProcessStartOptions type clean and focused on cross-platform features. Also, all the information can be easily obtained through the getters.

List of advanced options

An abstract class, with public static factory methods for creating instances of AdvancedProcessStartOptions with specific features enabled. For example:

Details
namespace System.Diagnostics;

public sealed class ProcessStartOptions
{
    // Simple and cross-platform
    public string FileName { get; }
    public IList<string> Arguments { get; set; }
    public IDictionary<string, string?> Environment { get; }
    public string? WorkingDirectory { get; set; }
    public bool KillOnParentExit { get; set; }

    public IList<AdvancedProcessStartOptions> AdvancedOptions { get; set; }

    public ProcessStartOptions(string fileName);
}

public abstract class AdvancedProcessStartOptions
{
    // Factory methods for creating internal types that derive from AdvancedProcessStartOptions
    public static AdvancedProcessStartOptions InheritHandles(IList<SafeHandle> additionalHandles);
    public static AdvancedProcessStartOptions StartSuspended();
    public static AdvancedProcessStartOptions StartDetached();

    [SupportedOSPlatform("windows")]
    public static AdvancedProcessStartOptions WindowsCreationFlags(WindowsProcessCreationFlags flags);

    [UnsupportedOSPlatform("windows")]
    public static AdvancedProcessStartOptions CreateNewProcessGroup();

    [UnsupportedOSPlatform("windows")]
    public static AdvancedProcessStartOptions StartNewSession();

    [SupportedOSPlatform("linux")]
    public static AdvancedProcessStartOptions RootDirectory(string chroot);
}

Usage:

ProcessStartOptions options = new("myserver")
{ 
    Arguments = { "--port", "8080" },
    AdvancedOptions = 
    {
        AdvancedProcessStartOptions.StartSuspended(),
        AdvancedProcessStartOptions.InheritHandles(new[] { someHandle })
    }
};

SafeFileHandle nullHandle = File.OpenNullHandle();
SafeProcessHandle handle = SafeProcessHandle.Start(options, input: nullHandle, output: nullHandle, error: nullHandle);

The API would be more discoverable than the previous design, and it would allow for better composition of different advanced options. However, it would require creating and JITing a lot of small internal types that derive from AdvancedProcessStartOptions. And it would just move the complexity from one place to another (with the benefit of hiding it from 95% of the users). Not to mention the lack of ability to read the advanced options back from the ProcessStartOptions instance, which could be a problem for some scenarios.

Builder for advanced options

A builder type that allows users to fluently configure advanced options for process creation.

Details
namespace System.Diagnostics;

public sealed class ProcessStartOptions
{
    // Simple and cross-platform
    public string FileName { get; }
    public IList<string> Arguments { get; set; }
    public IDictionary<string, string?> Environment { get; }
    public string? WorkingDirectory { get; set; }
    public bool KillOnParentExit { get; set; }

    public AdvancedProcessStartOptionsBuilder Advanced { get; set; }

    public ProcessStartOptions(string fileName);
}

public class AdvancedProcessStartOptionsBuilder
{
    // Factory methods for creating internal types that derive from AdvancedProcessStartOptions
    public AdvancedProcessStartOptionsBuilder InheritHandles(IList<SafeHandle> additionalHandles);
    public AdvancedProcessStartOptionsBuilder StartSuspended();

    [SupportedOSPlatform("windows")]
    public AdvancedProcessStartOptionsBuilder WindowsCreationFlags(WindowsProcessCreationFlags flags);

    [UnsupportedOSPlatform("windows")]
    public AdvancedProcessStartOptionsBuilder CreateNewProcessGroup();

    [UnsupportedOSPlatform("windows")]
    public AdvancedProcessStartOptionsBuilder StartNewSession();

    [SupportedOSPlatform("linux")]
    public AdvancedProcessStartOptionsBuilder RootDirectory(string chroot);
}

Usage:

ProcessStartOptions options = new("myserver") { Arguments = { "--port", "8080" } };

options.Advanced
    .StartSuspended()
    .InheritHandles(new[] { someHandle });

SafeFileHandle nullHandle = File.OpenNullHandle();
SafeProcessHandle handle = SafeProcessHandle.Start(options, input: nullHandle, output: nullHandle, error: nullHandle);

The ProcessStartOptions would still be clean and focused on cross-platform features, while the advanced options would be discoverable through the builder. The builder would most likely require fewer internal types than the previous design, as it could be implemented as a single type with properties for each advanced option. But there would be no public API for reading these options, which could be a problem for some scenarios.

Risks

Starting a detached process is advanced scenario, when used incorrectly, it can lead to issues such as orphaned processes that continue running indefinitely.

Activity

  1. added this to the 11.0.0 milestone on Feb 12, 2026
  2. self-assigned this
    on Feb 12, 2026
  3. adamsitnik commented on Feb 12, 2026

    @adamsitnik
    MemberAuthor
  4. dotnet-policy-service commented on Feb 12, 2026

    @dotnet-policy-service
    Contributor

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

  5. jkotas commented on Feb 12, 2026

    @jkotas
    Member

    Should StartSuspended, StartDetached, ... really be separate methods? It feels like these should be properties in ProcessStartOptions, so you can e.g. start a process that is both suspended and detached.

  6. adamsitnik commented on Feb 12, 2026

    @adamsitnik
    MemberAuthor

    Should StartSuspended, StartDetached, ... really be separate methods? It feels like these should be properties in ProcessStartOptions, so you can e.g. start a process that is both suspended and detached.

    Great question!

    This was my initial design, but when I started providing the high-level helpers for most common-scenarios (#123959), I realized that each of these helpers would need to check if the option bag does not request a Suspended process and just throw in such case.

    ProcessStartOptions info = new("dotnet")
    {
        Arguments = { "restore" }
    };
    
    ProcessOutput processOutput = ChildProcess.CaptureOutput(info);

    Moreover, exposing these APIs as part of the option bag would "pollute" the API surface for the 99% of users who don't need to support these two niche (but valid and IMO important) scenarios.

  7. jkotas commented on Feb 12, 2026

    @jkotas
    Member

    Moreover, exposing these APIs as part of the option bag would "pollute" the API surface for the 99% of users who don't need to support these two niche (but valid and IMO important) scenarios.

    ProcessStartOptions has other very niche features like InheritedHandles that 99% of users won't ever use. I think we struggling with deciding whether ProcessStartOptions should or should not have the niche features. It is a mix right now that leads to poor overall design.

  8. adamsitnik commented on Feb 12, 2026

    @adamsitnik
    MemberAuthor

    Moreover, exposing these APIs as part of the option bag would "pollute" the API surface for the 99% of users who don't need to support these two niche (but valid and IMO important) scenarios.

    ProcessStartOptions has other very niche features like InheritedHandles that 99% of users won't ever use. I think we struggling with deciding whether ProcessStartOptions should or should not have the niche features. It is a mix right now that leads to poor overall design.

    I agree InheritedHandles are niche, but I can see some valid scenarios where they would be useful in high level helpers.

    FWIW I've mentioned this in the API proposal: #123380

    Image
  9. jkotas commented on Feb 12, 2026

    @jkotas
    Member

    I agree InheritedHandles are niche, but I can see some valid scenarios where they would be useful in high level helpers.

    Right, you can always find a case where somebody might want to use a niche feature together with high level helpers.

  10. adamsitnik commented on Feb 12, 2026

    @adamsitnik
    MemberAuthor

    I agree InheritedHandles are niche, but I can see some valid scenarios where they would be useful in high level helpers.

    Right, you can always find a case where somebody might want to use a niche feature together with high level helpers.

    I was not sure myself, that is why I've mentioned this in the API proposal. I was hoping that we discuss it during the API review. Should I discuss it with API review board again? For example along with the current issue (StartDetached)?

  11. stephentoub commented on Feb 12, 2026

    @stephentoub
    Member

    In general, the goal of API review isn't to question fundamental assertions about a feature being needed (though we often end up discussing that); rather, it's assumed that the area owners are asserting that such functionality is necessary, and then the discussion is about the right way to expose it.

  12. jkotas commented on Feb 12, 2026

    @jkotas
    Member

    Should I discuss it with API review board again? For example along with the current issue (StartDetached)?

    I think StartSuspended should be re-reviewed together with StartDetached. StartSuspended + StartDetached is not composable pattern. I think it is flawed design. API composability is more important than whether we need to have an extra check in the implementation or whether somebody will see an extra property in IntelliSense.

  13. adamsitnik commented on Feb 18, 2026

    @adamsitnik
    MemberAuthor

    I think StartSuspended should be re-reviewed together with StartDetached. StartSuspended + StartDetached is not composable pattern. I think it is flawed design. API composability is more important than whether we need to have an extra check in the implementation or whether somebody will see an extra property in IntelliSense.

    StartDetached is very specific, as it must not inherit any handles (the detached process could live way longer than the parent and hold plenty of resources opened, there are some security aspects involved as well).

    I agree that composability is very important, but I don't believe that we should expose two additional boolean properties that would be used by 0.01% of the users (and most likely almost never used together) at the cost of polluting the option bag api surface for 99.99% of other users. I really want to avoid the situation where people want to try to use ProcessStartOptions for the first time and they thinks it's just another ProcessStartInfo with a lipstick on a pig.

    Let's bring it to the API review and discuss in detail.

  14. 7 remaining items

  15. jkotas commented on Mar 16, 2026

    @jkotas
    Member

    for features specific to Linux/macOS, add them to UnixProcessStartOption but annotate with SupportedOSPlatform.

    It looks odd to me to separate Windows and Unix, and lump Linux and macOS together. If we are not doing specific types for every OS, I think we should go with one type for all advances options.

    The problem is that we can't do that in .NET, because this logic is executed in the child process and we can't execute any managed code in the child process because there is no JIT to or GC.

    We can expose this callback and require people to implement this callback in unmanaged code. We have existing APIs like that. For example,. callbacks provided for ObjectiveCMarshal.Initialize must be implemented in unmanaged code.

    This works cross-platform

    Cross-platform does not mean latest Windows/Linux/macOS only. Does it really well work cross-platform, including other Unix flavors? I think you need specialized syscalls like Linux close_range to make it work well.

  16. adamsitnik commented on Mar 16, 2026

    @adamsitnik
    MemberAuthor

    It looks odd to me to separate Windows and Unix, and lump Linux and macOS together. If we are not doing specific types for every OS, I think we should go with one type for all advances options.

    I agree, it's definitely not a perfect solution. I would love to hear more from others.

    Cross-platform does not mean latest Windows/Linux/macOS only. Does it really well work cross-platform, including other Unix flavors? I think you need specialized syscalls like Linux close_range to make it work well.

    It does, because the goal of this API is to ensure that the new process is going to derive the provided handles. When it comes to not deriving other handles, it's best effort attempt (works on every macOS, modern Linux, modern Android and modern FreeBSD) and will be documented as such. FWIW all modern programming languages (golang, rust) do that already.

    We can expose this callback and require people to implement this callback in unmanaged code. We have existing APIs like that. For example,. callbacks provided for ObjectiveCMarshal.Initialize must be implemented in unmanaged code.

    For my education. how delegate* unmanaged works exactly? If we have code like this:

            static unsafe void Main()
            {
                delegate* unmanaged<void> del1 = &RequireUnreferencedCode;
            }
    
            [UnmanagedCallersOnly(EntryPoint = nameof(RequireUnreferencedCode))]
            static void RequireUnreferencedCode()
            {
            }

    Can we somehow ensure that RequireUnreferencedCode is pre-compiled and invoking it does not introduce any JIT or allocations? Or do we somehow need to load the native library and get a reference to its method?

  17. jkotas commented on Mar 16, 2026

    @jkotas
    Member

    Can we somehow ensure that RequireUnreferencedCode is pre-compiled and invoking it does not introduce any JIT or allocations?

    We can ensure that we will crash deterministically when you run your code snippet.

    it does not introduce any JIT or allocations

    There is no way to write a code like that in C# today. When I said unmanaged code, I meant unmanaged code (like C/C++).

  18. bartonjs commented on Mar 17, 2026

    @bartonjs
    Member

    Video

    The discussion on this made us seem like we're already regretting ProcessStartOptions, and maybe it should go back to ProcessStartInfo and be rethought from there.

  19. added
    api-needs-workAPI needs work before it is approved, it is NOT ready for implementation
    and removed
    api-ready-for-reviewAPI is ready for review, it is NOT ready for implementation
    on Mar 17, 2026
  20. removed
    api-needs-workAPI needs work before it is approved, it is NOT ready for implementation
    on Mar 25, 2026
  21. adamsitnik commented on Mar 25, 2026

    @adamsitnik
    MemberAuthor

    Update: the API got approved in #125838 (comment)

    It's blocked by #13943

    Coming soon (famous last words)

  22. artdedeco commented on Apr 7, 2026

    @artdedeco

    Hey folks, I was reading up on this and I'm a bit confused from following the discussion so far; is the intention of this new API to have StartDetached start the new process such that its lifetime is independent of the parent process, in a cross-platform way? The docs for the property in the OP suggests the answer is yes, but from reading the discussion I'm not sure.

  23. adamsitnik commented on Apr 7, 2026

    @adamsitnik
    MemberAuthor

    is the intention of this new API to have StartDetached start the new process such that its lifetime is independent of the parent process, in a cross-platform way?

    Yes, this is the intention.

  24. locked and limited conversation to collaborators on May 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions