Skip to content

Implement SafeProcessHandle.Kill and Signal#126313

Open
Copilot wants to merge 10 commits intomainfrom
copilot/implement-safeprocesshandle-kill-signal
Open

Implement SafeProcessHandle.Kill and Signal#126313
Copilot wants to merge 10 commits intomainfrom
copilot/implement-safeprocesshandle-kill-signal

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 30, 2026

Adds Kill() and Signal(PosixSignal) to SafeProcessHandle, enabling callers to terminate or signal a process via a handle without going through Process.

Description

Public API (SafeProcessHandle.cs + ref assembly)

  • Kill() — sends a request to the OS to terminate the process; silently no-ops if already exited (matches Process.Kill semantics). On Windows, the handle must have PROCESS_TERMINATE access.
  • Signal(PosixSignal) — sends an arbitrary signal; returns false if process no longer exists (or never existed), true on delivery, throws PlatformNotSupportedException for unsupported signals. On Windows, the handle must have PROCESS_TERMINATE access.

Both methods validate the handle (throw InvalidOperationException on invalid handle), carry the same [UnsupportedOSPlatform("ios/tvos")] / [SupportedOSPlatform("maccatalyst")] annotations as Start, and throw PlatformNotSupportedException at runtime on iOS/tvOS (matching the ProcessUtils.PlatformDoesNotSupportProcessStartAndKill pattern used by Process.Kill).

Windows (SafeProcessHandle.Windows.cs)

  • SignalCore: only SIGKILL is supported (maps to TerminateProcess), matching PosixSignalRegistration.Create exception behavior for unsupported signals. Retrieves the error code via Marshal.GetLastWin32Error() before any checks and only constructs Win32Exception(errorCode) when actually throwing. Detects already-exited processes via ERROR_ACCESS_DENIED + GetExitCodeProcess returning non-STILL_ACTIVE.
  • No separate KillCore; Kill() calls SignalCore(PosixSignal.SIGKILL) directly from the shared file.

Unix (SafeProcessHandle.Unix.cs)

  • SignalCore: checks ProcessUtils.PlatformDoesNotSupportProcessStartAndKill first (throws PlatformNotSupportedException on iOS/tvOS); uses Interop.Sys.GetPlatformSignalNumber(signal) to convert the managed PosixSignal to its platform-native signal number (throws PlatformNotSupportedException if unsupported, matching PosixSignalRegistration.Create behavior); passes the native signal number directly to Interop.Sys.Kill; returns false on ESRCH; uses Interop.Sys.GetLastErrorInfo() and passes errorInfo.RawErrno to the Win32Exception constructor.
  • No separate KillCore; Kill() calls SignalCore(PosixSignal.SIGKILL) directly from the shared file.

Native (pal_process.c)

  • SystemNative_Kill simplified to call kill(pid, signal) directly with no PAL mapping switch — it now takes the platform-native signal number, consistent with other System.Native APIs such as SystemNative_EnablePosixSignalHandling. The old Signals PAL enum (PAL_NONE, PAL_SIGKILL, PAL_SIGSTOP) has been removed from pal_process.h.
  • Added SystemNative_GetPlatformSIGSTOP() to expose the platform-native SIGSTOP value (varies by OS: Linux=19, macOS=17), used by Process.KillTree.

Process.Kill refactoring (Process.Windows.cs)

  • Process.Kill() on Windows now delegates to SafeProcessHandle.Kill() after obtaining the handle, eliminating the duplicated TerminateProcess + error-handling logic.

Interop (Interop.Kill.cs)

  • Removed the Signals managed enum; Interop.Sys.Kill now takes a plain int signal parameter (platform-native number).
  • Added Interop.Sys.GetPlatformSIGSTOP() P/Invoke.
  • All existing callers updated: Process.Unix.cs uses GetPlatformSignalNumber(PosixSignal.SIGKILL) and GetPlatformSIGSTOP(); ProcessWaitState.Unix.cs and ProcessManager.Unix.cs pass 0 directly for the "probe" call.

Project / interop

  • Added Interop.PosixSignal.cs to the Unix ItemGroup in System.Diagnostics.Process.csproj to expose GetPlatformSignalNumber in the main assembly.

