Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
abb8c0e
Implement ProcessStartInfo.KillOnParentExit for Windows
Copilot Apr 9, 2026
31f6e6b
Address review feedback: defensive UserName check, resume only on hap…
Copilot Apr 9, 2026
b910623
address my own feedback:
adamsitnik Apr 9, 2026
cc04598
Address review feedback: add SupportedOSPlatform("windows"), check Re…
Copilot Apr 9, 2026
afab02e
Add Windows SDK doc link for JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE and r…
Copilot Apr 10, 2026
309480c
increase test coverage:
adamsitnik Apr 10, 2026
f0f9714
fix a bug discovered by the tests: when all standard handles are inva…
adamsitnik Apr 10, 2026
7f887bd
Don't create (and upload) a memory dump for this test, as AV is inten…
adamsitnik Apr 10, 2026
afbcc1e
Merge remote-tracking branch 'origin/main' into copilot/add-kill-on-p…
adamsitnik Apr 10, 2026
ede5e33
Apply suggestions from code review
adamsitnik Apr 10, 2026
9bb0ef4
Apply suggestions from code review
adamsitnik Apr 10, 2026
7b0e652
make the tests less flaky
adamsitnik Apr 10, 2026
c432cd5
refactor the code: move all logic related to extended startup info in…
adamsitnik Apr 10, 2026
6bae099
Merge branch 'main' into copilot/add-kill-on-parent-exit-property
adamsitnik Apr 10, 2026
bbab88f
Update src/libraries/System.Diagnostics.Process/tests/KillOnParentExi…
jkotas Apr 11, 2026
baea489
Move ResumeThread P/Invoke to dedicated file and remove redundant Ski…
Copilot Apr 11, 2026
4fdc31a
Use nuint for numerical fields in job object structs and fix processI…
Copilot Apr 11, 2026
04d67a9
Change ResumeThread return type to uint (matches DWORD) and remove in…
Copilot Apr 11, 2026
20559e0
Use IsInvalidHandle for thread handle assertion (checks both 0 and -1)
Copilot Apr 11, 2026
35b273c
try to avoid creating a memory dump
adamsitnik Apr 11, 2026
8beb026
Apply suggestions from code review
adamsitnik Apr 11, 2026
c5b7d59
Replace local ResumeThread P/Invoke in ProcessHandlesTests with share…
Copilot Apr 11, 2026
84ca617
Remove unused System.Runtime.InteropServices using after removing loc…
Copilot Apr 11, 2026
96e2804
Remove defensive HELIX_WORKITEM_UPLOAD_ROOT env var from crash test
Copilot Apr 11, 2026
f497166
Address jkotas feedback: remove unused enum, unnecessary early return…
Copilot Apr 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ internal static partial class StartupInfoOptions
internal const int CREATE_UNICODE_ENVIRONMENT = 0x00000400;
internal const int CREATE_NO_WINDOW = 0x08000000;
internal const int CREATE_NEW_PROCESS_GROUP = 0x00000200;
internal const int CREATE_SUSPENDED = 0x00000004;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

