Skip to content

Implement SafeProcessHandle APIs for macOS#124923

Open
Copilot wants to merge 11 commits intocopilot/add-new-apis-to-safeprocesshandlefrom
copilot/implement-safeprocesshandle-apis
Open

Implement SafeProcessHandle APIs for macOS#124923
Copilot wants to merge 11 commits intocopilot/add-new-apis-to-safeprocesshandlefrom
copilot/implement-safeprocesshandle-apis

Conversation

Copy link
Contributor

Copilot AI commented Feb 26, 2026

Description

Implements the macOS-specific managed and native layers for the new SafeProcessHandle APIs (Start, Open, WaitForExit*, Kill, Resume, Signal), using posix_spawn for process creation and kqueue for wait/timeout/cancellation.

Managed changes

  • SafeProcessHandle.Unix.cs: Full implementation of StartCore, OpenCore, WaitForExitCore, TryWaitForExitCore, WaitForExitOrKillOnTimeoutCore, WaitForExitAsyncCore, WaitForExitOrKillOnCancellationAsyncCore, KillCore, ResumeCore, SendSignalCore. Uses NoPidFd = int.MinValue (not -1) since the handle derives from SafeHandleZeroOrMinusOneIsInvalid. All error paths use throw new Win32Exception() (parameterless) to let it fetch the last error automatically.
  • ProcessUtils.Unix.cs: Extracted CreateEnvp, AllocNullTerminatedArray, FreeArray as internal shared methods, reused by both Process.Unix.cs and SafeProcessHandle.Unix.cs.
  • Process.Unix.cs: Inlined the private CreateEnvp wrapper to call ProcessUtils.CreateEnvp directly.
  • Interop.SpawnProcess.cs: New LibraryImport declarations (EntryPoint = "SystemNative_*") for SpawnProcess, SendSignal, WaitForExitAndReap, TryWaitForExit, TryWaitForExitCancellable, WaitForExitOrKillOnTimeout, TryGetExitCode, OpenProcess. The detached parameter is omitted as the API proposal is still pending review.
  • Interop.ForkAndExecProcess.cs: Delegates to shared ProcessUtils methods.
  • csproj: Added Interop.Pipe.cs and Interop.SpawnProcess.cs for Unix builds.

