Implement SafeProcessHandle APIs for Linux and other Unixes#124979
Implement SafeProcessHandle APIs for Linux and other Unixes#124979Copilot wants to merge 4 commits intocopilot/implement-safeprocesshandle-apisfrom
Conversation
|
Tagging subscribers to this area: @dotnet/area-system-io |
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
adamsitnik
left a comment
There was a problem hiding this comment.
@copilot So far I've found only one nit.
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
|
We configure the terminal for
For more info, see dotnet/corefx#35621. |
|
Some other
Exit of
|
|
The Consider: using handle = SafeChildProcessHandle.Start(...);
...
if (!handle.TryWaitForExit(TimeSpan.FromSeconds(1), out _)
handle.Kill();If we're in the case where the child process is killed nothing calls waitpid on the killed child. Its kernel resources won't be returned until the .NET process itself terminates. We added the SIGCHLD handling to deal with this issue for |
Thanks for sharing that, I was unaware of it.
It's intentional, I want to keep it as simple as possible. And since it's a new API, I can just document it. The problem is that Process process = Process.Start()
process.SafeProcessHandle.UseNewApi();That is why I wanted to introduce a new process.SafeProcessHandle.Signal(PosixSignal.SIGKILL);I will need to somehow integrate both FWIW my plan is to get Windows impl merged first, then macOS and then this one (this PR is a very dirty draft as of now) |
Then we should address this feedback in the macOS PR because it also applies there. Did you see #124979 (comment)? I'm asking since you haven't commented on it. |
| { | ||
| #if HAVE_PDEATHSIG | ||
| // On systems with PR_SET_PDEATHSIG (Linux), use it to set up parent death signal | ||
| if (prctl(PR_SET_PDEATHSIG, SIGTERM) == -1) |
There was a problem hiding this comment.
-
Why SIGTERM and not SIGKILL?
-
From https://man7.org/linux/man-pages/man2/pr_set_pdeathsig.2const.html:
The "parent" in this case is considered to be the thread that
created this process. In other words, the signal will be sent
when that thread terminates (via, for example, pthread_exit(3)),
rather than after all of the threads in the parent process
terminate.
This sounds like the child process will be terminated when the .NET thread that started it exists rather than when the .NET parent process exits?
There was a problem hiding this comment.
My understanding is that this logic is executed after fork, in the child process. So I would expect it to be executed by the main thread of the new child process?
Is that correct @tmds?
There was a problem hiding this comment.
I don't think it is the main thread of the child process because then "In other words, the signal will be sent when that thread terminates" makes no sense.
There was a problem hiding this comment.
I think it makes sense when called in other scenarios.
I will try to test it and get back to you with my findings.
There was a problem hiding this comment.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
#include <sys/prctl.h>
#include <sys/wait.h>
static void *worker_thread(void *arg)
{
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return NULL;
}
if (pid == 0) {
/* Child process */
prctl(PR_SET_PDEATHSIG, SIGKILL);
printf("[child %d] set PR_SET_PDEATHSIG to SIGKILL\n", getpid());
printf("[child %d] parent process is %d\n", getpid(), getppid());
printf("[child %d] waiting... (expect to be killed when creating thread exits)\n", getpid());
/* Sleep long enough to observe the behavior */
for (int i = 1; i <= 10; i++) {
sleep(1);
printf("[child %d] still alive after %d seconds (ppid=%d)\n", getpid(), i, getppid());
}
printf("[child %d] survived! (should not reach here in the gotcha case)\n", getpid());
_exit(0);
}
/* Back in the worker thread of the parent process */
printf("[thread] forked child %d, now exiting thread (but NOT the process)\n", pid);
/* Ensure the child has time to call prctl(PR_SET_PDEATHSIG) */
sleep(1);
return NULL;
}
int main(void)
{
printf("[main] pid=%d\n", getpid());
pthread_t tid;
if (pthread_create(&tid, NULL, worker_thread, NULL) != 0) {
perror("pthread_create");
return 1;
}
/* Wait for the thread to finish (this causes it to be joined/terminated) */
pthread_join(tid, NULL);
printf("[main] worker thread has exited, but parent process is still alive\n");
printf("[main] waiting for child...\n");
int status;
pid_t w = wait(&status);
if (w > 0) {
if (WIFSIGNALED(status))
printf("[main] child %d was killed by signal %d (%s) — no exit code\n", w, WTERMSIG(status), WTERMSIG(status) == SIGKILL ? "SIGKILL" : "other");
else if (WIFEXITED(status))
printf("[main] child %d exited normally with exit code %d\n", w, WEXITSTATUS(status));
}
printf("[main] parent process exiting now\n");
return 0;
}The above program waits for the child to exit. PR_SET_PDEATHSIG causes the child to get killed when the thread that started it exits:
[main] pid=102816
[thread] forked child 102818, now exiting thread (but NOT the process)
[child 102818] set PR_SET_PDEATHSIG to SIGKILL
[child 102818] parent process is 102816
[child 102818] waiting... (expect to be killed when creating thread exits)
[child 102818] still alive after 1 seconds (ppid=102816)
[main] worker thread has exited, but parent process is still alive
[main] waiting for child...
[main] child 102818 was killed by signal 9 (SIGKILL) — no exit code
[main] parent process exiting now
There was a problem hiding this comment.
@tmds Big thanks for providing a repro! It's true for fork, but we prefer vfork:
And when I change your sample to use vfork:
[main] pid=505
[child 507] set PR_SET_PDEATHSIG to SIGKILL
[child 507] parent process is 505
[child 507] waiting... (expect to be killed when creating thread exits)
[child 507] still alive after 1 seconds (ppid=505)
[child 507] still alive after 2 seconds (ppid=505)
[child 507] still alive after 3 seconds (ppid=505)
[child 507] still alive after 4 seconds (ppid=505)
[child 507] still alive after 5 seconds (ppid=505)
[child 507] still alive after 6 seconds (ppid=505)
[child 507] still alive after 7 seconds (ppid=505)
[child 507] still alive after 8 seconds (ppid=505)
[child 507] still alive after 9 seconds (ppid=505)
[child 507] still alive after 10 seconds (ppid=505)
[child 507] survived! (should not reach here in the gotcha case)
[thread] forked child 507, now exiting thread (but NOT the process)
[main] worker thread has exited, but parent process is still alive
[main] waiting for child...
[main] child 507 exited normally with exit code 0
[main] parent process exiting now
There was a problem hiding this comment.
Can you share your code?
Adjusting the code to use vfork and execve (to match .NET implementation):
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
#include <sys/prctl.h>
#include <sys/wait.h>
static void *worker_thread(void *arg)
{
pid_t pid = vfork();
if (pid < 0) {
perror("vfork");
_exit(1);
}
if (pid == 0) {
/* Child process — set pdeathsig then exec */
prctl(PR_SET_PDEATHSIG, SIGKILL);
char msg[128];
int n = snprintf(msg, sizeof(msg),
"[child %d] set PR_SET_PDEATHSIG to SIGKILL, execing sleep 10...\n",
getpid());
write(STDOUT_FILENO, msg, n);
char *argv[] = {"sleep", "10", NULL};
char *envp[] = {NULL};
execve("/usr/bin/sleep", argv, envp);
perror("execve");
_exit(1);
}
/* Back in the worker thread of the parent process */
printf("[thread] forked child %d, now exiting thread (but NOT the process)\n", pid);
/* Give the child time to exec */
sleep(1);
return NULL;
}
int main(void)
{
printf("[main] pid=%d\n", getpid());
pthread_t tid;
if (pthread_create(&tid, NULL, worker_thread, NULL) != 0) {
perror("pthread_create");
return 1;
}
pthread_join(tid, NULL);
printf("[main] worker thread has exited, but parent process is still alive\n");
printf("[main] waiting for child...\n");
int status;
pid_t w = wait(&status);
if (w > 0) {
if (WIFSIGNALED(status))
printf("[main] child %d was killed by signal %d (%s)\n", w, WTERMSIG(status),
WTERMSIG(status) == SIGKILL ? "SIGKILL" : "other");
else if (WIFEXITED(status))
printf("[main] child %d exited normally with code %d\n", w, WEXITSTATUS(status));
}
printf("[main] parent process exiting now\n");
return 0;
}Gives the same result for me:
./a.out
[main] pid=18488
[child 18490] set PR_SET_PDEATHSIG to SIGKILL, execing sleep 10...
[thread] forked child 18490, now exiting thread (but NOT the process)
[main] worker thread has exited, but parent process is still alive
[main] waiting for child...
[main] child 18490 was killed by signal 9 (SIGKILL)
[main] parent process exiting now
There was a problem hiding this comment.
but we prefer vfork:
I think you didn't call execve?
from vfork man page:
vfork() differs from fork(2) in that the calling thread is
suspended until the child terminates (either normally, by calling
_exit(2), or abnormally, after delivery of a fatal signal), or it
makes a call to execve(2).
This means your calling thread couldn't exit.
There was a problem hiding this comment.
I think you didn't call
execve?
I did not, just changed fork to vfork.
So basically to get this to work we would need to have a dedicated thread that would be kept alive for the whole time the application runs?
There was a problem hiding this comment.
Yes, PR_SET_PDEATHSIG signals when the thread that called fork exits.
|
Closing due the removal of ProcessStartOptions |
src/native/libs/configure.cmakewith new feature detection checks for Linux (clone3, pidfd_send_signal, close_range, pdeathsig, sys_tgkill)src/native/libs/Common/pal_config.h.inwith new#cmakedefine01entries for the new featuressrc/native/libs/System.Native/pal_process.c:sys/syscall.h,linux/sched.h,sys/prctl.hHAVE_PIDFDwhenHAVE_CLONE3is available#else(ENOTSUP) branch inSystemNative_SpawnProcesswith fork/exec path using clone3/fork/vforkSystemNative_SendSignalto usepidfd_send_signalwhen availablemap_wait_status_pidfdusingsiginfo_tfor pidfd pathSystemNative_TryGetExitCodeto usewaitid(P_PIDFD)when pidfd availableSystemNative_WaitForExitAndReapto usewaitid(P_PIDFD)when pidfd availableSystemNative_TryWaitForExitCancellableto usepollwith pidfd when availableSystemNative_TryWaitForExitto usepollwith pidfd when availableSystemNative_OpenProcessto usewaitidverification andpidfd_openwhen availableSafeProcessHandleTests.Unix.csfrom[PlatformSpecific(TestPlatforms.OSX)]to[PlatformSpecific(TestPlatforms.AnyUnix)]SafeProcessHandleTests.csfrom[PlatformSpecific(TestPlatforms.OSX | TestPlatforms.Windows)]to[PlatformSpecific(TestPlatforms.AnyUnix | TestPlatforms.Windows)]sys/syscall.hincludes💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.