/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
 * Copyright 2015, Joyent, Inc.
 */

#define	_POSIX_PTHREAD_SEMANTICS 1

#include <sys/param.h>
#include <sys/klpd.h>
#include <sys/syscall.h>
#include <sys/systeminfo.h>

#include <alloca.h>
#include <ctype.h>
#include <deflt.h>
#include <door.h>
#include <errno.h>
#include <grp.h>
#include <priv.h>
#include <pwd.h>
#include <regex.h>
#include <secdb.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>

#include <auth_attr.h>
#include <exec_attr.h>
#include <prof_attr.h>
#include <user_attr.h>

static int doorfd = -1;

static size_t repsz, setsz;

static uid_t get_uid(const char *, boolean_t *, char *);
static gid_t get_gid(const char *, boolean_t *, char *);
static priv_set_t *get_privset(const char *, boolean_t *, char *);
static priv_set_t *get_granted_privs(uid_t);

/*
 * Remove the isaexec path of an executable if we can't find the
 * executable at the first attempt.
 */

static regex_t regc;
static boolean_t cansplice = B_TRUE;

static void
init_isa_regex(void)
{
	char *isalist;
	size_t isalen = 255;		/* wild guess */
	size_t len;
	long ret;
	char *regexpr;
	char *p;

	/*
	 * Extract the isalist(5) for userland from the kernel.
	 */
	isalist = malloc(isalen);
	do {
		ret = sysinfo(SI_ISALIST, isalist, isalen);
		if (ret == -1l) {
			free(isalist);
			return;
		}
		if (ret > isalen) {
			isalen = ret;
			isalist = realloc(isalist, isalen);
		} else
			break;
	} while (isalist != NULL);


	if (isalist == NULL)
		return;

	/* allocate room for the regex + (/())/[^/]*$ + needed \\. */
#define	LEFT	"(/("
#define	RIGHT	"))/[^/]*$"

	regexpr = alloca(ret * 2 + sizeof (LEFT RIGHT));
	(void) strcpy(regexpr, LEFT);
	len = strlen(regexpr);

	for (p = isalist; *p; p++) {
		switch (*p) {
		case '+':
		case '|':
		case '*':
		case '[':
		case ']':
		case '{':
		case '}':
		case '\\':
			regexpr[len++] = '\\';
		default:
			regexpr[len++] = *p;
			break;
		case ' ':
		case '\t':
			regexpr[len++] = '|';
			break;
		}
	}

	free(isalist);
	regexpr[len] = '\0';
	(void) strcat(regexpr, RIGHT);

	if (regcomp(&regc, regexpr, REG_EXTENDED) != 0)
		return;

	cansplice = B_TRUE;
}

#define	NMATCH	2

static boolean_t
removeisapath(char *path)
{
	regmatch_t match[NMATCH];

	if (!cansplice || regexec(&regc, path, NMATCH, match, 0) != 0)
		return (B_FALSE);

	/*
	 * The first match includes the whole matched expression including the
	 * end of the string.  The second match includes the "/" + "isa" and
	 * that is the part we need to remove.
	 */

	if (match[1].rm_so == -1)
		return (B_FALSE);

	/* match[0].rm_eo == strlen(path) */
	(void) memmove(path + match[1].rm_so, path + match[1].rm_eo,
	    match[0].rm_eo - match[1].rm_eo + 1);

	return (B_TRUE);
}

static int
register_pfexec(int fd)
{
	int ret = syscall(SYS_privsys, PRIVSYS_PFEXEC_REG, fd);

	return (ret);
}

/* ARGSUSED */
static void
unregister_pfexec(int sig)
{
	if (doorfd != -1)
		(void) syscall(SYS_privsys, PRIVSYS_PFEXEC_UNREG, doorfd);
	_exit(0);
}

static int
alldigits(const char *s)
{
	int c;

	if (*s == '\0')
		return (0);

	while ((c = *s++) != '\0') {
		if (!isdigit(c)) {
			return (0);
		}
	}

	return (1);
}

static uid_t
get_uid(const char *v, boolean_t *ok, char *path)
{
	struct passwd *pwd, pwdm;
	char buf[1024];

	if (getpwnam_r(v, &pwdm, buf, sizeof (buf), &pwd) == 0 && pwd != NULL)
		return (pwd->pw_uid);

	if (alldigits(v))
		return (atoi(v));

	*ok = B_FALSE;
	syslog(LOG_ERR, "%s: %s: unknown username\n", path, v);
	return ((uid_t)-1);
}

static uid_t
get_gid(const char *v, boolean_t *ok, char *path)
{
	struct group *grp, grpm;
	char buf[1024];

	if (getgrnam_r(v, &grpm, buf, sizeof (buf), &grp) == 0 && grp != NULL)
		return (grp->gr_gid);

	if (alldigits(v))
		return (atoi(v));

	*ok = B_FALSE;
	syslog(LOG_ERR, "%s: %s: unknown groupname\n", path, v);
	return ((gid_t)-1);
}