internal static partial class Interop
{
internal static partial class Kernel32
{
internal sealed class SafeJobHandle : SafeHandleZeroOrMinusOneIsInvalid
{
public SafeJobHandle() : base(true) { }

protected override bool ReleaseHandle() => Interop.Kernel32.CloseHandle(handle);
}

[LibraryImport(Libraries.Kernel32, SetLastError = true)]
internal static partial SafeJobHandle CreateJobObjectW(IntPtr lpJobAttributes, IntPtr lpName);

internal const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;

internal enum JOBOBJECTINFOCLASS
{
JobObjectBasicLimitInformation = 2,
Comment thread
adamsitnik marked this conversation as resolved.
Outdated
JobObjectExtendedLimitInformation = 9
}

[StructLayout(LayoutKind.Sequential)]
internal struct IO_COUNTERS
{
internal ulong ReadOperationCount;
internal ulong WriteOperationCount;
internal ulong OtherOperationCount;
internal ulong ReadTransferCount;
internal ulong WriteTransferCount;
internal ulong OtherTransferCount;
}

[StructLayout(LayoutKind.Sequential)]
internal struct JOBOBJECT_BASIC_LIMIT_INFORMATION
{
internal long PerProcessUserTimeLimit;
internal long PerJobUserTimeLimit;
internal uint LimitFlags;
internal UIntPtr MinimumWorkingSetSize;
Comment thread
adamsitnik marked this conversation as resolved.
Outdated
internal UIntPtr MaximumWorkingSetSize;
internal uint ActiveProcessLimit;
internal UIntPtr Affinity;
internal uint PriorityClass;
internal uint SchedulingClass;
}

[StructLayout(LayoutKind.Sequential)]
internal struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
internal JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
internal IO_COUNTERS IoInfo;
internal UIntPtr ProcessMemoryLimit;
internal UIntPtr JobMemoryLimit;
internal UIntPtr PeakProcessMemoryUsed;
internal UIntPtr PeakJobMemoryUsed;
}

[LibraryImport(Libraries.Kernel32, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool SetInformationJobObject(SafeJobHandle hJob, JOBOBJECTINFOCLASS JobObjectInfoClass, ref JOBOBJECT_EXTENDED_LIMIT_INFORMATION lpJobObjectInfo, uint cbJobObjectInfoLength);

[LibraryImport(Libraries.Kernel32, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool AssignProcessToJobObject(SafeJobHandle hJob, IntPtr hProcess);

[LibraryImport(Libraries.Kernel32, SetLastError = true)]
internal static partial int ResumeThread(IntPtr hThread);

internal const int PROC_THREAD_ATTRIBUTE_JOB_LIST = 0x0002000D;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ public ProcessStartInfo(string fileName, System.Collections.Generic.IEnumerable<
[System.Diagnostics.CodeAnalysis.AllowNullAttribute]
public string FileName { get { throw null; } set { } }
public System.Collections.Generic.IList<System.Runtime.InteropServices.SafeHandle>? InheritedHandles { get { throw null; } set { } }
Comment thread
adamsitnik marked this conversation as resolved.
public bool KillOnParentExit { get { throw null; } set { } }
[System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")]
public bool LoadUserProfile { get { throw null; } set { } }
[System.CLSCompliantAttribute(false)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,38 @@ namespace Microsoft.Win32.SafeHandles
{
public sealed partial class SafeProcessHandle : SafeHandleZeroOrMinusOneIsInvalid
{
// Static job object used for KillOnParentExit functionality.
// All child processes with KillOnParentExit=true are assigned to this job.
// The job handle is intentionally never closed - it should live for the
// lifetime of the process. When this process exits, the job object is destroyed
// by the OS, which terminates all child processes in the job.
private static readonly Lazy<Interop.Kernel32.SafeJobHandle> s_killOnParentExitJob = new(CreateKillOnParentExitJob);

protected override bool ReleaseHandle()
{
return Interop.Kernel32.CloseHandle(handle);
}

private static unsafe Interop.Kernel32.SafeJobHandle CreateKillOnParentExitJob()
{
Interop.Kernel32.JOBOBJECT_EXTENDED_LIMIT_INFORMATION limitInfo = default;
limitInfo.BasicLimitInformation.LimitFlags = Interop.Kernel32.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;

Interop.Kernel32.SafeJobHandle jobHandle = Interop.Kernel32.CreateJobObjectW(IntPtr.Zero, IntPtr.Zero);
if (jobHandle.IsInvalid || !Interop.Kernel32.SetInformationJobObject(
jobHandle,
Interop.Kernel32.JOBOBJECTINFOCLASS.JobObjectExtendedLimitInformation,
ref limitInfo,
(uint)sizeof(Interop.Kernel32.JOBOBJECT_EXTENDED_LIMIT_INFORMATION)))
{
int error = Marshal.GetLastWin32Error();
jobHandle.Dispose();
throw new Win32Exception(error);
}

return jobHandle;
}

private static Func<ProcessStartInfo, SafeProcessHandle>? s_startWithShellExecute;

internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle,
Expand Down Expand Up @@ -48,6 +75,10 @@ internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, S
// Inheritable copies of the child handles for CreateProcess
bool stdinRefAdded = false, stdoutRefAdded = false, stderrRefAdded = false;
bool hasInheritedHandles = inheritedHandles is not null;
bool killOnParentExit = startInfo.KillOnParentExit;
// We need extended startup info when we have inherited handles or when we need to pass
// a job list via PROC_THREAD_ATTRIBUTE_JOB_LIST (only for CreateProcess, not CreateProcessWithLogonW).
bool useExtendedStartupInfo = hasInheritedHandles || (killOnParentExit && startInfo.UserName.Length == 0);
Comment thread
adamsitnik marked this conversation as resolved.
Outdated

// When InheritedHandles is set, we use PROC_THREAD_ATTRIBUTE_HANDLE_LIST to restrict inheritance.
// For that, we need a reader lock (concurrent starts with different explicit lists are safe).
Expand All @@ -68,10 +99,11 @@ internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, S
void* attributeListBuffer = null;
SafeHandle?[]? handlesToRelease = null;
IntPtr* handlesToInherit = null;
IntPtr* jobHandles = null;

try
{
startupInfoEx.StartupInfo.cb = hasInheritedHandles ? sizeof(Interop.Kernel32.STARTUPINFOEX) : sizeof(Interop.Kernel32.STARTUPINFO);
startupInfoEx.StartupInfo.cb = useExtendedStartupInfo ? sizeof(Interop.Kernel32.STARTUPINFOEX) : sizeof(Interop.Kernel32.STARTUPINFO);

ProcessUtils.DuplicateAsInheritableIfNeeded(stdinHandle, ref startupInfoEx.StartupInfo.hStdInput, ref stdinRefAdded);
ProcessUtils.DuplicateAsInheritableIfNeeded(stdoutHandle, ref startupInfoEx.StartupInfo.hStdOutput, ref stdoutRefAdded);
Expand All @@ -87,7 +119,7 @@ internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, S
}

// set up the creation flags parameter
int creationFlags = hasInheritedHandles ? Interop.Kernel32.EXTENDED_STARTUPINFO_PRESENT : 0;
int creationFlags = useExtendedStartupInfo ? Interop.Kernel32.EXTENDED_STARTUPINFO_PRESENT : 0;
if (startInfo.CreateNoWindow) creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_NO_WINDOW;
if (startInfo.CreateNewProcessGroup) creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_NEW_PROCESS_GROUP;

Expand Down Expand Up @@ -120,7 +152,28 @@ internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, S
AddToInheritListIfValid(startupInfoEx.StartupInfo.hStdError, handlesToInheritSpan, ref handleCount);

EnableInheritanceAndAddRef(inheritedHandles, handlesToInheritSpan, ref handleCount, ref handlesToRelease);
BuildProcThreadAttributeList(handlesToInherit, handleCount, ref attributeListBuffer);
}

if (useExtendedStartupInfo)
{
// Determine the number of attributes we need to set in the proc thread attribute list.
int attributeCount = 0;
if (hasInheritedHandles)
{
attributeCount++; // PROC_THREAD_ATTRIBUTE_HANDLE_LIST
}
if (killOnParentExit)
{
attributeCount++; // PROC_THREAD_ATTRIBUTE_JOB_LIST
}

BuildProcThreadAttributeList(
hasInheritedHandles ? handlesToInherit : null,
handleCount,
killOnParentExit,
attributeCount,
ref attributeListBuffer,
ref jobHandles);
}

startupInfoEx.lpAttributeList = attributeListBuffer;
Expand Down Expand Up @@ -155,6 +208,14 @@ internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, S
Debug.Assert(!hasInheritedHandles, "Inheriting handles is not supported when starting with alternate credentials.");
Debug.Assert(startupInfoEx.StartupInfo.cb == sizeof(Interop.Kernel32.STARTUPINFO));

// When KillOnParentExit is set and we use CreateProcessWithLogonW (which doesn't support
Comment thread
jkotas marked this conversation as resolved.
// PROC_THREAD_ATTRIBUTE_JOB_LIST), we create the process suspended, assign it to the job,
// then resume it.
if (killOnParentExit)
{
creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_SUSPENDED;
Comment thread
adamsitnik marked this conversation as resolved.
}

commandLine.NullTerminate();
fixed (char* passwordInClearTextPtr = startInfo.PasswordInClearText ?? string.Empty)
fixed (char* environmentBlockPtr = environmentBlock)
Expand Down Expand Up @@ -218,17 +279,47 @@ internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, S

if (!IsInvalidHandle(processInfo.hProcess))
Marshal.InitHandle(procSH, processInfo.hProcess);
if (!IsInvalidHandle(processInfo.hThread))
Interop.Kernel32.CloseHandle(processInfo.hThread);

if (!retVal)
{
if (!IsInvalidHandle(processInfo.hThread))
Interop.Kernel32.CloseHandle(processInfo.hThread);

string nativeErrorMessage = errorCode == Interop.Errors.ERROR_BAD_EXE_FORMAT || errorCode == Interop.Errors.ERROR_EXE_MACHINE_TYPE_MISMATCH
? SR.InvalidApplication
: Interop.Kernel32.GetMessage(errorCode);

throw ProcessUtils.CreateExceptionForErrorStartingProcess(nativeErrorMessage, errorCode, startInfo.FileName, workingDirectory);
}

// When the process was started suspended for KillOnParentExit with CreateProcessWithLogonW,
// assign it to the job object and then resume the thread.
if (killOnParentExit && startInfo.UserName.Length != 0)
{
Debug.Assert(!IsInvalidHandle(processInfo.hThread), "Thread handle must be valid for suspended process.");
try
{
if (!Interop.Kernel32.AssignProcessToJobObject(s_killOnParentExitJob.Value, processInfo.hProcess))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
catch
{
// If we fail to assign to the job, terminate the suspended process.
Interop.Kernel32.TerminateProcess(procSH, -1);
throw;
}
finally
{
Interop.Kernel32.ResumeThread(processInfo.hThread);
Comment thread
adamsitnik marked this conversation as resolved.
Outdated
Interop.Kernel32.CloseHandle(processInfo.hThread);
}
}
else if (!IsInvalidHandle(processInfo.hThread))
{
Interop.Kernel32.CloseHandle(processInfo.hThread);
}
}
catch
{
Expand Down Expand Up @@ -256,6 +347,7 @@ internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, S
Interop.Kernel32.CloseHandle(startupInfoEx.StartupInfo.hStdError);

NativeMemory.Free(handlesToInherit);
NativeMemory.Free(jobHandles);

if (attributeListBuffer is not null)
{
Expand Down Expand Up @@ -421,15 +513,18 @@ private static void AddToInheritListIfValid(nint handle, Span<nint> handlesToInh
}

/// <summary>
/// Creates and populates a PROC_THREAD_ATTRIBUTE_LIST with a PROC_THREAD_ATTRIBUTE_HANDLE_LIST entry.
/// Creates and populates a PROC_THREAD_ATTRIBUTE_LIST with optional PROC_THREAD_ATTRIBUTE_HANDLE_LIST
/// and PROC_THREAD_ATTRIBUTE_JOB_LIST entries.
/// </summary>
private static unsafe void BuildProcThreadAttributeList(
IntPtr* handlesToInherit,
int handleCount,
ref void* attributeListBuffer)
bool killOnParentExit,
int attributeCount,
ref void* attributeListBuffer,
ref IntPtr* jobHandles)
{
nuint size = 0;
int attributeCount = handleCount > 0 ? 1 : 0;
Interop.Kernel32.InitializeProcThreadAttributeList(null, attributeCount, 0, ref size);

attributeListBuffer = NativeMemory.Alloc(size);
Expand All @@ -439,7 +534,7 @@ private static unsafe void BuildProcThreadAttributeList(
throw new Win32Exception(Marshal.GetLastWin32Error());
}

if (handleCount > 0 && !Interop.Kernel32.UpdateProcThreadAttribute(
if (handlesToInherit is not null && handleCount > 0 && !Interop.Kernel32.UpdateProcThreadAttribute(
Comment thread
adamsitnik marked this conversation as resolved.
Outdated
attributeListBuffer,
0,
(IntPtr)Interop.Kernel32.PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
Expand All @@ -450,6 +545,24 @@ private static unsafe void BuildProcThreadAttributeList(
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}

if (killOnParentExit)
{
jobHandles = (IntPtr*)NativeMemory.Alloc(1, (nuint)sizeof(IntPtr));
jobHandles[0] = s_killOnParentExitJob.Value.DangerousGetHandle();

Comment thread
adamsitnik marked this conversation as resolved.
Outdated
if (!Interop.Kernel32.UpdateProcThreadAttribute(
attributeListBuffer,
0,
(IntPtr)Interop.Kernel32.PROC_THREAD_ATTRIBUTE_JOB_LIST,
jobHandles,
(nuint)sizeof(IntPtr),
null,
null))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
}

private static void EnableInheritanceAndAddRef(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,9 @@
<data name="InheritedHandlesRequiresCreateProcess" xml:space="preserve">
<value>The InheritedHandles property cannot be used with UseShellExecute or UserName.</value>
</data>
<data name="KillOnParentExitCannotBeUsedWithUseShellExecute" xml:space="preserve">
<value>The KillOnParentExit property cannot be used with UseShellExecute.</value>
</data>
<data name="CantSetRedirectForSafeProcessHandleStart" xml:space="preserve">
<value>The RedirectStandardInput, RedirectStandardOutput, and RedirectStandardError properties cannot be used by SafeProcessHandle.Start. Use the StandardInputHandle, StandardOutputHandle, and StandardErrorHandle properties.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@
Link="Common\Interop\Windows\Kernel32\Interop.GetThreadTimes.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.CreateProcess.cs"
Link="Common\Interop\Windows\Kernel32\Interop.CreateProcess.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.JobObjects.cs"
Link="Common\Interop\Windows\Kernel32\Interop.JobObjects.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.TerminateProcess.cs"
Link="Common\Interop\Windows\Kernel32\Interop.TerminateProcess.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.GetCurrentProcess.cs"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,24 @@ public string Arguments
/// </value>
public IList<SafeHandle>? InheritedHandles { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the child process should be terminated when the parent process exits.
/// </summary>
/// <remarks>
/// <para>
/// When this property is set to <see langword="true"/>, the operating system will automatically terminate
/// the child process when the parent process exits, regardless of whether the parent exits gracefully or crashes.
/// </para>
/// <para>
/// This property cannot be used together with <see cref="UseShellExecute"/> set to <see langword="true"/>.
/// </para>
/// <para>
/// On Windows, this is implemented using Job Objects with the <c>JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE</c> flag.
Comment thread
adamsitnik marked this conversation as resolved.
Outdated
/// </para>
/// </remarks>
/// <value><see langword="true"/> to terminate the child process when the parent exits; otherwise, <see langword="false"/>. The default is <see langword="false"/>.</value>
public bool KillOnParentExit { get; set; }
Comment thread
adamsitnik marked this conversation as resolved.
Comment thread
jkotas marked this conversation as resolved.

public Encoding? StandardInputEncoding { get; set; }

public Encoding? StandardErrorEncoding { get; set; }
Expand Down Expand Up @@ -399,6 +417,11 @@ internal void ThrowIfInvalid(out bool anyRedirection, out SafeHandle[]? inherite
throw new InvalidOperationException(SR.InheritedHandlesRequiresCreateProcess);
}

if (KillOnParentExit && UseShellExecute)
{
throw new InvalidOperationException(SR.KillOnParentExitCannotBeUsedWithUseShellExecute);
}

if (InheritedHandles is not null)
{
IList<SafeHandle> list = InheritedHandles;
Expand Down
Loading
Loading