Skip to content

Implement ProcessStartInfo.KillOnParentExit for Windows#126699

Open
Copilot wants to merge 5 commits intomainfrom
copilot/add-kill-on-parent-exit-property
Open

Implement ProcessStartInfo.KillOnParentExit for Windows#126699
Copilot wants to merge 5 commits intomainfrom
copilot/add-kill-on-parent-exit-property

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 9, 2026

Description

Implements the ProcessStartInfo.KillOnParentExit property for Windows, which terminates child processes when the parent process exits. This is the Windows part of the KillOnParentExit feature.

API approved at #125838 (comment)

Changes Made

New API surface:

  • Added KillOnParentExit property to ProcessStartInfo with [SupportedOSPlatform("windows")] attribute (linux support will be added in upcoming PRs)
  • Added to reference assembly (ref/System.Diagnostics.Process.cs) with matching [SupportedOSPlatformAttribute("windows")]
  • Added XML documentation with a link to the Windows SDK description of JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE

Windows implementation (SafeProcessHandle.Windows.cs):

  • Added a lazily-initialized static SafeJobHandle with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE flag, shared across all child processes with KillOnParentExit=true
  • For CreateProcess path (no user credentials): uses PROC_THREAD_ATTRIBUTE_JOB_LIST in the extended startup info for atomic job assignment at process creation
  • For CreateProcessWithLogonW path (with user credentials): starts the process suspended, assigns it to the job object via AssignProcessToJobObject, then resumes the thread only on the happy path — on failure, the suspended process is terminated
  • ResumeThread return value is checked; failure throws Win32Exception and the suspended process is terminated
  • Extended BuildProcThreadAttributeList to support both PROC_THREAD_ATTRIBUTE_HANDLE_LIST and PROC_THREAD_ATTRIBUTE_JOB_LIST attributes
  • Uses string.IsNullOrEmpty(startInfo.UserName) for defensive checks when determining the code path

P/Invoke definitions (Interop.JobObjects.cs):

  • CreateJobObjectW, SetInformationJobObject, AssignProcessToJobObject, ResumeThread
  • JOBOBJECT_EXTENDED_LIMIT_INFORMATION, IO_COUNTERS, JOBOBJECTINFOCLASS structs/enums
  • SafeJobHandle, PROC_THREAD_ATTRIBUTE_JOB_LIST constant

Validation:

  • Extended ProcessStartInfo.ThrowIfInvalid to throw InvalidOperationException when KillOnParentExit is combined with UseShellExecute
  • Added KillOnParentExitCannotBeUsedWithUseShellExecute string resource

Tests (KillOnParentExitTests.cs):

  • KillOnParentExit_DefaultsToFalse / KillOnParentExit_CanBeSetToTrue — property basics
  • KillOnParentExit_WithUseShellExecute_Throws — validation test
  • KillOnParentExit_ProcessStartsAndExitsNormally — smoke test
  • KillOnParentExit_KillsTheChild_WhenParentExits — verifies child is killed (enabled=true) or survives (enabled=false) on normal parent exit, reports grandchild pid via stdout
  • KillOnParentExit_KillsTheChild_WhenParentIsKilled — same when parent is forcefully killed
  • KillOnParentExit_KillsTheChild_WhenParentCrashes — same when parent crashes (access violation)
  • KillOnParentExit_WithUserCredentials — exercises the CreateProcessWithLogonW + suspended + job assignment path using a Windows test account (admin/OuterLoop)
  • VerifyProcessIsRunning uses polling with Environment.TickCount64 and a 10-second timeout to avoid flaky assertions from PID reuse on Windows

Add KillOnParentExit property to ProcessStartInfo that terminates child
processes when the parent process exits. Windows implementation uses Job
Objects with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE flag.

For CreateProcess: uses PROC_THREAD_ATTRIBUTE_JOB_LIST for atomic assignment.
For CreateProcessWithLogonW: starts suspended, assigns to job, then resumes.

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/61612ca1-930c-4836-8554-d143d05c8321

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

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

Copy link
Copy Markdown
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot address my feedback

@adamsitnik adamsitnik added this to the 11.0.0 milestone Apr 9, 2026
@adamsitnik adamsitnik added the NO-REVIEW Experimental/testing PR, do NOT review it label Apr 9, 2026
…py path, add user credentials test, fix pid reporting via stdout, remove nop UseShellExecute test

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/c4dd95b4-cd8e-4740-83f9-7358444c04ce

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot April 9, 2026 12:50
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 9, 2026

