root/usr/src/lib/libproject/common/setproject.c
/*
 * 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) 2001, 2010, Oracle and/or its affiliates. All rights reserved.
 * Copyright (c) 2018, Joyent, Inc.
 */


#include <sys/task.h>
#include <sys/types.h>
#include <unistd.h>

#include <ctype.h>
#include <project.h>
#include <rctl.h>
#include <secdb.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <nss_dbdefs.h>
#include <pwd.h>
#include <pool.h>
#include <libproc.h>
#include <priv.h>
#include <priv_utils.h>
#include <zone.h>
#include <sys/pool.h>
#include <sys/pool_impl.h>
#include <sys/rctl_impl.h>

static void
xstrtolower(char *s)
{
        for (; *s != '\0'; s++)
                *s = tolower(*s);
}

static void
remove_spaces(char *s)
{
        char *current;
        char *next;

        current = next = s;

        while (*next != '\0') {
                while (isspace(*next))
                        next++;
                *current++ = *next++;
        }
        *current = '\0';
}

int
build_rctlblk(rctlblk_t *blk, int comp_num, char *component)
{
        char *signam;
        int sig = 0;
        uint_t act = rctlblk_get_local_action(blk, &sig);

        if (comp_num == 0) {
                /*
                 * Setting privilege level for resource control block.
                 */
                xstrtolower(component);

                if (strcmp("basic", component) == 0) {
                        rctlblk_set_privilege(blk, RCPRIV_BASIC);
                        return (0);
                }

                if (strcmp("priv", component) == 0 ||
                    strcmp("privileged", component) == 0) {
                        rctlblk_set_privilege(blk, RCPRIV_PRIVILEGED);
                        return (0);
                }

                return (-1);
        }

        if (comp_num == 1) {

                /*
                 * Setting value for resource control block.
                 */
                unsigned long long val;
                char *t;

                /* Negative numbers are not allowed */
                if (strchr(component, '-') != NULL)
                        return (-1);

                errno = 0;
                val = strtoull(component, &t, 10);
                if (errno != 0 || t == component || *t != '\0')
                        return (-1);

                rctlblk_set_value(blk, (rctl_qty_t)val);
                return (0);
        }

        /*
         * Setting one or more actions on this resource control block.
         */
        if (comp_num >= 2) {
                if (strcmp("none", component) == 0) {
                        rctlblk_set_local_action(blk, 0, 0);
                        return (0);
                }

                if (strcmp("deny", component) == 0) {
                        act |= RCTL_LOCAL_DENY;

                        rctlblk_set_local_action(blk, act, sig);

                        return (0);
                }

                /*
                 * The last, and trickiest, form of action is the signal
                 * specification.
                 */
                if ((signam = strchr(component, '=')) == NULL)
                        return (-1);

                *signam++ = '\0';

                if (strcmp("sig", component) == 0 ||
                    strcmp("signal", component) == 0) {
                        if (strncmp("SIG", signam, 3) == 0)
                                signam += 3;

                        if (str2sig(signam, &sig) == -1)
                                return (-1);

                        act |= RCTL_LOCAL_SIGNAL;

                        rctlblk_set_local_action(blk, act, sig);

                        return (0);
                }
        }
        return (-1);
}

/*
 * States:
 */
#define INPAREN         0x1

/*
 * Errors:
 */
#define SETFAILED       (-1)
#define COMPLETE        1
#define NESTING         2
#define UNCLOSED        3
#define CLOSEBEFOREOPEN 4
#define BADSPEC         5

static void
reinit_blk(rctlblk_t *blk, int local_action)
{
        rctlblk_set_privilege(blk, RCPRIV_PRIVILEGED);
        rctlblk_set_value(blk, 0);
        rctlblk_set_local_flags(blk, 0);
        rctlblk_set_local_action(blk, local_action, 0);
}

