view src/privsep.c @ 5441:ff7c7b4799b3 draft

dhcpcd: Redirect stdout/stderr to the launcher stderr descriptor This actually make life really simple! We no longer need to redirect stdout/stderr to /dev/null for privsep and any script output is now captured again - and it all goes to stderr as it should even if a script wants it to go to stdout. On the happy path, only the master process will actually log anything to stderr so we turn that off after we "fork". On the unhappy path, logging to stderr/stdout *may* fail because the launcher process *may* have exited. We *could* have the master process as an intermediary but that's just excess code to avoid errors which *should* not happen. Regardless, any errror should still hit syslog.
author Roy Marples <roy@marples.name>
date Sun, 06 Sep 2020 02:41:08 +0100
parents 248013138b09
children 2d1bbc57daeb
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.
 * All privsep processes ignore signals - only the master process accepts them.
 *
 * dhcpcd will maintain the config file in the chroot, no need to handle
 * this in a script or something.
 */

#include <sys/resource.h>
#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 "dev.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' as %s", pw->pw_dir, pw->pw_name);
	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;
	}

	struct rlimit rzero = { .rlim_cur = 0, .rlim_max = 0 };

	if (ctx->ps_control_pid != getpid()) {
		/* Prohibit new files, sockets, etc */
#if defined(__linux__) || defined(__sun) || defined(__OpenBSD__)
		/*
		 * If poll(2) is called with nfds > RLIMIT_NOFILE
		 * then it returns EINVAL.
		 * This blows.
		 * Do the best we can and limit to what we need.
		 * An attacker could potentially close a file and
		 * open a new one still, but that cannot be helped.
		 */
		unsigned long maxfd;
		maxfd = (unsigned long)eloop_event_count(ctx->eloop);
		if (IN_PRIVSEP_SE(ctx))
			maxfd++; /* XXX why? */

		struct rlimit rmaxfd = {
		    .rlim_cur = maxfd,
		    .rlim_max = maxfd
		};
		if (setrlimit(RLIMIT_NOFILE, &rmaxfd) == -1)
			logerr("setrlimit RLIMIT_NOFILE");
#else
		if (setrlimit(RLIMIT_NOFILE, &rzero) == -1)
			logerr("setrlimit RLIMIT_NOFILE");
#endif
	}

	/* Prohibit writing to files.
	 * Obviously this won't work if we are using a logfile
	 * or redirecting stderr to a file. */
	if (ctx->logfile == NULL) {
		if (setrlimit(RLIMIT_FSIZE, &rzero) == -1)
			logerr("setrlimit RLIMIT_FSIZE");
	}

#ifdef RLIMIT_NPROC
	/* Prohibit forks */
	if (setrlimit(RLIMIT_NPROC, &rzero) == -1)
		logerr("setrlimit RLIMIT_NPROC");
#endif

	return 0;
}

static int
ps_setbuf0(int fd, int ctl, int minlen)
{
	int len;
	socklen_t slen;

	slen = sizeof(len);
	if (getsockopt(fd, SOL_SOCKET, ctl, &len, &slen) == -1)
		return -1;

#ifdef __linux__
	len /= 2;
#endif
	if (len >= minlen)
		return 0;

	return setsockopt(fd, SOL_SOCKET, ctl, &minlen, sizeof(minlen));
}

static int
ps_setbuf(int fd)
{
	/* Ensure we can receive a fully sized privsep message.
	 * Double the send buffer. */
	int minlen = (int)sizeof(struct ps_msg);

	if (ps_setbuf0(fd, SO_RCVBUF, minlen) == -1 ||
	    ps_setbuf0(fd, SO_SNDBUF, minlen * 2) == -1)
	{
		logerr(__func__);
		return -1;
	}
	return 0;
}

int
ps_setbuf_fdpair(int fd[])
{

	if (ps_setbuf(fd[0]) == -1 || ps_setbuf(fd[1]) == -1)
		return -1;
	return 0;
}

#ifdef PRIVSEP_RIGHTS
int
ps_rights_limit_ioctl(int fd)
{
	cap_rights_t rights;

	cap_rights_init(&rights, CAP_IOCTL);
	if (cap_rights_limit(fd, &rights) == -1 && errno != ENOSYS)
		return -1;
	return 0;
}

int
ps_rights_limit_fd_fctnl(int fd)
{
	cap_rights_t rights;

	cap_rights_init(&rights, CAP_READ, CAP_WRITE, CAP_EVENT,
	    CAP_ACCEPT, CAP_FCNTL);
	if (cap_rights_limit(fd, &rights) == -1 && errno != ENOSYS)
		return -1;
	return 0;
}

int
ps_rights_limit_fd(int fd)
{
	cap_rights_t rights;

	cap_rights_init(&rights, CAP_READ, CAP_WRITE, CAP_EVENT, CAP_SHUTDOWN);
	if (cap_rights_limit(fd, &rights) == -1 && errno != ENOSYS)
		return -1;
	return 0;
}