@copilot address my feedback

All feedback addressed in 31f6e6b. All 403 tests pass.

Copilot AI changed the title Implementing KillOnParentExit property in ProcessStartInfo class Implement ProcessStartInfo.KillOnParentExit for Windows Apr 9, 2026
Copilot AI requested a review from adamsitnik April 9, 2026 12:53
- simplify the tests
- avoid duplication!!
Copilot AI review requested due to automatic review settings April 9, 2026 14:13
@adamsitnik adamsitnik marked this pull request as ready for review April 9, 2026 14:20
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements Windows support for ProcessStartInfo.KillOnParentExit by using Windows Job Objects to ensure spawned processes are terminated when the parent exits.

Changes:

  • Added ProcessStartInfo.KillOnParentExit API surface (implementation + ref assembly) and validation against UseShellExecute.
  • Implemented Windows job-object based process start paths (CreateProcess job-list attribute, and CreateProcessWithLogonW suspended + assign + resume).
  • Added Windows-focused tests validating the behavior.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj Includes the new KillOnParentExit test file in the test project.
src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs Extends existing Windows credential tests to cover KillOnParentExit on that path.
src/libraries/System.Diagnostics.Process/tests/KillOnParentExitTests.cs Adds new Windows tests validating KillOnParentExit behavior under various parent-exit scenarios.
src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs Adds the KillOnParentExit property, docs, and validation with UseShellExecute.
src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj Links in the new Windows job-object interop file.
src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx Adds a resource string for the KillOnParentExit + UseShellExecute validation.
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs Windows implementation: shared job handle, job-list attribute support, and logon path job assignment.
src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs Adds the KillOnParentExit property to the reference assembly surface.
src/libraries/Common/src/Interop/Windows/Kernel32/Interop.JobObjects.cs Adds required job-object P/Invokes/structs/constants.
src/libraries/Common/src/Interop/Windows/Advapi32/Interop.ProcessOptions.cs Adds CREATE_SUSPENDED for the CreateProcessWithLogonW suspended-start path.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 9, 2026

🤖 Copilot Code Review — PR #126699

Note

This review was generated by GitHub Copilot.

Holistic Assessment

Motivation: Adding ProcessStartInfo.KillOnParentExit addresses a real, well-understood need — ensuring child processes don't become orphans when their parent exits or crashes. This is a frequently requested feature with clear real-world use cases (build systems, process supervisors, test harnesses).