static int
rctl_set(char *ctl_name, char *val, struct ps_prochandle *Pr, int flags)
{
        int error = 0;
        uint_t component = 0;
        int valuecount = 0;
        uint_t state = 0;
        char *component_head;
        rctlblk_t *blk;
        rctlblk_t *ablk;
        int project_entity = 0;
        int count = 0;
        char *tmp;
        int local_act;
        rctlblk_t *rnext;
        int teardown_basic = 0;
        int teardown_priv = 0;

        /* We cannot modify a zone resource control */
        if (strncmp(ctl_name, "zone.", strlen("zone.")) == 0) {
                return (SETFAILED);
        }

        remove_spaces(val);

        if (strncmp(ctl_name, "project.", strlen("project.")) == 0) {
                project_entity = 1;
        } else if ((strncmp(ctl_name, "process.", strlen("process.")) != 0) &&
            (strncmp(ctl_name, "task.", strlen("task.")) != 0)) {
                return (SETFAILED);
        }

        /* Determine how many attributes we'll be setting */
        for (tmp = val; *tmp != '\0'; tmp++) {
                if (*tmp == '(')
                        count++;
        }
        /* Allocate sufficient memory for rctl blocks */
        if ((count == 0) || ((ablk =
            (rctlblk_t *)malloc(rctlblk_size() * count)) == NULL)) {
                return (SETFAILED);
        }
        blk = ablk;

        /*
         * In order to set the new rctl's local_action, we'll need the
         * current value of global_flags.  We obtain global_flags by
         * performing a pr_getrctl().
         *
         * The ctl_name has been verified as valid, so we have no reason
         * to suspect that pr_getrctl() will return an error.
         */
        (void) pr_getrctl(Pr, ctl_name, NULL, blk, RCTL_FIRST);


        /*
         * Set initial local action based on global deny properties.
         */
        rctlblk_set_privilege(blk, RCPRIV_PRIVILEGED);
        rctlblk_set_value(blk, 0);
        rctlblk_set_local_flags(blk, 0);

        if (rctlblk_get_global_flags(blk) & RCTL_GLOBAL_DENY_ALWAYS)
                local_act = RCTL_LOCAL_DENY;
        else
                local_act = RCTL_LOCAL_NOACTION;

        rctlblk_set_local_action(blk, local_act, 0);

        for (; ; val++) {

                switch (*val) {
                        case '(':
                                if (state & INPAREN) {
                                        error = NESTING;
                                        break;
                                }

                                state |= INPAREN;
                                component_head = (char *)val + 1;

                                break;
                        case ')':
                                if (state & INPAREN) {
                                        *val = '\0';
                                        if (component < 2) {
                                                error = BADSPEC;
                                                break;
                                        }
                                        if (build_rctlblk(blk, component,
                                            component_head) == -1) {
                                                error = BADSPEC;
                                                break;
                                        }
                                        state &= ~INPAREN;
                                        component = 0;
                                        valuecount++;

                                        if (project_entity &&
                                            (rctlblk_get_privilege(blk) ==
                                            RCPRIV_BASIC)) {
                                                error = SETFAILED;
                                        } else {
                                                if (rctlblk_get_privilege(blk)
                                                    == RCPRIV_BASIC)
                                                        teardown_basic = 1;

                                                if (rctlblk_get_privilege(blk)
                                                    == RCPRIV_PRIVILEGED)
                                                        teardown_priv = 1;

                                                if (valuecount > count) {
                                                        free(ablk);
                                                        return (SETFAILED);
                                                }

                                                if (valuecount != count) {
                                                        blk = RCTLBLK_INC(ablk,
                                                            valuecount);
                                                        /* re-initialize blk */
                                                        reinit_blk(blk,
                                                            local_act);
                                                }
                                        }

                                } else {
                                        error = CLOSEBEFOREOPEN;
                                }
                                break;
                        case ',':
                                if (state & INPAREN) {
                                        *val = '\0';
                                        if (build_rctlblk(blk, component,
                                            component_head) == -1)
                                                error = BADSPEC;

                                        component++;
                                        component_head = (char *)val + 1;

                                }
                                break;
                        case '\0':
                                if (valuecount == 0)
                                        error = BADSPEC;
                                else if (state & INPAREN)
                                        error = UNCLOSED;
                                else
                                        error = COMPLETE;
                                break;
                        default:
                                if (!(state & INPAREN))
                                        error = BADSPEC;
                                break;
                }

                if (error)
                        break;
        }
        /* ablk points to array of rctlblk_t */

        if (valuecount == 0)
                error = BADSPEC;

        if (error != COMPLETE) {
                free(ablk);
                return (error);
        }

        /* teardown rctls if required */
        if (!project_entity) {

                if ((rnext = (rctlblk_t *)malloc(rctlblk_size())) == NULL) {
                        free(ablk);
                        return (SETFAILED);
                }

restart:
                if (pr_getrctl(Pr, ctl_name, NULL, rnext, RCTL_FIRST) == 0) {
                        while (1) {
                                if ((rctlblk_get_privilege(rnext) ==
                                    RCPRIV_PRIVILEGED) &&
                                    (teardown_priv == 1)) {
                                        (void) pr_setrctl(Pr, ctl_name, NULL,
                                            rnext, RCTL_DELETE);
                                        goto restart;
                                }
                                if ((rctlblk_get_privilege(rnext) ==
                                    RCPRIV_BASIC) && (teardown_basic == 1)) {
                                        (void) pr_setrctl(Pr, ctl_name, NULL,
                                            rnext, RCTL_DELETE);
                                        goto restart;
                                }

                                if (pr_getrctl(Pr, ctl_name, rnext, rnext,
                                    RCTL_NEXT) == -1)
                                        break;
                        }
                }

                free(rnext);
        }

        /* set rctls */

        blk = ablk;

        if (project_entity) {
                if (pr_setprojrctl(Pr, ctl_name, blk, count, flags) == -1)
                        error = SETFAILED;
        } else {
                valuecount = 0;
                while (valuecount < count) {
                        if (pr_setrctl(Pr, ctl_name,
                            NULL, blk, RCTL_INSERT) == -1) {
                                error = SETFAILED;
                                break;
                                }
                        valuecount++;
                        blk = RCTLBLK_INC(ablk, valuecount);
                }
        }



        free(ablk);

        if (error != COMPLETE)
                return (error);

        return (0);
}

