Mercurial > hg > dhcpcd
view src/privsep.c @ 5231:a2c342295221 draft
privsep: Enable Capsicum for all processes.
Except for the priviledged process.
This is quite an in-depth change:
* ARP is now one process per address
* BPF flags are now returned via privsep
* BPF write filters are locked when supported
* The root process sends to the network
The last step is done by opening RAW sockets and then sending a UDP
header (where applicable) to avoid binding to an address
which is already in use by the reader sockets.
This is slightly wasteful for OS's without sandboxing but does
have the very nice side effect of not needing a source address
to unicast DHCPs replies from which makes the code smaller.
| author | Roy Marples <roy@marples.name> |
|---|---|
| date | Tue, 19 May 2020 16:19:05 +0100 |
| parents | 82c7e8204e9b |
| children | bcd021398c1d |
line wrap: on
line source
/* SPDX-License-Identifier: BSD-2-Clause */ /* * Privilege Separation for dhcpcd * Copyright (c) 2006-2020 Roy Marples <roy@marples.name> * All rights reserved * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ /* * The current design is this: * Spawn a priv process to carry out privileged actions and * spawning unpriv process to initate network connections such as BPF * or address specific listener. * Spawn an unpriv process to send/receive common network data. * Then drop all privs and start running. * Every process aside from the privileged actioneer is chrooted. * * dhcpcd will maintain the config file in the chroot, no need to handle * this in a script or something. */ #include <sys/socket.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/wait.h> #ifdef AF_LINK #include <net/if_dl.h> #endif #include <assert.h> #include <errno.h> #include <fcntl.h> #include <grp.h> #include <paths.h> #include <pwd.h> #include <stddef.h> /* For offsetof, struct padding debug */ #include <signal.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include "arp.h" #include "common.h" #include "control.h" #include "dhcp.h" #include "dhcp6.h" #include "eloop.h" #include "ipv6nd.h" #include "logerr.h" #include "privsep.h" #ifdef HAVE_CAPSICUM #include <sys/capsicum.h> #endif #ifdef HAVE_UTIL_H #include <util.h> #endif int ps_init(struct dhcpcd_ctx *ctx) { struct passwd *pw; struct stat st; errno = 0; if ((ctx->ps_user = pw = getpwnam(PRIVSEP_USER)) == NULL) { ctx->options &= ~DHCPCD_PRIVSEP; if (errno == 0) { logerrx("no such user %s", PRIVSEP_USER); /* Just incase logerrx caused an error... */ errno = 0; } else logerr("getpwnam"); return -1; } if (stat(pw->pw_dir, &st) == -1 || !S_ISDIR(st.st_mode)) { ctx->options &= ~DHCPCD_PRIVSEP; logerrx("refusing chroot: %s: %s", PRIVSEP_USER, pw->pw_dir); errno = 0; return -1; } ctx->options |= DHCPCD_PRIVSEP; return 0; } int ps_dropprivs(struct dhcpcd_ctx *ctx) { struct passwd *pw = ctx->ps_user; if (!(ctx->options & DHCPCD_FORKED)) logdebugx("chrooting to `%s'", pw->pw_dir); if (chroot(pw->pw_dir) == -1) logerr("%s: chroot `%s'", __func__, pw->pw_dir); if (chdir("/") == -1) logerr("%s: chdir `/'", __func__); if (setgroups(1, &pw->pw_gid) == -1 || setgid(pw->pw_gid) == -1 || setuid(pw->pw_uid) == -1) { logerr("failed to drop privileges"); return -1; } return 0; } pid_t ps_dostart(struct dhcpcd_ctx *ctx, pid_t *priv_pid, int *priv_fd, void (*recv_msg)(void *), void (*recv_unpriv_msg), void *recv_ctx, int (*callback)(void *), void (*signal_cb)(int, void *), unsigned int flags) { int stype; int fd[2]; pid_t pid; #ifdef HAVE_CAPSICUM cap_rights_t rights; cap_rights_init(&rights, CAP_READ, CAP_WRITE, CAP_EVENT, CAP_SHUTDOWN); #endif stype = SOCK_CLOEXEC | SOCK_NONBLOCK; if (socketpair(AF_UNIX, SOCK_DGRAM | stype, 0, fd) == -1) { logerr("socketpair"); return -1; } switch (pid = fork()) { case -1: logerr("fork"); return -1; case 0: *priv_fd = fd[1]; close(fd[0]); break; default: *priv_pid = pid; *priv_fd = fd[0]; close(fd[1]); if (recv_unpriv_msg == NULL) ; #ifdef HAVE_CAPSICUM else if (cap_rights_limit(*priv_fd, &rights) == -1 && errno != ENOSYS) { logerr("%s: cap_rights_limit", __func__); return -1; } #endif else if (eloop_event_add(ctx->eloop, *priv_fd, recv_unpriv_msg, recv_ctx) == -1) { logerr("%s: eloop_event_add", __func__); return -1; } return pid; } ctx->options |= DHCPCD_UNPRIV | DHCPCD_FORKED; if (ctx->fork_fd != -1) { close(ctx->fork_fd); ctx->fork_fd = -1; } pidfile_clean(); eloop_clear(ctx->eloop); /* We are not root */ if (priv_fd != &ctx->ps_root_fd) { ps_freeprocesses(ctx, recv_ctx); if (ctx->ps_root_fd != -1) { close(ctx->ps_root_fd); ctx->ps_root_fd = -1; } } if (priv_fd != &ctx->ps_inet_fd && ctx->ps_inet_fd != -1) { close(ctx->ps_inet_fd); ctx->ps_inet_fd = -1; } if (eloop_signal_set_cb(ctx->eloop, dhcpcd_signals, dhcpcd_signals_len, signal_cb, ctx) == -1) { logerr("%s: eloop_signal_set_cb", __func__); goto errexit; } /* ctx->sigset aready has the initial sigmask set in main() */ if (eloop_signal_mask(ctx->eloop, NULL) == -1) { logerr("%s: eloop_signal_mask", __func__); goto errexit; } #ifdef HAVE_CAPSICUM if (cap_rights_limit(*priv_fd, &rights) == -1 && errno != ENOSYS) goto errexit; #endif if (eloop_event_add(ctx->eloop, *priv_fd, recv_msg, recv_ctx) == -1) { logerr("%s: eloop_event_add", __func__); goto errexit; } if (callback(recv_ctx) == -1) goto errexit; if (!(ctx->options & DHCPCD_DEBUG) && (!(ctx->options & DHCPCD_TEST) || loggetopts() & LOGERR_QUIET)) { (void)freopen(_PATH_DEVNULL, "w", stdout); (void)freopen(_PATH_DEVNULL, "w", stderr); } if (flags & PSF_DROPPRIVS) ps_dropprivs(ctx); return 0; errexit: /* Failure to start root or inet processes is fatal. */ if (priv_fd == &ctx->ps_root_fd || priv_fd == &ctx->ps_inet_fd) ps_sendcmd(ctx, *priv_fd, PS_STOP, 0, NULL, 0); shutdown(*priv_fd, SHUT_RDWR); *priv_fd = -1; return -1; } int ps_dostop(struct dhcpcd_ctx *ctx, pid_t *pid, int *fd) { int status; #ifdef PRIVSEP_DEBUG logdebugx("%s: pid %d fd %d", __func__, *pid, *fd); #endif if (*pid == 0) return 0; eloop_event_delete(ctx->eloop, *fd); if (ps_sendcmd(ctx, *fd, PS_STOP, 0, NULL, 0) == -1 && errno != ECONNRESET) logerr(__func__); if (shutdown(*fd, SHUT_RDWR) == -1 && errno != ENOTCONN) logerr(__func__); close(*fd); *fd = -1; /* We won't have permission for all processes .... */ #if 0 if (kill(*pid, SIGTERM) == -1) logerr(__func__); #endif status = 0; #ifdef HAVE_CAPSICUM unsigned int cap_mode = 0; int cap_err = cap_getmode(&cap_mode); if (cap_err == -1) { if (errno != ENOSYS) logerr("%s: cap_getmode", __func__); } else if (cap_mode != 0) goto nowait; #endif /* Wait for the process to finish */ while (waitpid(*pid, &status, 0) == -1) { if (errno != EINTR) { logerr("%s: waitpid", __func__); status = 0; break; } #ifdef PRIVSEP_DEBUG else logerr("%s: waitpid ", __func__); #endif } #ifdef HAVE_CAPSICUM nowait: #endif *pid = 0; #ifdef PRIVSEP_DEBUG logdebugx("%s: status %d", __func__, status); #endif return status; } int ps_start(struct dhcpcd_ctx *ctx) { pid_t pid; TAILQ_INIT(&ctx->ps_processes); switch (pid = ps_root_start(ctx)) { case -1: logerr("ps_root_start"); return -1; case 0: return 0; default: logdebugx("spawned privileged actioneer on PID %d", pid); } /* No point in spawning the generic network listener if we're * not going to use it. */ if (!(ctx->options & (DHCPCD_MASTER | DHCPCD_IPV6))) goto started; switch (pid = ps_inet_start(ctx)) { case -1: if (errno == ENXIO) return 0; return -1; case 0: return 0; default: logdebugx("spawned network proxy on PID %d", pid); } started: return 1; } int ps_stop(struct dhcpcd_ctx *ctx) { int r, ret = 0; if (!(ctx->options & DHCPCD_PRIVSEP) || ctx->options & DHCPCD_FORKED || ctx->eloop == NULL) return 0; r = ps_inet_stop(ctx); if (r != 0) ret = r; /* We've been chrooted, so we need to tell the * privileged actioneer to remove the pidfile. */ ps_root_unlink(ctx, ctx->pidfile); r = ps_root_stop(ctx); if (r != 0) ret = r; ctx->options &= ~DHCPCD_PRIVSEP; return ret; } void ps_freeprocess(struct ps_process *psp) { TAILQ_REMOVE(&psp->psp_ctx->ps_processes, psp, next); if (psp->psp_fd != -1) { eloop_event_delete(psp->psp_ctx->eloop, psp->psp_fd); close(psp->psp_fd); } if (psp->psp_work_fd != -1) { eloop_event_delete(psp->psp_ctx->eloop, psp->psp_work_fd); close(psp->psp_work_fd); } #ifdef INET if (psp->psp_bpf != NULL) bpf_close(psp->psp_bpf); #endif free(psp); } static void ps_free(struct dhcpcd_ctx *ctx) { struct ps_process *psp; bool stop = ctx->ps_root_pid == getpid(); while ((psp = TAILQ_FIRST(&ctx->ps_processes)) != NULL) { if (stop) ps_dostop(ctx, &psp->psp_pid, &psp->psp_fd); ps_freeprocess(psp); } } int ps_unrollmsg(struct msghdr *msg, struct ps_msghdr *psm, const void *data, size_t len) { uint8_t *datap, *namep, *controlp; namep = UNCONST(data); controlp = namep + psm->ps_namelen; datap = controlp + psm->ps_controllen; if (psm->ps_namelen != 0) { if (psm->ps_namelen > len) { errno = EINVAL; return -1; } msg->msg_name = namep; len -= psm->ps_namelen; } else msg->msg_name = NULL; msg->msg_namelen = psm->ps_namelen; if (psm->ps_controllen != 0) { if (psm->ps_controllen > len) { errno = EINVAL; return -1; } msg->msg_control = controlp; len -= psm->ps_controllen; } else msg->msg_control = NULL; msg->msg_controllen = psm->ps_controllen; if (len != 0) { msg->msg_iovlen = 1; msg->msg_iov[0].iov_base = datap; msg->msg_iov[0].iov_len = len; } else { msg->msg_iovlen = 0; msg->msg_iov[0].iov_base = NULL; msg->msg_iov[0].iov_len = 0; } return 0; } ssize_t ps_sendpsmmsg(struct dhcpcd_ctx *ctx, int fd, struct ps_msghdr *psm, const struct msghdr *msg) { struct iovec iov[] = { { .iov_base = UNCONST(psm), .iov_len = sizeof(*psm) }, { .iov_base = NULL, }, /* name */ { .iov_base = NULL, }, /* control */ { .iov_base = NULL, }, /* payload 1 */ { .iov_base = NULL, }, /* payload 2 */ { .iov_base = NULL, }, /* payload 3 */ }; int iovlen; ssize_t len; if (msg != NULL) { struct iovec *iovp = &iov[1]; size_t i; psm->ps_namelen = msg->msg_namelen; psm->ps_controllen = (socklen_t)msg->msg_controllen; iovp->iov_base = msg->msg_name; iovp->iov_len = msg->msg_namelen; iovp++; iovp->iov_base = msg->msg_control; iovp->iov_len = msg->msg_controllen; iovlen = 3; for (i = 0; i < (size_t)msg->msg_iovlen; i++) { if ((size_t)iovlen + i > __arraycount(iov)) { errno = ENOBUFS; return -1; } iovp++; iovp->iov_base = msg->msg_iov[i].iov_base; iovp->iov_len = msg->msg_iov[i].iov_len; } iovlen += i; } else iovlen = 1; len = writev(fd, iov, iovlen); #ifdef PRIVSEP_DEBUG logdebugx("%s: %zd", __func__, len); #endif if ((len == -1 || len == 0) && ctx->options & DHCPCD_FORKED) eloop_exit(ctx->eloop, len == 0 ? EXIT_SUCCESS : EXIT_FAILURE); return len; } ssize_t ps_sendpsmdata(struct dhcpcd_ctx *ctx, int fd, struct ps_msghdr *psm, const void *data, size_t len) { struct iovec iov[] = { { .iov_base = UNCONST(data), .iov_len = len }, }; struct msghdr msg = { .msg_iov = iov, .msg_iovlen = 1, }; return ps_sendpsmmsg(ctx, fd, psm, &msg); } ssize_t ps_sendmsg(struct dhcpcd_ctx *ctx, int fd, uint16_t cmd, unsigned long flags, const struct msghdr *msg) { struct ps_msghdr psm = { .ps_cmd = cmd, .ps_flags = flags, .ps_namelen = msg->msg_namelen, .ps_controllen = (socklen_t)msg->msg_controllen, }; size_t i; for (i = 0; i < (size_t)msg->msg_iovlen; i++) psm.ps_datalen += msg->msg_iov[i].iov_len; #if 0 /* For debugging structure padding. */ logerrx("psa.family %lu %zu", offsetof(struct ps_addr, psa_family), sizeof(psm.ps_id.psi_addr.psa_family)); logerrx("psa.pad %lu %zu", offsetof(struct ps_addr, psa_pad), sizeof(psm.ps_id.psi_addr.psa_pad)); logerrx("psa.psa_u %lu %zu", offsetof(struct ps_addr, psa_u), sizeof(psm.ps_id.psi_addr.psa_u)); logerrx("psa %zu", sizeof(psm.ps_id.psi_addr)); logerrx("psi.addr %lu %zu", offsetof(struct ps_id, psi_addr), sizeof(psm.ps_id.psi_addr)); logerrx("psi.index %lu %zu", offsetof(struct ps_id, psi_ifindex), sizeof(psm.ps_id.psi_ifindex)); logerrx("psi.cmd %lu %zu", offsetof(struct ps_id, psi_cmd), sizeof(psm.ps_id.psi_cmd)); logerrx("psi.pad %lu %zu", offsetof(struct ps_id, psi_pad), sizeof(psm.ps_id.psi_pad)); logerrx("psi %zu", sizeof(struct ps_id)); logerrx("ps_cmd %lu", offsetof(struct ps_msghdr, ps_cmd)); logerrx("ps_pad %lu %zu", offsetof(struct ps_msghdr, ps_pad), sizeof(psm.ps_pad)); logerrx("ps_flags %lu %zu", offsetof(struct ps_msghdr, ps_flags), sizeof(psm.ps_flags)); logerrx("ps_id %lu %zu", offsetof(struct ps_msghdr, ps_id), sizeof(psm.ps_id)); logerrx("ps_namelen %lu %zu", offsetof(struct ps_msghdr, ps_namelen), sizeof(psm.ps_namelen)); logerrx("ps_controllen %lu %zu", offsetof(struct ps_msghdr, ps_controllen), sizeof(psm.ps_controllen)); logerrx("ps_pad2 %lu %zu", offsetof(struct ps_msghdr, ps_pad2), sizeof(psm.ps_pad2)); logerrx("ps_datalen %lu %zu", offsetof(struct ps_msghdr, ps_datalen), sizeof(psm.ps_datalen)); logerrx("psm %zu", sizeof(psm)); #endif return ps_sendpsmmsg(ctx, fd, &psm, msg); } ssize_t ps_sendcmd(struct dhcpcd_ctx *ctx, int fd, uint16_t cmd, unsigned long flags, const void *data, size_t len) { struct ps_msghdr psm = { .ps_cmd = cmd, .ps_flags = flags, }; struct iovec iov[] = { { .iov_base = UNCONST(data), .iov_len = len } }; struct msghdr msg = { .msg_iov = iov, .msg_iovlen = 1, }; return ps_sendpsmmsg(ctx, fd, &psm, &msg); } static ssize_t ps_sendcmdmsg(int fd, uint16_t cmd, const struct msghdr *msg) { struct ps_msghdr psm = { .ps_cmd = cmd }; uint8_t data[PS_BUFLEN], *p = data; struct iovec iov[] = { { .iov_base = &psm, .iov_len = sizeof(psm) }, { .iov_base = data, .iov_len = 0 }, }; size_t dl = sizeof(data); if (msg->msg_namelen != 0) { if (msg->msg_namelen > dl) goto nobufs; psm.ps_namelen = msg->msg_namelen; memcpy(p, msg->msg_name, msg->msg_namelen); p += msg->msg_namelen; dl -= msg->msg_namelen; } if (msg->msg_controllen != 0) { if (msg->msg_controllen > dl) goto nobufs; psm.ps_controllen = (socklen_t)msg->msg_controllen; memcpy(p, msg->msg_control, msg->msg_controllen); p += msg->msg_controllen; dl -= msg->msg_controllen; } psm.ps_datalen = msg->msg_iov[0].iov_len; if (psm.ps_datalen > dl) goto nobufs; iov[1].iov_len = psm.ps_namelen + psm.ps_controllen + psm.ps_datalen; if (psm.ps_datalen != 0) memcpy(p, msg->msg_iov[0].iov_base, psm.ps_datalen); return writev(fd, iov, __arraycount(iov)); nobufs: errno = ENOBUFS; return -1; } ssize_t ps_recvmsg(struct dhcpcd_ctx *ctx, int rfd, uint16_t cmd, int wfd) { struct sockaddr_storage ss = { .ss_family = AF_UNSPEC }; uint8_t controlbuf[sizeof(struct sockaddr_storage)] = { 0 }; uint8_t databuf[64 * 1024]; struct iovec iov[] = { { .iov_base = databuf, .iov_len = sizeof(databuf) } }; struct msghdr msg = { .msg_name = &ss, .msg_namelen = sizeof(ss), .msg_control = controlbuf, .msg_controllen = sizeof(controlbuf), .msg_iov = iov, .msg_iovlen = 1, }; ssize_t len = recvmsg(rfd, &msg, 0); #ifdef PRIVSEP_DEBUG logdebugx("%s: recv fd %d, %zd bytes", __func__, rfd, len); #endif if ((len == -1 || len == 0) && ctx->options & DHCPCD_FORKED) { eloop_exit(ctx->eloop, len == 0 ? EXIT_SUCCESS : EXIT_FAILURE); return len; } iov[0].iov_len = (size_t)len; len = ps_sendcmdmsg(wfd, cmd, &msg); #ifdef PRIVSEP_DEBUG logdebugx("%s: send fd %d, %zu bytes", __func__, wfd, len); #endif if ((len == -1 || len == 0) && ctx->options & DHCPCD_FORKED) eloop_exit(ctx->eloop, len == 0 ? EXIT_SUCCESS : EXIT_FAILURE); return len; } ssize_t ps_recvpsmsg(struct dhcpcd_ctx *ctx, int fd, ssize_t (*callback)(void *, struct ps_msghdr *, struct msghdr *), void *cbctx) { struct ps_msg psm; ssize_t len; size_t dlen; struct iovec iov[1]; struct msghdr msg = { .msg_iov = iov, .msg_iovlen = 1 }; bool stop = false; len = read(fd, &psm, sizeof(psm)); #ifdef PRIVSEP_DEBUG logdebugx("%s: %zd", __func__, len); #endif if (len == -1 && (errno == ECONNRESET || errno == EBADF)) len = 0; if (len == -1 || len == 0) stop = true; else { dlen = (size_t)len; if (dlen < sizeof(psm.psm_hdr)) { errno = EINVAL; return -1; } if (psm.psm_hdr.ps_cmd == PS_STOP) { stop = true; len = 0; } } if (stop) { #ifdef PRIVSEP_DEBUG logdebugx("process %d stopping", getpid()); #endif ps_free(ctx); eloop_exit(ctx->eloop, len != -1 ? EXIT_SUCCESS : EXIT_FAILURE); return len; } dlen -= sizeof(psm.psm_hdr); if (ps_unrollmsg(&msg, &psm.psm_hdr, psm.psm_data, dlen) == -1) return -1; if (callback == NULL) return 0; errno = 0; return callback(cbctx, &psm.psm_hdr, &msg); } struct ps_process * ps_findprocess(struct dhcpcd_ctx *ctx, struct ps_id *psid) { struct ps_process *psp; TAILQ_FOREACH(psp, &ctx->ps_processes, next) { if (memcmp(&psp->psp_id, psid, sizeof(psp->psp_id)) == 0) return psp; } errno = ESRCH; return NULL; } struct ps_process * ps_newprocess(struct dhcpcd_ctx *ctx, struct ps_id *psid) { struct ps_process *psp; psp = calloc(1, sizeof(*psp)); if (psp == NULL) return NULL; psp->psp_ctx = ctx; memcpy(&psp->psp_id, psid, sizeof(psp->psp_id)); psp->psp_work_fd = -1; TAILQ_INSERT_TAIL(&ctx->ps_processes, psp, next); return psp; } void ps_freeprocesses(struct dhcpcd_ctx *ctx, struct ps_process *notthis) { struct ps_process *psp, *psn; TAILQ_FOREACH_SAFE(psp, &ctx->ps_processes, next, psn) { if (psp == notthis) continue; ps_freeprocess(psp); } }
