Skip to content

Add ProcessStartInfo.InheritedHandles to restrict child process handle inheritance#126318

Merged
adamsitnik merged 48 commits intomainfrom
copilot/add-inherited-handles-to-processstartinfo
Apr 8, 2026
Merged

Add ProcessStartInfo.InheritedHandles to restrict child process handle inheritance#126318
adamsitnik merged 48 commits intomainfrom
copilot/add-inherited-handles-to-processstartinfo

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 30, 2026

fixes #13943

Copilot AI and others added 2 commits March 30, 2026 14:49
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/9d1807f7-d4f3-4d70-bf98-1a9cdca1ea2f

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
…sAddRef ordering, test robustness

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/9d1807f7-d4f3-4d70-bf98-1a9cdca1ea2f

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
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 please address my feedback

@adamsitnik adamsitnik added the NO-REVIEW Experimental/testing PR, do NOT review it label Mar 30, 2026
…OEX refactoring, auto-property, RemoteExecutor tests

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/8ed756c4-f9e3-4311-902b-2e2b283c2682

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Copilot AI requested a review from adamsitnik March 30, 2026 16:47
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 please address my feedback

@adamsitnik adamsitnik added this to the 11.0.0 milestone Mar 30, 2026
… improve tests

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/cbca9e9f-f00e-4bc2-86ee-96f18d939d95

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
… where it is used

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/8109f39a-053e-4c14-840d-2d6c4e6cd104

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
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 please address my feedback

…ts, fdwalk support

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/87042cef-4c5c-4a21-971b-4c746ab48ee8

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

Copilot AI commented Mar 30, 2026

@copilot please address my feedback

All addressed in 6d4cbac. Summary of changes:

  • Merged Interop.ProcThreadAttributeList.cs into Interop.CreateProcess.cs (deleted the former file, updated CreateProcess to accept STARTUPINFOEX)
  • Fixed braces style in AddHandleToInheritList
  • Simplified all restrictHandles test code to startInfo.InheritedHandles = restrictHandles ? [] : null
  • Moved HandleNotInheritedExitCode const inside InheritedHandles_CanRestrictHandleInheritance
  • Moved CLOSE_RANGE_CLOEXEC define to the includes section as #if (HAVE_CLOSE_RANGE || HAVE_CLOSE_RANGE_SYSCALL) && !defined(CLOSE_RANGE_CLOEXEC)
  • Added Illumos/Solaris fdwalk support via check_function_exists in cmake, HAVE_FDWALK in pal_config.h.in, and a SetCloexecForFd callback + #elif HAVE_FDWALK branch in pal_process.c

Copilot AI requested a review from adamsitnik March 30, 2026 18:28
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

Copilot AI and others added 2 commits April 6, 2026 07:20
…or messages

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/291e4ac4-3f92-47ff-b73a-f47d7ce83920

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
@github-actions

This comment has been minimized.

@adamsitnik
Copy link
Copy Markdown
Member

Detailed Findings

⚠️ Duplicate stdio handles in PROC_THREAD_ATTRIBUTE_HANDLE_LIST — Dedup check removed

Files: SafeProcessHandle.Windows.cs:408-418, SafeProcessHandle.Windows.cs:117-120

The old AddHandleToInheritList checked for duplicates before adding to the handle list:

// Old code (removed)
for (int i = 0; i < handleCount; i++)
{
    if (handlesToInherit[i] == rawValue)
        return; // already in list
}

The new AddToInheritListIfValid unconditionally appends valid handles. This means if hStdOutput and hStdError resolve to the same raw handle value (which occurs when stdout and stderr are redirected to the same target, e.g., 2>&1, or in some CI environments), both would be added to the PROC_THREAD_ATTRIBUTE_HANDLE_LIST.

Some empirical evidence suggests UpdateProcThreadAttribute returns ERROR_INVALID_PARAMETER with duplicate handle entries. If true, this would cause Process.Start to fail with a Win32Exception when InheritedHandles is set (even []) and stdout/stderr share the same underlying handle.

The user's InheritedHandles list is already deduplicated in ThrowIfInvalid, and ValidateInheritedHandles prevents overlap with stdio handles — but the stdio handles themselves are not deduplicated against each other. (Flagged by all 3 models)

Recommendation: Consider restoring a simple dedup check in AddToInheritListIfValid, or verify with a test that Windows tolerates duplicate entries.

