sudo源码分析(二)
来源:互联网 发布:网店seo 编辑:程序博客网 时间:2024/05/21 19:27
本篇主要分析sudo的信号处理函数。
首先回顾下上篇博客分析的sudo执行的5个步骤:
- 修改信号处理函数:保存原来的信号处理函数,设置新的信号处理函数
- 调用setuid将实际用户设置为ROOT
- 恢复信号处理函数
- 设置用户程序指定的权限(默认ROOT),并设置其他运行环境参数
- 调用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
- sudo源码分析(二)
- sudo源码分析(一)
- JUnit源码分析(二)
- Log4net源码分析(二)
- Log4net源码分析(二)
- Logcat源码分析(二)
- FFMPEG源码分析(二)
- FFMPEG源码分析(二)
- opendpi 源码分析(二)
- Log4net源码分析(二)
- pomelo源码分析(二)
- H264源码分析(二)
- FFMPEG源码分析(二)
- FFMPEG源码分析(二)
- mosquitto源码分析(二)
- FFMPEG源码分析(二)
- openMPM源码分析(二)
- EventBus (二) 源码分析
- PAT 1092. To Buy or Not to Buy (20)
- android两种签名
- 银河移动工作平台
- Java 文件或者文件夹的压缩和解压
- foxmail邮件只能显示邮件头,不能显示内容
- sudo源码分析(二)
- JVM指令集(指令码、助记符、功能描述) --- 逐渐更新
- 欢迎使用CSDN-markdown编辑器
- eclipse修改默认工作空间
- WifiDocs/WirelessAccessPoint
- React.js的核心入门知识
- 整数中1出现的次数
- 高性能JavaScript模板引擎原理解析
- 二维码扫描 IOS原生API IOS7.0及以上系统支持