Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ internal static unsafe int ForkAndExecProcess(
string filename, string[] argv, IDictionary<string, string?> 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;
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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);

/// <summary>
/// Allocates a single native memory block containing both a null-terminated pointer array
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand All @@ -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)
Expand Down Expand Up @@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,40 @@ public SafeProcessHandle(IntPtr existingHandle, bool ownsHandle)
SetHandle(existingHandle);
}

/// <summary>
/// Opens an existing process by its process ID.
/// </summary>
/// <param name="processId">The process ID of the process to open.</param>
/// <returns>A <see cref="SafeProcessHandle"/> that represents the opened process.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="processId"/> is negative or zero.</exception>
/// <exception cref="Win32Exception">Thrown when the process could not be opened.</exception>
/// <remarks>
/// <para>
/// On Windows, this method uses OpenProcess with PROCESS_QUERY_LIMITED_INFORMATION, SYNCHRONIZE, and PROCESS_TERMINATE permissions.
/// </para>
/// <para>
/// On Linux with pidfd support, this method uses the pidfd_open syscall.
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
[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);
}

/// <summary>
/// Starts a process using the specified <see cref="ProcessStartInfo"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,8 @@
Link="Common\Interop\Unix\Interop.InitializeTerminalAndSignalHandling.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Kill.cs"
Link="Common\Interop\Unix\Interop.Kill.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.OpenProcess.cs"
Link="Common\Interop\Unix\Interop.OpenProcess.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.PosixSignal.cs"
Link="Common\Interop\Unix\Interop.PosixSignal.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.ReadLink.cs"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,60 @@ public void Kill_HandleWithoutTerminatePermission_ThrowsWin32Exception()
}
}

[Theory]
[InlineData(0)]
[InlineData(-1)]
public void Open_InvalidProcessId_ThrowsArgumentOutOfRangeException(int processId)
{
Assert.Throws<ArgumentOutOfRangeException>(() => SafeProcessHandle.Open(processId));
}

[Fact]
public void Open_NonExistentProcessId_ThrowsWin32Exception()
{
// Use an unlikely process ID that should not exist.
Assert.Throws<Win32Exception>(() => 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)]
Expand Down
1 change: 1 addition & 0 deletions src/native/libs/System.Native/entrypoints.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
63 changes: 60 additions & 3 deletions src/native/libs/System.Native/pal_process.c
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,23 @@
# define __NR_close_range 436
# endif
#endif // !defined(__NR_close_range)
#else // HAVE_CLOSE_RANGE
#include <sys/syscall.h>
#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)
Expand Down Expand Up @@ -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);
Comment on lines +352 to +353
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 is SYS_pidfd_open available on Android as well? If so, we should do it for both platforms

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.

Yes, SYS_pidfd_open is available on Android. The __linux__ macro is defined on Android, so both the syscall number definitions (lines 43-56) and the TryOpenPidfd function body (line 352) already cover Android — no change needed.

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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);
}
14 changes: 13 additions & 1 deletion src/native/libs/System.Native/pal_process.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Loading
Loading