Mercurial > hg > dhcpcd
view src/privsep-root.c @ 5253:7a0d53acbb06 draft
privsep: Validate UDP ports
Just like we filter the ioctls.
| author | Roy Marples <roy@marples.name> |
|---|---|
| date | Thu, 21 May 2020 16:53:54 +0100 |
| parents | a8c2969955f9 |
| children | ee23398a68db |
line wrap: on
line source
/* SPDX-License-Identifier: BSD-2-Clause */ /* * Privilege Separation for dhcpcd, privileged actioneer * 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. */ #include <sys/ioctl.h> #include <sys/socket.h> #include <sys/stat.h> #include <sys/time.h> #include <sys/types.h> #include <sys/wait.h> #include <assert.h> #include <errno.h> #include <fcntl.h> #include <pwd.h> #include <signal.h> #include <stddef.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include "common.h" #include "dhcpcd.h" #include "dhcp6.h" #include "eloop.h" #include "if.h" #include "ipv6nd.h" #include "logerr.h" #include "privsep.h" #include "sa.h" #include "script.h" __CTASSERT(sizeof(ioctl_request_t) <= sizeof(unsigned long)); struct psr_error { ssize_t psr_result; int psr_errno; char psr_pad[sizeof(ssize_t) - sizeof(int)]; size_t psr_datalen; }; struct psr_ctx { struct dhcpcd_ctx *psr_ctx; struct psr_error psr_error; size_t psr_datalen; void *psr_data; }; static void ps_root_readerrorsig(__unused int sig, void *arg) { struct dhcpcd_ctx *ctx = arg; eloop_exit(ctx->ps_eloop, EXIT_FAILURE); } static void ps_root_readerrorcb(void *arg) { struct psr_ctx *psr_ctx = arg; struct dhcpcd_ctx *ctx = psr_ctx->psr_ctx; struct psr_error *psr_error = &psr_ctx->psr_error; struct iovec iov[] = { { .iov_base = psr_error, .iov_len = sizeof(*psr_error) }, { .iov_base = psr_ctx->psr_data, .iov_len = psr_ctx->psr_datalen }, }; ssize_t len; int exit_code = EXIT_FAILURE; #define PSR_ERROR(e) \ do { \ psr_error->psr_result = -1; \ psr_error->psr_errno = (e); \ goto out; \ } while (0 /* CONSTCOND */) len = readv(ctx->ps_root_fd, iov, __arraycount(iov)); if (len == -1) PSR_ERROR(errno); else if ((size_t)len < sizeof(*psr_error)) PSR_ERROR(EINVAL); exit_code = EXIT_SUCCESS; out: eloop_exit(ctx->ps_eloop, exit_code); } ssize_t ps_root_readerror(struct dhcpcd_ctx *ctx, void *data, size_t len) { struct psr_ctx psr_ctx = { .psr_ctx = ctx, .psr_data = data, .psr_datalen = len, }; if (eloop_event_add(ctx->ps_eloop, ctx->ps_root_fd, ps_root_readerrorcb, &psr_ctx) == -1) return -1; eloop_start(ctx->ps_eloop, &ctx->sigset); errno = psr_ctx.psr_error.psr_errno; return psr_ctx.psr_error.psr_result; } #ifdef HAVE_CAPSICUM static void ps_root_mreaderrorcb(void *arg) { struct psr_ctx *psr_ctx = arg; struct dhcpcd_ctx *ctx = psr_ctx->psr_ctx; struct psr_error *psr_error = &psr_ctx->psr_error; struct iovec iov[] = { { .iov_base = psr_error, .iov_len = sizeof(*psr_error) }, { .iov_base = NULL, .iov_len = 0 }, }; ssize_t len; int exit_code = EXIT_FAILURE; len = recv(ctx->ps_root_fd, psr_error, sizeof(*psr_error), MSG_PEEK); if (len == -1) PSR_ERROR(errno); else if ((size_t)len < sizeof(*psr_error)) PSR_ERROR(EINVAL); if (psr_error->psr_datalen != 0) { psr_ctx->psr_data = malloc(psr_error->psr_datalen); if (psr_ctx->psr_data == NULL) PSR_ERROR(errno); psr_ctx->psr_datalen = psr_error->psr_datalen; iov[1].iov_base = psr_ctx->psr_data; iov[1].iov_len = psr_ctx->psr_datalen; } len = readv(ctx->ps_root_fd, iov, __arraycount(iov)); if (len == -1) PSR_ERROR(errno); else if ((size_t)len != sizeof(*psr_error) + psr_ctx->psr_datalen) PSR_ERROR(EINVAL); exit_code = EXIT_SUCCESS; out: eloop_exit(ctx->ps_eloop, exit_code); } ssize_t ps_root_mreaderror(struct dhcpcd_ctx *ctx, void **data, size_t *len) { struct psr_ctx psr_ctx = { .psr_ctx = ctx, }; if (eloop_event_add(ctx->ps_eloop, ctx->ps_root_fd, ps_root_mreaderrorcb, &psr_ctx) == -1) return -1; eloop_start(ctx->ps_eloop, &ctx->sigset); errno = psr_ctx.psr_error.psr_errno; *data = psr_ctx.psr_data; *len = psr_ctx.psr_datalen; return psr_ctx.psr_error.psr_result; } #endif static ssize_t ps_root_writeerror(struct dhcpcd_ctx *ctx, ssize_t result, void *data, size_t len) { struct psr_error psr = { .psr_result = result, .psr_errno = errno, .psr_datalen = len, }; struct iovec iov[] = { { .iov_base = &psr, .iov_len = sizeof(psr) }, { .iov_base = data, .iov_len = len }, }; #ifdef PRIVSEP_DEBUG logdebugx("%s: result %zd errno %d", __func__, result, errno); #endif return writev(ctx->ps_root_fd, iov, __arraycount(iov)); } static ssize_t ps_root_doioctl(unsigned long req, void *data, size_t len) { int s, err; /* Only allow these ioctls */ switch(req) { #ifdef SIOCAIFADDR case SIOCAIFADDR: /* FALLTHROUGH */ case SIOCDIFADDR: /* FALLTHROUGH */ #endif #ifdef SIOCSIFHWADDR case SIOCSIFHWADDR: /* FALLTHROUGH */ #endif #ifdef SIOCGIFPRIORITY case SIOCGIFPRIORITY: /* FALLTHROUGH */ #endif case SIOCSIFFLAGS: /* FALLTHROUGH */ case SIOCGIFMTU: /* FALLTHROUGH */ case SIOCSIFMTU: break; default: errno = EPERM; return -1; } s = socket(PF_INET, SOCK_DGRAM, 0); if (s != -1) #ifdef IOCTL_REQUEST_TYPE { ioctl_request_t reqt; memcpy(&reqt, &req, sizeof(reqt)); err = ioctl(s, reqt, data, len); } #else err = ioctl(s, req, data, len); #endif else err = -1; if (s != -1) close(s); return err; } static ssize_t ps_root_run_script(struct dhcpcd_ctx *ctx, const void *data, size_t len) { const char *envbuf = data; char * const argv[] = { UNCONST(data), NULL }; pid_t pid; int status; #ifdef PRIVSEP_DEBUG logdebugx("%s: IN %zu", __func__, len); #endif if (len == 0) return 0; /* Script is the first one, find the environment buffer. */ while (*envbuf != '\0') { if (len == 0) return EINVAL; envbuf++; len--; } if (len != 0) { envbuf++; len--; } #ifdef PRIVSEP_DEBUG logdebugx("%s: run script: %s", __func__, argv[0]); #endif if (script_buftoenv(ctx, UNCONST(envbuf), len) == NULL) return -1; pid = script_exec(argv, ctx->script_env); if (pid == -1) return -1; /* Wait for the script to finish */ while (waitpid(pid, &status, 0) == -1) { if (errno != EINTR) { logerr(__func__); status = 0; break; } } return status; } static bool ps_root_validpath(const struct dhcpcd_ctx *ctx, uint16_t cmd, const char *path) { if (cmd == PS_READFILE) { if (strcmp(ctx->cffile, path) == 0) return true; } if (strncmp(DBDIR, path, strlen(DBDIR)) == 0) return true; if (strncmp(RUNDIR, path, strlen(RUNDIR)) == 0) return true; errno = EPERM; return false; } static ssize_t ps_root_dowritefile(const struct dhcpcd_ctx *ctx, mode_t mode, void *data, size_t len) { char *file = data, *nc; nc = memchr(file, '\0', len); if (nc == NULL) { errno = EINVAL; return -1; } if (!ps_root_validpath(ctx, PS_WRITEFILE, file)) return -1; nc++; return writefile(file, mode, nc, len - (size_t)(nc - file)); } #ifdef HAVE_CAPSICUM #define IFA_NADDRS 3 static ssize_t ps_root_dogetifaddrs(void **rdata, size_t *rlen) { struct ifaddrs *ifaddrs, *ifa; size_t len; uint8_t *buf, *sap; socklen_t salen; void *ifdata; if (getifaddrs(&ifaddrs) == -1) return -1; if (ifaddrs == NULL) { *rdata = NULL; *rlen = 0; return 0; } /* Work out the buffer length required. * Ensure everything is aligned correctly, which does * create a larger buffer than what is needed to send, * but makes creating the same structure in the client * much easier. */ len = 0; for (ifa = ifaddrs; ifa != NULL; ifa = ifa->ifa_next) { len += ALIGN(sizeof(*ifa)); len += ALIGN(IFNAMSIZ); len += ALIGN(sizeof(salen) * IFA_NADDRS); if (ifa->ifa_addr != NULL) len += ALIGN(sa_len(ifa->ifa_addr)); if (ifa->ifa_netmask != NULL) len += ALIGN(sa_len(ifa->ifa_netmask)); if (ifa->ifa_broadaddr != NULL) len += ALIGN(sa_len(ifa->ifa_broadaddr)); } /* Use calloc to set everything to zero. * This satisfies memory sanitizers because don't write * where we don't need to. */ buf = calloc(1, len); if (buf == NULL) { freeifaddrs(ifaddrs); return -1; } *rdata = buf; *rlen = len; for (ifa = ifaddrs; ifa != NULL; ifa = ifa->ifa_next) { /* Don't carry ifa_data. */ ifdata = ifa->ifa_data; ifa->ifa_data = NULL; memcpy(buf, ifa, sizeof(*ifa)); buf += ALIGN(sizeof(*ifa)); ifa->ifa_data = ifdata; strlcpy((char *)buf, ifa->ifa_name, IFNAMSIZ); buf += ALIGN(IFNAMSIZ); sap = buf; buf += ALIGN(sizeof(salen) * IFA_NADDRS); #define COPYINSA(addr) \ do { \ salen = sa_len((addr)); \ if (salen != 0) { \ memcpy(sap, &salen, sizeof(salen)); \ memcpy(buf, (addr), salen); \ buf += ALIGN(salen); \ } \ sap += sizeof(salen); \ } while (0 /*CONSTCOND */) if (ifa->ifa_addr != NULL) COPYINSA(ifa->ifa_addr); if (ifa->ifa_netmask != NULL) COPYINSA(ifa->ifa_netmask); if (ifa->ifa_broadaddr != NULL) COPYINSA(ifa->ifa_broadaddr); } freeifaddrs(ifaddrs); return 0; } #endif static ssize_t ps_root_recvmsgcb(void *arg, struct ps_msghdr *psm, struct msghdr *msg) { struct dhcpcd_ctx *ctx = arg; uint16_t cmd; struct ps_process *psp; struct iovec *iov = msg->msg_iov; void *data = iov->iov_base, *rdata = NULL; size_t len = iov->iov_len, rlen = 0; uint8_t buf[PS_BUFLEN]; time_t mtime; ssize_t err; bool free_rdata = false; cmd = (uint16_t)(psm->ps_cmd & ~(PS_START | PS_STOP)); psp = ps_findprocess(ctx, &psm->ps_id); #ifdef PRIVSEP_DEBUG logerrx("%s: IN cmd %x, psp %p", __func__, psm->ps_cmd, psp); #endif if (psp != NULL) { if (psm->ps_cmd & PS_STOP) { int ret = ps_dostop(ctx, &psp->psp_pid, &psp->psp_fd); ps_freeprocess(psp); return ret; } else if (!(psm->ps_cmd & PS_START)) return ps_sendpsmmsg(ctx, psp->psp_fd, psm, msg); /* Process has already started .... */ return 0; } if (psm->ps_cmd & PS_STOP && psp == NULL) return 0; switch (cmd) { #ifdef INET #ifdef ARP case PS_BPF_ARP: /* FALLTHROUGH */ #endif case PS_BPF_BOOTP: return ps_bpf_cmd(ctx, psm, msg); #endif #ifdef INET case PS_BOOTP: return ps_inet_cmd(ctx, psm, msg); #endif #ifdef INET6 #ifdef DHCP6 case PS_DHCP6: /* FALLTHROUGH */ #endif case PS_ND: return ps_inet_cmd(ctx, psm, msg); #endif default: break; } assert(msg->msg_iovlen == 0 || msg->msg_iovlen == 1); /* Reset errno */ errno = 0; switch (psm->ps_cmd) { case PS_IOCTL: err = ps_root_doioctl(psm->ps_flags, data, len); if (err != -1) { rdata = data; rlen = len; } break; case PS_SCRIPT: err = ps_root_run_script(ctx, data, len); break; case PS_UNLINK: if (!ps_root_validpath(ctx, psm->ps_cmd, data)) { err = -1; break; } err = unlink(data); break; case PS_READFILE: if (!ps_root_validpath(ctx, psm->ps_cmd, data)) { err = -1; break; } err = readfile(data, buf, sizeof(buf)); if (err != -1) { rdata = buf; rlen = (size_t)err; } break; case PS_WRITEFILE: err = ps_root_dowritefile(ctx, (mode_t)psm->ps_flags, data, len); break; case PS_FILEMTIME: err = filemtime(data, &mtime); if (err != -1) { rdata = &mtime; rlen = sizeof(mtime); } break; #ifdef HAVE_CAPSICUM case PS_GETIFADDRS: err = ps_root_dogetifaddrs(&rdata, &rlen); free_rdata = true; break; #endif default: err = ps_root_os(psm, msg); break; } err = ps_root_writeerror(ctx, err, rlen != 0 ? rdata : 0, rlen); if (free_rdata) free(rdata); return err; } /* Receive from state engine, do an action. */ static void ps_root_recvmsg(void *arg) { struct dhcpcd_ctx *ctx = arg; if (ps_recvpsmsg(ctx, ctx->ps_root_fd, ps_root_recvmsgcb, ctx) == -1 && errno != ECONNRESET) logerr(__func__); } static int ps_root_startcb(void *arg) { struct dhcpcd_ctx *ctx = arg; if (ctx->options & DHCPCD_MASTER) setproctitle("[privileged actioneer]"); else setproctitle("[privileged actioneer] %s%s%s", ctx->ifv[0], ctx->options & DHCPCD_IPV4 ? " [ip4]" : "", ctx->options & DHCPCD_IPV6 ? " [ip6]" : ""); ctx->ps_root_pid = getpid(); ctx->options |= DHCPCD_PRIVSEPROOT; /* Open network sockets for sending. * This is a small bit wasteful for non sandboxed OS's * but makes life very easy for unicasting DHCPv6 in non master * mode as we no longer care about address selection. */ #ifdef INET ctx->udp_wfd = xsocket(PF_INET, SOCK_RAW | SOCK_CXNB, IPPROTO_UDP); if (ctx->udp_wfd == -1) return -1; #endif #ifdef INET6 ctx->nd_fd = ipv6nd_open(false); if (ctx->nd_fd == -1) return -1; #endif #ifdef DHCP6 ctx->dhcp6_wfd = dhcp6_openraw(); if (ctx->dhcp6_wfd == -1) return -1; #endif return 0; } static void ps_root_signalcb(int sig, void *arg) { struct dhcpcd_ctx *ctx = arg; /* Ignore SIGINT, respect PS_STOP command or SIGTERM. */ if (sig == SIGINT) return; logerrx("process %d unexpectedly terminating on signal %d", getpid(), sig); if (ctx->ps_root_pid == getpid()) { shutdown(ctx->ps_root_fd, SHUT_RDWR); shutdown(ctx->ps_data_fd, SHUT_RDWR); } eloop_exit(ctx->eloop, sig == SIGTERM ? EXIT_SUCCESS : EXIT_FAILURE); } static ssize_t ps_root_dispatchcb(void *arg, struct ps_msghdr *psm, struct msghdr *msg) { struct dhcpcd_ctx *ctx = arg; ssize_t err; #ifdef INET err = ps_bpf_dispatch(ctx, psm, msg); if (err == -1 && errno == ENOTSUP) #endif err = ps_inet_dispatch(ctx, psm, msg); return err; } static void ps_root_dispatch(void *arg) { struct dhcpcd_ctx *ctx = arg; if (ps_recvpsmsg(ctx, ctx->ps_data_fd, ps_root_dispatchcb, ctx) == -1) logerr(__func__); } pid_t ps_root_start(struct dhcpcd_ctx *ctx) { int fd[2]; pid_t pid; if (socketpair(AF_UNIX, SOCK_DGRAM | SOCK_CXNB, 0, fd) == -1) return -1; pid = ps_dostart(ctx, &ctx->ps_root_pid, &ctx->ps_root_fd, ps_root_recvmsg, NULL, ctx, ps_root_startcb, ps_root_signalcb, 0); if (pid == 0) { ctx->ps_data_fd = fd[1]; close(fd[0]); return 0; } else if (pid == -1) return -1; ctx->ps_data_fd = fd[0]; close(fd[1]); if (eloop_event_add(ctx->eloop, ctx->ps_data_fd, ps_root_dispatch, ctx) == -1) return -1; if ((ctx->ps_eloop = eloop_new()) == NULL) return -1; if (eloop_signal_set_cb(ctx->ps_eloop, dhcpcd_signals, dhcpcd_signals_len, ps_root_readerrorsig, ctx) == -1) return -1; return pid; } int ps_root_stop(struct dhcpcd_ctx *ctx) { return ps_dostop(ctx, &ctx->ps_root_pid, &ctx->ps_root_fd); } ssize_t ps_root_script(const struct interface *ifp, const void *data, size_t len) { char buf[PS_BUFLEN], *p = buf; size_t blen = PS_BUFLEN, slen = strlen(ifp->options->script) + 1; #ifdef PRIVSEP_DEBUG logdebugx("%s: sending script: %zu %s len %zu", __func__, slen, ifp->options->script, len); #endif if (slen > blen) { errno = ENOBUFS; return -1; } memcpy(p, ifp->options->script, slen); p += slen; blen -= slen; if (len > blen) { errno = ENOBUFS; return -1; } memcpy(p, data, len); #ifdef PRIVSEP_DEBUG logdebugx("%s: sending script data: %zu", __func__, slen + len); #endif if (ps_sendcmd(ifp->ctx, ifp->ctx->ps_root_fd, PS_SCRIPT, 0, buf, slen + len) == -1) return -1; return ps_root_readerror(ifp->ctx, NULL, 0); } ssize_t ps_root_ioctl(struct dhcpcd_ctx *ctx, ioctl_request_t req, void *data, size_t len) { #ifdef IOCTL_REQUEST_TYPE unsigned long ulreq = 0; memcpy(&ulreq, &req, sizeof(req)); if (ps_sendcmd(ctx, ctx->ps_root_fd, PS_IOCTL, ulreq, data, len) == -1) return -1; #else if (ps_sendcmd(ctx, ctx->ps_root_fd, PS_IOCTL, req, data, len) == -1) return -1; #endif return ps_root_readerror(ctx, data, len); } ssize_t ps_root_unlink(struct dhcpcd_ctx *ctx, const char *file) { if (ps_sendcmd(ctx, ctx->ps_root_fd, PS_UNLINK, 0, file, strlen(file) + 1) == -1) return -1; return ps_root_readerror(ctx, NULL, 0); } ssize_t ps_root_readfile(struct dhcpcd_ctx *ctx, const char *file, void *data, size_t len) { if (ps_sendcmd(ctx, ctx->ps_root_fd, PS_READFILE, 0, file, strlen(file) + 1) == -1) return -1; return ps_root_readerror(ctx, data, len); } ssize_t ps_root_writefile(struct dhcpcd_ctx *ctx, const char *file, mode_t mode, const void *data, size_t len) { char buf[PS_BUFLEN]; size_t flen; flen = strlcpy(buf, file, sizeof(buf)); flen += 1; if (flen > sizeof(buf) || flen + len > sizeof(buf)) { errno = ENOBUFS; return -1; } memcpy(buf + flen, data, len); if (ps_sendcmd(ctx, ctx->ps_root_fd, PS_WRITEFILE, mode, buf, flen + len) == -1) return -1; return ps_root_readerror(ctx, NULL, 0); } ssize_t ps_root_filemtime(struct dhcpcd_ctx *ctx, const char *file, time_t *time) { if (ps_sendcmd(ctx, ctx->ps_root_fd, PS_FILEMTIME, 0, file, strlen(file) + 1) == -1) return -1; return ps_root_readerror(ctx, time, sizeof(*time)); } #ifdef HAVE_CAPSICUM int ps_root_getifaddrs(struct dhcpcd_ctx *ctx, struct ifaddrs **ifahead) { struct ifaddrs *ifa; void *buf = NULL; char *bp, *sap; socklen_t salen; size_t len; ssize_t err; if (ps_sendcmd(ctx, ctx->ps_root_fd, PS_GETIFADDRS, 0, NULL, 0) == -1) return -1; err = ps_root_mreaderror(ctx, &buf, &len); if (err == -1) return -1; /* Should be impossible - lo0 will always exist. */ if (len == 0) { *ifahead = NULL; return 0; } bp = buf; *ifahead = (struct ifaddrs *)(void *)bp; for (ifa = *ifahead; len != 0; ifa = ifa->ifa_next) { if (len < ALIGN(sizeof(*ifa)) + ALIGN(IFNAMSIZ) + ALIGN(sizeof(salen) * IFA_NADDRS)) goto err; bp += ALIGN(sizeof(*ifa)); ifa->ifa_name = bp; bp += ALIGN(IFNAMSIZ); sap = bp; bp += ALIGN(sizeof(salen) * IFA_NADDRS); len -= ALIGN(sizeof(*ifa)) + ALIGN(IFNAMSIZ) + ALIGN(sizeof(salen) * IFA_NADDRS); #define COPYOUTSA(addr) \ do { \ memcpy(&salen, sap, sizeof(salen)); \ if (len < salen) \ goto err; \ if (salen != 0) { \ (addr) = (struct sockaddr *)bp; \ bp += ALIGN(salen); \ len -= ALIGN(salen); \ } \ sap += sizeof(salen); \ } while (0 /* CONSTCOND */) COPYOUTSA(ifa->ifa_addr); COPYOUTSA(ifa->ifa_netmask); COPYOUTSA(ifa->ifa_broadaddr); ifa->ifa_next = (struct ifaddrs *)(void *)bp; } ifa->ifa_next = NULL; return 0; err: free(buf); *ifahead = NULL; errno = EINVAL; return -1; } #endif