static priv_set_t *
get_privset(const char *s, boolean_t *ok, char *path)
{
	priv_set_t *res;

	if ((res = priv_str_to_set(s, ",", NULL)) == NULL) {
		syslog(LOG_ERR, "%s: %s: bad privilege set\n", path, s);
		if (ok != NULL)
			*ok = B_FALSE;
	}
	return (res);
}

/*ARGSUSED*/
static int
ggp_callback(const char *prof, kva_t *attr, void *ctxt, void *vres)
{
	priv_set_t *res = vres;
	char *privs;

	if (attr == NULL)
		return (0);

	/* get privs from this profile */
	privs = kva_match(attr, PROFATTR_PRIVS_KW);
	if (privs != NULL) {
		priv_set_t *tmp = priv_str_to_set(privs, ",", NULL);
		if (tmp != NULL) {
			priv_union(tmp, res);
			priv_freeset(tmp);
		}
	}

	return (0);
}

/*
 * This routine exists on failure and returns NULL if no granted privileges
 * are set.
 */
static priv_set_t *
get_granted_privs(uid_t uid)
{
	priv_set_t *res;
	struct passwd *pwd, pwdm;
	char buf[1024];

	if (getpwuid_r(uid, &pwdm, buf, sizeof (buf), &pwd) != 0 || pwd == NULL)
		return (NULL);

	res = priv_allocset();
	if (res == NULL)
		return (NULL);

	priv_emptyset(res);

	(void) _enum_profs(pwd->pw_name, ggp_callback, NULL, res);

	return (res);
}

static void
callback_forced_privs(pfexec_arg_t *pap)
{
	execattr_t *exec;
	char *value;
	priv_set_t *fset;
	void *res = alloca(setsz);

	/* Empty set signifies no forced privileges. */
	priv_emptyset(res);

	exec = getexecprof("Forced Privilege", KV_COMMAND, pap->pfa_path,
	    GET_ONE);

	if (exec == NULL && removeisapath(pap->pfa_path)) {
		exec = getexecprof("Forced Privilege", KV_COMMAND,
		    pap->pfa_path, GET_ONE);
	}

	if (exec == NULL) {
		(void) door_return(res, setsz, NULL, 0);
		return;
	}

	if ((value = kva_match(exec->attr, EXECATTR_IPRIV_KW)) == NULL ||
	    (fset = get_privset(value, NULL, pap->pfa_path)) == NULL) {
		free_execattr(exec);
		(void) door_return(res, setsz, NULL, 0);
		return;
	}

	priv_copyset(fset, res);
	priv_freeset(fset);

	free_execattr(exec);
	(void) door_return(res, setsz, NULL, 0);
}

static void
callback_user_privs(pfexec_arg_t *pap)
{
	priv_set_t *gset, *wset;
	uint32_t res;

	wset = (priv_set_t *)&pap->pfa_buf;
	gset = get_granted_privs(pap->pfa_uid);

	res = priv_issubset(wset, gset);
	priv_freeset(gset);

	(void) door_return((char *)&res, sizeof (res), NULL, 0);
}