Approach: The Windows implementation using Job Objects with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE is the correct, well-established pattern. The dual code paths — PROC_THREAD_ATTRIBUTE_JOB_LIST for CreateProcess (atomic assignment) and CREATE_SUSPENDED + assign + resume for CreateProcessWithLogonW (which doesn't support extended startup info) — are both the canonical approaches for their respective scenarios. The shared singleton job object avoids per-process overhead.

Summary: ⚠️ Needs Human Review. The Windows implementation is solid and well-tested. However, there are two design-level questions that require human judgment: (1) whether this new public API has completed the API approval process, and (2) whether the intentional lack of [SupportedOSPlatform("windows")] reflects an approved cross-platform API design where Unix support is planned, or is an oversight. A human reviewer should verify API approval status and confirm the cross-platform strategy.


Detailed Findings

⚠️ API Approval — No linked api-approved issue found

This PR adds new public API surface:

// ref assembly
public bool KillOnParentExit { get { throw null; } set { } }

Per dotnet/runtime convention, new public APIs require an approved proposal before PR submission. I was unable to find a linked api-approved issue in the PR description or commit messages. The PR author (adamsitnik) is a maintainer who would know this process, so this may have been handled through channels not visible here (e.g., an API review meeting or a linked issue I couldn't locate).

Action: A human reviewer should verify that this API shape has been formally approved.


⚠️ Cross-Platform Behavior — Silently ignored on Unix

The property is defined in the shared ProcessStartInfo.cs (not in platform-specific files) without any [SupportedOSPlatform("windows")] attribute. This is inconsistent with how other Windows-only properties are handled in this class:

Property [SupportedOSPlatform("windows")] Throws PNSE on Unix
CreateNewProcessGroup ✅ Yes (both ref and src) ✅ Yes
LoadUserProfile ✅ Yes ✅ Yes
Domain ✅ Yes ✅ Yes
Password ✅ Yes ✅ Yes
KillOnParentExit No No
InheritedHandles ❌ No (cross-platform) N/A (implemented on both)

The XML documentation says "On Windows, this is implemented using Job Objects" — which could imply cross-platform support is planned (Linux has prctl(PR_SET_PDEATHSIG), macOS has kqueue with EVFILT_PROC). If that's the intent, the current behavior (silently ignored on Unix) is reasonable as a stepping stone. If Unix support is not planned, the property should follow the established pattern with [SupportedOSPlatform("windows")] and throw PlatformNotSupportedException on Unix.

Action: A human reviewer should confirm the intended cross-platform strategy. If Unix support is planned, a tracking issue should exist. If not, the property should be decorated with [SupportedOSPlatform("windows")] and throw PNSE on Unix via ProcessStartInfo.Unix.cs.


💡 ResumeThread Return Value — Not checked

In AssignJobAndResumeThread (SafeProcessHandle.Windows.cs line 608):

Interop.Kernel32.ResumeThread(processInfo.hThread);

ResumeThread returns (DWORD)-1 on failure. If it fails, the child process remains suspended indefinitely. While this is extremely unlikely with a valid thread handle from CreateProcessWithLogonW, checking the return value and handling the error (terminate the suspended process) would be more defensive:

if (Interop.Kernel32.ResumeThread(processInfo.hThread) == -1)
{
    Interop.Kernel32.TerminateProcess(procSH, -1);
    throw new Win32Exception(Marshal.GetLastWin32Error());
}

This is a low-probability concern but aligns with the defensive error handling already present in this method.


✅ Windows Implementation — Correct and well-structured

The implementation handles both code paths correctly:

  • CreateProcess path: Uses PROC_THREAD_ATTRIBUTE_JOB_LIST for atomic job assignment — the child process is in the job from its very first instruction. No window for orphaning.
  • CreateProcessWithLogonW path: Correctly uses CREATE_SUSPENDEDAssignProcessToJobObjectResumeThread. The error recovery (TerminateProcess in the catch block) ensures no zombie suspended processes are left behind.
  • Singleton job object: Lazy<T> provides thread-safe, once-only initialization. The SafeJobHandle is intentionally never disposed (rooted by the static field) — this is correct since the OS automatically destroys the job and kills its processes when the parent exits.
  • DangerousGetHandle(): Safe here because the Lazy<T> roots the SafeJobHandle for the process lifetime, so GC cannot collect it.
  • Thread handle closure: Correctly moved to the finally block to ensure cleanup even when AssignJobAndResumeThread throws.

✅ Interop Definitions — Correct

The Job Object structs (IO_COUNTERS, JOBOBJECT_BASIC_LIMIT_INFORMATION, JOBOBJECT_EXTENDED_LIMIT_INFORMATION) are correctly laid out with [StructLayout(LayoutKind.Sequential)]. P/Invoke signatures use SetLastError = true and [return: MarshalAs(UnmanagedType.Bool)] where appropriate. The SafeJobHandle correctly extends SafeHandleZeroOrMinusOneIsInvalid.


✅ Validation Logic — Correct

The ThrowIfInvalid method properly prevents combining KillOnParentExit with UseShellExecute. The useExtendedStartupInfo condition correctly avoids using extended startup info when UserName is set (since CreateProcessWithLogonW doesn't support it), falling back to the suspended-process approach.


✅ Test Coverage — Comprehensive for Windows

Tests cover:

  • Default value (false) and set/get
  • UseShellExecute incompatibility (throws InvalidOperationException)
  • Normal process start and exit with KillOnParentExit=true
  • Child killed when parent exits normally (WhenParentExits)
  • Child killed when parent is killed (WhenParentIsKilled)
  • Child killed when parent crashes via access violation (WhenParentCrashes)
  • Both enabled=true and enabled=false paths via [Theory]/[InlineData]
  • User credentials path (TestUserCredentialsPropertiesOnWindows) — tests the CreateProcessWithLogonW suspended-process code path

All tests are correctly marked [PlatformSpecific(TestPlatforms.Windows)].


💡 Lazy(T) Exception Caching — Acceptable but worth noting

If CreateJobObjectW or SetInformationJobObject fails during the first KillOnParentExit process start, the Lazy<T> default mode (ExecutionAndPublication) caches the exception permanently. All subsequent KillOnParentExit=true starts will fail with the same cached exception, even if the underlying system condition was transient. This is acceptable since these calls only fail under extreme system conditions (out of handles/memory), where retrying is unlikely to help.

Generated by Code Review for issue #126699 ·

Copy link
Copy Markdown
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot address my feedback

…sumeThread return, remove unused using, improve VerifyProcessIsRunning polling

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/06c58cfa-bdef-4f35-9345-9ab03e7dc542

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 9, 2026

@copilot address my feedback

All feedback addressed in cc04598. Source builds and all 401 tests pass.

Copilot AI requested a review from adamsitnik April 9, 2026 17:06
/// </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>
[SupportedOSPlatform("windows")]
public bool KillOnParentExit { get; set; }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to other reviewers: I did not bother throwing PNSE right now as I intend to add Unix support in upcoming PR

VerifyProcessIsRunning(enabled, grandChildPid);
}

private static void VerifyProcessIsRunning(bool shouldBeKilled, int processId)
Copy link
Copy Markdown
Member

@adamsitnik adamsitnik Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to other reviewers: I've marked #101582 as ready for review to make it easier to perform similar checks

@adamsitnik
Copy link
Copy Markdown
Member

/azp list

@azure-pipelines

This comment was marked as resolved.

@adamsitnik
Copy link
Copy Markdown
Member

/azp run runtime-libraries-coreclr outerloop-windows

@adamsitnik adamsitnik removed the NO-REVIEW Experimental/testing PR, do NOT review it label Apr 9, 2026
@adamsitnik adamsitnik requested a review from jkotas April 9, 2026 18:31
@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jkotas For most of it, this PR is just a subset of #124375. This is the new part

}

if (handleCount > 0 && !Interop.Kernel32.UpdateProcThreadAttribute(
if (handlesToInherit is not null && handleCount > 0 && !Interop.Kernel32.UpdateProcThreadAttribute(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (handlesToInherit is not null && handleCount > 0 && !Interop.Kernel32.UpdateProcThreadAttribute(
if (handleCount > 0 && !Interop.Kernel32.UpdateProcThreadAttribute(

This looks unnecessary

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot address this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the unnecessary handleCount > 0 check in afab02e. The handlesToInherit is not null guard is sufficient.


string firstLine = childHandle.Process.StandardOutput.ReadLine();
int grandChildPid = int.Parse(firstLine);
childHandle.Process.StandardInput.WriteLine("One AccessViolationException please.");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these intentionally crashing processes going to produce crash dumps? We tend to have a problem with these. The crash dumps fill disk on Helix machines.

See #113732

Do we need to do something similar here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these intentionally crashing processes going to produce crash dumps? We tend to have a problem with these. The crash dumps fill disk on Helix machines.

See #113732

Do we need to do something similar here?

When I run this locally on Windows, no crush dump is being generated. But once we enable it on Unix (upcoming PR), then it's going to become a real problem.

I'll keep that in mind and extract this logic into a helper (that reads the current limit, sets to 0 and restores after runninig):

// At present, RLimit is defined in a way where the fields are always 64-bit.
// Before adding support for a new platform, its definition of rlimit should be confirmed.
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())

Copy link
Copy Markdown
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot address my feedback where you were tagged

}

if (handleCount > 0 && !Interop.Kernel32.UpdateProcThreadAttribute(
if (handlesToInherit is not null && handleCount > 0 && !Interop.Kernel32.UpdateProcThreadAttribute(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot address this


string firstLine = childHandle.Process.StandardOutput.ReadLine();
int grandChildPid = int.Parse(firstLine);
childHandle.Process.StandardInput.WriteLine("One AccessViolationException please.");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these intentionally crashing processes going to produce crash dumps? We tend to have a problem with these. The crash dumps fill disk on Helix machines.

See #113732

Do we need to do something similar here?

When I run this locally on Windows, no crush dump is being generated. But once we enable it on Unix (upcoming PR), then it's going to become a real problem.

I'll keep that in mind and extract this logic into a helper (that reads the current limit, sets to 0 and restores after runninig):

// At present, RLimit is defined in a way where the fields are always 64-bit.
// Before adding support for a new platform, its definition of rlimit should be confirmed.
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())

…emove unnecessary handleCount > 0 check

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/8f476c7f-4f16-4bdf-b34d-93e9e161ecdf

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot April 10, 2026 07:50
Copilot AI requested a review from adamsitnik April 10, 2026 07:53
@adamsitnik
Copy link
Copy Markdown
Member

/azp run runtime-libraries-coreclr outerloop-windows

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants