view src/script.c @ 5255:ee23398a68db draft

dhcpcd: Move the script file from per interface to global context This *should* affect no-one, but you never know. The primary motivation for this is to ensure that nothing arbitary can be executed by the root process if anyone breaks into the chrooted unprivileged master process. It also makes for smaller code.
author Roy Marples <roy@marples.name>
date Thu, 21 May 2020 18:28:27 +0100
parents 7406014c9120
children d569724efab3
line wrap: on
line source

/* stSPDX-License-Identifier: BSD-2-Clause */
/*
 * dhcpcd - DHCP client daemon
 * 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/stat.h>
#include <sys/uio.h>
#include <sys/wait.h>

#include <netinet/in.h>
#include <arpa/inet.h>

#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <pwd.h>
#include <signal.h>
#include <spawn.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "config.h"
#include "common.h"
#include "dhcp.h"
#include "dhcp6.h"
#include "eloop.h"
#include "if.h"
#include "if-options.h"
#include "ipv4ll.h"
#include "ipv6nd.h"
#include "logerr.h"
#include "privsep.h"
#include "script.h"

#define DEFAULT_PATH	"/usr/bin:/usr/sbin:/bin:/sbin"

static const char * const if_params[] = {
	"interface",
	"protocol",
	"reason",
	"pid",
	"ifcarrier",
	"ifmetric",
	"ifwireless",
	"ifflags",
	"ssid",
	"profile",
	"interface_order",
	NULL
};

void
if_printoptions(void)
{
	const char * const *p;

	for (p = if_params; *p; p++)
		printf(" -  %s\n", *p);
}

pid_t
script_exec(char *const *argv, char *const *env)
{
	pid_t pid = 0;
	posix_spawnattr_t attr;
	int r;
#ifdef USE_SIGNALS
	size_t i;
	short flags;
	sigset_t defsigs;
#else
	UNUSED(ctx);
#endif

	/* posix_spawn is a safe way of executing another image
	 * and changing signals back to how they should be. */
	if (posix_spawnattr_init(&attr) == -1)
		return -1;
#ifdef USE_SIGNALS
	flags = POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF;
	posix_spawnattr_setflags(&attr, flags);
	sigemptyset(&defsigs);
	posix_spawnattr_setsigmask(&attr, &defsigs);
	for (i = 0; i < dhcpcd_signals_len; i++)
		sigaddset(&defsigs, dhcpcd_signals[i]);
	for (i = 0; i < dhcpcd_signals_ignore_len; i++)
		sigaddset(&defsigs, dhcpcd_signals_ignore[i]);
	posix_spawnattr_setsigdefault(&attr, &defsigs);
#endif
	errno = 0;
	r = posix_spawn(&pid, argv[0], NULL, &attr, argv, env);
	posix_spawnattr_destroy(&attr);
	if (r) {
		errno = r;
		return -1;
	}
	return pid;
}

#ifdef INET
static int
append_config(FILE *fp, const char *prefix, const char *const *config)
{
	size_t i;

	if (config == NULL)
		return 0;

	/* Do we need to replace existing config rather than append? */
	for (i = 0; config[i] != NULL; i++) {
		if (efprintf(fp, "%s_%s", prefix, config[i]) == -1)
			return -1;
	}
	return 1;
}

#endif

#define	PROTO_LINK	0
#define	PROTO_DHCP	1
#define	PROTO_IPV4LL	2
#define	PROTO_RA	3
#define	PROTO_DHCP6	4
#define	PROTO_STATIC6	5
static const char *protocols[] = {
	"link",
	"dhcp",
	"ipv4ll",
	"ra",
	"dhcp6",
	"static6"
};

int
efprintf(FILE *fp, const char *fmt, ...)
{
	va_list args;
	int r;

	va_start(args, fmt);
	r = vfprintf(fp, fmt, args);
	va_end(args);
	if (r == -1)
		return -1;
	/* Write a trailing NULL so we can easily create env strings. */
	if (fputc('\0', fp) == EOF)
		return -1;
	return r;
}