static int
rctlwalkfunc(const char *name, void *data)
{

        if (strcmp(name, (char *)data) == 0)
                return (-1);
        else
                return (0);

}

/*
 * This routine determines if /dev/pool device is present on the system and
 * pools are currently enabled.  We want to do this directly from libproject
 * without using libpool's pool_get_status() routine because pools could be
 * completely removed from the system.  Return 1 if pools are enabled, or
 * 0 otherwise.  When used inside local zones, always pretend that pools
 * are disabled because binding is not allowed and we're already in the
 * right pool.
 */
static int
pools_enabled(void)
{
        pool_status_t status;
        int fd;

        if (getzoneid() != GLOBAL_ZONEID)
                return (0);
        if ((fd = open("/dev/pool", O_RDONLY)) < 0)
                return (0);
        if (ioctl(fd, POOL_STATUSQ, &status) < 0) {
                (void) close(fd);
                return (0);
        }
        (void) close(fd);
        return (status.ps_io_state);
}

/*
 * A pool_name of NULL means to attempt to bind to the default pool.
 * If the "force" flag is non-zero, the value of "system.bind-default" will be
 * ignored, and the process will be bound to the default pool if one exists.
 */
static int
bind_to_pool(const char *pool_name, pid_t pid, int force)
{
        pool_value_t *pvals[] = { NULL, NULL };
        pool_t **pools;
        uint_t nelem;
        uchar_t bval;
        pool_conf_t *conf;
        const char *nm;
        int retval;

        if ((conf = pool_conf_alloc()) == NULL)
                return (-1);
        if (pool_conf_open(conf, pool_dynamic_location(), PO_RDONLY) < 0) {
                /*
                 * Pools configuration file is corrupted; allow logins.
                 */
                pool_conf_free(conf);
                return (0);
        }
        if (pool_name != NULL && pool_get_pool(conf, pool_name) != NULL) {
                /*
                 * There was a project.pool entry, and the pool it refers to
                 * is a valid (active) pool.
                 */
                (void) pool_conf_close(conf);
                pool_conf_free(conf);
                if (pool_set_binding(pool_name, P_PID, pid) != PO_SUCCESS) {
                        if (pool_error() != POE_SYSTEM)
                                errno = EINVAL;
                        return (-1);
                }
                return (0);
        }

        /*
         * Bind to the pool with 'pool.default' = 'true' if
         * 'system.bind-default' = 'true'.
         */
        if ((pvals[0] = pool_value_alloc()) == NULL) {
                (void) pool_conf_close(conf);
                pool_conf_free(conf);
                return (-1);
        }
        if (!force && pool_get_property(conf, pool_conf_to_elem(conf),
            "system.bind-default", pvals[0]) != POC_BOOL ||
            pool_value_get_bool(pvals[0], &bval) != PO_SUCCESS ||
            bval == PO_FALSE) {
                pool_value_free(pvals[0]);
                (void) pool_conf_close(conf);
                pool_conf_free(conf);
                errno = pool_name == NULL ? EACCES : ESRCH;
                return (-1);
        }
        (void) pool_value_set_name(pvals[0], "pool.default");
        pool_value_set_bool(pvals[0], PO_TRUE);
        if ((pools = pool_query_pools(conf, &nelem, pvals)) == NULL) {
                /*
                 * No default pools exist.
                 */
                pool_value_free(pvals[0]);
                (void) pool_conf_close(conf);
                pool_conf_free(conf);
                errno = pool_name == NULL ? EACCES : ESRCH;
                return (-1);
        }
        if (nelem != 1 ||
            pool_get_property(conf, pool_to_elem(conf, pools[0]), "pool.name",
            pvals[0]) != POC_STRING) {
                /*
                 * Configuration is invalid.
                 */
                free(pools);
                pool_value_free(pvals[0]);
                (void) pool_conf_close(conf);
                pool_conf_free(conf);
                return (0);
        }
        free(pools);
        (void) pool_conf_close(conf);
        pool_conf_free(conf);
        (void) pool_value_get_string(pvals[0], &nm);
        if (pool_set_binding(nm, P_PID, pid) != PO_SUCCESS) {
                if (pool_error() != POE_SYSTEM)
                        errno = EINVAL;
                retval = -1;
        } else {
                retval = 0;
        }
        pool_value_free(pvals[0]);
        return (retval);
}

