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 d33e60fa426eb6..7f2622b6d49c19 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,7 @@ 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, SafeHandle[]? inheritedHandles = null) + bool startDetached, bool killOnParentExit, SafeHandle[]? inheritedHandles = null) { byte** argvPtr = null, envpPtr = null; int result = -1; @@ -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(); } @@ -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); /// /// Allocates a single native memory block containing both a null-terminated pointer array 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 0907c2dc7b69f1..a170891ae0e55a 100644 --- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs +++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs @@ -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? 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")] 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 0b48b5e9d1e191..748cfcf6fe54c7 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 @@ -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) { diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs index 11ff3f54fe152e..570c102086dbb9 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs @@ -281,8 +281,12 @@ public string Arguments /// On Windows, this is implemented using Job Objects with the /// JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE flag. /// + /// + /// On Linux, this is implemented using prctl(PR_SET_PDEATHSIG). + /// /// /// to terminate the child process when the parent exits; otherwise, . The default is . + [SupportedOSPlatform("linux")] [SupportedOSPlatform("windows")] public bool KillOnParentExit { get; set; } diff --git a/src/libraries/System.Diagnostics.Process/tests/KillOnParentExitTests.cs b/src/libraries/System.Diagnostics.Process/tests/KillOnParentExitTests.cs index 34a100e7e80bbf..ba4e418ddad3a6 100644 --- a/src/libraries/System.Diagnostics.Process/tests/KillOnParentExitTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/KillOnParentExitTests.cs @@ -7,7 +7,7 @@ namespace System.Diagnostics.Tests { - [PlatformSpecific(TestPlatforms.Windows)] + [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.Linux)] public class KillOnParentExitTests : ProcessTestBase { [Fact] diff --git a/src/native/libs/Common/pal_config.h.in b/src/native/libs/Common/pal_config.h.in index 58f883951f0397..c960a4cfbb9f24 100644 --- a/src/native/libs/Common/pal_config.h.in +++ b/src/native/libs/Common/pal_config.h.in @@ -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 diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index bc2a713849c1b7..5231128c288efa 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -44,6 +44,11 @@ #endif #include +#if HAVE_PR_SET_PDEATHSIG +#include +#include +#endif + #if HAVE_SCHED_SETAFFINITY || HAVE_SCHED_GETAFFINITY #include #endif @@ -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 + 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[], @@ -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 @@ -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; @@ -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 @@ -752,6 +1012,7 @@ done:; (void)inheritedFds; (void)inheritedFdCount; (void)startDetached; + (void)applyPDeathSig; return -1; #endif } diff --git a/src/native/libs/System.Native/pal_process.h b/src/native/libs/System.Native/pal_process.h index abcc7d87439cd7..1bf5ef32f04bff 100644 --- a/src/native/libs/System.Native/pal_process.h +++ b/src/native/libs/System.Native/pal_process.h @@ -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. diff --git a/src/native/libs/System.Native/pal_process_wasi.c b/src/native/libs/System.Native/pal_process_wasi.c index 1ad9f888bbc04c..4aeb370054e856 100644 --- a/src/native/libs/System.Native/pal_process_wasi.c +++ b/src/native/libs/System.Native/pal_process_wasi.c @@ -32,7 +32,8 @@ 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) { return -1; } diff --git a/src/native/libs/configure.cmake b/src/native/libs/configure.cmake index ba504df4834918..87a37cca4af0b3 100644 --- a/src/native/libs/configure.cmake +++ b/src/native/libs/configure.cmake @@ -196,6 +196,11 @@ check_symbol_exists( unistd.h HAVE_VFORK) +check_symbol_exists( + PR_SET_PDEATHSIG + "sys/prctl.h" + HAVE_PR_SET_PDEATHSIG) + check_symbol_exists( pipe unistd.h