sudo源码分析(二)

来源:互联网 发布:网店seo 编辑:程序博客网 时间:2024/05/21 19:27

本篇主要分析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,就能在地撒个参数中得到对应信号的信号处理函数。
voidsave_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)不设置新的信号处理函数,这几个信号的信号处理会在后面其他地方根据不同的需要进行设置。
voidinit_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 voidsudo_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 intdispatch_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留给父进程的节奏啊~~~)。
voidhandler(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 voidsignal_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 intdispatch_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进程。











0 0