Native changes

  • pal_process.c: macOS implementation using posix_spawn with POSIX_SPAWN_CLOEXEC_DEFAULT, posix_spawn_file_actions_addinherit_np, posix_spawn_file_actions_addchdir_np, POSIX_SPAWN_START_SUSPENDED. The implementation is guarded by #if defined(__APPLE__) && !defined(TARGET_MACCATALYST) && !defined(TARGET_TVOS) to exclude Mac Catalyst and tvOS (iOS is supported). posix_spawn_file_actions_addchdir_np retains HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP feature detection since it is not available on tvOS. Wait functions use kqueue/EVFILT_PROC/NOTE_EXIT (guarded by existing HAVE_KQUEUE) for timeout and cancellation support. Signal mapping reuses SystemNative_GetPlatformSignalNumber and TryConvertSignalCodeToPosixSignal from pal_signal.c instead of duplicating the logic. Environment variable resolution is extracted into a GetEnvVars helper for reuse. OpenProcess uses kill(pid, 0) to check process existence without requiring a child-process relationship.
  • pal_signal.c / pal_signal.h: Made TryConvertSignalCodeToPosixSignal non-static and added its declaration to the header for reuse by pal_process.c.
  • pal_process_wasi.c: Stubs returning -1 with errno = ENOTSUP.
  • pal_process.h: New function declarations with reordered SpawnProcess arguments (path, argv, envp, working_dir, inherited_handles, inherited_handles_count, stdin/stdout/stderr fds, flags, out params).
  • entrypoints.c: New DllImportEntry entries.
  • configure.cmake / pal_config.h.in: Added HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP feature detection (not available on tvOS). No other HAVE_POSIX_SPAWN* defines are added — macOS platform checks (#ifdef __APPLE__) are used instead. Existing HAVE_KQUEUE is reused for sys/event.h inclusion.

Test changes

  • SafeProcessHandleTests.cs: Extended [PlatformSpecific] to TestPlatforms.OSX | TestPlatforms.Windows. All ProcessStartOptions creation uses platform detection (OperatingSystem.IsWindows() ternary). CreateTenSecondSleep returns sleep 10 on Unix. Start_WithNoArguments_Succeeds uses hostname on Windows and pwd on Unix (platform-specific, since pwd is not a standalone executable on Windows). Kill tests assert PosixSignal.SIGKILL and exit code > 128 on Unix. Open_CurrentProcess_Succeeds is annotated [PlatformSpecific(TestPlatforms.Windows)].
  • SafeProcessHandleTests.Unix.cs: macOS-specific tests for Signal(SIGTERM), Signal(SIGINT), invalid signal, and signal-to-exited-process behavior. All long-running tests use CreateTenSecondSleep().

CI changes

  • eng/pipelines/runtime.yml: Added copilot/*.* to CI branch triggers to enable pipeline runs for this branch.

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

@dotnet-policy-service
Copy link
Contributor

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

Copilot AI changed the title [WIP] Implement SafeProcessHandle APIs for macOS Implement SafeProcessHandle APIs for macOS Feb 26, 2026
Copy link
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.

Moreover, please modify the .yml file that is required to let the CI run for this branch. I am not an expert in this area, but I think it should be enough to modify this file:

pr:
branches:
include:
- main
- release/*.*

and list copilot/*.* after release/*.*

     - release/*.* 
     - copilot/*.* 

…on throws, update tests, add CI branch trigger

Co-authored-by: adamsitnik <[email protected]>
Copy link
Contributor Author

Copilot AI commented Feb 27, 2026

@copilot please address my feedback.

Moreover, please modify the .yml file that is required to let the CI run for this branch. I am not an expert in this area, but I think it should be enough to mo...

All feedback addressed and copilot/*.* added to the CI branch triggers in eng/pipelines/runtime.yml in 28845d4.

Copy link
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

This PR implements SafeProcessHandle APIs for macOS using posix_spawn for process creation and kqueue for wait/timeout/cancellation operations. The implementation provides parity with the Windows implementation, enabling cross-platform process management through the new SafeProcessHandle APIs.

Changes:

  • Native layer: Added macOS-specific implementations using posix_spawn, kqueue, and waitpid for process creation and management
  • Managed layer: Implemented all core SafeProcessHandle methods (Start, Open, WaitForExit*, Kill, Resume, Signal) with proper error handling
  • Code refactoring: Extracted shared utility methods (CreateEnvp, AllocNullTerminatedArray, FreeArray) from Interop.ForkAndExecProcess.cs to ProcessUtils.Unix.cs for reuse
  • Tests: Extended cross-platform tests to support macOS and added macOS-specific signal tests

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/native/libs/configure.cmake Added CMake feature detection for posix_spawn APIs and kqueue on macOS
src/native/libs/Common/pal_config.h.in Added configuration defines for detected posix_spawn features
src/native/libs/System.Native/pal_process.h Declared new native API functions for process spawning, signaling, and waiting
src/native/libs/System.Native/pal_process.c Implemented macOS-specific process APIs using posix_spawn and kqueue, including signal mapping
src/native/libs/System.Native/pal_process_wasi.c Added ENOTSUP stubs for WASI platform
src/native/libs/System.Native/entrypoints.c Registered new native function entry points
src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs Added P/Invoke declarations for new native APIs
src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.cs Refactored to use shared ProcessUtils methods
src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Unix.cs Added shared utility methods for environment and array marshalling
src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs Updated to use ProcessUtils.CreateEnvp instead of local implementation
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs Implemented all SafeProcessHandle core methods for macOS
src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj Added Interop.SpawnProcess.cs and Interop.Pipe.cs to Unix builds
src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs Extended platform support to include macOS and updated tests for cross-platform compatibility
src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Unix.cs Added macOS-specific signal tests
src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj Added SafeProcessHandleTests.Unix.cs to Unix test builds
eng/pipelines/runtime.yml Added copilot/. branch trigger (intended for development, should be removed)

Copy link
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 all unresolved feedback

Copy link
Contributor Author

Copilot AI commented Feb 27, 2026

@copilot please address all unresolved feedback

All unresolved feedback addressed in cdd13b0: simplified OpenProcess to kill(pid, 0), removed SIGSTOP from signal mappings, and fixed pwd test to be platform-specific.

Copy link
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 give my suggestion a try.

Copy link
Contributor Author

Copilot AI commented Feb 27, 2026

@copilot please give my suggestion a try.

Done in 4b02d2e — replaced all HAVE_POSIX_SPAWN* feature detection with #ifdef __APPLE__ platform checks and removed the cmake/config defines.

Copy link
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

Also, annotate the Open_CurrentProcess_Succeeds test with [PlatformSpecific(TestPlatforms.Windows)]

…der args, add GetEnvVars helper, restore HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP, fix test

Co-authored-by: adamsitnik <[email protected]>
Copy link
Contributor Author

Copilot AI commented Feb 27, 2026

@copilot please address my feedback

Also, annotate the Open_CurrentProcess_Succeeds test with [PlatformSpecific(TestPlatforms.Windows)]

Annotated Open_CurrentProcess_Succeeds with [PlatformSpecific(TestPlatforms.Windows)] and addressed all feedback. Done in 300ba39.

Copy link
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 apply my suggestion

Copy link
Contributor Author

Copilot AI commented Feb 27, 2026

@copilot please apply my suggestion

Applied in 2240ff7.

@adamsitnik
Copy link
Member

@copilot please invoke the code-review skill and post the analysis/results as a comment on this PR

Copy link
Contributor Author

Copilot AI commented Feb 27, 2026

@copilot please invoke the code-review skill and post the analysis/results as a comment on this PR

🤖 Copilot Code Review — PR #115913

Holistic Assessment

Motivation: This PR implements the macOS-specific managed and native layers for the new SafeProcessHandle APIs. The change is well-motivated — it provides feature parity with the Windows implementation, enabling cross-platform process management.

Approach: The approach is solid — using posix_spawn for process creation and kqueue/EVFILT_PROC/NOTE_EXIT for wait/timeout/cancellation is the idiomatic macOS way. Reusing existing signal mapping from pal_signal.c and extracting GetEnvVars/ProcessUtils for shared code are good decisions.

Summary: ⚠️ Needs Human Review. The code is well-structured overall and follows the existing codebase patterns. I found a few issues that range from potential correctness concerns to minor style points. I'm unable to verify some concerns without access to a macOS test environment, so human review of the kqueue/timeout behavior is recommended.


Detailed Findings

⚠️ Timeout drift with EINTR in SystemNative_TryWaitForExit

In pal_process.c:1275, the kevent call with a timeout is retried on EINTR without recalculating the remaining timeout:

while ((ret = kevent(queue, &change_list, 1, &event_list, 1, &timeout)) < 0 && errno == EINTR);

On macOS, kevent may or may not update the timeout struct on EINTR (the behavior varies by OS). If it doesn't update the timeout, each retry restarts with the full original timeout, meaning the actual wait could be significantly longer than requested. On macOS specifically, the kevent man page states the timeout value is not updated — so this loop could wait much longer than timeout_ms if signals are being delivered frequently.

This is a low-probability concern in practice (signal storms are rare), but for correctness-sensitive timeout code it's worth noting. A human reviewer should decide whether the current behavior is acceptable or whether a clock-based recalculation is needed.

⚠️ map_wait_status — uninitialized output on WIFSTOPPED

In pal_process.c:1122-1139, map_wait_status handles WIFEXITED and WIFSIGNALED but returns -1 without initializing out_exitCode and out_signal when neither condition is true (e.g., WIFSTOPPED or WIFCONTINUED from a traced/stopped process). While waitpid without WUNTRACED shouldn't return stopped status, this leaves output parameters uninitialized on the error return path. Callers check the return value, so this is unlikely to cause an actual bug, but initializing outputs to 0 at the start of the function (or in TryGetExitCode/WaitForExitAndReap before calling map_wait_status) would be defensive.

⚠️ ReleaseHandle — double-cast of handle field

In SafeProcessHandle.Unix.cs:53-57:

return (int)handle switch
{
    NoPidFd => true,
    _ => Interop.Sys.Close((IntPtr)(int)handle) == 0,
};

The handle field (inherited from SafeHandle) is of type IntPtr. The cast (IntPtr)(int)handle truncates on 64-bit if the handle value ever exceeds 32-bit range. For file descriptors this shouldn't happen (they're small ints), but the double cast is unusual. Consider using Interop.Sys.Close(handle) directly since Close takes IntPtr, or at least document why the truncation is safe.

⚠️ kill_on_parent_death is silently ignored on macOS

In pal_process.c:1076-1077:

// kill_on_parent_death is not supported with posix_spawn on macOS
(void)kill_on_parent_death;

The managed test (KillOnParentExit_CanBeSetToTrue) sets this option and runs on macOS, but the native layer silently ignores it. The test passes because it doesn't actually verify the kill-on-parent-exit behavior — it only checks that the process starts and exits normally. This is a feature gap that should be documented, possibly with a comment in the managed layer about the limitation on macOS. A human reviewer should confirm this is the intended behavior.

💡 SendSignal takes raw int for pidfd but managed code passes NoPidFd (int.MinValue)

In SafeProcessHandle.Unix.cs:271:

int result = Interop.Sys.SendSignal(pidfd, pid, PosixSignal.SIGKILL);

The native SystemNative_SendSignal ignores pidfd entirely ((void)pidfd), so this is not a bug, but the managed code computes pidfd values that are never used. This is fine for now (future Linux pidfd support), just noting the asymmetry.

✅ Signal mapping reuse

Good refactoring — reusing SystemNative_GetPlatformSignalNumber and TryConvertSignalCodeToPosixSignal from pal_signal.c instead of duplicating signal mapping logic. The declaration in pal_signal.h is clean.

✅ Error handling pattern

Consistent use of throw new Win32Exception() (parameterless) across all managed error paths lets the runtime fetch the last error automatically. The ESRCH fallback in KillCore correctly returns false instead of throwing.

✅ Resource cleanup in StartProcessInternal

The try/finally block in StartProcessInternal (lines 97-148) properly frees all allocated native memory even on failure paths.

HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP for tvOS

The restored feature detection for addchdir_np with the comment about tvOS is correct. The refactored #if/#else with unified error handling (post-review suggestion) is clean.

✅ Test coverage

Tests cover the main scenarios: start, kill, wait with timeout, async wait with cancellation, signal sending, and edge cases (invalid signal, signaling exited process). Platform-specific branching in tests is reasonable.

💡 fcntl.h include guard removal

The PR changed #if HAVE_PIPE2 / #include <fcntl.h> to unconditional #include <fcntl.h>. This is correct because:

  1. The existing code uses fcntl in !HAVE_PIPE2 paths
  2. The new create_kqueue_cloexec function needs fcntl/FD_CLOEXEC

However, note that the original guard was technically wrong (fcntl.h was needed regardless of HAVE_PIPE2), so this change is actually a pre-existing bug fix.

💡 poll.h, time.h, sys/time.h includes

These headers are added unconditionally but only used in the #if HAVE_KQUEUE paths and #ifdef __APPLE__ blocks. They're harmless on all Unix platforms but could be moved under guards for cleanliness. Not blocking.

)
{
#if defined(__APPLE__) && !defined(TARGET_MACCATALYST) && !defined(TARGET_TVOS)
// ========== POSIX_SPAWN PATH (macOS) ==========
Copy link
Member

Choose a reason for hiding this comment

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

@wfurt If you could take some time to review this PR, I would appreciate it a lot.

To tell the long story, I want to introduce a low-level API that will allow to build anything process-related on top of it.

Since it's a new API, I don't need to worry about breaking changes and I really want us to stop inheriting all file descriptors when starting a process. On macOS, this can be done with POSIX_SPAWN_CLOEXEC_DEFAULT which is supported by posix_spawn which based on my benchmarking works way FASTER on macOS compared to typical fork/execv.

For other process-related methods I use kqueue to monitor for process exit (with a timeout).

Some of the native methods accept pidfd it's short for process descriptors, which will be used and introduced in separate PR for Linux (#124979, it's still WIP and not ready for review)

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.

3 participants