From c879e76d91b79f5b61a49fdeec36480e7c5b0126 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 21:45:27 +0000 Subject: [PATCH 01/11] Add Linux support for ProcessStartInfo.KillOnParentExit using prctl(PR_SET_PDEATHSIG) Implements PR_SET_PDEATHSIG-based KillOnParentExit on Linux using a dedicated long-lived thread to ensure the death signal fires on process exit, not thread exit. Changes: - Add HAVE_PR_SET_PDEATHSIG cmake check and config - Add killOnParentExit parameter to SystemNative_ForkAndExecProcess - Implement dedicated pdeathsig thread with mutex/condvar synchronization - Call prctl(PR_SET_PDEATHSIG, SIGKILL) in child after fork - Pass KillOnParentExit through managed interop layer - Add [SupportedOSPlatform("linux")] to ref and impl - Enable KillOnParentExitTests on Linux Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/85b53068-8457-4ee3-bdbf-4339fc5a007e Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../Interop.ForkAndExecProcess.cs | 6 +- .../ref/System.Diagnostics.Process.cs | 1 + .../SafeHandles/SafeProcessHandle.Unix.cs | 4 +- .../System/Diagnostics/ProcessStartInfo.cs | 4 + .../tests/KillOnParentExitTests.cs | 2 +- src/native/libs/Common/pal_config.h.in | 1 + src/native/libs/System.Native/pal_process.c | 229 +++++++++++++++++- src/native/libs/System.Native/pal_process.h | 3 +- .../libs/System.Native/pal_process_wasi.c | 3 +- src/native/libs/configure.cmake | 5 + 10 files changed, 250 insertions(+), 8 deletions(-) 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 4fa2522868feae..5a2d8b139a9cdf 100644 --- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs +++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs @@ -275,6 +275,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 2a290ea098724e..11976d1b7d4ea6 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 @@ -255,7 +255,9 @@ private static SafeProcessHandle ForkAndExecProcess( resolvedFilename, argv, env, cwd, setCredentials, userId, groupId, groups, out childPid, stdinHandle, stdoutHandle, stderrHandle, - startInfo.StartDetached, inheritedHandles); +#pragma warning disable CA1416 // KillOnParentExit getter works on all platforms; the attribute guards the actual effect + startInfo.StartDetached, startInfo.KillOnParentExit, inheritedHandles); +#pragma warning restore CA1416 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 82c00925143113..22fd752953a7e9 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -43,6 +43,10 @@ #endif #include +#if HAVE_PR_SET_PDEATHSIG +#include +#endif + #if HAVE_SCHED_SETAFFINITY || HAVE_SCHED_GETAFFINITY #include #endif @@ -324,6 +328,176 @@ static void RestrictHandleInheritance(int32_t* inheritedFds, int32_t inheritedFd } } +#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. Calls prctl(PR_SET_PDEATHSIG, SIGKILL) once (this applies to the thread itself). +// 2. Performs fork+exec for each request where killOnParentExit is set. +// 3. Lives for the lifetime of the application. +// +// Because this thread never exits (until process exit), the child processes will +// receive SIGKILL 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; +} PDeathSigForkRequest; + +// 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); + +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 int s_pdeathsig_thread_started = 0; + +static void* PDeathSigThreadFunc(void* arg) +{ + (void)arg; + + // Set PR_SET_PDEATHSIG on this thread so all children forked from it + // will inherit the parent death signal. + prctl(PR_SET_PDEATHSIG, (unsigned long)SIGKILL, 0, 0, 0); + + 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; + + // Signal completion + s_pdeathsig_request = NULL; + pthread_cond_signal(&s_pdeathsig_done_cond); + } +} + +static int EnsurePDeathSigThread(void) +{ + // Double-checked locking: fast path avoids the lock + if (s_pdeathsig_thread_started) + { + return 0; + } + + pthread_mutex_lock(&s_pdeathsig_mutex); + if (!s_pdeathsig_thread_started) + { + pthread_t thread; + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + + int result = pthread_create(&thread, &attr, PDeathSigThreadFunc, NULL); + pthread_attr_destroy(&attr); + + if (result != 0) + { + pthread_mutex_unlock(&s_pdeathsig_mutex); + errno = result; + return -1; + } + + s_pdeathsig_thread_started = 1; + } + 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; + + pthread_mutex_lock(&s_pdeathsig_mutex); + + // Submit request and signal the dedicated thread + s_pdeathsig_request = &req; + pthread_cond_signal(&s_pdeathsig_request_cond); + + // Wait for the dedicated thread to complete the fork+exec + while (s_pdeathsig_request != NULL) + { + 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[], @@ -339,12 +513,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 @@ -669,6 +874,27 @@ 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); + } + + // Check if the parent already died between fork and prctl. + // getppid() returns 1 (init) or the subreaper if the original parent is gone. + if (getppid() == 1) + { + 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 @@ -751,6 +977,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 00bddcd17d78f4..acf77623635295 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 ff32ba0a39bba5..c90f457cecdb16 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 From 362a8b9d1b36ff0a0b4ee325ac157dc2748656a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 21:57:08 +0000 Subject: [PATCH 02/11] Address code review: use atomic for double-checked locking, remove unnecessary prctl in thread, improve getppid comment Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/85b53068-8457-4ee3-bdbf-4339fc5a007e Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- src/native/libs/System.Native/pal_process.c | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index 22fd752953a7e9..93c9b5656ea4b4 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -376,16 +376,12 @@ 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 int s_pdeathsig_thread_started = 0; +static volatile int s_pdeathsig_thread_started = 0; static void* PDeathSigThreadFunc(void* arg) { (void)arg; - // Set PR_SET_PDEATHSIG on this thread so all children forked from it - // will inherit the parent death signal. - prctl(PR_SET_PDEATHSIG, (unsigned long)SIGKILL, 0, 0, 0); - pthread_mutex_lock(&s_pdeathsig_mutex); while (1) @@ -416,8 +412,8 @@ static void* PDeathSigThreadFunc(void* arg) static int EnsurePDeathSigThread(void) { - // Double-checked locking: fast path avoids the lock - if (s_pdeathsig_thread_started) + // Fast path: check if the thread is already started using an atomic load + if (__atomic_load_n(&s_pdeathsig_thread_started, __ATOMIC_ACQUIRE)) { return 0; } @@ -440,7 +436,7 @@ static int EnsurePDeathSigThread(void) return -1; } - s_pdeathsig_thread_started = 1; + __atomic_store_n(&s_pdeathsig_thread_started, 1, __ATOMIC_RELEASE); } pthread_mutex_unlock(&s_pdeathsig_mutex); return 0; @@ -886,8 +882,9 @@ static int32_t ForkAndExecProcessInternal( ExitChild(waitForChildToExecPipe[WRITE_END_OF_PIPE], errno); } - // Check if the parent already died between fork and prctl. - // getppid() returns 1 (init) or the subreaper if the original parent is gone. + // Best-effort check: if the parent already died between fork and prctl, + // getppid() returns 1 (init) or the subreaper's PID. Checking for PID 1 + // covers the common case where no subreaper is configured. if (getppid() == 1) { ExitChild(waitForChildToExecPipe[WRITE_END_OF_PIPE], ESRCH); From dc2712a7250e69e1fe7e2dc71f000675ecc933b3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 11:03:16 +0000 Subject: [PATCH 03/11] Fix GCC -Werror=return-type in PDeathSigThreadFunc Add return NULL after infinite while(1) loop to satisfy GCC's -Werror=return-type, with clang pragma to suppress -Wunreachable-code-return. Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/2659e9ab-8d2c-48d8-a7af-d5356d056e46 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- src/native/libs/System.Native/pal_process.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index 93c9b5656ea4b4..9218aa56fb09cb 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -408,6 +408,15 @@ static void* PDeathSigThreadFunc(void* arg) s_pdeathsig_request = NULL; pthread_cond_signal(&s_pdeathsig_done_cond); } + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunreachable-code-return" +#endif + return NULL; // Unreachable, but required to satisfy -Werror=return-type on GCC +#ifdef __clang__ +#pragma clang diagnostic pop +#endif } static int EnsurePDeathSigThread(void) From 3723cc48bad9f18aea47afa9cbd77262e37c432e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 11:44:38 +0000 Subject: [PATCH 04/11] Fix WASM build and simplify PDeathSigThreadFunc return Move ForkAndExecProcessInternal forward declaration outside #if HAVE_PR_SET_PDEATHSIG so it's visible on all platforms (fixes WASM build). Simplify return NULL in PDeathSigThreadFunc by removing pragma guards. Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/0261d155-fd8a-43c3-80bc-9085dec73b97 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- src/native/libs/System.Native/pal_process.c | 23 +++++++-------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index 9218aa56fb09cb..67937b87a133e9 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -328,6 +328,13 @@ 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. // @@ -365,13 +372,6 @@ typedef struct int32_t errnoValue; } PDeathSigForkRequest; -// 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); - 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; @@ -409,14 +409,7 @@ static void* PDeathSigThreadFunc(void* arg) pthread_cond_signal(&s_pdeathsig_done_cond); } -#ifdef __clang__ -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wunreachable-code-return" -#endif - return NULL; // Unreachable, but required to satisfy -Werror=return-type on GCC -#ifdef __clang__ -#pragma clang diagnostic pop -#endif + return NULL; } static int EnsurePDeathSigThread(void) From 065717f92d2a151a2fbe5e23481158fdb897c04e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 20:40:06 +0000 Subject: [PATCH 05/11] Address review feedback: fix atomics, comment, orphan check, and platform guard - Update comment to match actual mechanism (thread stays alive for fork, no prctl in thread) - Use _Atomic int with atomic_load_explicit/atomic_store_explicit for s_pdeathsig_thread_started - Capture parent PID before fork and verify with getppid() after prctl to detect subreaper reparenting - Use OperatingSystem.IsLinux() guard instead of #pragma warning disable CA1416 Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/ebd5aea5-4038-4fb1-9bd8-839879fe4839 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../SafeHandles/SafeProcessHandle.Unix.cs | 4 +-- src/native/libs/System.Native/pal_process.c | 34 +++++++++++-------- 2 files changed, 21 insertions(+), 17 deletions(-) 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 11976d1b7d4ea6..fb8819014de16e 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 @@ -255,9 +255,7 @@ private static SafeProcessHandle ForkAndExecProcess( resolvedFilename, argv, env, cwd, setCredentials, userId, groupId, groups, out childPid, stdinHandle, stdoutHandle, stderrHandle, -#pragma warning disable CA1416 // KillOnParentExit getter works on all platforms; the attribute guards the actual effect - startInfo.StartDetached, startInfo.KillOnParentExit, inheritedHandles); -#pragma warning restore CA1416 + startInfo.StartDetached, OperatingSystem.IsLinux() ? startInfo.KillOnParentExit : false, inheritedHandles); if (errno == 0) { diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index 67937b87a133e9..a24a60bc6ed684 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -45,6 +45,7 @@ #if HAVE_PR_SET_PDEATHSIG #include +#include #endif #if HAVE_SCHED_SETAFFINITY || HAVE_SCHED_GETAFFINITY @@ -341,12 +342,11 @@ static int32_t ForkAndExecProcessInternal( // 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. Calls prctl(PR_SET_PDEATHSIG, SIGKILL) once (this applies to the thread itself). -// 2. Performs fork+exec for each request where killOnParentExit is set. -// 3. Lives for the lifetime of the application. +// 1. Performs fork+exec for each request where killOnParentExit is set. +// 2. Lives for the lifetime of the application. // -// Because this thread never exits (until process exit), the child processes will -// receive SIGKILL when the process exits. +// 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 { @@ -376,7 +376,7 @@ 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 volatile int s_pdeathsig_thread_started = 0; +static _Atomic int s_pdeathsig_thread_started = 0; static void* PDeathSigThreadFunc(void* arg) { @@ -414,14 +414,14 @@ static void* PDeathSigThreadFunc(void* arg) static int EnsurePDeathSigThread(void) { - // Fast path: check if the thread is already started using an atomic load - if (__atomic_load_n(&s_pdeathsig_thread_started, __ATOMIC_ACQUIRE)) + // 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 (!s_pdeathsig_thread_started) + if (!atomic_load_explicit(&s_pdeathsig_thread_started, memory_order_acquire)) { pthread_t thread; pthread_attr_t attr; @@ -438,7 +438,7 @@ static int EnsurePDeathSigThread(void) return -1; } - __atomic_store_n(&s_pdeathsig_thread_started, 1, __ATOMIC_RELEASE); + atomic_store_explicit(&s_pdeathsig_thread_started, 1, memory_order_release); } pthread_mutex_unlock(&s_pdeathsig_mutex); return 0; @@ -750,6 +750,12 @@ static int32_t ForkAndExecProcessInternal( sigfillset(&signal_set); pthread_sigmask(SIG_SETMASK, &signal_set, &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 = applyPDeathSig ? getpid() : 0; +#endif + // vfork on OS X is deprecated // On Android, signal handlers between parent and child processes are shared with vfork, so when we reset // the signal handlers during child startup, we end up incorrectly clearing also the ones for the parent. @@ -884,10 +890,10 @@ static int32_t ForkAndExecProcessInternal( ExitChild(waitForChildToExecPipe[WRITE_END_OF_PIPE], errno); } - // Best-effort check: if the parent already died between fork and prctl, - // getppid() returns 1 (init) or the subreaper's PID. Checking for PID 1 - // covers the common case where no subreaper is configured. - if (getppid() == 1) + // 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); } From f6b411bd482ac3920033257cd18c0791f473bf8c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 20:41:19 +0000 Subject: [PATCH 06/11] Simplify expectedParentPid: remove unnecessary ternary Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/ebd5aea5-4038-4fb1-9bd8-839879fe4839 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- src/native/libs/System.Native/pal_process.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index a24a60bc6ed684..498efeb524d87d 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -753,7 +753,7 @@ static int32_t ForkAndExecProcessInternal( #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 = applyPDeathSig ? getpid() : 0; + pid_t expectedParentPid = getpid(); #endif // vfork on OS X is deprecated From 4bdf43556598d5ddabadbdc154bb3ec42709f5da Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 20:56:57 +0000 Subject: [PATCH 07/11] Fix potential deadlock: use pthread_cond_broadcast for done_cond Multiple callers can wait on s_pdeathsig_done_cond simultaneously (managed code uses a read lock allowing concurrent Process.Start calls). Using pthread_cond_signal only wakes one waiter, causing a lost-wakeup deadlock when a second caller overwrites s_pdeathsig_request before the first caller re-acquires the mutex. pthread_cond_broadcast wakes all waiters, each of which correctly re-checks the while loop condition. Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/26dd9c0c-a39c-45c0-aa1f-54f51d609e3f Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- src/native/libs/System.Native/pal_process.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index 498efeb524d87d..7a9f7a05134079 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -404,9 +404,11 @@ static void* PDeathSigThreadFunc(void* arg) req->childPid = childPid; req->errnoValue = errno; - // Signal completion + // Signal completion. Use broadcast (not signal) because multiple callers may be + // waiting on done_cond simultaneously. Each caller checks s_pdeathsig_request == NULL + // in a while loop, so spurious wakeups are handled correctly. s_pdeathsig_request = NULL; - pthread_cond_signal(&s_pdeathsig_done_cond); + pthread_cond_broadcast(&s_pdeathsig_done_cond); } return NULL; From 06a6f9c419c571cba5845a59eb181f218c68e545 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 21:02:49 +0000 Subject: [PATCH 08/11] Add error checking for pthread_attr_init and pthread_attr_setdetachstate Check return values of pthread_attr_init and pthread_attr_setdetachstate in EnsurePDeathSigThread. On failure, properly clean up (destroy attr if initialized, unlock mutex) and return -1 with errno set. This follows the established pattern from pal_signal.c:CreateSignalHandlerThread. Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/8d3b4a6a-87fe-4e15-af38-9098e79bb977 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- src/native/libs/System.Native/pal_process.c | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index 7a9f7a05134079..e15fd95d4f35b6 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -427,10 +427,24 @@ static int EnsurePDeathSigThread(void) { pthread_t thread; pthread_attr_t attr; - pthread_attr_init(&attr); - pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + 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; + } - int result = pthread_create(&thread, &attr, PDeathSigThreadFunc, NULL); + result = pthread_create(&thread, &attr, PDeathSigThreadFunc, NULL); pthread_attr_destroy(&attr); if (result != 0) From 61bfc8011ae848193f24e4f0e3721acd0afe7fd6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 06:17:32 +0000 Subject: [PATCH 09/11] Move expectedParentPid declaration before first goto to fix C++ jump-misses-init error Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/8ab2e56c-b825-4c02-8cd2-7f906426a595 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- src/native/libs/System.Native/pal_process.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index e15fd95d4f35b6..b48ab2d113c5a3 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -731,6 +731,11 @@ static int32_t ForkAndExecProcessInternal( 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; @@ -766,12 +771,6 @@ static int32_t ForkAndExecProcessInternal( sigfillset(&signal_set); pthread_sigmask(SIG_SETMASK, &signal_set, &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 - // vfork on OS X is deprecated // On Android, signal handlers between parent and child processes are shared with vfork, so when we reset // the signal handlers during child startup, we end up incorrectly clearing also the ones for the parent. From 0f92552e73d2a4e729c93c3fbd86f90b1c98243a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 07:01:26 +0000 Subject: [PATCH 10/11] Fix concurrent-submit race: wait for slot and check per-request done flag Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/9df92df5-eb9e-4e09-80ab-f2b4640737e4 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- src/native/libs/System.Native/pal_process.c | 23 ++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index b48ab2d113c5a3..e1fba6ea369c25 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -370,6 +370,7 @@ typedef struct int32_t childPid; int32_t result; int32_t errnoValue; + int32_t done; } PDeathSigForkRequest; static pthread_mutex_t s_pdeathsig_mutex = PTHREAD_MUTEX_INITIALIZER; @@ -404,9 +405,10 @@ static void* PDeathSigThreadFunc(void* arg) req->childPid = childPid; req->errnoValue = errno; - // Signal completion. Use broadcast (not signal) because multiple callers may be - // waiting on done_cond simultaneously. Each caller checks s_pdeathsig_request == NULL - // in a while loop, so spurious wakeups are handled correctly. + // 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); } @@ -491,15 +493,26 @@ static int32_t ForkAndExecOnPDeathSigThread( 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 s_pdeathsig_request = &req; pthread_cond_signal(&s_pdeathsig_request_cond); - // Wait for the dedicated thread to complete the fork+exec - while (s_pdeathsig_request != NULL) + // 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); } From 4a9f261fac6f2eb94ccea514d2f248a0cb2969f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 15:51:56 +0000 Subject: [PATCH 11/11] Add assert(s_pdeathsig_request == NULL) before submitting request Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/4ca5bf12-28f7-4dc4-8f03-a60786fa9bfe Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- src/native/libs/System.Native/pal_process.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index 5d6082588908d1..5231128c288efa 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -506,6 +506,7 @@ static int32_t ForkAndExecOnPDeathSigThread( } // Submit request and signal the dedicated thread + assert(s_pdeathsig_request == NULL); s_pdeathsig_request = &req; pthread_cond_signal(&s_pdeathsig_request_cond);