sudo源码分析(二)

本篇主要分析sudo的信号处理函数。

首先回顾下上篇博客分析的sudo执行的5个步骤:

  1. 修改信号处理函数:保存原来的信号处理函数,设置新的信号处理函数
  2. 调用setuid将实际用户设置为ROOT
  3. 恢复信号处理函数
  4. 设置用户程序指定的权限(默认ROOT),并设置其他运行环境参数
  5. 调用execve执行用户程序
将信号相关的部分单独列出来就是这样:
    (void) sigemptyset(&mask);
    (void) sigprocmask(SIG_SETMASK, &mask, NULL);
    save_signals();
    // do something check and prepare
    init_signals();
    // setuid(ROOT_ID);
    restore_signals();
    // seteuid and exec
除了一开始将所有信号都设置为非阻塞状态,主要就是save_signals()、init_signals()和restore_signals()三个函数,在分析这三个函数之前我们需要先了解一个数组:
static struct signal_state {
    int signo;
    int restore;
    sigaction_t sa;
} saved_signals[] = {
    { SIGALRM },    /* SAVED_SIGALRM */
    { SIGCHLD },    /* SAVED_SIGCHLD */
    { SIGCONT },    /* SAVED_SIGCONT */
    { SIGHUP },     /* SAVED_SIGHUP */
    { SIGINT },     /* SAVED_SIGINT */
    { SIGPIPE },    /* SAVED_SIGPIPE */
    { SIGQUIT },    /* SAVED_SIGQUIT */
    { SIGTERM },    /* SAVED_SIGTERM */
    { SIGTSTP },    /* SAVED_SIGTSTP */
    { SIGTTIN },    /* SAVED_SIGTTIN */
    { SIGTTOU },    /* SAVED_SIGTTOU */
    { SIGUSR1 },    /* SAVED_SIGUSR1 */
    { SIGUSR2 },    /* SAVED_SIGUSR2 */
    { -1 }
};
这个saved_signals数组保存了sudo在调用execve之前需要修改的信号处理函数。save_signals将上述信号的信号处理函数保存到数组中,restore_signals将保存的信号处理函数恢复。这个数组的每个元素是一个signal_state结构体,这个结构体包含信号值,该信号处理函数是否需要被恢复,以及一个sigaction_t变量。

首先看下save_signals函数,这个函数很简单,就是将saved_signals中初始化了的信号的信号处理函数取出并保存。将sigaction函数的第二个参数设置为NULL,就能在地撒个参数中得到对应信号的信号处理函数。
void
save_signals(void)
{
    struct signal_state *ss;
    debug_decl(save_signals, SUDO_DEBUG_MAIN)

    for (ss = saved_signals; ss->signo != -1; ss++) {
    if (sigaction(ss->signo, NULL, &ss->sa) != 0)
        sudo_warn(U_("unable to save handler for signal %d"), ss->signo);
    }

    debug_return;
}
然后是init_signals函数,这个函数首先创建一个非阻塞的管道(用处后面会说),然后将saved_signals中的信号的信号处理函数设置为sudo_handler,信号处理函数被调用时将阻塞所有信号以防函数重入,而且这些信号并不会中断系统的某些阻塞调用(flags=SA_RESTART)。另外,五个信号(SIGCHLD、SIGCONT、SIGPIPE、SIGTTIN和SIGTTOU)不设置新的信号处理函数,这几个信号的信号处理会在后面其他地方根据不同的需要进行设置。
void
init_signals(void)
{
    struct sigaction sa;
    struct signal_state *ss;
    debug_decl(init_signals, SUDO_DEBUG_MAIN)

    /*
     * We use a pipe to atomically handle signal notification within
     * the select() loop without races (we may not have pselect()).
     */
    if (pipe_nonblock(signal_pipe) != 0)
    sudo_fatal(U_("unable to create pipe"));

    memset(&sa, 0, sizeof(sa));
    sigfillset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    sa.sa_handler = sudo_handler;

    for (ss = saved_signals; ss->signo > 0; ss++) {
    switch (ss->signo) {
        case SIGCHLD:
        case SIGCONT:
        case SIGPIPE:
        case SIGTTIN:
        case SIGTTOU:
        /* Don't install these until exec time. */
        break;
        default:
        if (ss->sa.sa_handler != SIG_IGN) {
            if (sigaction(ss->signo, &sa, NULL) != 0) {
            sudo_warn(U_("unable to set handler for signal %d"),
                ss->signo);
            }
        }
        break;
    }
    }
    debug_return;
}
至此,不得不看一眼神秘的sudo_handler,这个信号处理函数做的仅仅是将信号值(一个字节)通过管道发送出去。
static void
sudo_handler(int s)
{
    unsigned char signo = (unsigned char)s;

    /*
     * The pipe is non-blocking, if we overflow the kernel's pipe
     * buffer we drop the signal.  This is not a problem in practice.
     */
    while (write(signal_pipe[1], &signo, sizeof(signo)) == -1) {
    if (errno != EINTR)
        break;
    }
}
管道的写端在哪里?一共有两处,一个是绑定在signal_pipe[0]读事件的回调函数,一个就是dispatch_pending_signals函数。前者到目前为止还没看到,而且在现在的讨论场景下确实没有。而dispathch_pengding_signals函数主要从signal_pipe[0]中读取信号。如果发现有终止信号(SIGINT和SIGQUIT)就设置返回状态值并退出,如果发现最后一个信号值是SIGTSTP就向自己发送SIGTSTP信号。
我们目前讨论的场景都是单进程的场景,即sudo进程自己将收到的信号发送到管道,又自己从管道读出信号并处理。这个过程只处理三种信号:SIGINT、SIGQUIT和SIGTSTP。从管道读数据发生在调用execve之前,造成的效果就是这些信号被阻塞至调用execve。