Tests

  • SafeProcessHandleTests.cs: Invalid handle → InvalidOperationException; Kill / Signal(SIGKILL) on running process terminates it; Kill on exited process does not throw; Signal(SIGKILL) returns false on exited process; Windows: non-SIGKILL signal → PlatformNotSupportedException; Unix: Signal(SIGTERM) on running process returns true and terminates it; Windows: Kill_HandleWithoutTerminatePermission_ThrowsWin32Exception — opens a handle with only PROCESS_QUERY_LIMITED_INFORMATION and verifies Kill() throws Win32Exception.
  • ProcessTests.Unix.cs: Updated ChildProcess_WithParentSignalHandler_CanReceiveSignals to use SafeProcessHandle.Signal instead of the raw SendSignal helper.
  • ProcessTests.Unix.cs / ProcessTests.Windows.cs: Refactored SendSignal helper to accept a Process parameter instead of a raw int PID — Windows continues to use process.Id for GenerateConsoleCtrlEvent, while Unix now calls process.SafeHandle.Signal(signal) directly (simplified to a single-line Assert.True). All call sites in ProcessTests.cs updated accordingly.
  • ProcessTests.Unix.cs: Removed the class-level SIGKILL constant and kill P/Invoke declaration; Kill_ExitedNonChildProcess_DoesNotThrow now uses SafeHandle.Signal(PosixSignal.SIGKILL) to kill the process externally and Process.HasExited to poll for reaping.

Copilot AI linked an issue Mar 30, 2026 that may be closed by this pull request
@github-actions github-actions bot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Mar 30, 2026
@adamsitnik adamsitnik added area-System.Diagnostics.Process and removed area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI labels Mar 30, 2026
@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.

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/f1233ff0-2352-488d-ae71-9947c67f09c6

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Copilot AI changed the title [WIP] Add SafeProcessHandle.Kill and Signal methods Implement SafeProcessHandle.Kill and Signal Mar 30, 2026
Copilot AI requested a review from adamsitnik March 30, 2026 13:50
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, also do search for other places in the repo where the new Signal API could be used

…r handling

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/33138282-b24e-4a35-b495-385af456c851

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

Copilot AI commented Mar 30, 2026

@copilot address my feedback, also do search for other places in the repo where the new Signal API could be used

All feedback addressed in 7c03c8c. For other usages: the main candidate in the codebase is the SendSignal helper in tests (ProcessTests.cs:173, :246, :256, :260) which sends signals via kill() directly. However, those tests operate against a Process instance using its Id, while our Signal API requires a SafeProcessHandle — on Unix the handle's ProcessId is only set when the process was started via SafeProcessHandle.Start (or Process.Start). The ChildProcess_WithParentSignalHandler_CanReceiveSignals test was the natural fit since it already uses RemoteInvokeHandle with a started Process, so Process.SafeHandle.Signal() works correctly there.

Copilot AI requested a review from adamsitnik March 30, 2026 14:07
@adamsitnik adamsitnik marked this pull request as ready for review March 30, 2026 14:14
Copilot AI review requested due to automatic review settings March 30, 2026 14:14
@adamsitnik adamsitnik added this to the 11.0.0 milestone Mar 30, 2026
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

Adds process-termination and signal-sending capabilities directly to SafeProcessHandle, enabling callers to manage processes via handles without going through Process, with platform-specific implementations for Windows and Unix plus corresponding test updates.

Changes:

  • Added SafeProcessHandle.Kill() and SafeProcessHandle.Signal(PosixSignal) public APIs (with ref-assembly updates).
  • Implemented SignalCore on Windows (via TerminateProcess) and Unix (via kill(2) + GetPlatformSignalNumber).
  • Added/updated tests to validate new APIs and switched an existing Unix test to use SafeProcessHandle.Signal.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs Introduces the new public Kill/Signal APIs and validation path.
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs Implements Windows SignalCore mapping SIGKILL to TerminateProcess with exited-process detection.
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs Implements Unix SignalCore using GetPlatformSignalNumber + kill, returning false on ESRCH.
src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj Adds Interop.PosixSignal.cs to the Unix compile item group.
src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs Updates the public ref surface for SafeProcessHandle with Kill/Signal.
src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs Adds unit tests covering invalid-handle, running-process, exited-process, and platform-specific behaviors.
src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs Updates signal-delivery test to use SafeProcessHandle.Signal instead of a raw P/Invoke helper.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #126313

Note

This review was generated by Copilot. Multi-model review: Claude Opus 4.6 (primary), Claude Sonnet 4.5, GPT-5.4.

Holistic Assessment