/*
 * Changes the assigned project, task and resource pool of a stopped target
 * process.
 *
 * We may not have access to the project table if our target process is in
 * getprojbyname()'s execution path. Similarly, we may not be able to get user
 * information if the target process is in getpwnam()'s execution path. Thus we
 * give the caller the option of skipping these checks by providing a pointer to
 * a pre-validated project structure in proj (whose name matches project_name)
 * and taking responsibility for ensuring that the target process' owner is a
 * member of the target project.
 *
 * Callers of this function should always provide a pre-validated project
 * structure in proj unless they can be sure that the target process will never
 * be in setproject_proc()'s execution path.
 */

projid_t
setproject_proc(const char *project_name, const char *user_name, int flags,
    pid_t pid, struct ps_prochandle *Pr, struct project *proj)
{
        char pwdbuf[NSS_BUFLEN_PASSWD];
        char prbuf[PROJECT_BUFSZ];
        projid_t projid;
        struct passwd pwd;
        int i;
        int unknown = 0;
        int ret = 0;
        kva_t *kv_array;
        struct project local_proj; /* space to store proj if not provided */
        const char *pool_name = NULL;

        if (project_name != NULL) {
                /*
                 * Sanity checks.
                 */
                if (strcmp(project_name, "") == 0 ||
                    user_name == NULL) {
                        errno = EINVAL;
                        return (SETPROJ_ERR_TASK);
                }

                /*
                 * If proj is NULL, acquire project information to ensure that
                 * project_name is a valid project, and confirm that user_name
                 * exists and is a member of the specified project.
                 */
                if (proj == NULL) {
                        if ((proj = getprojbyname(project_name, &local_proj,
                            prbuf, PROJECT_BUFSZ)) == NULL) {
                                errno = ESRCH;
                                return (SETPROJ_ERR_TASK);
                        }

                        if (getpwnam_r(user_name, &pwd,
                            pwdbuf, NSS_BUFLEN_PASSWD) == NULL) {
                                errno = ESRCH;
                                return (SETPROJ_ERR_TASK);
                        }
                        /*
                         * Root can join any project.
                         */
                        if (pwd.pw_uid != (uid_t)0 &&
                            !inproj(user_name, project_name, prbuf,
                            PROJECT_BUFSZ)) {
                                errno = ESRCH;
                                return (SETPROJ_ERR_TASK);
                        }
                }
                projid = proj->pj_projid;
        } else {
                projid = getprojid();
        }


        if ((kv_array = _str2kva(proj->pj_attr, KV_ASSIGN,
            KV_DELIMITER)) != NULL) {
                for (i = 0; i < kv_array->length; i++) {
                        if (strcmp(kv_array->data[i].key,
                            "project.pool") == 0) {
                                pool_name = kv_array->data[i].value;
                        }
                        if (strcmp(kv_array->data[i].key, "task.final") == 0) {
                                flags |= TASK_FINAL;
                        }
                }
        }

        /*
         * Bind process to a pool only if pools are configured
         */
        if (pools_enabled() == 1) {
                char *old_pool_name;
                /*
                 * Attempt to bind to pool before calling
                 * settaskid().
                 */
                old_pool_name = pool_get_binding(pid);
                if (bind_to_pool(pool_name, pid, 0) != 0) {
                        if (old_pool_name)
                                free(old_pool_name);
                        _kva_free(kv_array);
                        return (SETPROJ_ERR_POOL);
                }
                if (pr_settaskid(Pr, projid, flags & TASK_MASK) == -1) {
                        int saved_errno = errno;

                        /*
                         * Undo pool binding.
                         */
                        (void) bind_to_pool(old_pool_name, pid, 1);
                        if (old_pool_name)
                                free(old_pool_name);
                        _kva_free(kv_array);
                        /*
                         * Restore errno
                         */
                        errno = saved_errno;
                        return (SETPROJ_ERR_TASK);
                }
                if (old_pool_name)
                        free(old_pool_name);
        } else {
                /*
                 * Pools are not configured, so simply create new task.
                 */
                if (pr_settaskid(Pr, projid, flags & TASK_MASK) == -1) {
                        _kva_free(kv_array);
                        return (SETPROJ_ERR_TASK);
                }
        }

        if (project_name == NULL) {
                /*
                 * In the case that we are starting a new task in the
                 * current project, we are finished, since the current
                 * resource controls will still apply. (Implicit behaviour:
                 * a project must be entirely logged out before name
                 * service changes will take effect.)
                 */
                _kva_free(kv_array);
                return (projid);
        }

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

        for (i = 0; i < kv_array->length; i++) {
                /*
                 * Providing a special, i.e. a non-resource control, key?  Then
                 * parse that key here and end with "continue;".
                 */

                /*
                 * For generic bindings, the kernel performs the binding, as
                 * these are resource controls advertised by kernel subsystems.
                 */

                /*
                 * Check for known attribute name.
                 */
                errno = 0;
                if (rctl_walk(rctlwalkfunc, (void *)kv_array->data[i].key)
                    == 0)
                        continue;
                if (errno) {
                        _kva_free(kv_array);
                        return (SETPROJ_ERR_TASK);
                }

                ret = rctl_set(kv_array->data[i].key,
                    kv_array->data[i].value, Pr, flags & TASK_PROJ_MASK);

                if (ret && unknown == 0) {
                        /*
                         * We only report the first failure.
                         */
                        unknown = i + 1;
                }

                if (ret && ret != SETFAILED) {
                        /*
                         * We abort if we couldn't set a component, but if
                         * it's merely that the system didn't recognize it, we
                         * continue, as this could be a third party attribute.
                         */
                        break;
                }
        }
        _kva_free(kv_array);

        return (unknown);
}