int
ps_rights_limit_fd_rdonly(int fd)
{
	cap_rights_t rights;

	cap_rights_init(&rights, CAP_READ, CAP_EVENT);
	if (cap_rights_limit(fd, &rights) == -1 && errno != ENOSYS)
		return -1;
	return 0;
}

int
ps_rights_limit_fdpair(int fd[])
{

	if (ps_rights_limit_fd(fd[0]) == -1 || ps_rights_limit_fd(fd[1]) == -1)
		return -1;
	return 0;
}
#endif

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 fd[2];
	pid_t pid;

	if (xsocketpair(AF_UNIX, SOCK_DGRAM | SOCK_CXNB, 0, fd) == -1) {
		logerr("%s: socketpair", __func__);
		return -1;
	}
	if (ps_setbuf_fdpair(fd) == -1) {
		logerr("%s: ps_setbuf_fdpair", __func__);
		return -1;
	}
#ifdef PRIVSEP_RIGHTS
	if (ps_rights_limit_fdpair(fd) == -1) {
		logerr("%s: ps_rights_limit_fdpair", __func__);
		return -1;
	}
#endif

	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)
			;
		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;
	}

	eloop_signal_set_cb(ctx->eloop,
	    dhcpcd_signals, dhcpcd_signals_len, signal_cb, ctx);

	/* 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;
	}

	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 (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)
		(void)ps_sendcmd(ctx, *priv_fd, PS_STOP, 0, NULL, 0);
	shutdown(*priv_fd, SHUT_RDWR);
	*priv_fd = -1;
	eloop_exit(ctx->eloop, EXIT_FAILURE);
	return -1;
}

int
ps_dostop(struct dhcpcd_ctx *ctx, pid_t *pid, int *fd)
{
	int err = 0;

#ifdef PRIVSEP_DEBUG
	logdebugx("%s: pid=%d fd=%d", __func__, *pid, *fd);
#endif

	if (*fd != -1) {
		eloop_event_delete(ctx->eloop, *fd);
		if (ps_sendcmd(ctx, *fd, PS_STOP, 0, NULL, 0) == -1) {
			logerr(__func__);
			err = -1;
		}
		(void)shutdown(*fd, SHUT_RDWR);
		close(*fd);
		*fd = -1;
	}

	/* Don't wait for the process as it may not respond to the shutdown
	 * request. We'll reap the process on receipt of SIGCHLD. */
	*pid = 0;
	return err;
}

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_net;

	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_net:
	if (!(ctx->options & DHCPCD_TEST)) {
		switch (pid = ps_ctl_start(ctx)) {
		case -1:
			return -1;
		case 0:
			return 0;
		default:
			logdebugx("spawned controller proxy on PID %d", pid);
		}
	}

#ifdef ARC4RANDOM_H
	/* Seed the random number generator early incase it needs /dev/urandom
	 * which won't be available in the chroot. */
	arc4random();
#endif

	return 1;
}

int
ps_mastersandbox(struct dhcpcd_ctx *ctx)
{

	if (ps_dropprivs(ctx) == -1) {
		logerr("%s: ps_dropprivs", __func__);
		return -1;
	}

#ifdef PRIVSEP_RIGHTS
	if ((ps_rights_limit_ioctl(ctx->pf_inet_fd) == -1 ||
	     ps_rights_limit_fd(ctx->link_fd) == -1) &&
	    errno != ENOSYS)
	{
		logerr("%s: cap_rights_limit", __func__);
		return -1;
	}
#endif
#ifdef HAVE_CAPSICUM
	if (cap_enter() == -1 && errno != ENOSYS) {
		logerr("%s: cap_enter", __func__);
		return -1;
	}
#endif
#ifdef HAVE_PLEDGE
	if (pledge("stdio route", NULL) == -1) {
		logerr("%s: pledge", __func__);
		return -1;
	}
#endif

	return 0;
}

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_ctl_stop(ctx);
	if (r != 0)
		ret = r;

	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];
		int 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 < (int)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);
	if (len == -1) {
		logerr(__func__);
		if (ctx->options & DHCPCD_FORKED &&
		    !(ctx->options & DHCPCD_PRIVSEPROOT))
			eloop_exit(ctx->eloop, 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);

	if (len == -1)
		logerr("%s: recvmsg", __func__);
	if (len == -1 || len == 0) {
		if (ctx->options & DHCPCD_FORKED &&
		    !(ctx->options & DHCPCD_PRIVSEPROOT))
			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);
	if (len == -1) {
		logerr("ps_sendcmdmsg");
		if (ctx->options & DHCPCD_FORKED &&
		    !(ctx->options & DHCPCD_PRIVSEPROOT))
			eloop_exit(ctx->eloop, 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 || 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);
#ifdef PLUGIN_DEV
		dev_stop(ctx);
#endif
		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);
	}
}