diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.cs index 7f2622b6d49c19..30bb10037e90c6 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.cs @@ -17,7 +17,8 @@ internal static unsafe int ForkAndExecProcess( string filename, string[] argv, IDictionary env, string? cwd, bool setUser, uint userId, uint groupId, uint[]? groups, out int lpChildPid, SafeFileHandle? stdinFd, SafeFileHandle? stdoutFd, SafeFileHandle? stderrFd, - bool startDetached, bool killOnParentExit, SafeHandle[]? inheritedHandles = null) + bool startDetached, bool killOnParentExit, SafeHandle[]? inheritedHandles, + out int lpPidfd) { byte** argvPtr = null, envpPtr = null; int result = -1; @@ -76,7 +77,7 @@ internal static unsafe int ForkAndExecProcess( filename, argvPtr, envpPtr, cwd, setUser ? 1 : 0, userId, groupId, pGroups, groups?.Length ?? 0, out lpChildPid, stdinRawFd, stdoutRawFd, stderrRawFd, - pInheritedFds, inheritedFdCount, startDetached ? 1 : 0, killOnParentExit ? 1 : 0); + pInheritedFds, inheritedFdCount, startDetached ? 1 : 0, killOnParentExit ? 1 : 0, out lpPidfd); } return result == 0 ? 0 : Marshal.GetLastPInvokeError(); } @@ -105,7 +106,7 @@ private static unsafe partial int ForkAndExecProcess( string filename, byte** argv, byte** envp, string? cwd, int setUser, uint userId, uint groupId, uint* groups, int groupsLength, out int lpChildPid, int stdinFd, int stdoutFd, int stderrFd, - int* inheritedFds, int inheritedFdCount, int startDetached, int killOnParentExit); + int* inheritedFds, int inheritedFdCount, int startDetached, int killOnParentExit, out int outPidfd); /// /// Allocates a single native memory block containing both a null-terminated pointer array diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.OpenProcess.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.OpenProcess.cs new file mode 100644 index 00000000000000..bb94fc51bacd87 --- /dev/null +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.OpenProcess.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Sys + { + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_OpenProcess", SetLastError = true)] + internal static partial int OpenProcess(int processId, out int pidfd); + } +} diff --git a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs index 1294bc9accf1c0..6a0abcb60fe39d 100644 --- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs +++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs @@ -23,6 +23,10 @@ public void Kill() { } [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")] [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")] [System.Runtime.Versioning.SupportedOSPlatformAttribute("maccatalyst")] + public static Microsoft.Win32.SafeHandles.SafeProcessHandle Open(int processId) { throw null; } + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")] + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")] + [System.Runtime.Versioning.SupportedOSPlatformAttribute("maccatalyst")] public static Microsoft.Win32.SafeHandles.SafeProcessHandle Start(System.Diagnostics.ProcessStartInfo startInfo) { throw null; } public bool TryWaitForExit(System.TimeSpan timeout, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Diagnostics.ProcessExitStatus? exitStatus) { throw null; } public System.Diagnostics.ProcessExitStatus WaitForExit() { throw null; } diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs index 580c921aa243ec..90f74eed6c6efc 100644 --- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs @@ -28,6 +28,7 @@ public sealed partial class SafeProcessHandle : SafeHandleZeroOrMinusOneIsInvali private readonly SafeWaitHandle? _handle; private readonly bool _releaseRef; + private int _pidfd = -1; private readonly ProcessWaitState.Holder? _waitStateHolder; internal SafeProcessHandle(ProcessWaitState.Holder waitStateHolder) : base(ownsHandle: true) @@ -71,10 +72,32 @@ protected override bool ReleaseHandle() Debug.Assert(_handle != null); _handle.DangerousRelease(); } + + if (_pidfd >= 0) + { + Interop.Sys.Close(_pidfd); + } + _waitStateHolder?.Dispose(); return true; } + private static SafeProcessHandle OpenCore(int processId) + { + int result = Interop.Sys.OpenProcess(processId, out int pidfd); + + if (result == -1) + { + throw new Win32Exception(); + } + + ProcessWaitState.Holder waitStateHolder = new(processId); + SafeProcessHandle processHandle = new SafeProcessHandle(waitStateHolder); + processHandle._pidfd = pidfd != -1 ? pidfd : int.MinValue; + return processHandle; + } + + private bool SignalCore(PosixSignal signal) { if (!ProcessUtils.PlatformSupportsProcessStartAndKill) @@ -306,6 +329,7 @@ private static SafeProcessHandle ForkAndExecProcess( } int childPid, errno; + int pidfd = -1; // Lock to avoid races with OnSigChild // By using a ReaderWriterLock we allow multiple processes to start concurrently. @@ -327,7 +351,7 @@ private static SafeProcessHandle ForkAndExecProcess( setCredentials, userId, groupId, groups, out childPid, stdinHandle, stdoutHandle, stderrHandle, #pragma warning disable CA1416 // KillOnParentExit getter works on all platforms; the native shim is a no-op where unsupported - startInfo.StartDetached, startInfo.KillOnParentExit, inheritedHandles); + startInfo.StartDetached, startInfo.KillOnParentExit, inheritedHandles, out pidfd); #pragma warning restore CA1416 if (errno == 0) @@ -365,7 +389,9 @@ private static SafeProcessHandle ForkAndExecProcess( throw ProcessUtils.CreateExceptionForErrorStartingProcess(new Interop.ErrorInfo(errno).GetErrorMessage(), errno, resolvedFilename, cwd); } - return new SafeProcessHandle(waitStateHolder!); + SafeProcessHandle processHandle = new SafeProcessHandle(waitStateHolder!); + processHandle._pidfd = pidfd; + return processHandle; } } } diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs index 6410a78817e170..60679088bb156f 100644 --- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs @@ -51,6 +51,25 @@ protected override bool ReleaseHandle() return Interop.Kernel32.CloseHandle(handle); } + private static SafeProcessHandle OpenCore(int processId) + { + const int desiredAccess = Interop.Advapi32.ProcessOptions.PROCESS_QUERY_LIMITED_INFORMATION + | Interop.Advapi32.ProcessOptions.SYNCHRONIZE + | Interop.Advapi32.ProcessOptions.PROCESS_TERMINATE; + + SafeProcessHandle safeHandle = Interop.Kernel32.OpenProcess(desiredAccess, inherit: false, processId); + + if (safeHandle.IsInvalid) + { + int error = Marshal.GetLastPInvokeError(); + safeHandle.Dispose(); + throw new Win32Exception(error); + } + + safeHandle.ProcessId = processId; + return safeHandle; + } + private static unsafe Interop.Kernel32.SafeJobHandle CreateKillOnParentExitJob() { Interop.Kernel32.JOBOBJECT_EXTENDED_LIMIT_INFORMATION limitInfo = default; diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs index 1ea7ecfd94cbd0..499bf7d5e237e1 100644 --- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs +++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs @@ -56,6 +56,40 @@ public SafeProcessHandle(IntPtr existingHandle, bool ownsHandle) SetHandle(existingHandle); } + /// + /// Opens an existing process by its process ID. + /// + /// The process ID of the process to open. + /// A that represents the opened process. + /// Thrown when is negative or zero. + /// Thrown when the process could not be opened. + /// + /// + /// On Windows, this method uses OpenProcess with PROCESS_QUERY_LIMITED_INFORMATION, SYNCHRONIZE, and PROCESS_TERMINATE permissions. + /// + /// + /// On Linux with pidfd support, this method uses the pidfd_open syscall. + /// + /// + /// On other Unix systems, this method uses kill(pid, 0) to verify the process exists and the caller has permission to signal it. + /// If it's not a child process of the current process, the returned handle is prone to process ID reuse issues in this case. + /// + /// + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] + public static SafeProcessHandle Open(int processId) + { + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(processId, 0); + + if (!ProcessUtils.PlatformSupportsProcessStartAndKill) + { + throw new PlatformNotSupportedException(); + } + + return OpenCore(processId); + } + /// /// Starts a process using the specified . /// diff --git a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj index bc1e3b89bb10de..11bc5af8104f3b 100644 --- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj @@ -288,6 +288,8 @@ Link="Common\Interop\Unix\Interop.InitializeTerminalAndSignalHandling.cs" /> + (() => SafeProcessHandle.Open(processId)); + } + + [Fact] + public void Open_NonExistentProcessId_ThrowsWin32Exception() + { + // Use an unlikely process ID that should not exist. + Assert.Throws(() => SafeProcessHandle.Open(int.MaxValue)); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void Open_RunningProcess_ReturnsValidHandle() + { + using Process process = CreateProcess(static () => + { + Thread.Sleep(Timeout.Infinite); + return RemoteExecutor.SuccessExitCode; + }); + process.Start(); + + try + { + using SafeProcessHandle handle = SafeProcessHandle.Open(process.Id); + Assert.False(handle.IsInvalid); + Assert.Equal(process.Id, handle.ProcessId); + } + finally + { + process.Kill(); + process.WaitForExit(); + } + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void Open_ThenKill_TerminatesProcess() + { + using Process process = CreateProcess(static () => + { + Thread.Sleep(Timeout.Infinite); + return RemoteExecutor.SuccessExitCode; + }); + process.Start(); + + using SafeProcessHandle handle = SafeProcessHandle.Open(process.Id); + handle.Kill(); + + Assert.True(handle.TryWaitForExit(TimeSpan.FromMilliseconds(WaitInMS), out _)); + } + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] [InlineData(true)] [InlineData(false)] diff --git a/src/native/libs/System.Native/entrypoints.c b/src/native/libs/System.Native/entrypoints.c index 34131dee0312e5..30448488251947 100644 --- a/src/native/libs/System.Native/entrypoints.c +++ b/src/native/libs/System.Native/entrypoints.c @@ -229,6 +229,7 @@ static const Entry s_sysNative[] = DllImportEntry(SystemNative_SchedSetAffinity) DllImportEntry(SystemNative_SchedGetAffinity) DllImportEntry(SystemNative_GetProcessPath) + DllImportEntry(SystemNative_OpenProcess) DllImportEntry(SystemNative_GetNonCryptographicallySecureRandomBytes) DllImportEntry(SystemNative_GetCryptographicallySecureRandomBytes) DllImportEntry(SystemNative_GetUnixRelease) diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index 51cc809e1f7d6a..00c4214432fa6a 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -37,7 +37,23 @@ # define __NR_close_range 436 # endif #endif // !defined(__NR_close_range) +#else // HAVE_CLOSE_RANGE +#include #endif // !defined(HAVE_CLOSE_RANGE) +#if !defined(SYS_pidfd_open) && !defined(__NR_pidfd_open) +// pidfd_open was added in Linux 5.3. The syscall number is 434 for all +// architectures using the generic syscall table (asm-generic/unistd.h), +// which covers aarch64, riscv, s390x, ppc64le, and others. The exception +// is alpha, which has its own syscall table and uses 544 instead. +# if defined(__alpha__) +# define __NR_pidfd_open 544 +# else +# define __NR_pidfd_open 434 +# endif +#endif // !defined(SYS_pidfd_open) && !defined(__NR_pidfd_open) +#if !defined(SYS_pidfd_open) && defined(__NR_pidfd_open) +#define SYS_pidfd_open __NR_pidfd_open +#endif #endif // defined(__linux__) #if (HAVE_CLOSE_RANGE || defined(__NR_close_range)) && !defined(CLOSE_RANGE_CLOEXEC) #define CLOSE_RANGE_CLOEXEC (1U << 2) @@ -329,6 +345,22 @@ static void RestrictHandleInheritance(int32_t* inheritedFds, int32_t inheritedFd } } +// Attempts to open a pidfd for the given pid using pidfd_open. +// Returns the pidfd on success, or -1 if pidfd is not available. +static int32_t TryOpenPidfd(int32_t pid) +{ +#if defined(__linux__) + int pidfd = (int)syscall(SYS_pidfd_open, pid, 0); + if (pidfd >= 0) + { + return pidfd; + } +#else + (void)pid; +#endif + return -1; +} + // Forward declaration of the internal fork+exec function static int32_t ForkAndExecProcessInternal( const char* filename, char* const argv[], char* const envp[], const char* cwd, @@ -535,26 +567,40 @@ int32_t SystemNative_ForkAndExecProcess(const char* filename, int32_t* inheritedFds, int32_t inheritedFdCount, int32_t startDetached, - int32_t killOnParentExit) + int32_t killOnParentExit, + int32_t* outPidfd) { + assert(outPidfd != NULL); + *outPidfd = -1; + #if HAVE_PR_SET_PDEATHSIG if (killOnParentExit) { - return ForkAndExecOnPDeathSigThread( + int32_t result = ForkAndExecOnPDeathSigThread( filename, argv, envp, cwd, setCredentials, userId, groupId, groups, groupsLength, childPid, stdinFd, stdoutFd, stderrFd, inheritedFds, inheritedFdCount, startDetached); + if (result == 0 && *childPid > 0) + { + *outPidfd = TryOpenPidfd(*childPid); + } + return result; } #else (void)killOnParentExit; #endif - return ForkAndExecProcessInternal( + int32_t result = ForkAndExecProcessInternal( filename, argv, envp, cwd, setCredentials, userId, groupId, groups, groupsLength, childPid, stdinFd, stdoutFd, stderrFd, inheritedFds, inheritedFdCount, startDetached, 0); + if (result == 0 && *childPid > 0) + { + *outPidfd = TryOpenPidfd(*childPid); + } + return result; } static int32_t ForkAndExecProcessInternal( @@ -1375,3 +1421,14 @@ char* SystemNative_GetProcessPath(void) { return minipal_getexepath(); } + +int32_t SystemNative_OpenProcess(int32_t pid, int32_t* out_pidfd) +{ + *out_pidfd = TryOpenPidfd(pid); + if (*out_pidfd >= 0) + { + return 0; + } + + return kill(pid, 0); +} diff --git a/src/native/libs/System.Native/pal_process.h b/src/native/libs/System.Native/pal_process.h index 1bf5ef32f04bff..08337116063ba5 100644 --- a/src/native/libs/System.Native/pal_process.h +++ b/src/native/libs/System.Native/pal_process.h @@ -35,7 +35,8 @@ PALEXPORT int32_t SystemNative_ForkAndExecProcess( int32_t* inheritedFds, // array of fds to explicitly inherit (-1 to disable restriction) int32_t inheritedFdCount, // count of fds in inheritedFds; -1 means no restriction int32_t startDetached, // whether to start the process as a leader of a new session - int32_t killOnParentExit); // whether to kill the child when the parent exits + int32_t killOnParentExit, // whether to kill the child when the parent exits + int32_t* outPidfd); // [out] the pidfd for the child process (-1 if not available) /************ * The values below in the header are fixed and correct for managed callers to use forever. @@ -242,3 +243,14 @@ PALEXPORT int32_t SystemNative_SchedGetAffinity(int32_t pid, intptr_t* mask); * resolving symbolic links. The caller is responsible for releasing the buffer. */ PALEXPORT char* SystemNative_GetProcessPath(void); + +/** + * Opens a process by its process ID. + * + * On Linux with pidfd support, uses pidfd_open to obtain a process file descriptor. + * On other systems, uses kill(pid, 0) to verify the process exists. + * + * Returns 0 on success; returns -1 on failure and errno is set. + * On success, out_pidfd is set to the pidfd (or -1 if pidfd is not available). + */ +PALEXPORT int32_t SystemNative_OpenProcess(int32_t pid, int32_t* out_pidfd); diff --git a/src/native/libs/System.Native/pal_process_wasi.c b/src/native/libs/System.Native/pal_process_wasi.c index 4aeb370054e856..c3d6f37775e0d1 100644 --- a/src/native/libs/System.Native/pal_process_wasi.c +++ b/src/native/libs/System.Native/pal_process_wasi.c @@ -33,7 +33,8 @@ int32_t SystemNative_ForkAndExecProcess(const char* filename, int32_t* inheritedFds, int32_t inheritedFdCount, int32_t startDetached, - int32_t killOnParentExit) + int32_t killOnParentExit, + int32_t* outPidfd) { return -1; } @@ -126,3 +127,11 @@ char* SystemNative_GetProcessPath(void) { return minipal_getexepath(); } + +int32_t SystemNative_OpenProcess(int32_t pid, int32_t* out_pidfd) +{ + (void)pid; + *out_pidfd = -1; + errno = ENOTSUP; + return -1; +}