static void
callback_pfexec(pfexec_arg_t *pap)
{
	pfexec_reply_t *res = alloca(repsz);
	uid_t uid, euid, uuid;
	gid_t gid, egid;
	struct passwd pw, *pwd;
	char buf[1024];
	execattr_t *exec = NULL;
	char *value;
	priv_set_t *lset, *iset;
	size_t mysz = repsz - 2 * setsz;
	char *path = pap->pfa_path;

	/*
	 * Initialize the pfexec_reply_t to a sane state.
	 */
	res->pfr_vers = pap->pfa_vers;
	res->pfr_len = 0;
	res->pfr_ruid = PFEXEC_NOTSET;
	res->pfr_euid = PFEXEC_NOTSET;
	res->pfr_rgid = PFEXEC_NOTSET;
	res->pfr_egid = PFEXEC_NOTSET;
	res->pfr_setcred = B_FALSE;
	res->pfr_scrubenv = B_TRUE;
	res->pfr_allowed = B_FALSE;
	res->pfr_ioff = 0;
	res->pfr_loff = 0;

	uuid = pap->pfa_uid;

	if (getpwuid_r(uuid, &pw, buf, sizeof (buf), &pwd) != 0 || pwd == NULL)
		goto stdexec;

	exec = getexecuser(pwd->pw_name, KV_COMMAND, path, GET_ONE);

	if ((exec == NULL || exec->attr == NULL) && removeisapath(path)) {
		free_execattr(exec);
		exec = getexecuser(pwd->pw_name, KV_COMMAND, path, GET_ONE);
	}

	if (exec == NULL) {
		res->pfr_allowed = B_FALSE;
		goto ret;
	}

	if (exec->attr == NULL)
		goto stdexec;

	/* Found in execattr, so clearly we can use it */
	res->pfr_allowed = B_TRUE;

	uid = euid = (uid_t)-1;
	gid = egid = (gid_t)-1;
	lset = iset = NULL;

	/*
	 * If there's an error in parsing uid, gid, privs, then return
	 * failure.
	 */
	if ((value = kva_match(exec->attr, EXECATTR_UID_KW)) != NULL)
		euid = uid = get_uid(value, &res->pfr_allowed, path);

	if ((value = kva_match(exec->attr, EXECATTR_GID_KW)) != NULL)
		egid = gid = get_gid(value, &res->pfr_allowed, path);

	if ((value = kva_match(exec->attr, EXECATTR_EUID_KW)) != NULL)
		euid = get_uid(value, &res->pfr_allowed, path);

	if ((value = kva_match(exec->attr, EXECATTR_EGID_KW)) != NULL)
		egid = get_gid(value, &res->pfr_allowed, path);

	if ((value = kva_match(exec->attr, EXECATTR_LPRIV_KW)) != NULL)
		lset = get_privset(value, &res->pfr_allowed, path);

	if ((value = kva_match(exec->attr, EXECATTR_IPRIV_KW)) != NULL)
		iset = get_privset(value, &res->pfr_allowed, path);

	/*
	 * Remove LD_* variables in the kernel when the runtime linker might
	 * use them later on because the uids are equal.
	 */
	res->pfr_scrubenv = (uid != (uid_t)-1 && euid == uid) ||
	    (gid != (gid_t)-1 && egid == gid) || iset != NULL;

	res->pfr_euid = euid;
	res->pfr_ruid = uid;
	res->pfr_egid = egid;
	res->pfr_rgid = gid;

	/* Now add the privilege sets */
	res->pfr_ioff = res->pfr_loff = 0;
	if (iset != NULL) {
		res->pfr_ioff = mysz;
		priv_copyset(iset, PFEXEC_REPLY_IPRIV(res));
		mysz += setsz;
		priv_freeset(iset);
	}
	if (lset != NULL) {
		res->pfr_loff = mysz;
		priv_copyset(lset, PFEXEC_REPLY_LPRIV(res));
		mysz += setsz;
		priv_freeset(lset);
	}

	res->pfr_setcred = uid != (uid_t)-1 || euid != (uid_t)-1 ||
	    egid != (gid_t)-1 || gid != (gid_t)-1 || iset != NULL ||
	    lset != NULL;

	/* If the real uid changes, we stop running under a profile shell */
	res->pfr_clearflag = uid != (uid_t)-1 && uid != uuid;
	free_execattr(exec);
ret:
	(void) door_return((char *)res, mysz, NULL, 0);
	return;

stdexec:
	free_execattr(exec);

	res->pfr_scrubenv = B_FALSE;
	res->pfr_setcred = B_FALSE;
	res->pfr_allowed = B_TRUE;

	(void) door_return((char *)res, mysz, NULL, 0);
}

/* ARGSUSED */
static void
callback(void *cookie, char *argp, size_t asz, door_desc_t *dp, uint_t ndesc)
{
	/* LINTED ALIGNMENT */
	pfexec_arg_t *pap = (pfexec_arg_t *)argp;

	if (asz < sizeof (pfexec_arg_t) || pap->pfa_vers != PFEXEC_ARG_VERS) {
		(void) door_return(NULL, 0, NULL, 0);
		return;
	}

	switch (pap->pfa_call) {
	case PFEXEC_EXEC_ATTRS:
		callback_pfexec(pap);
		break;
	case PFEXEC_FORCED_PRIVS:
		callback_forced_privs(pap);
		break;
	case PFEXEC_USER_PRIVS:
		callback_user_privs(pap);
		break;
	default:
		syslog(LOG_ERR, "Bad Call: %d\n", pap->pfa_call);
		break;
	}

	/*
	 * If the door_return(ptr, size, NULL, 0) fails, make sure we
	 * don't lose server threads.
	 */
	(void) door_return(NULL, 0, NULL, 0);
}

int
main(void)
{
	const priv_impl_info_t *info;

	(void) signal(SIGINT, unregister_pfexec);
	(void) signal(SIGQUIT, unregister_pfexec);
	(void) signal(SIGTERM, unregister_pfexec);
	(void) signal(SIGHUP, unregister_pfexec);

	info = getprivimplinfo();
	if (info == NULL)
		exit(1);

	if (fork() > 0)
		_exit(0);

	openlog("pfexecd", LOG_PID, LOG_DAEMON);
	setsz = info->priv_setsize * sizeof (priv_chunk_t);
	repsz = 2 * setsz + sizeof (pfexec_reply_t);

	init_isa_regex();

	doorfd = door_create(callback, NULL, DOOR_REFUSE_DESC);

	if (doorfd == -1 || register_pfexec(doorfd) != 0) {
		perror("doorfd");
		exit(1);
	}

	/* LINTED CONSTCOND */
	while (1)
		(void) sigpause(SIGINT);

	return (0);
}