projid_t
setproject(const char *project_name, const char *user_name, int flags)
{
        return (setproject_proc(project_name, user_name, flags, P_MYID, NULL,
            NULL));
}


priv_set_t *
setproject_initpriv(void)
{
        static priv_t taskpriv = PRIV_PROC_TASKID;
        static priv_t rctlpriv = PRIV_SYS_RESOURCE;
        static priv_t poolpriv = PRIV_SYS_RES_CONFIG;
        static priv_t schedpriv = PRIV_PROC_PRIOCNTL;
        int res;

        priv_set_t *nset;

        if (getzoneid() == GLOBAL_ZONEID) {
                res = __init_suid_priv(0, taskpriv, rctlpriv, poolpriv,
                    schedpriv, (char *)NULL);
        } else {
                res = __init_suid_priv(0, taskpriv, rctlpriv, (char *)NULL);
        }

        if (res != 0)
                return (NULL);

        nset = priv_allocset();
        if (nset != NULL) {
                priv_emptyset(nset);
                (void) priv_addset(nset, taskpriv);
                (void) priv_addset(nset, rctlpriv);
                /*
                 * Only need these if we need to change pools, which can
                 * only happen if the target is in the global zone.  Rather
                 * than checking the target's zone just check our own
                 * (since if we're in a non-global zone we won't be able
                 * to control processes in other zones).
                 */
                if (getzoneid() == GLOBAL_ZONEID) {
                        (void) priv_addset(nset, poolpriv);
                        (void) priv_addset(nset, schedpriv);
                }
        }
        return (nset);
}