Skip to content
Open
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,7 @@ 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, SafeHandle[]? inheritedHandles = null)
bool startDetached, bool killOnParentExit, SafeHandle[]? inheritedHandles = null)
{
byte** argvPtr = null, envpPtr = null;
int result = -1;
Expand Down Expand Up @@ -76,7 +76,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);
pInheritedFds, inheritedFdCount, startDetached ? 1 : 0, killOnParentExit ? 1 : 0);
}
return result == 0 ? 0 : Marshal.GetLastPInvokeError();
}
Expand Down Expand Up @@ -105,7 +105,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* inheritedFds, int inheritedFdCount, int startDetached, int killOnParentExit);

/// <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
Expand Up @@ -318,6 +318,7 @@ public ProcessStartInfo(string fileName, System.Collections.Generic.IEnumerable<
[System.Diagnostics.CodeAnalysis.AllowNullAttribute]
public string FileName { get { throw null; } set { } }
public System.Collections.Generic.IList<System.Runtime.InteropServices.SafeHandle>? InheritedHandles { get { throw null; } set { } }
[System.Runtime.Versioning.SupportedOSPlatformAttribute("linux")]
[System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")]
public bool KillOnParentExit { get { throw null; } set { } }
[System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ private static SafeProcessHandle ForkAndExecProcess(
resolvedFilename, argv, env, cwd,
setCredentials, userId, groupId, groups,
out childPid, stdinHandle, stdoutHandle, stderrHandle,
startInfo.StartDetached, inheritedHandles);
startInfo.StartDetached, OperatingSystem.IsLinux() ? startInfo.KillOnParentExit : false, inheritedHandles);

if (errno == 0)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,12 @@ public string Arguments
/// On Windows, this is implemented using Job Objects with the
/// <see href="https://learn.microsoft.com/windows/win32/api/winnt/ns-winnt-jobobject_basic_limit_information">JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE</see> flag.
/// </para>
/// <para>
/// On Linux, this is implemented using <c>prctl(PR_SET_PDEATHSIG)</c>.
/// </para>
/// </remarks>
/// <value><see langword="true"/> to terminate the child process when the parent exits; otherwise, <see langword="false"/>. The default is <see langword="false"/>.</value>
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("windows")]
public bool KillOnParentExit { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace System.Diagnostics.Tests
{
[PlatformSpecific(TestPlatforms.Windows)]
[PlatformSpecific(TestPlatforms.Windows | TestPlatforms.Linux)]
public class KillOnParentExitTests : ProcessTestBase
{
[Fact]
Expand Down
1 change: 1 addition & 0 deletions src/native/libs/Common/pal_config.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#cmakedefine01 HAVE_FORK
#cmakedefine01 HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP
#cmakedefine01 HAVE_VFORK
#cmakedefine01 HAVE_PR_SET_PDEATHSIG
#cmakedefine01 HAVE_CLOSE_RANGE
#cmakedefine01 HAVE_FDWALK
#cmakedefine01 HAVE_CHMOD
Expand Down
263 changes: 262 additions & 1 deletion src/native/libs/System.Native/pal_process.c
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@
#endif
#include <pthread.h>

#if HAVE_PR_SET_PDEATHSIG
#include <sys/prctl.h>
#include <stdatomic.h>
#endif

#if HAVE_SCHED_SETAFFINITY || HAVE_SCHED_GETAFFINITY
#include <sched.h>
#endif
Expand Down Expand Up @@ -325,6 +330,203 @@ static void RestrictHandleInheritance(int32_t* inheritedFds, int32_t inheritedFd
}
}

// Forward declaration of the internal fork+exec function
static int32_t ForkAndExecProcessInternal(
const char* filename, char* const argv[], char* const envp[], const char* cwd,
int32_t setCredentials, uint32_t userId, uint32_t groupId, uint32_t* groups, int32_t groupsLength,
int32_t* childPid, int32_t stdinFd, int32_t stdoutFd, int32_t stderrFd,
int32_t* inheritedFds, int32_t inheritedFdCount, int32_t startDetached, int32_t applyPDeathSig);

#if HAVE_PR_SET_PDEATHSIG
// Dedicated thread infrastructure for PR_SET_PDEATHSIG.
//
// On Linux, PR_SET_PDEATHSIG sends the death signal when the *thread* that called
// prctl exits, not when the process exits. To ensure the signal is sent only when
// the process truly exits, we use a long-lived dedicated thread that:
// 1. Performs fork+exec for each request where killOnParentExit is set.
// 2. Lives for the lifetime of the application.
//
// Because the forking thread does not exit until process exit, children forked from
// it can use PR_SET_PDEATHSIG so the signal is delivered when the process exits.

typedef struct
{
const char* filename;
char* const* argv;
char* const* envp;
const char* cwd;
int32_t setCredentials;
uint32_t userId;
uint32_t groupId;
uint32_t* groups;
int32_t groupsLength;
int32_t stdinFd;
int32_t stdoutFd;
int32_t stderrFd;
int32_t* inheritedFds;
int32_t inheritedFdCount;
int32_t startDetached;

// Output
int32_t childPid;
int32_t result;
int32_t errnoValue;
int32_t done;
} PDeathSigForkRequest;

static pthread_mutex_t s_pdeathsig_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t s_pdeathsig_request_cond = PTHREAD_COND_INITIALIZER;
static pthread_cond_t s_pdeathsig_done_cond = PTHREAD_COND_INITIALIZER;
static PDeathSigForkRequest* s_pdeathsig_request = NULL;
static _Atomic int s_pdeathsig_thread_started = 0;

static void* PDeathSigThreadFunc(void* arg)
{
(void)arg;

pthread_mutex_lock(&s_pdeathsig_mutex);

while (1)
{
// Wait for a fork request
while (s_pdeathsig_request == NULL)
{
pthread_cond_wait(&s_pdeathsig_request_cond, &s_pdeathsig_mutex);
}

PDeathSigForkRequest* req = s_pdeathsig_request;

// Perform the fork+exec with applyPDeathSig=1 so prctl is called after fork in child
int32_t childPid = -1;
req->result = ForkAndExecProcessInternal(
req->filename, req->argv, req->envp, req->cwd,
req->setCredentials, req->userId, req->groupId, req->groups, req->groupsLength,
&childPid, req->stdinFd, req->stdoutFd, req->stderrFd,
req->inheritedFds, req->inheritedFdCount, req->startDetached, 1);
req->childPid = childPid;
req->errnoValue = errno;

// Mark this request as done and clear the global slot.
// Use broadcast because multiple callers may be waiting: the submitter waits for
// its done flag, and other callers wait for the slot to become free.
req->done = 1;
s_pdeathsig_request = NULL;
pthread_cond_broadcast(&s_pdeathsig_done_cond);
}

return NULL;
}

static int EnsurePDeathSigThread(void)
{
// Fast path: check if the thread is already started
if (atomic_load_explicit(&s_pdeathsig_thread_started, memory_order_acquire))
{
return 0;
}

pthread_mutex_lock(&s_pdeathsig_mutex);
if (!atomic_load_explicit(&s_pdeathsig_thread_started, memory_order_acquire))
{
pthread_t thread;
pthread_attr_t attr;
int result = pthread_attr_init(&attr);
if (result != 0)
{
pthread_mutex_unlock(&s_pdeathsig_mutex);
errno = result;
return -1;
}

result = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
if (result != 0)
{
pthread_attr_destroy(&attr);
pthread_mutex_unlock(&s_pdeathsig_mutex);
errno = result;
return -1;
}

result = pthread_create(&thread, &attr, PDeathSigThreadFunc, NULL);
pthread_attr_destroy(&attr);

if (result != 0)
{
pthread_mutex_unlock(&s_pdeathsig_mutex);
errno = result;
return -1;
}

atomic_store_explicit(&s_pdeathsig_thread_started, 1, memory_order_release);
}
pthread_mutex_unlock(&s_pdeathsig_mutex);
return 0;
}

static int32_t ForkAndExecOnPDeathSigThread(
const char* filename, char* const argv[], char* const envp[], const char* cwd,
int32_t setCredentials, uint32_t userId, uint32_t groupId, uint32_t* groups, int32_t groupsLength,
int32_t* childPid, int32_t stdinFd, int32_t stdoutFd, int32_t stderrFd,
int32_t* inheritedFds, int32_t inheritedFdCount, int32_t startDetached)
{
if (EnsurePDeathSigThread() != 0)
{
*childPid = -1;
return -1;
}

PDeathSigForkRequest req;
req.filename = filename;
req.argv = argv;
req.envp = envp;
req.cwd = cwd;
req.setCredentials = setCredentials;
req.userId = userId;
req.groupId = groupId;
req.groups = groups;
req.groupsLength = groupsLength;
req.stdinFd = stdinFd;
req.stdoutFd = stdoutFd;
req.stderrFd = stderrFd;
req.inheritedFds = inheritedFds;
req.inheritedFdCount = inheritedFdCount;
req.startDetached = startDetached;
req.childPid = -1;
req.result = -1;
req.errnoValue = 0;
req.done = 0;

pthread_mutex_lock(&s_pdeathsig_mutex);

// Wait until no other request is being processed. This serializes
// concurrent callers so requests cannot overwrite each other.
while (s_pdeathsig_request != NULL)
{
pthread_cond_wait(&s_pdeathsig_done_cond, &s_pdeathsig_mutex);
}

// Submit request and signal the dedicated thread
Comment thread
adamsitnik marked this conversation as resolved.
assert(s_pdeathsig_request == NULL);
s_pdeathsig_request = &req;
pthread_cond_signal(&s_pdeathsig_request_cond);

// Wait for the dedicated thread to complete OUR fork+exec.
// We check req.done (not s_pdeathsig_request) so that a concurrent
// caller submitting the next request doesn't prevent us from seeing
// that our request has already completed.
while (!req.done)
{
pthread_cond_wait(&s_pdeathsig_done_cond, &s_pdeathsig_mutex);
}

pthread_mutex_unlock(&s_pdeathsig_mutex);

*childPid = req.childPid;
errno = req.errnoValue;
return req.result;
}
#endif // HAVE_PR_SET_PDEATHSIG

int32_t SystemNative_ForkAndExecProcess(const char* filename,
char* const argv[],
char* const envp[],
Expand All @@ -340,12 +542,43 @@ int32_t SystemNative_ForkAndExecProcess(const char* filename,
int32_t stderrFd,
int32_t* inheritedFds,
int32_t inheritedFdCount,
int32_t startDetached)
int32_t startDetached,
int32_t killOnParentExit)
{
#if HAVE_PR_SET_PDEATHSIG
if (killOnParentExit)
{
return ForkAndExecOnPDeathSigThread(
filename, argv, envp, cwd,
setCredentials, userId, groupId, groups, groupsLength,
childPid, stdinFd, stdoutFd, stderrFd,
inheritedFds, inheritedFdCount, startDetached);
}
#else
(void)killOnParentExit;
#endif

return ForkAndExecProcessInternal(
filename, argv, envp, cwd,
setCredentials, userId, groupId, groups, groupsLength,
childPid, stdinFd, stdoutFd, stderrFd,
inheritedFds, inheritedFdCount, startDetached, 0);
}

static int32_t ForkAndExecProcessInternal(
const char* filename, char* const argv[], char* const envp[], const char* cwd,
int32_t setCredentials, uint32_t userId, uint32_t groupId, uint32_t* groups, int32_t groupsLength,
int32_t* childPid, int32_t stdinFd, int32_t stdoutFd, int32_t stderrFd,
int32_t* inheritedFds, int32_t inheritedFdCount, int32_t startDetached, int32_t applyPDeathSig)
{
#if HAVE_FORK || defined(TARGET_OSX) || defined(TARGET_MACCATALYST)
assert(NULL != filename && NULL != argv && NULL != envp && NULL != childPid &&
(groupsLength == 0 || groups != NULL) && "null argument.");

#if !HAVE_PR_SET_PDEATHSIG
(void)applyPDeathSig;
#endif

*childPid = -1;

// Make sure we can find and access the executable. exec will do this, of course, but at that point it's already
Expand Down Expand Up @@ -513,6 +746,11 @@ int32_t SystemNative_ForkAndExecProcess(const char* filename,
uint32_t* getGroupsBuffer = NULL;
sigset_t signal_set;
sigset_t old_signal_set;
#if HAVE_PR_SET_PDEATHSIG
// Capture the parent PID before fork so the child can verify it hasn't been
// reparented (e.g., to a subreaper) between fork and prctl.
pid_t expectedParentPid = getpid();
#endif

#if HAVE_PTHREAD_SETCANCELSTATE
int thread_cancel_state;
Expand Down Expand Up @@ -670,6 +908,28 @@ int32_t SystemNative_ForkAndExecProcess(const char* filename,
RestrictHandleInheritance(inheritedFds, inheritedFdCount);
}

#if HAVE_PR_SET_PDEATHSIG
if (applyPDeathSig)
{
// Set the parent death signal on the child process. When the parent thread
// that forked this child exits, SIGKILL will be sent to this child.
// We fork from a dedicated long-lived thread to ensure the signal is only
// sent when the process (not an arbitrary thread) exits.
if (prctl(PR_SET_PDEATHSIG, (unsigned long)SIGKILL, 0, 0, 0) == -1)
{
ExitChild(waitForChildToExecPipe[WRITE_END_OF_PIPE], errno);
}

// If the parent died between fork and prctl, this child may already have
// been reparented (for example, to a subreaper in a container). Verify
// that our parent PID is still the original parent we inherited at fork.
if (getppid() != expectedParentPid)
{
ExitChild(waitForChildToExecPipe[WRITE_END_OF_PIPE], ESRCH);
}
}
#endif

// Finally, execute the new process. execve will not return if it's successful.
execve(filename, argv, envp);
ExitChild(waitForChildToExecPipe[WRITE_END_OF_PIPE], errno); // execve failed
Expand Down Expand Up @@ -752,6 +1012,7 @@ done:;
(void)inheritedFds;
(void)inheritedFdCount;
(void)startDetached;
(void)applyPDeathSig;
return -1;
#endif
}
Expand Down
3 changes: 2 additions & 1 deletion src/native/libs/System.Native/pal_process.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ PALEXPORT int32_t SystemNative_ForkAndExecProcess(
int32_t stderrFd, // the fd for the child's stderr
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 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

/************
* The values below in the header are fixed and correct for managed callers to use forever.
Expand Down
Loading
Loading