static int
dispatch_pending_signals(struct command_status *cstat)
{
    ssize_t nread;
    struct sigaction sa;
    unsigned char signo = 0;
    int rval = 0;
    debug_decl(dispatch_pending_signals, SUDO_DEBUG_EXEC);

    for (;;) {
        nread = read(signal_pipe[0], &signo, sizeof(signo));
        if (nread <= 0) {
            /* It should not be possible to get EOF but just in case. */
            if (nread == 0)
                errno = ECONNRESET;
            /* Restart if interrupted by signal so the pipe doesn't fill. */
            if (errno == EINTR)
                continue;
            /* If pipe is empty, we are done. */
            if (errno == EAGAIN)
                break;
            sudo_debug_printf(SUDO_DEBUG_ERROR, "error reading signal pipe %s",
                              strerror(errno));
            cstat->type = CMD_ERRNO;
            cstat->val = errno;
            rval = 1;
            break;
        }
        /* Take the first terminal signal. */
        if (signo == SIGINT || signo == SIGQUIT) {
            cstat->type = CMD_WSTATUS;
            cstat->val = signo + 128;
            rval = 1;
            break;
        }
    }
    /* Only stop if we haven't already been terminated. */
    if (signo == SIGTSTP)
    {
        memset(&sa, 0, sizeof(sa));
        sigemptyset(&sa.sa_mask);
        sa.sa_flags = SA_RESTART;
        sa.sa_handler = SIG_DFL;
        if (sudo_sigaction(SIGTSTP, &sa, NULL) != 0)
            sudo_warn(U_("unable to set handler for signal %d"), SIGTSTP);
        if (kill(getpid(), SIGTSTP) != 0)
            sudo_warn("kill(%d, SIGTSTP)", (int)getpid());
        /* No need to reinstall SIGTSTP handler. */
    }
    debug_return_int(rval);
}
另外在调用execve之前还会调用restore_signals将信号处理函数恢复,这样在执行命令的时候就都是程序启动时的信号处理函数了。至此,一个sudo的简单场景下的信号处理机制就讲完了,是不是觉得这种处理方法很别扭,而且貌似还有很多信号没处理。你若真以为本文到此为止就输了,sudo的设计者显然不可能专门在单进程下使用管道。再回到最初说的5个步骤,这5个步骤被我简化的太多了,比如fork。但是我这样的简化也无可厚非,sudo的手册中这样说了“As a special case, if the policy plugin does not define a close function and no pty is required, sudo will execute the command directly instead of calling fork(2) first.”也就是说:sudo确实是存在这样的步骤的,但是对于大部分情况来说,在exec之前是需要先fork的。这种情况下,首先由sudo_execute调用fork_cmnd,在fork_cmnd中进行fork后调用exec_cmnd。
static int fork_cmnd(struct command_details *details, int sv[2])
{
    struct command_status cstat;
    sigaction_t sa;

    memset(&sa, 0, sizeof(sa));
    sigfillset(&sa.sa_mask);
    sa.sa_flags = SA_INTERRUPT; /* do not restart syscalls */
#ifdef SA_SIGINFO
    sa.sa_flags |= SA_SIGINFO;
    sa.sa_sigaction = handler;
#else
    sa.sa_handler = handler;
#endif
    if (sudo_sigaction(SIGCHLD, &sa, NULL) != 0)
        sudo_warn(U_("unable to set handler for signal %d"), SIGCHLD);
    if (sudo_sigaction(SIGCONT, &sa, NULL) != 0)
        sudo_warn(U_("unable to set handler for signal %d"), SIGCONT);
#ifdef SA_SIGINFO
    sa.sa_sigaction = handler_user_only;
#endif
    if (sudo_sigaction(SIGTSTP, &sa, NULL) != 0)
        sudo_warn(U_("unable to set handler for signal %d"), SIGTSTP);

    cmnd_pid = sudo_debug_fork();
    switch (cmnd_pid) {
    case -1:
        sudo_fatal(U_("unable to fork"));
        break;
    case 0:
        /* child */
        close(sv[0]);
        close(signal_pipe[0]);
        close(signal_pipe[1]);
        fcntl(sv[1], F_SETFD, FD_CLOEXEC);
        exec_cmnd(details, &cstat, sv[1]);
        send(sv[1], &cstat, sizeof(cstat), 0);
        sudo_debug_exit_int(__func__, __FILE__, __LINE__, sudo_debug_subsys, 1);
        _exit(1);
    }
    sudo_debug_printf(SUDO_DEBUG_INFO, "executed %s, pid %d", details->command,
                      (int)cmnd_pid);
    debug_return_int(cmnd_pid);
}
这个函数同时做了我们上面提到的init_signals函数和fork、exec_cmnd的事。首先,设置SIGCHLD、SIGCONT和SIGTSTP的信号处理函数,然后fork,子进程会关闭sv[0]、signal_pipe[0]和signal_pipe[1](看到这里我真是醉了,两个signal_pipe居然都被关了,这是彻底想把signal_pipe留给父进程的节奏啊~~~)。
void
handler(int s, siginfo_t *info, void *context)
{
    unsigned char signo = (unsigned char)s;

    if (s != SIGCHLD && USER_SIGNALED(info)) {
        pid_t si_pgrp = getpgid(info->si_pid);
        if (si_pgrp != (pid_t)-1) {
            if (si_pgrp == ppgrp || si_pgrp == cmnd_pid)
                return;
        } else if (info->si_pid == cmnd_pid) {
            return;
        }
    }

    while (write(signal_pipe[1], &signo, sizeof(signo)) == -1) {
        if (errno != EINTR)
            break;
    }
}
新绑定的信号处理函数也是将信号发送到管道。不过它过滤了来自sudo进程组和命令进程的进程组的信号(SIGCHLD除外)。
父进程从fork返回后很快就将一个读取管道数据的回调函数绑定到管道读端上。该回调函数如下:
static void
signal_pipe_cb(int fd, int what, void *v)
{
    struct exec_closure *ec = v;
    char signame[SIG2STR_MAX];
    unsigned char signo;
    ssize_t nread;
    int rc = 0;
    debug_decl(signal_pipe_cb, SUDO_DEBUG_EXEC)

    do {
        nread = read(fd, &signo, sizeof(signo));
        if (nread <= 0) {
            /* It should not be possible to get EOF but just in case... */
            if (nread == 0)
                errno = ECONNRESET;
            /* Restart if interrupted by signal so the pipe doesn't fill. */
            if (errno == EINTR)
                continue;
            /* On error, store errno and break out of the event loop. */
            if (errno != EAGAIN) {
                ec->cstat->type = CMD_ERRNO;
                ec->cstat->val = errno;
                sudo_warn(U_("error reading from signal pipe"));
                sudo_ev_loopbreak(ec->evbase);
            }
            break;
        }
        if (sig2str(signo, signame) == -1)
            snprintf(signame, sizeof(signame), "%d", signo);
        sudo_debug_printf(SUDO_DEBUG_DIAG, "received SIG%s", signame);
        rc = dispatch_signal(ec->evbase, ec->child, signo, signame,
                             ec->cstat);
    } while (rc == 0);
    debug_return;
}
该回调函数无非也是从管道读出信号值,并调用dispatch_signal函数。后者对于SIGCHLD信号则调用waitpid回收子进程资源,获取返回值并完成相关处理;对于其他信号则调用kill将该信号转发给子进程(即cmmand进程)。
static int
dispatch_signal(struct sudo_event_base *evbase, pid_t child,
                int signo, char *signame, struct command_status *cstat)
{
    int rc = 1;
    debug_decl(dispatch_signal, SUDO_DEBUG_EXEC)

        sudo_debug_printf(SUDO_DEBUG_INFO,
                          "%s: evbase %p, child: %d, signo %s(%d), cstat %p",
                          __func__, evbase, (int)child, signame, signo, cstat);

    if (signo == SIGCHLD) {
        pid_t pid;
        int status;
        do {
            pid = waitpid(child, &status, WUNTRACED|WNOHANG);
        } while (pid == -1 && errno == EINTR);
        if (pid == child) {
            // do something with child
        }
    } else {
        /* Send signal to child. */
        if (signo == SIGALRM) {
            terminate_command(child, false);
        } else if (kill(child, signo) != 0) {
            sudo_warn("kill(%d, SIG%s)", (int)child, signame);
        }
    }
    rc = 0;
 done:
    debug_return_int(rc);
}
至此,sudo的信号处理机制基本上分析清楚了。总结如下:
sudo进程(即父进程)首先创建一个管道,注意这个管道并不是给父子进程通信用,仅仅是sudo进程自己用。sudo进程将与自己和command有关的信号处理函数改为sudo_handler,该函数将发生的信号写入管道。管道有个读取数据的回调函数,该函数依次读取信号值,并将除SIGCHLD以外的信号通过kill发送给command进程。











你可能感兴趣的:(Linux,C++)