/* $OpenBSD: privsep.c,v 1.30 2007/03/15 05:18:32 djm Exp $ */ /* * Copyright (c) 2003 Anil Madhavapeddy <anil@recoil.org> * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include <sys/ioctl.h> #include <sys/param.h> #include <sys/queue.h> #include <sys/uio.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/stat.h> #include <sys/wait.h> #include <err.h> #include <errno.h> #include <fcntl.h> #include <netdb.h> #include <paths.h> #include <poll.h> #include <pwd.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <util.h> #include <utmp.h> #include "syslogd.h" /* * syslogd can only go forward in these states; each state should represent * less privilege. After STATE_INIT, the child is allowed to parse its * config file once, and communicate the information regarding what logfiles * it needs access to back to the parent. When that is done, it sends a * message to the priv parent revoking this access, moving to STATE_RUNNING. * In this state, any log-files not in the access list are rejected. * * This allows a HUP signal to the child to reopen its log files, and * the config file to be parsed if it hasn't been changed (this is still * useful to force resolution of remote syslog servers again). * If the config file has been modified, then the child dies, and * the priv parent restarts itself. */ enum priv_state { STATE_INIT, /* just started up */ STATE_CONFIG, /* parsing config file for first time */ STATE_RUNNING, /* running and accepting network traffic */ STATE_QUIT /* shutting down */ }; enum cmd_types { PRIV_OPEN_TTY, /* open terminal or console device */ PRIV_OPEN_LOG, /* open logfile for appending */ PRIV_OPEN_PIPE, /* fork & exec child that gets logs on stdin */ PRIV_OPEN_UTMP, /* open utmp for reading only */ PRIV_OPEN_CONFIG, /* open config file for reading only */ PRIV_CONFIG_MODIFIED, /* check if config file has been modified */ PRIV_GETHOSTSERV, /* resolve host/service names */ PRIV_GETHOSTBYADDR, /* resolve numeric address into hostname */ PRIV_DONE_CONFIG_PARSE /* signal that the initial config parse is done */ }; static int priv_fd = -1; static volatile pid_t child_pid = -1; static char config_file[MAXPATHLEN]; static struct stat cf_info; static int allow_gethostbyaddr = 0; static volatile sig_atomic_t cur_state = STATE_INIT; /* Queue for the allowed logfiles */ struct logname { char path[MAXPATHLEN]; TAILQ_ENTRY(logname) next; }; static TAILQ_HEAD(, logname) lognames; static void check_log_name(char *, size_t); static int open_file(char *); static int open_pipe(char *); static void check_tty_name(char *, size_t); static void increase_state(int); static void sig_pass_to_chld(int); static void sig_got_chld(int); static void must_read(int, void *, size_t); static void must_write(int, void *, size_t); static int may_read(int, void *, size_t); int priv_init(char *conf, int numeric, int lockfd, int nullfd, char *argv[]) { int i, fd, socks[2], cmd, addr_len, addr_af, result, restart; size_t path_len, hostname_len, servname_len; char path[MAXPATHLEN], hostname[MAXHOSTNAMELEN]; char servname[MAXHOSTNAMELEN]; struct stat cf_stat; struct hostent *hp; struct passwd *pw; struct addrinfo hints, *res0; struct sigaction sa; memset(&sa, 0, sizeof(sa)); sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; sa.sa_handler = SIG_DFL; for (i = 1; i < _NSIG; i++) sigaction(i, &sa, NULL); /* Create sockets */ if (socketpair(AF_LOCAL, SOCK_STREAM, PF_UNSPEC, socks) == -1) err(1, "socketpair() failed"); pw = getpwnam("_syslogd"); if (pw == NULL) errx(1, "unknown user _syslogd"); child_pid = fork(); if (child_pid < 0) err(1, "fork() failed"); if (!child_pid) { /* Child - drop privileges and return */ if (chroot(pw->pw_dir) != 0) err(1, "unable to chroot"); if (chdir("/") != 0) err(1, "unable to chdir"); if (setgroups(1, &pw->pw_gid) == -1) err(1, "setgroups() failed"); if (setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) == -1) err(1, "setresgid() failed"); if (setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) == -1) err(1, "setresuid() failed"); close(socks[0]); priv_fd = socks[1]; return 0; } close(lockfd); if (!Debug) { dup2(nullfd, STDIN_FILENO); dup2(nullfd, STDOUT_FILENO); dup2(nullfd, STDERR_FILENO); } if (nullfd > 2) close(nullfd); /* Father */ /* Pass TERM/HUP/INT/QUIT through to child, and accept CHLD */ sa.sa_handler = sig_pass_to_chld; sigaction(SIGTERM, &sa, NULL); sigaction(SIGHUP, &sa, NULL); sigaction(SIGINT, &sa, NULL); sigaction(SIGQUIT, &sa, NULL); sa.sa_handler = sig_got_chld; sa.sa_flags |= SA_NOCLDSTOP; sigaction(SIGCHLD, &sa, NULL); setproctitle("[priv]"); close(socks[1]); /* Close descriptors that only the unpriv child needs */ for (i = 0; i < nfunix; i++) if (pfd[PFD_UNIX_0 + i].fd != -1) close(pfd[PFD_UNIX_0 + i].fd); if (pfd[PFD_INET].fd != -1) close(pfd[PFD_INET].fd); if (pfd[PFD_CTLSOCK].fd != -1) close(pfd[PFD_CTLSOCK].fd); if (pfd[PFD_CTLCONN].fd != -1) close(pfd[PFD_CTLCONN].fd); if (pfd[PFD_KLOG].fd) close(pfd[PFD_KLOG].fd); /* Save the config file specified by the child process */ if (strlcpy(config_file, conf, sizeof config_file) >= sizeof(config_file)) errx(1, "config_file truncation"); if (stat(config_file, &cf_info) < 0) err(1, "stat config file failed"); /* Save whether or not the child can have access to gethostbyaddr(3) */ if (numeric > 0) allow_gethostbyaddr = 0; else allow_gethostbyaddr = 1; TAILQ_INIT(&lognames); increase_state(STATE_CONFIG); restart = 0; while (cur_state < STATE_QUIT) { if (may_read(socks[0], &cmd, sizeof(int))) break; switch (cmd) { case PRIV_OPEN_TTY: dprintf("[priv]: msg PRIV_OPEN_TTY received\n"); /* Expecting: length, path */ must_read(socks[0], &path_len, sizeof(size_t)); if (path_len == 0 || path_len > sizeof(path)) _exit(0); must_read(socks[0], &path, path_len); path[path_len - 1] = '\0'; check_tty_name(path, path_len); fd = open(path, O_WRONLY|O_NONBLOCK, 0); send_fd(socks[0], fd); if (fd < 0) warnx("priv_open_tty failed"); else close(fd); break; case PRIV_OPEN_LOG: case PRIV_OPEN_PIPE: dprintf("[priv]: msg PRIV_OPEN_%s received\n", cmd == PRIV_OPEN_PIPE ? "PIPE" : "LOG"); /* Expecting: length, path */ must_read(socks[0], &path_len, sizeof(size_t)); if (path_len == 0 || path_len > sizeof(path)) _exit(0); must_read(socks[0], &path, path_len); path[path_len - 1] = '\0'; check_log_name(path, path_len); if (cmd == PRIV_OPEN_LOG) fd = open_file(path); else if (cmd == PRIV_OPEN_PIPE) fd = open_pipe(path); else errx(1, "invalid cmd"); send_fd(socks[0], fd); if (fd < 0) warnx("priv_open_log failed"); else close(fd); break; case PRIV_OPEN_UTMP: dprintf("[priv]: msg PRIV_OPEN_UTMP received\n"); fd = open(_PATH_UTMP, O_RDONLY|O_NONBLOCK, 0); send_fd(socks[0], fd); if (fd < 0) warnx("priv_open_utmp failed"); else close(fd); break; case PRIV_OPEN_CONFIG: dprintf("[priv]: msg PRIV_OPEN_CONFIG received\n"); stat(config_file, &cf_info); fd = open(config_file, O_RDONLY|O_NONBLOCK, 0); send_fd(socks[0], fd); if (fd < 0) warnx("priv_open_config failed"); else close(fd); break; case PRIV_CONFIG_MODIFIED: dprintf("[priv]: msg PRIV_CONFIG_MODIFIED received\n"); if (stat(config_file, &cf_stat) < 0 || timespeccmp(&cf_info.st_mtimespec, &cf_stat.st_mtimespec, <) || cf_info.st_size != cf_stat.st_size) { dprintf("config file modified: restarting\n"); restart = result = 1; must_write(socks[0], &result, sizeof(int)); } else { result = 0; must_write(socks[0], &result, sizeof(int)); } break; case PRIV_DONE_CONFIG_PARSE: dprintf("[priv]: msg PRIV_DONE_CONFIG_PARSE received\n"); increase_state(STATE_RUNNING); break; case PRIV_GETHOSTSERV: dprintf("[priv]: msg PRIV_GETHOSTSERV received\n"); /* Expecting: len, hostname, len, servname */ must_read(socks[0], &hostname_len, sizeof(size_t)); if (hostname_len == 0 || hostname_len > sizeof(hostname)) _exit(0); must_read(socks[0], &hostname, hostname_len); hostname[hostname_len - 1] = '\0'; must_read(socks[0], &servname_len, sizeof(size_t)); if (servname_len == 0 || servname_len > sizeof(servname)) _exit(0); must_read(socks[0], &servname, servname_len); servname[servname_len - 1] = '\0'; memset(&hints, '\0', sizeof(hints)); hints.ai_family = AF_INET; hints.ai_socktype = SOCK_DGRAM; i = getaddrinfo(hostname, servname, &hints, &res0); if (i != 0 || res0 == NULL) { addr_len = 0; must_write(socks[0], &addr_len, sizeof(int)); } else { /* Just send the first address */ i = res0->ai_addrlen; must_write(socks[0], &i, sizeof(int)); must_write(socks[0], res0->ai_addr, i); freeaddrinfo(res0); } break; case PRIV_GETHOSTBYADDR: dprintf("[priv]: msg PRIV_GETHOSTBYADDR received\n"); if (!allow_gethostbyaddr) errx(1, "rejected attempt to gethostbyaddr"); /* Expecting: length, address, address family */ must_read(socks[0], &addr_len, sizeof(int)); if (addr_len <= 0 || addr_len > sizeof(hostname)) _exit(0); must_read(socks[0], hostname, addr_len); must_read(socks[0], &addr_af, sizeof(int)); hp = gethostbyaddr(hostname, addr_len, addr_af); if (hp == NULL) { addr_len = 0; must_write(socks[0], &addr_len, sizeof(int)); } else { addr_len = strlen(hp->h_name) + 1; must_write(socks[0], &addr_len, sizeof(int)); must_write(socks[0], hp->h_name, addr_len); } break; default: errx(1, "unknown command %d", cmd); break; } } close(socks[0]); /* Unlink any domain sockets that have been opened */ for (i = 0; i < nfunix; i++) if (funixn[i] != NULL && pfd[PFD_UNIX_0 + i].fd != -1) (void)unlink(funixn[i]); if (ctlsock_path != NULL && pfd[PFD_CTLSOCK].fd != -1) (void)unlink(ctlsock_path); if (restart) { int r; wait(&r); execvp(argv[0], argv); } unlink(_PATH_LOGPID); _exit(1); } static int open_file(char *path) { /* must not start with | */ if (path[0] == '|') return (-1); return (open(path, O_WRONLY|O_APPEND|O_NONBLOCK, 0)); } static int open_pipe(char *cmd) { char *argp[] = {"sh", "-c", NULL, NULL}; struct passwd *pw; int fd[2]; int bsize, flags; pid_t pid; /* skip over leading | and whitespace */ if (cmd[0] != '|') return (-1); for(cmd++; *cmd && *cmd == ' '; cmd++) ; /* nothing */ if (!*cmd) return (-1); argp[2] = cmd; if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, fd) == -1) { warnx("open_pipe"); return (-1); } /* make the fd on syslogd's side nonblocking */ if ((flags = fcntl(fd[1], F_GETFL, 0)) == -1) { warnx("fcntl"); return (-1); } flags |= O_NONBLOCK; if ((flags = fcntl(fd[1], F_SETFL, flags)) == -1) { warnx("fcntl"); return (-1); } switch (pid = fork()) { case -1: warnx("fork error"); return (-1); case 0: break; default: close(fd[0]); return (fd[1]); } close(fd[1]); /* grow receive buffer */ bsize = 65535; while (bsize > 0 && setsockopt(fd[0], SOL_SOCKET, SO_RCVBUF, &bsize, sizeof(bsize)) == -1) bsize /= 2; if ((pw = getpwnam("_syslogd")) == NULL) errx(1, "unknown user _syslogd"); if (setgroups(1, &pw->pw_gid) == -1 || setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) == -1 || setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) == -1) err(1, "failure dropping privs"); endpwent(); if (dup2(fd[0], STDIN_FILENO) == -1) err(1, "dup2 failed"); if (execv("/bin/sh", argp) == -1) err(1, "execv %s", cmd); /* NOTREACHED */ return (-1); } /* Check that the terminal device is ok, and if not, rewrite to /dev/null. * Either /dev/console or /dev/tty* are allowed. */ static void check_tty_name(char *tty, size_t ttylen) { const char ttypre[] = "/dev/tty"; char *p; /* Any path containing '..' is invalid. */ for (p = tty; *p && (p - tty) < ttylen; p++) if (*p == '.' && *(p + 1) == '.') goto bad_path; if (strcmp(_PATH_CONSOLE, tty) && strncmp(tty, ttypre, strlen(ttypre))) goto bad_path; return; bad_path: warnx ("%s: invalid attempt to open %s: rewriting to /dev/null", "check_tty_name", tty); strlcpy(tty, "/dev/null", ttylen); } /* If we are in the initial configuration state, accept a logname and add * it to the list of acceptable logfiles. Otherwise, check against this list * and rewrite to /dev/null if it's a bad path. */ static void check_log_name(char *lognam, size_t loglen) { struct logname *lg; char *p; /* Any path containing '..' is invalid. */ for (p = lognam; *p && (p - lognam) < loglen; p++) if (*p == '.' && *(p + 1) == '.') goto bad_path; switch (cur_state) { case STATE_CONFIG: lg = malloc(sizeof(struct logname)); if (!lg) err(1, "check_log_name() malloc"); strlcpy(lg->path, lognam, MAXPATHLEN); TAILQ_INSERT_TAIL(&lognames, lg, next); break; case STATE_RUNNING: TAILQ_FOREACH(lg, &lognames, next) if (!strcmp(lg->path, lognam)) return; goto bad_path; break; default: /* Any other state should just refuse the request */ goto bad_path; break; } return; bad_path: warnx("%s: invalid attempt to open %s: rewriting to /dev/null", "check_log_name", lognam); strlcpy(lognam, "/dev/null", loglen); } /* Crank our state into less permissive modes */ static void increase_state(int state) { if (state <= cur_state) errx(1, "attempt to decrease or match current state"); if (state < STATE_INIT || state > STATE_QUIT) errx(1, "attempt to switch to invalid state"); cur_state = state; } /* Open console or a terminal device for writing */ int priv_open_tty(const char *tty) { char path[MAXPATHLEN]; int cmd, fd; size_t path_len; if (priv_fd < 0) errx(1, "%s: called from privileged portion", "priv_open_tty"); if (strlcpy(path, tty, sizeof path) >= sizeof(path)) return -1; path_len = strlen(path) + 1; cmd = PRIV_OPEN_TTY; must_write(priv_fd, &cmd, sizeof(int)); must_write(priv_fd, &path_len, sizeof(size_t)); must_write(priv_fd, path, path_len); fd = receive_fd(priv_fd); return fd; } /* Open log-file */ int priv_open_log(const char *lognam) { char path[MAXPATHLEN]; int cmd, fd; size_t path_len; if (priv_fd < 0) errx(1, "%s: called from privileged child", "priv_open_log"); if (strlcpy(path, lognam, sizeof path) >= sizeof(path)) return -1; path_len = strlen(path) + 1; if (lognam[0] == '|') cmd = PRIV_OPEN_PIPE; else cmd = PRIV_OPEN_LOG; must_write(priv_fd, &cmd, sizeof(int)); must_write(priv_fd, &path_len, sizeof(size_t)); must_write(priv_fd, path, path_len); fd = receive_fd(priv_fd); return fd; } /* Open utmp for reading */ FILE * priv_open_utmp(void) { int cmd, fd; FILE *fp; if (priv_fd < 0) errx(1, "%s: called from privileged portion", "priv_open_utmp"); cmd = PRIV_OPEN_UTMP; must_write(priv_fd, &cmd, sizeof(int)); fd = receive_fd(priv_fd); if (fd < 0) return NULL; fp = fdopen(fd, "r"); if (!fp) { warn("priv_open_utmp: fdopen() failed"); close(fd); return NULL; } return fp; } /* Open syslog config file for reading */ FILE * priv_open_config(void) { int cmd, fd; FILE *fp; if (priv_fd < 0) errx(1, "%s: called from privileged portion", "priv_open_config"); cmd = PRIV_OPEN_CONFIG; must_write(priv_fd, &cmd, sizeof(int)); fd = receive_fd(priv_fd); if (fd < 0) return NULL; fp = fdopen(fd, "r"); if (!fp) { warn("priv_open_config: fdopen() failed"); close(fd); return NULL; } return fp; } /* Ask if config file has been modified since last attempt to read it */ int priv_config_modified(void) { int cmd, res; if (priv_fd < 0) errx(1, "%s: called from privileged portion", "priv_config_modified"); cmd = PRIV_CONFIG_MODIFIED; must_write(priv_fd, &cmd, sizeof(int)); /* Expect back integer signalling 1 for modification */ must_read(priv_fd, &res, sizeof(int)); return res; } /* Child can signal that its initial parsing is done, so that parent * can revoke further logfile permissions. This call only works once. */ void priv_config_parse_done(void) { int cmd; if (priv_fd < 0) errx(1, "%s: called from privileged portion", "priv_config_parse_done"); cmd = PRIV_DONE_CONFIG_PARSE; must_write(priv_fd, &cmd, sizeof(int)); } /* Name/service to address translation. Response is placed into addr, and * the length is returned (zero on error) */ int priv_gethostserv(char *host, char *serv, struct sockaddr *addr, size_t addr_len) { char hostcpy[MAXHOSTNAMELEN], servcpy[MAXHOSTNAMELEN]; int cmd, ret_len; size_t hostname_len, servname_len; if (priv_fd < 0) errx(1, "%s: called from privileged portion", "priv_gethostserv"); if (strlcpy(hostcpy, host, sizeof hostcpy) >= sizeof(hostcpy)) errx(1, "%s: overflow attempt in hostname", "priv_gethostserv"); hostname_len = strlen(hostcpy) + 1; if (strlcpy(servcpy, serv, sizeof servcpy) >= sizeof(servcpy)) errx(1, "%s: overflow attempt in servname", "priv_gethostserv"); servname_len = strlen(servcpy) + 1; cmd = PRIV_GETHOSTSERV; must_write(priv_fd, &cmd, sizeof(int)); must_write(priv_fd, &hostname_len, sizeof(size_t)); must_write(priv_fd, hostcpy, hostname_len); must_write(priv_fd, &servname_len, sizeof(size_t)); must_write(priv_fd, servcpy, servname_len); /* Expect back an integer size, and then a string of that length */ must_read(priv_fd, &ret_len, sizeof(int)); /* Check there was no error (indicated by a return of 0) */ if (!ret_len) return 0; /* Make sure we aren't overflowing the passed in buffer */ if (addr_len < ret_len) errx(1, "%s: overflow attempt in return", "priv_gethostserv"); /* Read the resolved address and make sure we got all of it */ memset(addr, '\0', addr_len); must_read(priv_fd, addr, ret_len); return ret_len; } /* Reverse address resolution; response is placed into res, and length of * response is returned (zero on error) */ int priv_gethostbyaddr(char *addr, int addr_len, int af, char *res, size_t res_len) { int cmd, ret_len; if (priv_fd < 0) errx(1, "%s called from privileged portion", "priv_gethostbyaddr"); cmd = PRIV_GETHOSTBYADDR; must_write(priv_fd, &cmd, sizeof(int)); must_write(priv_fd, &addr_len, sizeof(int)); must_write(priv_fd, addr, addr_len); must_write(priv_fd, &af, sizeof(int)); /* Expect back an integer size, and then a string of that length */ must_read(priv_fd, &ret_len, sizeof(int)); /* Check there was no error (indicated by a return of 0) */ if (!ret_len) return 0; /* Check we don't overflow the passed in buffer */ if (res_len < ret_len) errx(1, "%s: overflow attempt in return", "priv_gethostbyaddr"); /* Read the resolved hostname */ must_read(priv_fd, res, ret_len); return ret_len; } /* Pass the signal through to child */ static void sig_pass_to_chld(int sig) { int oerrno = errno; if (child_pid != -1) kill(child_pid, sig); errno = oerrno; } /* When child dies, move into the shutdown state */ /* ARGSUSED */ static void sig_got_chld(int sig) { pid_t pid; do { pid = waitpid(WAIT_ANY, NULL, WNOHANG); } while (pid == -1 && errno == EINTR); if (pid == child_pid && cur_state < STATE_QUIT) cur_state = STATE_QUIT; } /* Read all data or return 1 for error. */ static int may_read(int fd, void *buf, size_t n) { char *s = buf; ssize_t res, pos = 0; while (n > pos) { res = read(fd, s + pos, n - pos); switch (res) { case -1: if (errno == EINTR || errno == EAGAIN) continue; case 0: return (1); default: pos += res; } } return (0); } /* Read data with the assertion that it all must come through, or * else abort the process. Based on atomicio() from openssh. */ static void must_read(int fd, void *buf, size_t n) { char *s = buf; ssize_t res, pos = 0; while (n > pos) { res = read(fd, s + pos, n - pos); switch (res) { case -1: if (errno == EINTR || errno == EAGAIN) continue; case 0: _exit(0); default: pos += res; } } } /* Write data with the assertion that it all has to be written, or * else abort the process. Based on atomicio() from openssh. */ static void must_write(int fd, void *buf, size_t n) { char *s = buf; ssize_t res, pos = 0; while (n > pos) { res = write(fd, s + pos, n - pos); switch (res) { case -1: if (errno == EINTR || errno == EAGAIN) continue; case 0: _exit(0); default: pos += res; } } }