As I understand, the best way to achieve terminating a child process when its parent dies is via prctl(PR_SET_PDEATHSIG)
(at least on Linux): How to make child process die after parent exits?
There is one caveat to this mentioned in man prctl
:
This value is cleared for the child of a fork(2) and (since Linux 2.4.36 / 2.6.23) when executing a set-user-ID or set-group-ID binary, or a binary that has associated capabilities (see capabilities(7)). This value is preserved across execve(2).
So, the following code has a race condition:
parent.c:
#include <unistd.h>
int main(int argc, char **argv) {
int f = fork();
if (fork() == 0) {
execl("./child", "child", NULL, NULL);
}
return 0;
}
child.c:
#include <sys/prctl.h>
#include <signal.h>
int main(int argc, char **argv) {
prctl(PR_SET_PDEATHSIG, SIGKILL); // ignore error checking for now
// ...
return 0;
}
Namely, the parent count die before prctl()
is executed in the child (and thus the child will not receive the SIGKILL
). The proper way to address this is to prctl()
in the parent before the exec()
:
parent.c:
#include <unistd.h>
#include <sys/prctl.h>
#include <signal.h>
int main(int argc, char **argv) {
int f = fork();
if (fork() == 0) {
prctl(PR_SET_PDEATHSIG, SIGKILL); // ignore error checking for now
execl("./child", "child", NULL, NULL);
}
return 0;
}
child.c:
int main(int argc, char **argv) {
// ...
return 0;
}
However, if ./child
is a setuid/setgid binary, then this trick to avoid the race condition doesn't work (exec()
ing the setuid/setgid binary causes the PDEATHSIG
to be lost as per the man page quoted above), and it seems like you are forced to employ the first (racy) solution.
Is there any way if child
is a setuid/setgid binary to prctl(PR_SET_PDEATH_SIG)
in a non-racy way?
It is much more common to have the parent process set up a pipe. Parent process keeps the write end open (
pipefd[1]
), closing the read end (pipefd[0]
). Child process closes the write end (pipefd[1]
), and sets the read end (pipefd[1]
) nonblocking.This way, the child process can use
read(pipefd[0], buffer, 1)
to check if the parent process is still alive. If the parent is still running, it will return-1
witherrno == EAGAIN
(orerrno == EINTR
).Now, in Linux, the child process can also set the read end async, in which case it will be sent a signal (
SIGIO
by default) when the parent process exits:Use a siginfo handler for
desired_signal
. Ifinfo->si_code == POLL_IN && info->si_fd == pipefd[0]
, the parent process either exited or wrote something to the pipe. Becauseread()
is async-signal safe, and the pipe is nonblocking, you can useread(pipefd[0], &buffer, sizeof buffer)
in the signal handler whether the parent wrote something, or if parent exited (closed the pipe). In the latter case, theread()
will return0
.As far as I can see, this approach has no race conditions (if you use a realtime signal, so that the signal is not lost because an user-sent one is already pending), although it is very Linux-specific. After setting the signal handler, and at any point during the lifetime of the child process, the child can always explicitly check if the parent is still alive, without affecting the signal generation.
So, to recap, in pseudocode:
I do not expect you to believe my word.
Below is a crude example program you can use to check this behaviour yourself. It is long, but only because I wanted it to be easy to see what is happening at runtime. To implement this in a normal program, you only need a couple of dozen lines of code. example.c:
Compile and run the above using e.g.
The parent process will print to standard output, and the child process to standard error. The parent process will exit if you press Ctrl+C; the child process will ignore that signal. The child process uses
SIGHUP
instead ofSIGIO
(although a realtime signal, saySIGRTMIN+0
, would be safer); if generated by the parent process exiting, theSIGHUP
signal handler will raiseSIGTERM
in the child.To make the termination causes easy to see, the child catches
SIGTERM
, and exits the next iteration (a second later). If so desired, the handler can use e.g.raise(SIGKILL)
to terminate itself immediately.Both parent and child processes show their process IDs, so you can easily send a
SIGINT
/SIGHUP
/SIGTERM
signal from another terminal window. (The child process ignoresSIGINT
andSIGHUP
sent from outside the process.)I don't know this for sure, but clearing the parent death signal on
execve
when invoking a set-id binary looks like an intentional restriction for security reasons. I'm not sure why, considering that you can usekill
to send signals to setuid programs that share your real user ID, but they wouldn't have bothered making that change in 2.6.23 if there wasn't a reason to disallow it.Since you control the code of the set-id child, here is a kludge: make the call to
prctl
, then immediately afterward, callgetppid
and see if it returns 1. If it does, then either the process was started directly byinit
(which is not as rare as it used to be) or the process was reparented toinit
before it had a chance to callprctl
, which means its original parent is dead and it should exit.(This is a kludge because I know of no way to rule out the possibility that the process was started directly by
init
.init
never exits, so you have one case where it should exit and one case where it shouldn't and no way to tell which. But if you know from the larger design that the process will not be started directly byinit
, it should be reliable.)(You must call
getppid
afterprctl
, or you have only narrowed the race window, not eliminated it.)