prctl(PR_SET_PDEATHSIG)竞争条件

据我所知,实现终止一个subprocess,当其父母死亡的最佳途径是通过prctl(PR_SET_PDEATHSIG) (至less在Linux上): 如何让父进程后,subprocess死亡?

有一个告诫人们提到这个man prctl

在执行set-user-ID或set-group-ID二进制文件或具有关联function的二进制文件时,该值将被fork(2)和(从Linux 2.4.36 / 2.6.23开始)的子节点清除能力(7))。 这个值保存在execve(2)中。

所以,下面的代码有一个竞争条件:

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; } 

也就是说,在prctl()之前死亡的父计数在孩子中执行(因此孩子将不会收到SIGKILL )。 解决这个问题的正确方法是在exec()之前的父级中的prctl() 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; } 

但是,如果./child是一个setuid / setgid二进制文件,那么避免竞争条件的这个技巧就不起作用了( exec() setuid / setgid二进制文件导致PDEATHSIG按照上面引用的手册页丢失),而且似乎你被迫采用第一个(活泼的)解决scheme。

有没有什么办法,如果child是一个setuid / setgid二进制prctl(PR_SET_PDEATH_SIG)以非racy的方式?

让父进程建立一个管道更为常见。 父进程保持写入结束( pipefd[1] ),关闭读取结束( pipefd[0] )。 子进程关闭写入结束( pipefd[1] ),并设置读取结束( pipefd[1] )非阻塞。

这样,子进程可以使用read(pipefd[0], buffer, 1)来检查父进程是否仍然活着。 如果父节点仍在运行,它将返回-1 errno == EAGAIN (或errno == EINTR )。

现在,在Linux中,子进程还可以将读取结束设置为异步,在这种情况下,父进程退出时将发送一个信号(缺省为SIGIO ):

 fcntl(pipefd[0], F_SETSIG, desired_signal); fcntl(pipefd[0], F_SETOWN, getpid()); fcntl(pipefd[0], F_SETFL, O_NONBLOCK | O_ASYNC); 

对于desired_signal使用siginfo处理程序。 如果info->si_code == POLL_IN && info->si_fd == pipefd[0] ,父进程退出或写入管道的东西。 因为read()是异步信号安全的,并且管道是非阻塞的,所以无论父节点是否写入内容,或者父节点是否退出(关闭管道),都可以在信号处理程序中使用read(pipefd[0], &buffer, sizeof buffer) ) 。 在后一种情况下, read()将返回0

就我所见,这种方法没有竞争条件(如果你使用实时信号,所以信号不会丢失,因为用户发送的信号已经挂起),尽管它是非常特定于Linux的。 设置完信号处理程序后,在子进程生命周期中的任何时刻,子进程总是可以明确地检查父进程是否仍然有效,而不会影响信号生成。

所以,用伪代码来回顾一下:

 Construct pipe Fork child process Child process: Close write end of pipe Install pipe signal handler (say, SIGRTMIN+0) Set read end of pipe to generate pipe signal (F_SETSIG) Set own PID as read end owner (F_SETOWN) Set read end of pipe nonblocking and async (F_SETFL, O_NONBLOCK | O_ASYNC) If read(pipefd[0], buffer, sizeof buffer) == 0, the parent process has already exited. Continue with normal work. Child process pipe signal handler: If siginfo->si_code == POLL_IN and siginfo->si_fd == pipefd[0], parent process has exited. To immediately die, use eg raise(SIGKILL). Parent process: Close read end of pipe Continue with normal work. 

我不指望你相信我的话。