We already have such a test (CanRedirectOutputAndErrorToSamePipe) and it's passing on Windows:

startInfo.StandardOutputHandle = writePipe;
startInfo.StandardErrorHandle = writePipe;
startInfo.InheritedHandles = restrictHandles ? [] : null;

…ck in pal_process.c

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/29c24dd2-ff38-42fd-9771-4449db8d3a96

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Member

@jkotas jkotas left a comment

Choose a reason for hiding this comment

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

- use indexed argument names in ArgumentExceptions, add local helper, remove resource
- introduce helper for checking if given handle is valid on Windows

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
@github-actions

This comment has been minimized.

@adamsitnik
Copy link
Copy Markdown
Member

Issue: If another thread calls Dispose() on the SafeHandle between SetHandleInformation and DangerousAddRef

The code uses reader locks for concurrent process creation with InheritedHandles, but multiple threads can race on the same handle's inheritance flags:

there's a theoretical race where handles could be closed between validation and actual use.

This API is documented as not safe for such scenarios. We are NOT going to try to prevent all possible misuses. This is by design.

If BuildProcThreadAttributeList throws an exception after EnableInheritanceAndAddRef has partially executed, the handles that were successfully processed remain in an incorrect state:

This is not true. EnableInheritanceAndAddRef populates an array of handles that require their inheritance to be disabled. If BuildProcThreadAttributeList throws, DisableInheritanceAndRelease is getting executed in the finally block and disables the inheritance for all non-null handles from the list.

Issue: If RLIMIT_NOFILE is set to a very high value (e.g., 1,048,576), this loop will make over 1 million system calls, causing significant performance degradation during process startup.

Fix Required: Add a reasonable upper bound (e.g., 65536) to prevent performance issues in environments with extremely high file descriptor limits.

This is a new API, that user has to explicitly enable. It's introduced for the sake of correctness, at a cost of potential performance degradation. This is documented and by design.

Consider logging the failure or handling it more robustly than just a debug assert.

This is by design. We have two options:

  • spawning the process has failed and exception was thrown: if we throw a different exception, it will overwrite it and the user is going to miss the actual source of the problem
  • spawning the process has succeeded: we can't throw an exception because it would lead to resource leak

We can't use any general purpose logger because it's base class library. We could emit some kind of event, but it's pointless because users would need to know about it's existence and sign up for it. And the only way to get to the point where we need to throw here is invalid usage of the API, where user has provided us with a handle that some other part of the application has closed.

- remove the test case that could potentially fail when OS uses same handle/descriptor value for a different pipe
- use NUL device for testing the "not inherited" scenario as runtime does not use CharacterDevice files for internal purposes
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Co-authored-by: Jan Kotas <jkotas@microsoft.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 7, 2026

🤖 Copilot Code Review — PR #126318

Note

This review was generated by GitHub Copilot.

Holistic Assessment

Motivation: Well-justified. This implements ProcessStartInfo.InheritedHandles from the API-approved issue #125838, addressing the long-standing request for restricting child process handle inheritance. This is a real-world need for security and resource management.

Approach: Sound. The implementation uses platform-appropriate mechanisms: PROC_THREAD_ATTRIBUTE_HANDLE_LIST on Windows (the recommended Win32 pattern), POSIX_SPAWN_CLOEXEC_DEFAULT on macOS, and close_range(CLOSE_RANGE_CLOEXEC) with multiple fallback layers on Linux/Unix. The code has clearly gone through extensive iteration with @jkotas as co-author.

Summary: ⚠️ Needs Human Review. The code is well-implemented and has already been through multiple review rounds. I have no blocking issues but flag a few items that warrant human confirmation on design trade-offs around thread safety and exception message changes.


Detailed Findings

✅ API Approval Verification — Matches approved shape

The api-approved label was applied by @bartonjs on issue #125838 on 2026-03-24. The approved API shape for this PR's scope is:

public sealed partial class ProcessStartInfo
{
    public IList<SafeHandle>? InheritedHandles { get; set; }
}

The ref assembly (ref/System.Diagnostics.Process.cs) and implementation both match this exactly. No extra unapproved public API surface was detected.

✅ Windows Implementation — PROC_THREAD_ATTRIBUTE_HANDLE_LIST