Motivation: Justified. This is part of the approved API expansion (#125838, api-approved by @bartonjs) to build lower-level process management on SafeProcessHandle. Following the merged Start/ProcessId PR (#126192), Kill and Signal are the natural next step. The need is real — callers should be able to manage process lifecycle directly through a handle without round-tripping through Process.

Approach: Clean and well-structured. Kill() delegates to SignalCore(PosixSignal.SIGKILL) — no code duplication between Kill/Signal or between platforms. The platform implementations closely follow existing Process.Kill() patterns (ESRCH handling on Unix, ERROR_ACCESS_DENIED + GetExitCodeProcess on Windows). Review feedback from @adamsitnik has been addressed (error code handling, doc comments, removed duplicate KillCore, updated existing test).

Summary: ⚠️ Needs Human Review. The code is well-implemented and matches the approved API shape exactly. One concern worth human judgment is the PID reuse race on Unix — SafeProcessHandle.Kill/Signal lacks the best-effort exit-state check that Process.Kill() has — though even Process.Kill() is still subject to a TOCTOU race. All other findings are minor or pre-existing.


Detailed Findings

✅ API Approval — Matches approved shape exactly

The approved API shape from #125838 (approved by @bartonjs on 2026-03-24):

public partial class SafeProcessHandle
{
    public void Kill();
    public bool Signal(PosixSignal signal);
}

Implementation matches exactly: Kill() is void, Signal() returns bool, parameter name is signal. The [UnsupportedOSPlatform("ios")] / [UnsupportedOSPlatform("tvos")] / [SupportedOSPlatform("maccatalyst")] annotations are consistent with the existing Start method on the same type.

✅ Implementation quality — Follows established patterns

  • Unix: Correctly reuses Interop.Sys.GetPlatformSignalNumber (same PAL as PosixSignalRegistration.Create). ESRCH → return false, unsupported signal (returns 0) → PlatformNotSupportedException. Error code passed explicitly to Win32Exception(errorInfo.RawErrno) as requested in review.
  • Windows: Only SIGKILL supported (maps to TerminateProcess), other signals → PlatformNotSupportedException. Already-exited detection via ERROR_ACCESS_DENIED + GetExitCodeProcess != STILL_ACTIVE matches Process.Kill() exactly. Marshal.GetLastWin32Error() captured immediately before any checks.
  • Shared: Kill() delegates to SignalCore(SIGKILL) — single code path, no duplication.

✅ Tests — Comprehensive coverage

Tests cover: invalid handle validation (Kill, Signal), kill/signal running process, kill/signal already-exited process, Windows non-SIGKILL throws PNSE, Unix SIGTERM on running process, and the existing ChildProcess_WithParentSignalHandler_CanReceiveSignals test was updated to use the new API. Good use of [PlatformSpecific] and [SkipOnPlatform] for platform-conditional tests.

⚠️ PID reuse race on Unix — Needs human judgment

(Flagged by GPT-5.4, verified by primary reviewer)

Process.Kill() on Unix has a best-effort check — GetHasExited(refresh: false) — before calling kill(), to reduce the window for PID reuse attacks. SafeProcessHandle.SignalCore() goes directly to kill(ProcessId, ...). If the original process exited and the PID was recycled, the signal could be delivered to an unrelated process.

Mitigating factors:

  • Process.Kill()'s check is also not bulletproof — there's still a TOCTOU race between GetHasExited and kill().
  • SafeProcessHandle doesn't have access to ProcessWaitState to perform this check (the Holder is disposed in StartCore).
  • PID reuse in practice is rare on modern Linux (PIDs increment before wrapping).
  • The long-term fix is pidfd support, which the code already comments on: "On Unix, we don't use process descriptors yet."
  • This is a lower-level API where callers accept more responsibility.

Recommendation: This is probably acceptable for a lower-level handle API, but a human reviewer should explicitly confirm this is an accepted tradeoff. Consider documenting it in the <remarks> (e.g., "On Unix, if the process has exited and the PID has been reused, the signal may be delivered to an unrelated process").

💡 Undocumented limitation for handles from public constructor (pre-existing, follow-up)

On Unix, calling Kill()/Signal() on a SafeProcessHandle created via the SafeProcessHandle(IntPtr, bool) constructor will throw PlatformNotSupportedException from ProcessId.getGetProcessIdCore(). This is a pre-existing limitation from #126192, not introduced by this PR. Still, Kill/Signal makes it more likely users hit this path. Consider documenting in the <remarks> that these methods require a handle obtained from Start() or Process.SafeHandle.

💡 Ref assembly member ordering — Very minor

The ref assembly places Kill() and Signal() before Start(). The convention is typically alphabetical ordering. Current order: Kill, Signal, Start — this happens to be alphabetical, so this is fine ✅.

Generated by Code Review for issue #126313 ·

@jkotas
Copy link
Copy Markdown
Member

jkotas commented Mar 30, 2026

@copilot

… in PROCESS_TERMINATE remarks

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/2db8e8e4-2b00-4c1a-990d-9e1137a16472

Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com>
Copilot AI requested a review from jkotas March 30, 2026 16:15
@adamsitnik adamsitnik enabled auto-merge (squash) March 30, 2026 16:29
@adamsitnik
Copy link
Copy Markdown
Member

@copilot fix following test failure:

/__w/1/s/src/native/libs/System.Native/pal_process.c (716): error 18: Unknown signal (false failed)
dotnet: /__w/1/s/src/native/libs/System.Native/pal_process.c:716: int32_t SystemNative_Kill(int32_t, int32_t): Assertion `false && "assert_msg failed"' failed.

I believe you have to update this method:

int32_t SystemNative_Kill(int32_t pid, int32_t signal)
{
switch (signal)
{
case PAL_NONE:
signal = 0;
break;
case PAL_SIGKILL:
signal = SIGKILL;
break;
case PAL_SIGSTOP:
signal = SIGSTOP;
break;
default:
assert_msg(false, "Unknown signal", signal);
errno = EINVAL;
return -1;
}
return kill(pid, signal);

You may need to use SystemNative_GetPlatformSignalNumber as I wrote in the requirements for this PR..

…impl detail from Kill() docs

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/a787bbc7-33f7-49aa-8dff-ad37c2cb68c3

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

Copilot AI commented Mar 30, 2026

@copilot fix following test failure:

...

Fixed in 1815c43.

The root cause was that SafeProcessHandle.Unix.cs was passing the native signal number (e.g., 15 for SIGTERM) to SystemNative_Kill, which only accepted PAL_NONE=0, PAL_SIGKILL=9, PAL_SIGSTOP=19 via a switch — anything else hit assert(false).

Changes made:

  1. Added #include "pal_signal.h" to pal_process.c to expose SystemNative_GetPlatformSignalNumber.
  2. Updated SystemNative_Kill to handle negative values (managed PosixSignal enum) by calling SystemNative_GetPlatformSignalNumber, while keeping the existing switch for backward-compatible non-negative PAL callers.
  3. Updated SafeProcessHandle.Unix.cs to pass the managed PosixSignal value directly (negative int) to Interop.Sys.Kill, letting the native side do the conversion.

return -1;
// Managed PosixSignal enum uses negative values for named signals.
// Convert to the native signal number.
signal = SystemNative_GetPlatformSignalNumber((PosixSignal)signal);
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 Change SystemNative_Kill to take the platform native signal number. It is consistent other System.Native APIs (e.g.SystemNative_EnablePosixSignalHandling) and it avoids mapping the managed PosixSignal enum to the native signal twice.

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.

Done in bddd72b.

SystemNative_Kill now just calls kill(pid, signal) directly — no PAL mapping switch. Changes:

  • SafeProcessHandle.Unix.cs calls GetPlatformSignalNumber(signal) once to get the native number and passes it directly to Kill (no double mapping)
  • Process.Unix.cs callers updated: SIGKILL via GetPlatformSignalNumber(PosixSignal.SIGKILL), SIGSTOP via a new SystemNative_GetPlatformSIGSTOP() export (needed since SIGSTOP is Linux=19, macOS=17 and is not in the managed PosixSignal enum)
  • ProcessWaitState.Unix.cs and ProcessManager.Unix.cs now pass 0 directly for the "probe" call
  • The Signals PAL enum removed from pal_process.h; Interop.Sys.Kill now takes int with no enum wrapper

}

// Pass the managed PosixSignal value (negative) directly; SystemNative_Kill handles the conversion.
int killResult = Interop.Sys.Kill(ProcessId, (Interop.Sys.Signals)(int)signal);
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 Delete Interop.Sys.Signals enum. It should not be needed once you address the other feedback

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.

Done in bddd72bInterop.Sys.Signals enum deleted. Interop.Sys.Kill now takes a plain int (platform-native signal number) directly.

…date all callers

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/d6014d25-6dbe-476e-8aa9-3d6c82c86b05

Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com>
Copilot AI requested a review from jkotas March 30, 2026 21:07
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.

Implement SafeProcessHandle.Kill and Signal

5 participants