下面是一个粗略的示例程序,您可以使用它自己检查此行为。 这是很长的,但只是因为我想让它很容易看到在运行时发生了什么。 要在一个正常的程序中实现这个,你只需要几十行代码。 example.c

 #define _GNU_SOURCE #define _POSIX_C_SOURCE 200809L #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <fcntl.h> #include <signal.h> #include <string.h> #include <stdio.h> #include <errno.h> static volatile sig_atomic_t done = 0; static void handle_done(int signum) { if (!done) done = signum; } static int install_done(const int signum) { struct sigaction act; memset(&act, 0, sizeof act); sigemptyset(&act.sa_mask); act.sa_handler = handle_done; act.sa_flags = 0; if (sigaction(signum, &act, NULL) == -1) return errno; return 0; } static int deathfd = -1; static void death(int signum, siginfo_t *info, void *context) { if (info->si_code == POLL_IN && info->si_fd == deathfd) raise(SIGTERM); } static int install_death(const int signum) { struct sigaction act; memset(&act, 0, sizeof act); sigemptyset(&act.sa_mask); act.sa_sigaction = death; act.sa_flags = SA_SIGINFO; if (sigaction(signum, &act, NULL) == -1) return errno; return 0; } int main(void) { pid_t child, p; int pipefd[2], status; char buffer[8]; if (install_done(SIGINT)) { fprintf(stderr, "Cannot set SIGINT handler: %s.\n", strerror(errno)); return EXIT_FAILURE; } if (pipe(pipefd) == -1) { fprintf(stderr, "Cannot create control pipe: %s.\n", strerror(errno)); return EXIT_FAILURE; } child = fork(); if (child == (pid_t)-1) { fprintf(stderr, "Cannot fork child process: %s.\n", strerror(errno)); return EXIT_FAILURE; } if (!child) { /* * Child process. */ /* Close write end of pipe. */ deathfd = pipefd[0]; close(pipefd[1]); /* Set a SIGHUP signal handler. */ if (install_death(SIGHUP)) { fprintf(stderr, "Child process: cannot set SIGHUP handler: %s.\n", strerror(errno)); return EXIT_FAILURE; } /* Set SIGTERM signal handler. */ if (install_done(SIGTERM)) { fprintf(stderr, "Child process: cannot set SIGTERM handler: %s.\n", strerror(errno)); return EXIT_FAILURE; } /* We want a SIGHUP instead of SIGIO. */ fcntl(deathfd, F_SETSIG, SIGHUP); /* We want the SIGHUP delivered when deathfd closes. */ fcntl(deathfd, F_SETOWN, getpid()); /* Make the deathfd (read end of pipe) nonblocking and async. */ fcntl(deathfd, F_SETFL, O_NONBLOCK | O_ASYNC); /* Check if the parent process is dead. */ if (read(deathfd, buffer, sizeof buffer) == 0) { printf("Child process (%ld): Parent process is already dead.\n", (long)getpid()); return EXIT_FAILURE; } while (1) { status = __atomic_fetch_and(&done, 0, __ATOMIC_SEQ_CST); if (status == SIGINT) printf("Child process (%ld): SIGINT caught and ignored.\n", (long)getpid()); else if (status) break; printf("Child process (%ld): Tick.\n", (long)getpid()); fflush(stdout); sleep(1); status = __atomic_fetch_and(&done, 0, __ATOMIC_SEQ_CST); if (status == SIGINT) printf("Child process (%ld): SIGINT caught and ignored.\n", (long)getpid()); else if (status) break; printf("Child process (%ld): Tock.\n", (long)getpid()); fflush(stdout); sleep(1); } printf("Child process (%ld): Exited due to %s.\n", (long)getpid(), (status == SIGINT) ? "SIGINT" : (status == SIGHUP) ? "SIGHUP" : (status == SIGTERM) ? "SIGTERM" : "Unknown signal.\n"); fflush(stdout); return EXIT_SUCCESS; } /* * Parent process. */ /* Close read end of pipe. */ close(pipefd[0]); while (!done) { fprintf(stderr, "Parent process (%ld): Tick.\n", (long)getpid()); fflush(stderr); sleep(1); fprintf(stderr, "Parent process (%ld): Tock.\n", (long)getpid()); fflush(stderr); sleep(1); /* Try reaping the child process. */ p = waitpid(child, &status, WNOHANG); if (p == child || (p == (pid_t)-1 && errno == ECHILD)) { if (p == child && WIFSIGNALED(status)) fprintf(stderr, "Child process died from %s. Parent will now exit, too.\n", (WTERMSIG(status) == SIGINT) ? "SIGINT" : (WTERMSIG(status) == SIGHUP) ? "SIGHUP" : (WTERMSIG(status) == SIGTERM) ? "SIGTERM" : "an unknown signal"); else fprintf(stderr, "Child process has exited, so the parent will too.\n"); fflush(stderr); break; } } if (done) { fprintf(stderr, "Parent process (%ld): Exited due to %s.\n", (long)getpid(), (done == SIGINT) ? "SIGINT" : (done == SIGHUP) ? "SIGHUP" : "Unknown signal.\n"); fflush(stderr); } /* Never reached! */ return EXIT_SUCCESS; } 

编译并运行上面的例如

 gcc -Wall -O2 example.c -o example ./example 

父进程将打印到标准输出,并将子进程打印到标准错误。 如果按Ctrl + C ,则父进程将退出; 子进程将忽略该信号。 子进程使用SIGHUP而不是SIGIO (尽管实时信号,比如说SIGRTMIN+0会更安全)。 如果由父进程退出生成, SIGHUP信号处理程序将在子进程中引发SIGTERM

为了使终止的原因容易看到,孩子捕获SIGTERM ,并退出下一个迭代(一秒钟后)。 如果需要,处理程序可以使用例如raise(SIGKILL)立即终止它自己。

父进程和子进程都显示进程ID,因此您可以轻松地从另一个终端窗口发送SIGINT / SIGHUP / SIGTERM信号。 (子进程忽略从进程外部发送的SIGINTSIGHUP 。)

我不知道这是肯定的,但是当调用set-id二进制文件时,清除execve上的execve信号看起来像是出于安全原因的有意限制。 我不确定为什么,考虑到你可以使用kill来发送信号给setuid共享你的真实用户ID的程序,但是如果没有理由拒绝它,他们不会在2.6.23中做这个改变。

既然你控制了set-id子代码,这里就是一个混乱的问题:先调用prctl ,然后立即调用getppid ,看它是否返回1.如果是,那么这个过程直接由init这并不像以前那么少见),或者在有机会调用prctl之前将进程重新init ,这意味着它的原始父项已经死了,应该退出。

(这是一个kludge,因为我知道没有办法排除这个过程是由init直接启动的可能性, init永远不会退出,所以你有一个应该退出的情况,以及一个不应该和不应该告诉哪一个,但是如果你从更大的设计中知道这个过程不会被init直接init ,那么它应该是可靠的。)

(你必须 prctl 之后调用getppid ,否则你只能缩小比赛窗口,不能消除。