char **
script_buftoenv(struct dhcpcd_ctx *ctx, char *buf, size_t len)
{
	char **env, **envp, *bufp, *endp;
	size_t nenv;

	/* Count the terminated env strings.
	 * Assert that the terminations are correct. */
	nenv = 0;
	endp = buf + len;
	for (bufp = buf; bufp < endp; bufp++) {
		if (*bufp == '\0') {
#ifndef NDEBUG
			if (bufp + 1 < endp)
				assert(*(bufp + 1) != '\0');
#endif
			nenv++;
		}
	}
	assert(*(bufp - 1) == '\0');

	if (ctx->script_envlen < nenv) {
		env = reallocarray(ctx->script_env, nenv + 1, sizeof(*env));
		if (env == NULL)
			return NULL;
		ctx->script_env = env;
		ctx->script_envlen = nenv;
	}

	bufp = buf;
	envp = ctx->script_env;
	*envp++ = bufp++;
	endp--; /* Avoid setting the last \0 to an invalid pointer */
	for (; bufp < endp; bufp++) {
		if (*bufp == '\0')
			*envp++ = bufp + 1;
	}
	*envp = NULL;

	return ctx->script_env;
}

static long
make_env(struct dhcpcd_ctx *ctx, const struct interface *ifp,
    const char *reason)
{
	FILE *fp;
	long buf_pos, i;
	char *path;
	int protocol = PROTO_LINK;
	const struct if_options *ifo;
	const struct interface *ifp2;
	int af;
#ifdef INET
	const struct dhcp_state *state;
#ifdef IPV4LL
	const struct ipv4ll_state *istate;
#endif
#endif
#ifdef DHCP6
	const struct dhcp6_state *d6_state;
#endif

#ifdef HAVE_OPEN_MEMSTREAM
	if (ctx->script_fp == NULL) {
		fp = open_memstream(&ctx->script_buf, &ctx->script_buflen);
		if (fp == NULL)
			goto eexit;
		ctx->script_fp = fp;
	} else {
		fp = ctx->script_fp;
		rewind(fp);
	}
#else
	char tmpfile[] = "/tmp/dhcpcd-script-env-XXXXXX";
	int tmpfd;

	fp = NULL;
	tmpfd = mkstemp(tmpfile);
	if (tmpfd == -1)
		goto eexit;
	unlink(tmpfile);
	fp = fdopen(tmpfd, "w+");
	if (fp == NULL) {
		close(tmpfd);
		goto eexit;
	}
#endif

	/* Needed for scripts */
	path = getenv("PATH");
	if (efprintf(fp, "PATH=%s", path == NULL ? DEFAULT_PATH:path) == -1)
		goto eexit;
	if (efprintf(fp, "reason=%s", reason) == -1)
		goto eexit;
	if (efprintf(fp, "pid=%d", getpid()) == -1)
		goto eexit;

#ifdef PRIVSEP
	if (ctx->options & DHCPCD_PRIVSEP && ctx->ps_user != NULL) {
		if (efprintf(fp, "chroot=%s", ctx->ps_user->pw_dir) == -1)
			goto eexit;
	}
	if (strcmp(reason, "CHROOT") == 0)
		goto make;
#endif

	ifo = ifp->options;
#ifdef INET
	state = D_STATE(ifp);
#ifdef IPV4LL
	istate = IPV4LL_CSTATE(ifp);
#endif
#endif
#ifdef DHCP6
	d6_state = D6_CSTATE(ifp);
#endif
	if (strcmp(reason, "TEST") == 0) {
		if (1 == 2) {
			/* This space left intentionally blank
			 * as all the below statements are optional. */
		}
#ifdef INET6
#ifdef DHCP6
		else if (d6_state && d6_state->new)
			protocol = PROTO_DHCP6;
#endif
		else if (ipv6nd_hasra(ifp))
			protocol = PROTO_RA;
#endif
#ifdef INET
#ifdef IPV4LL
		else if (istate && istate->addr != NULL)
			protocol = PROTO_IPV4LL;
#endif
		else
			protocol = PROTO_DHCP;
#endif
	}
#ifdef INET6
	else if (strcmp(reason, "STATIC6") == 0)
		protocol = PROTO_STATIC6;
#ifdef DHCP6
	else if (reason[strlen(reason) - 1] == '6')
		protocol = PROTO_DHCP6;
#endif
	else if (strcmp(reason, "ROUTERADVERT") == 0)
		protocol = PROTO_RA;
#endif
	else if (strcmp(reason, "PREINIT") == 0 ||
	    strcmp(reason, "CARRIER") == 0 ||
	    strcmp(reason, "NOCARRIER") == 0 ||
	    strcmp(reason, "UNKNOWN") == 0 ||
	    strcmp(reason, "DEPARTED") == 0 ||
	    strcmp(reason, "STOPPED") == 0)
		protocol = PROTO_LINK;
#ifdef INET
#ifdef IPV4LL
	else if (strcmp(reason, "IPV4LL") == 0)
		protocol = PROTO_IPV4LL;
#endif
	else
		protocol = PROTO_DHCP;
#endif


	if (efprintf(fp, "interface=%s", ifp->name) == -1)
		goto eexit;
	if (ifp->ctx->options & DHCPCD_DUMPLEASE)
		goto dumplease;
	if (efprintf(fp, "ifcarrier=%s",
	    ifp->carrier == LINK_UNKNOWN ? "unknown" :
	    ifp->carrier == LINK_UP ? "up" : "down") == -1)
		goto eexit;
	if (efprintf(fp, "ifmetric=%d", ifp->metric) == -1)
		goto eexit;
	if (efprintf(fp, "ifwireless=%d", ifp->wireless) == -1)
		goto eexit;
	if (efprintf(fp, "ifflags=%u", ifp->flags) == -1)
		goto eexit;
	if (efprintf(fp, "ifmtu=%d", if_getmtu(ifp)) == -1)
		goto eexit;

	if (fprintf(fp, "interface_order=") == -1)
		goto eexit;
	TAILQ_FOREACH(ifp2, ifp->ctx->ifaces, next) {
		if (ifp2 != TAILQ_FIRST(ifp->ctx->ifaces)) {
			if (fputc(' ', fp) == EOF)
				return -1;
		}
		if (fprintf(fp, "%s", ifp2->name) == -1)
			return -1;
	}
	if (fputc('\0', fp) == EOF)
		return -1;

	if (strcmp(reason, "STOPPED") == 0) {
		if (efprintf(fp, "if_up=false") == -1)
			goto eexit;
		if (efprintf(fp, "if_down=%s",
		    ifo->options & DHCPCD_RELEASE ? "true" : "false") == -1)
			goto eexit;
	} else if (strcmp(reason, "TEST") == 0 ||
	    strcmp(reason, "PREINIT") == 0 ||
	    strcmp(reason, "CARRIER") == 0 ||
	    strcmp(reason, "UNKNOWN") == 0)
	{
		if (efprintf(fp, "if_up=false") == -1)
			goto eexit;
		if (efprintf(fp, "if_down=false") == -1)
			goto eexit;
	} else if (1 == 2 /* appease ifdefs */
#ifdef INET
	    || (protocol == PROTO_DHCP && state && state->new)
#ifdef IPV4LL
	    || (protocol == PROTO_IPV4LL && IPV4LL_STATE_RUNNING(ifp))
#endif
#endif
#ifdef INET6
	    || (protocol == PROTO_STATIC6 && IPV6_STATE_RUNNING(ifp))
#ifdef DHCP6
	    || (protocol == PROTO_DHCP6 && d6_state && d6_state->new)
#endif
	    || (protocol == PROTO_RA && ipv6nd_hasra(ifp))
#endif
	    )
	{
		if (efprintf(fp, "if_up=true") == -1)
			goto eexit;
		if (efprintf(fp, "if_down=false") == -1)
			goto eexit;
	} else {
		if (efprintf(fp, "if_up=false") == -1)
			goto eexit;
		if (efprintf(fp, "if_down=true") == -1)
			goto eexit;
	}
	if (protocols[protocol] != NULL) {
		if (efprintf(fp, "protocol=%s", protocols[protocol]) == -1)
			goto eexit;
	}
	if ((af = dhcpcd_ifafwaiting(ifp)) != AF_MAX) {
		if (efprintf(fp, "if_afwaiting=%d", af) == -1)
			goto eexit;
	}
	if ((af = dhcpcd_afwaiting(ifp->ctx)) != AF_MAX) {
		TAILQ_FOREACH(ifp2, ifp->ctx->ifaces, next) {
			if ((af = dhcpcd_ifafwaiting(ifp2)) != AF_MAX)
				break;
		}
	}
	if (af != AF_MAX) {
		if (efprintf(fp, "af_waiting=%d", af) == -1)
			goto eexit;
	}
	if (ifo->options & DHCPCD_DEBUG) {
		if (efprintf(fp, "syslog_debug=true") == -1)
			goto eexit;
	}
	if (*ifp->profile != '\0') {
		if (efprintf(fp, "profile=%s", ifp->profile) == -1)
			goto eexit;
	}
	if (ifp->wireless) {
		char pssid[IF_SSIDLEN * 4];

		if (print_string(pssid, sizeof(pssid), OT_ESCSTRING,
		    ifp->ssid, ifp->ssid_len) != -1)
		{
			if (efprintf(fp, "ifssid=%s", pssid) == -1)
				goto eexit;
		}
	}
#ifdef INET
	if (protocol == PROTO_DHCP && state && state->old) {
		if (dhcp_env(fp, "old", ifp,
		    state->old, state->old_len) == -1)
			goto eexit;
		if (append_config(fp, "old",
		    (const char *const *)ifo->config) == -1)
			goto eexit;
	}
#endif
#ifdef DHCP6
	if (protocol == PROTO_DHCP6 && d6_state && d6_state->old) {
		if (dhcp6_env(fp, "old", ifp,
		    d6_state->old, d6_state->old_len) == -1)
			goto eexit;
	}
#endif

dumplease:
#ifdef INET
#ifdef IPV4LL
	if (protocol == PROTO_IPV4LL && istate) {
		if (ipv4ll_env(fp, istate->down ? "old" : "new", ifp) == -1)
			goto eexit;
	}
#endif
	if (protocol == PROTO_DHCP && state && state->new) {
		if (dhcp_env(fp, "new", ifp,
		    state->new, state->new_len) == -1)
			goto eexit;
		if (append_config(fp, "new",
		    (const char *const *)ifo->config) == -1)
			goto eexit;
	}
#endif
#ifdef INET6
	if (protocol == PROTO_STATIC6) {
		if (ipv6_env(fp, "new", ifp) == -1)
			goto eexit;
	}
#ifdef DHCP6
	if (protocol == PROTO_DHCP6 && D6_STATE_RUNNING(ifp)) {
		if (dhcp6_env(fp, "new", ifp,
		    d6_state->new, d6_state->new_len) == -1)
			goto eexit;
	}
#endif
	if (protocol == PROTO_RA) {
		if (ipv6nd_env(fp, ifp) == -1)
			goto eexit;
	}
#endif

	/* Add our base environment */
	if (ifo->environ) {
		for (i = 0; ifo->environ[i] != NULL; i++)
			if (efprintf(fp, "%s", ifo->environ[i]) == -1)
				goto eexit;
	}

#ifdef PRIVSEP
make:
#endif
	/* Convert buffer to argv */
	fflush(fp);

	buf_pos = ftell(fp);
	if (buf_pos == -1) {
		logerr(__func__);
		goto eexit;
	}

#ifndef HAVE_OPEN_MEMSTREAM
	size_t buf_len = (size_t)buf_pos;
	if (ctx->script_buflen < buf_len) {
		char *buf = realloc(ctx->script_buf, buf_len);
		if (buf == NULL)
			goto eexit;
		ctx->script_buf = buf;
		ctx->script_buflen = buf_len;
	}
	rewind(fp);
	if (fread(ctx->script_buf, sizeof(char), buf_len, fp) != buf_len)
		goto eexit;
	fclose(fp);
	fp = NULL;
#endif

	if (script_buftoenv(ctx, ctx->script_buf, (size_t)buf_pos) == NULL)
		goto eexit;

	return buf_pos;

eexit:
	logerr(__func__);
#ifndef HAVE_OPEN_MEMSTREAM
	if (fp != NULL)
		fclose(fp);
#endif
	return -1;
}

