prctl(PR_SET_PDEATHSIG)竞争条件

时间:2017-02-27 21:54:19

标签: c linux process signals

据我了解,当父进程死亡时终止子进程的最佳方法是通过prctl(PR_SET_PDEATHSIG)(至少在Linux上):How to make child process die after parent exits?

man prctl中提到了一个警告:

  

执行set-user-ID或set-group-ID二进制文件或具有关联的二进制文件时,为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)。解决此问题的正确方法是在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二进制文件导致{{1}根据上面引用的手册页丢失了,似乎你被迫使用第一个(racy)解决方案。

如果PDEATHSIG是一个非常有效的child的setuid / setgid二进制文件,有什么方法吗?

3 个答案:

答案 0 :(得分:3)

让父进程设置管道更为常见。父进程保持写入结束(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 e.g. 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。)

答案 1 :(得分:1)

我不确定这一点,但是在调用set-id二进制文件时清除execve上的父死亡信号看起来像是出于安全原因的故意限制。我不确定为什么,考虑到您可以使用kill向共享真实用户ID的setuid程序发送信号,但他们不会在2.6中进行更改。 23如果没有理由不允许它。

由于您控制了set-id子代码,因此这是一个kludge:拨打prctl,然后立即调用,调用getppid并查看是否它返回1.如果是,则该过程直接由init启动(这不像以前那么罕见),或者该过程在有机会之前被重新定位到init。调用prctl,这意味着它的原始父母已经死了,它应该退出。

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

(您必须在 getppid之后致电prctl ,或者您只是缩小了竞赛窗口,而没有将其消除。)

答案 2 :(得分:1)

您的最后一个代码段仍包含竞争条件:

int main(int argc, char **argv) {
  int f = fork();
  if (fork() == 0) {
    // <- !!!race time!!!
    prctl(PR_SET_PDEATHSIG, SIGKILL); // ignore error checking for now
    execl("./child", "child", NULL, NULL);
  }
  return 0;
}

这意味着在孩子中,在分叉之后,直到prctl()具有可见效果(认为:返回)之前,都有一个时间窗口,父母可以退出。

要解决此问题,您必须在派生之前保存父代的PID,并在prctl()调用之后对其进行检查,例如:

pid_t ppid_before_fork = getpid();
pid_t pid = fork();
if (pid == -1) { perror(0); exit(1); }
if (pid) {
    ; // continue parent execution
} else {
    int r = prctl(PR_SET_PDEATHSIG, SIGTERM);
    if (r == -1) { perror(0); exit(1); }
    // test in case the original parent exited just
    // before the prctl() call
    if (getppid() != ppid_before_fork)
        exit(1);
    // continue child execution ...

see also

关于执行setuid / setgid程序:然后,您可以通过其他方式(例如,在参数或环境矢量中)传递ppid_before_fork并在执行后执行prctl()(包括比较),即在执行的二进制文件中。