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,
SafeHandle[]? inheritedHandles = null)
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);
pInheritedFds, inheritedFdCount, 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* inheritedFds, int inheritedFdCount, 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; }
}
}
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 SafeProcessHandle(int processId, ProcessWaitState.Holder waitStateHolder) : base(ownsHandle: true)
{
Expand All @@ -46,19 +47,44 @@ internal SafeProcessHandle(int processId, SafeWaitHandle handle) :
handle.DangerousAddRef(ref _releaseRef);
}

private SafeProcessHandle(int pidfd, int processId) : base(ownsHandle: true)
{
ProcessId = processId;
_pidfd = pidfd;
SetHandle(new IntPtr(pidfd));
}

protected override bool ReleaseHandle()
{
if (_releaseRef)
{
Debug.Assert(_handle != null);
_handle.DangerousRelease();
}

if (_pidfd >= 0)
{
Interop.Sys.Close(_pidfd);
}

return true;
}

// On Unix, we don't use process descriptors yet, so we can't get PID.
private static int GetProcessIdCore() => throw new PlatformNotSupportedException();

private static SafeProcessHandle OpenCore(int processId)
{
int result = Interop.Sys.OpenProcess(processId, out int pidfd);

if (result == -1)
{
throw new Win32Exception();
}

return new SafeProcessHandle(pidfd != -1 ? pidfd : int.MinValue, processId);
}

private bool SignalCore(PosixSignal signal)
{
if (!ProcessUtils.PlatformSupportsProcessStartAndKill)
Expand Down Expand Up @@ -235,6 +261,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 @@ -255,7 +282,7 @@ private static SafeProcessHandle ForkAndExecProcess(
resolvedFilename, argv, env, cwd,
setCredentials, userId, groupId, groups,
out childPid, stdinHandle, stdoutHandle, stderrHandle,
inheritedHandles);
inheritedHandles, out pidfd);

if (errno == 0)
{
Expand Down Expand Up @@ -292,7 +319,9 @@ private static SafeProcessHandle ForkAndExecProcess(
throw ProcessUtils.CreateExceptionForErrorStartingProcess(new Interop.ErrorInfo(errno).GetErrorMessage(), errno, resolvedFilename, cwd);
}

return new SafeProcessHandle(childPid, waitStateHolder!);
SafeProcessHandle processHandle = new SafeProcessHandle(childPid, waitStateHolder!);
processHandle._pidfd = pidfd;
return processHandle;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,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 Func<ProcessStartInfo, SafeProcessHandle>? s_startWithShellExecute;

internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,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 @@ -264,6 +264,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 @@ -249,5 +249,59 @@ public void Kill_HandleWithoutTerminatePermission_ThrowsWin32Exception()
process.WaitForExit();
}
}

[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()
{
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()
{
Process process = CreateProcess(static () =>
{
Thread.Sleep(Timeout.Infinite);
return RemoteExecutor.SuccessExitCode;
});
process.Start();

using SafeProcessHandle handle = SafeProcessHandle.Open(process.Id);
handle.Kill();

Assert.True(process.WaitForExit(WaitInMS));
}
}
}
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 @@ -228,6 +228,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
55 changes: 53 additions & 2 deletions src/native/libs/System.Native/pal_process.c
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,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 @@ -324,6 +340,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;
}

int32_t SystemNative_ForkAndExecProcess(const char* filename,
char* const argv[],
char* const envp[],
Expand All @@ -338,13 +370,15 @@ int32_t SystemNative_ForkAndExecProcess(const char* filename,
int32_t stdoutFd,
int32_t stderrFd,
int32_t* inheritedFds,
int32_t inheritedFdCount)
int32_t inheritedFdCount,
int32_t* outPidfd)
{
#if HAVE_FORK || defined(TARGET_OSX)
assert(NULL != filename && NULL != argv && NULL != envp && NULL != childPid &&
(groupsLength == 0 || groups != NULL) && "null argument.");
outPidfd != NULL && (groupsLength == 0 || groups != NULL) && "null argument.");

*childPid = -1;
*outPidfd = -1;

// Make sure we can find and access the executable. exec will do this, of course, but at that point it's already
// in the child process, at which point it'll translate to the child process' exit code rather than to failing
Expand Down Expand Up @@ -697,6 +731,11 @@ done:;

free(getGroupsBuffer);

if (success)
{
*outPidfd = TryOpenPidfd(*childPid);
}

return success ? 0 : -1;
#else
// ignore unused parameters
Expand All @@ -715,6 +754,7 @@ done:;
(void)stderrFd;
(void)inheritedFds;
(void)inheritedFdCount;
(void)outPidfd;
return -1;
#endif
}
Expand Down Expand Up @@ -1073,3 +1113,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);
}
Loading
Loading