static int
send_interface1(struct fd_list *fd, const struct interface *ifp,
    const char *reason)
{
	struct dhcpcd_ctx *ctx = ifp->ctx;
	long len;

	len = make_env(ifp->ctx, ifp, reason);
	if (len == -1)
		return -1;
	return control_queue(fd, ctx->script_buf, (size_t)len, 1);
}

int
send_interface(struct fd_list *fd, const struct interface *ifp, int af)
{
	int retval = 0;
#ifdef INET
	const struct dhcp_state *d;
#endif
#ifdef DHCP6
	const struct dhcp6_state *d6;
#endif

#ifndef AF_LINK
#define	AF_LINK	AF_PACKET
#endif

	if (af == AF_UNSPEC || af == AF_LINK) {
		const char *reason;

		switch (ifp->carrier) {
		case LINK_UP:
			reason = "CARRIER";
			break;
		case LINK_DOWN:
		case LINK_DOWN_IFFUP:
			reason = "NOCARRIER";
			break;
		default:
			reason = "UNKNOWN";
			break;
		}
		if (fd != NULL) {
			if (send_interface1(fd, ifp, reason) == -1)
				retval = -1;
		} else
			retval++;
	}

#ifdef INET
	if (af == AF_UNSPEC || af == AF_INET) {
		if (D_STATE_RUNNING(ifp)) {
			d = D_CSTATE(ifp);
			if (fd != NULL) {
				if (send_interface1(fd, ifp, d->reason) == -1)
					retval = -1;
			} else
				retval++;
		}
#ifdef IPV4LL
		if (IPV4LL_STATE_RUNNING(ifp)) {
			if (fd != NULL) {
				if (send_interface1(fd, ifp, "IPV4LL") == -1)
					retval = -1;
			} else
				retval++;
		}
#endif
	}
#endif

#ifdef INET6
	if (af == AF_UNSPEC || af == AF_INET6) {
		if (IPV6_STATE_RUNNING(ifp)) {
			if (fd != NULL) {
				if (send_interface1(fd, ifp, "STATIC6") == -1)
					retval = -1;
			} else
				retval++;
		}
		if (RS_STATE_RUNNING(ifp)) {
			if (fd != NULL) {
				if (send_interface1(fd, ifp,
				    "ROUTERADVERT") == -1)
					retval = -1;
			} else
				retval++;
		}
#ifdef DHCP6
		if (D6_STATE_RUNNING(ifp)) {
			d6 = D6_CSTATE(ifp);
			if (fd != NULL) {
				if (send_interface1(fd, ifp, d6->reason) == -1)
					retval = -1;
			} else
				retval++;
		}
#endif
	}
#endif

	return retval;
}