The Windows path correctly:

  • Uses STARTUPINFOEX + InitializeProcThreadAttributeList + UpdateProcThreadAttribute with PROC_THREAD_ATTRIBUTE_HANDLE_LIST
  • Takes a read lock when InheritedHandles is set (allowing concurrent starts with explicit handle lists) vs. the existing write lock for the default path. This is safe because both concurrent paths use the attribute list to restrict inheritance, so temporarily-inheritable handles from one start won't leak to another.
  • Correctly handles cleanup: DisableInheritanceAndRelease unconditionally removes HANDLE_FLAG_INHERIT and releases refs even on error paths.
  • Sets bInheritHandles = false when handleCount == 0 (empty list + all-invalid stdio), correctly preventing any handle inheritance.

✅ Unix Implementation — Graceful fallback chain

The native C implementation provides a well-layered fallback:

  1. close_range(3, UINT_MAX, CLOSE_RANGE_CLOEXEC) — kernel function (FreeBSD 12.2+, glibc ≥ 2.34)
  2. syscall(__NR_close_range, ...) — raw syscall for older glibc on Linux ≥ 5.9
  3. fdwalk() — Illumos/Solaris
  4. Brute-force getrlimit/sysconf loop

Critically, the code uses CLOSE_RANGE_CLOEXEC (set flag) rather than direct close, preserving the waitForChildToExecPipe needed to report exec failures back to the parent. The macOS posix_spawn path uses POSIX_SPAWN_CLOEXEC_DEFAULT + posix_spawn_file_actions_addinherit_np, which is the idiomatic Apple API.

The per-architecture __NR_close_range fallback correctly uses 436 (generic syscall table) with an alpha-specific override of 546.

✅ Handle Lifecycle Management

  • DuplicateAsInheritableIfNeeded was refactored from returning SafeFileHandle? to outputting nint + refAdded. The cleanup in the finally block correctly distinguishes between "release ref" (handle was already inheritable) and "close duplicated handle" paths.
  • EnableInheritanceAndAddRef stores handles in handlesToRelease before calling SetHandleInformation, ensuring cleanup even when the Win32 call fails.
  • The DangerousAddRef/DangerousRelease pairing for inherited handles in Interop.ForkAndExecProcess.cs (Unix) uses inheritedRefsAdded counter to release only successfully AddRef'd handles.

✅ Test Coverage

Comprehensive tests covering:

  • Pipe redirection with/without InheritedHandles restriction
  • Multiple handles (CanInheritMoreThanOneHandle)
  • Both SafeFileHandle and SafePipeHandle inheritance
  • Validation: null, invalid, closed, duplicate, unsupported handle types, standard handle overlap
  • Edge cases: processes with invalid standard handles, nested child process starts
  • Windows-specific: INVALID_HANDLE_VALUE stdio handles with CreateProcess
  • Mobile platform coverage updated with InheritedHandles parameter
  • The ArgumentList[0] exception name change has a corresponding test update.

⚠️ Thread Safety — Documented but risky concurrent use

The PR documents that "Two concurrent process starts that pass same handle in InheritedHandles are not supported" because SetHandleInformation globally modifies handle inheritance flags. This is a reasonable trade-off, but consider whether the documentation is sufficient to prevent subtle bugs — users may not expect that sharing a SafeFileHandle across two ProcessStartInfo instances started concurrently is unsafe.

Question for reviewers: Is this limitation sufficiently discoverable? A runtime check detecting shared handles across concurrent starts would be expensive, but the current documentation-only approach relies on users reading the remarks.

💡 ArgumentList exception message change — Minor behavioral change

The ArgumentList null validation changed from throw new ArgumentNullException("item", SR.ArgumentListMayNotContainNull) to throw new ArgumentNullException($"ArgumentList[{i}]"). This is an improvement for debuggability (includes the index), but is a behavioral change that removes the resource string ArgumentListMayNotContainNull. The test was updated to match. This is a minor breaking change in exception details that shouldn't affect well-written code.

💡 Ref Assembly Reordering

The ref assembly changes include alphabetical reordering of existing SafeProcessHandle and ProcessStartInfo members (e.g., Kill() before ProcessId, StandardInputEncoding before StandardInputHandle). This is consistent with dotnet/runtime conventions for ref source.

Generated by Code Review for issue #126318 ·

@adamsitnik
Copy link
Copy Markdown
Member

/ba-g the failures and foremost timeouts are unrelated (Remote machine provider issue: Agent was purged, cancelling the pipeline)

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.

Make Process.Start have a option to change handle inheritance

5 participants