static int
script_run(struct dhcpcd_ctx *ctx, char **argv)
{
	pid_t pid;
	int status = 0;

	pid = script_exec(argv, ctx->script_env);
	if (pid == -1)
		logerr("%s: %s", __func__, argv[0]);
	else if (pid != 0) {
		/* Wait for the script to finish */
		while (waitpid(pid, &status, 0) == -1) {
			if (errno != EINTR) {
				logerr("%s: waitpid", __func__);
				status = 0;
				break;
			}
		}
		if (WIFEXITED(status)) {
			if (WEXITSTATUS(status))
				logerrx("%s: %s: WEXITSTATUS %d",
				    __func__, argv[0], WEXITSTATUS(status));
		} else if (WIFSIGNALED(status))
			logerrx("%s: %s: %s",
			    __func__, argv[0], strsignal(WTERMSIG(status)));
	}

	return WEXITSTATUS(status);
}

int
script_runreason(const struct interface *ifp, const char *reason)
{
	struct dhcpcd_ctx *ctx = ifp->ctx;
	char *argv[2];
	int status = 0;
	struct fd_list *fd;

	if (ctx->script == NULL &&
	    TAILQ_FIRST(&ifp->ctx->control_fds) == NULL)
		return 0;

	/* Make our env */
	if (make_env(ifp->ctx, ifp, reason) == -1) {
		logerr(__func__);
		return -1;
	}

	if (ctx->script == NULL)
		goto send_listeners;

	argv[0] = ctx->script;
	argv[1] = NULL;
	logdebugx("%s: executing `%s' %s", ifp->name, argv[0], reason);

#ifdef PRIVSEP
	if (ctx->options & DHCPCD_PRIVSEP) {
		if (ps_root_script(ctx,
		    ctx->script_buf, ctx->script_buflen) == -1)
			logerr(__func__);
		goto send_listeners;
	}
#endif

	status = script_run(ctx, argv);

send_listeners:
	/* Send to our listeners */
	status = 0;
	TAILQ_FOREACH(fd, &ctx->control_fds, next) {
		if (!(fd->flags & FD_LISTEN))
			continue;
		if (control_queue(fd, ctx->script_buf, ctx->script_buflen,
		    true) == -1)
			logerr("%s: control_queue", __func__);
		else
			status = 1;
	}

	return status;
}

#ifdef PRIVSEP
int
script_runchroot(struct dhcpcd_ctx *ctx)
{
	char *argv[2];

	/* Make our env */
	if (make_env(ctx, NULL, "CHROOT") == -1) {
		logerr(__func__);
		return -1;
	}

	argv[0] = ctx->script;
	argv[1] = NULL;
	logdebugx("executing `%s' %s", argv[0], "CHROOT");

	return script_run(ctx, argv);
}
#endif