/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (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) 2013 Gary Mills * Copyright 2015, Joyent, Inc. * * Copyright 2005 Sun Microsystems, Inc. All rights reserved. * Use is subject to license terms. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "utils.h" #define OPTIONS_STRING "Fc:lp:v" #define NENV 8 #define ENVSIZE 255 #define PATH "PATH=/usr/bin" #define SUPATH "PATH=/usr/sbin:/usr/bin" #define SHELL "/usr/bin/sh" #define SHELL2 "/sbin/sh" #define TIMEZONEFILE "/etc/default/init" #define LOGINFILE "/etc/default/login" #define GLOBAL_ERR_SZ 1024 #define GRAB_RETRY_MAX 100 static const char *pname; extern char **environ; static char *supath = SUPATH; static char *path = PATH; static char global_error[GLOBAL_ERR_SZ]; static int verbose = 0; static priv_set_t *nset; /* Private definitions for libproject */ extern projid_t setproject_proc(const char *, const char *, int, pid_t, struct ps_prochandle *, struct project *); extern priv_set_t *setproject_initpriv(void); static void usage(void); static void preserve_error(const char *format, ...); static int update_running_proc(int, char *, char *); static int set_ids(struct ps_prochandle *, struct project *, struct passwd *); static struct passwd *match_user(uid_t, char *, int); static void setproject_err(char *, char *, int, struct project *); static void usage(void) { (void) fprintf(stderr, gettext("usage: \n\t%s [-v] [-p project] " "[-c pid | [-Fl] [command [args ...]]]\n"), pname); exit(2); } int main(int argc, char *argv[]) { int c; struct passwd *pw; char *projname = NULL; uid_t uid; int login_flag = 0; int finalize_flag = TASK_NORMAL; int newproj_flag = 0; taskid_t taskid; char *shell; char *env[NENV]; char **targs; char *filename, *procname = NULL; int error; nset = setproject_initpriv(); if (nset == NULL) die(gettext("privilege initialization failed\n")); pname = getpname(argv[0]); while ((c = getopt(argc, argv, OPTIONS_STRING)) != EOF) { switch (c) { case 'v': verbose = 1; break; case 'p': newproj_flag = 1; projname = optarg; break; case 'F': finalize_flag = TASK_FINAL; break; case 'l': login_flag++; break; case 'c': procname = optarg; break; case '?': default: usage(); /*NOTREACHED*/ } } /* -c option is invalid with -F, -l, or a specified command */ if ((procname != NULL) && (finalize_flag == TASK_FINAL || login_flag || optind < argc)) usage(); if (procname != NULL) { /* Change project/task of an existing process */ return (update_running_proc(newproj_flag, procname, projname)); } /* * Get user data, so that we can confirm project membership as * well as construct an appropriate login environment. */ uid = getuid(); if ((pw = match_user(uid, projname, 1)) == NULL) { die("%s\n", global_error); } /* * If no projname was specified, we're just creating a new task * under the current project, so we can just set the new taskid. * If our project is changing, we need to update any attendant * pool/rctl bindings, so let setproject() do the dirty work. */ (void) __priv_bracket(PRIV_ON); if (projname == NULL) { if (settaskid(getprojid(), finalize_flag) == -1) if (errno == EAGAIN) die(gettext("resource control limit has been " "reached")); else die(gettext("settaskid failed")); } else { if ((error = setproject(projname, pw->pw_name, finalize_flag)) != 0) { setproject_err(pw->pw_name, projname, error, NULL); if (error < 0) die("%s\n", global_error); else warn("%s\n", global_error); } } __priv_relinquish(); taskid = gettaskid(); if (verbose) (void) fprintf(stderr, "%d\n", (int)taskid); /* * Validate user's shell from passwd database. */ if (strcmp(pw->pw_shell, "") == 0) { if (access(SHELL, X_OK) == 0) pw->pw_shell = SHELL; else pw->pw_shell = SHELL2; } if (login_flag) { /* * Since we've been invoked as a "simulated login", set up the * environment. */ char *cur_tz = getenv("TZ"); char *cur_term = getenv("TERM"); char **envnext; size_t len_home = strlen(pw->pw_dir) + strlen("HOME=") + 1; size_t len_logname = strlen(pw->pw_name) + strlen("LOGNAME=") + 1; size_t len_shell = strlen(pw->pw_shell) + strlen("SHELL=") + 1; size_t len_mail = strlen(pw->pw_name) + strlen("MAIL=/var/mail/") + 1; size_t len_tz; size_t len_term; char *env_home = safe_malloc(len_home); char *env_logname = safe_malloc(len_logname); char *env_shell = safe_malloc(len_shell); char *env_mail = safe_malloc(len_mail); char *env_tz; char *env_term; (void) snprintf(env_home, len_home, "HOME=%s", pw->pw_dir); (void) snprintf(env_logname, len_logname, "LOGNAME=%s", pw->pw_name); (void) snprintf(env_shell, len_shell, "SHELL=%s", pw->pw_shell); (void) snprintf(env_mail, len_mail, "MAIL=/var/mail/%s", pw->pw_name); env[0] = env_home; env[1] = env_logname; env[2] = (pw->pw_uid == 0 ? supath : path); env[3] = env_shell; env[4] = env_mail; env[5] = NULL; env[6] = NULL; env[7] = NULL; envnext = (char **)&env[5]; /* * It's possible that TERM wasn't defined in the outer * environment. */ if (cur_term != NULL) { len_term = strlen(cur_term) + strlen("TERM=") + 1; env_term = safe_malloc(len_term); (void) snprintf(env_term, len_term, "TERM=%s", cur_term); *envnext = env_term; envnext++; } /* * It is also possible that TZ wasn't defined in the outer * environment. In that case, we must attempt to open the file * defining the default timezone and select the appropriate * entry. If there is no default timezone there, try * TIMEZONE in /etc/default/login, duplicating the algorithm * that login uses. */ if (cur_tz != NULL) { len_tz = strlen(cur_tz) + strlen("TZ=") + 1; env_tz = safe_malloc(len_tz); (void) snprintf(env_tz, len_tz, "TZ=%s", cur_tz); *envnext = env_tz; } else { if ((env_tz = getdefault(TIMEZONEFILE, "TZ=", "TZ=")) != NULL) *envnext = env_tz; else { env_tz = getdefault(LOGINFILE, "TIMEZONE=", "TZ="); *envnext = env_tz; } } environ = (char **)&env[0]; /* * Prefix the shell string with a hyphen, indicating a login * shell. */ shell = safe_malloc(PATH_MAX); (void) snprintf(shell, PATH_MAX, "-%s", basename(pw->pw_shell)); } else { shell = basename(pw->pw_shell); } /* * If there are no arguments, we launch the user's shell; otherwise, the * remaining commands are assumed to form a valid command invocation * that we can exec. */ if (optind >= argc) { targs = alloca(2 * sizeof (char *)); filename = pw->pw_shell; targs[0] = shell; targs[1] = NULL; } else { targs = &argv[optind]; filename = targs[0]; } if (execvp(filename, targs) == -1) die(gettext("exec of %s failed"), targs[0]); /* * We should never get here. */ return (1); } static int update_running_proc(int newproj_flag, char *procname, char *projname) { struct ps_prochandle *p; prcred_t original_prcred, current_prcred; projid_t prprojid; taskid_t taskid; int error = 0, gret; struct project project; char prbuf[PROJECT_BUFSZ]; struct passwd *passwd_entry; int grab_retry_count = 0; /* * Catch signals from terminal. There isn't much sense in * doing anything but ignoring them since we don't do anything * after the point we'd be capable of handling them again. */ (void) sigignore(SIGHUP); (void) sigignore(SIGINT); (void) sigignore(SIGQUIT); (void) sigignore(SIGTERM); /* flush stdout before grabbing the proc to avoid deadlock */ (void) fflush(stdout); /* * We need to grab the process, which will force it to stop execution * until the grab is released, in order to aquire some information about * it, such as its current project (which is achieved via an injected * system call and therefore needs an agent) and its credentials. We * will then need to release it again because it may be a process that * we rely on for later calls, for example nscd. */ if ((p = proc_arg_grab(procname, PR_ARG_PIDS, 0, &gret)) == NULL) { warn(gettext("failed to grab for process %s: %s\n"), procname, Pgrab_error(gret)); return (1); } if (Pcreate_agent(p) != 0) { Prelease(p, 0); warn(gettext("cannot control process %s\n"), procname); return (1); } /* * The victim process is now held. Do not call any functions * which generate stdout/stderr until the process has been * released. */ /* * The target process will soon be restarted (in case it is in newtask's * execution path) and then stopped again. We need to ensure that our cached * data doesn't change while the process runs so return here if the target * process changes its user id in between our stop operations, so that we can * try again. */ pgrab_retry: /* Cache required information about the process. */ if (Pcred(p, &original_prcred, 0) != 0) { preserve_error(gettext("cannot get process credentials %s\n"), procname); error = 1; } if ((prprojid = pr_getprojid(p)) == -1) { preserve_error(gettext("cannot get process project id %s\n"), procname); error = 1; } /* * We now have all the required information, so release the target * process and perform our sanity checks. The process needs to be * running at this point because it may be in the execution path of the * calls made below. */ Pdestroy_agent(p); Prelease(p, 0); /* if our data acquisition failed, then we can't continue. */ if (error) { warn("%s\n", global_error); return (1); } if (newproj_flag == 0) { /* * Just changing the task, so set projname to the current * project of the running process. */ if (getprojbyid(prprojid, &project, &prbuf, PROJECT_BUFSZ) == NULL) { warn(gettext("unable to get project name " "for projid %d"), prprojid); return (1); } projname = project.pj_name; } else { /* * cache info for the project which user passed in via the * command line */ if (getprojbyname(projname, &project, &prbuf, PROJECT_BUFSZ) == NULL) { warn(gettext("unknown project \"%s\"\n"), projname); return (1); } } /* * Use our cached information to verify that the owner of the running * process is a member of proj */ if ((passwd_entry = match_user(original_prcred.pr_ruid, projname, 0)) == NULL) { warn("%s\n", global_error); return (1); } /* * We can now safely stop the process again in order to change the * project and taskid as required. */ if ((p = proc_arg_grab(procname, PR_ARG_PIDS, 0, &gret)) == NULL) { warn(gettext("failed to grab for process %s: %s\n"), procname, Pgrab_error(gret)); return (1); } if (Pcreate_agent(p) != 0) { Prelease(p, 0); warn(gettext("cannot control process %s\n"), procname); return (1); } /* * Now that the target process is stopped, check the validity of our * cached info. If we aren't superuser then match_user() will have * checked to make sure that the owner of the process is in the relevant * project. If our ruid has changed, then match_user()'s conclusion may * be invalid. */ if (getuid() != 0) { if (Pcred(p, ¤t_prcred, 0) != 0) { Pdestroy_agent(p); Prelease(p, 0); warn(gettext("can't get process credentials %s\n"), procname); return (1); } if (original_prcred.pr_ruid != current_prcred.pr_ruid) { if (grab_retry_count++ < GRAB_RETRY_MAX) goto pgrab_retry; warn(gettext("process consistently changed its " "user id %s\n"), procname); return (1); } } error = set_ids(p, &project, passwd_entry); if (verbose) taskid = pr_gettaskid(p); Pdestroy_agent(p); Prelease(p, 0); if (error) { /* * error is serious enough to stop, only if negative. * Otherwise, it simply indicates one of the resource * control assignments failed, which is worth warning * about. */ warn("%s\n", global_error); if (error < 0) return (1); } if (verbose) (void) fprintf(stderr, "%d\n", (int)taskid); return (0); } static int set_ids(struct ps_prochandle *p, struct project *project, struct passwd *passwd_entry) { int be_su = 0; prcred_t old_prcred; int error; prpriv_t *old_prpriv, *new_prpriv; size_t prsz = sizeof (prpriv_t); priv_set_t *eset, *pset; int ind; if (Pcred(p, &old_prcred, 0) != 0) { preserve_error(gettext("can't get process credentials")); return (1); } old_prpriv = proc_get_priv(Pstatus(p)->pr_pid); if (old_prpriv == NULL) { preserve_error(gettext("can't get process privileges")); return (1); } prsz = PRIV_PRPRIV_SIZE(old_prpriv); new_prpriv = malloc(prsz); if (new_prpriv == NULL) { preserve_error(gettext("can't allocate memory")); proc_free_priv(old_prpriv); return (1); } (void) memcpy(new_prpriv, old_prpriv, prsz); /* * If the process already has the proc_taskid privilege, * we don't need to elevate its privileges; if it doesn't, * we try to do it here. * As we do not wish to leave a window in which the process runs * with elevated privileges, we make sure that the process dies * when we go away unexpectedly. */ ind = priv_getsetbyname(PRIV_EFFECTIVE); eset = (priv_set_t *)&new_prpriv->pr_sets[new_prpriv->pr_setsize * ind]; ind = priv_getsetbyname(PRIV_PERMITTED); pset = (priv_set_t *)&new_prpriv->pr_sets[new_prpriv->pr_setsize * ind]; if (!priv_issubset(nset, eset)) { be_su = 1; priv_union(nset, eset); priv_union(nset, pset); if (Psetflags(p, PR_KLC) != 0) { preserve_error(gettext("cannot set process " "privileges")); (void) Punsetflags(p, PR_KLC); free(new_prpriv); proc_free_priv(old_prpriv); return (1); } (void) __priv_bracket(PRIV_ON); if (Psetpriv(p, new_prpriv) != 0) { (void) __priv_bracket(PRIV_OFF); preserve_error(gettext("cannot set process " "privileges")); (void) Punsetflags(p, PR_KLC); free(new_prpriv); proc_free_priv(old_prpriv); return (1); } (void) __priv_bracket(PRIV_OFF); } (void) __priv_bracket(PRIV_ON); if ((error = setproject_proc(project->pj_name, passwd_entry->pw_name, 0, Pstatus(p)->pr_pid, p, project)) != 0) { /* global_error is set by setproject_err */ setproject_err(passwd_entry->pw_name, project->pj_name, error, project); } (void) __priv_bracket(PRIV_OFF); /* relinquish added privileges */ if (be_su) { (void) __priv_bracket(PRIV_ON); if (Psetpriv(p, old_prpriv) != 0) { /* * We shouldn't ever be in a state where we can't * set the process back to its old creds, but we * don't want to take the chance of leaving a * non-privileged process with enhanced creds. So, * release the process from libproc control, knowing * that it will be killed. */ (void) __priv_bracket(PRIV_OFF); Pdestroy_agent(p); die(gettext("cannot relinquish superuser credentials " "for pid %d. The process was killed."), Pstatus(p)->pr_pid); } (void) __priv_bracket(PRIV_OFF); if (Punsetflags(p, PR_KLC) != 0) preserve_error(gettext("error relinquishing " "credentials. Process %d will be killed."), Pstatus(p)->pr_pid); } free(new_prpriv); proc_free_priv(old_prpriv); return (error); } /* * preserve_error() should be called rather than warn() by any * function that is called while the victim process is being * held by Pgrab. * * It saves a single error message to be printed until after * the process has been released. Since multiple errors are not * stored, any error should be considered critical. */ void preserve_error(const char *format, ...) { va_list alist; va_start(alist, format); /* * GLOBAL_ERR_SZ is pretty big. If the error is longer * than that, just truncate it, rather than chance missing * the error altogether. */ (void) vsnprintf(global_error, GLOBAL_ERR_SZ-1, format, alist); va_end(alist); } /* * Given the input arguments, return the passwd structure that matches best. * Also, since we use getpwnam() and friends, subsequent calls to this * function will re-use the memory previously returned. */ static struct passwd * match_user(uid_t uid, char *projname, int is_my_uid) { char prbuf[PROJECT_BUFSZ], username[LOGNAME_MAX+1]; struct project prj; char *tmp_name; struct passwd *pw = NULL; /* * In order to allow users with the same UID but distinguishable * user names to be in different projects we play a guessing * game of which username is most appropriate. If we're checking * for the uid of the calling process, the login name is a * good starting point. */ if (is_my_uid) { if ((tmp_name = getlogin()) == NULL || (pw = getpwnam(tmp_name)) == NULL || (pw->pw_uid != uid) || (pw->pw_name == NULL)) pw = NULL; } /* * If the login name doesn't work, we try the first match for * the current uid in the password file. */ if (pw == NULL) { if (((pw = getpwuid(uid)) == NULL) || pw->pw_name == NULL) { preserve_error(gettext("cannot find username " "for uid %d"), uid); return (NULL); } } /* * If projname wasn't supplied, we've done our best, so just return * what we've got now. Alternatively, if newtask's invoker has * superuser privileges, return the pw structure we've got now, with * no further checking from inproj(). Superuser should be able to * join any project, and the subsequent call to setproject() will * allow this. */ if (projname == NULL || getuid() == (uid_t)0) return (pw); (void) strlcpy(username, pw->pw_name, sizeof (username)); if (inproj(username, projname, prbuf, PROJECT_BUFSZ) == 0) { char **u; tmp_name = NULL; /* * If the previous guesses didn't work, walk through all * project members and test for UID-equivalence. */ if (getprojbyname(projname, &prj, prbuf, PROJECT_BUFSZ) == NULL) { preserve_error(gettext("unknown project \"%s\""), projname); return (NULL); } for (u = prj.pj_users; *u; u++) { if ((pw = getpwnam(*u)) == NULL) continue; if (pw->pw_uid == uid) { tmp_name = pw->pw_name; break; } } if (tmp_name == NULL) { preserve_error(gettext("user \"%s\" is not a member of " "project \"%s\""), username, projname); return (NULL); } } return (pw); } void setproject_err(char *username, char *projname, int error, struct project *proj) { kva_t *kv_array = NULL; char prbuf[PROJECT_BUFSZ]; struct project local_proj; switch (error) { case SETPROJ_ERR_TASK: if (errno == EAGAIN) preserve_error(gettext("resource control limit has " "been reached")); else if (errno == ESRCH) preserve_error(gettext("user \"%s\" is not a member of " "project \"%s\""), username, projname); else if (errno == EACCES) preserve_error(gettext("the invoking task is final")); else preserve_error( gettext("could not join project \"%s\""), projname); break; case SETPROJ_ERR_POOL: if (errno == EACCES) preserve_error(gettext("no resource pool accepting " "default bindings exists for project \"%s\""), projname); else if (errno == ESRCH) preserve_error(gettext("specified resource pool does " "not exist for project \"%s\""), projname); else preserve_error(gettext("could not bind to default " "resource pool for project \"%s\""), projname); break; default: if (error <= 0) { preserve_error(gettext("setproject failed for " "project \"%s\""), projname); return; } /* * If we have a stopped target process it may be in * getprojbyname()'s execution path which would make it unsafe * to access the project table, so only do that if the caller * hasn't provided a cached version of the project structure. */ if (proj == NULL) proj = getprojbyname(projname, &local_proj, prbuf, PROJECT_BUFSZ); if (proj == NULL || (kv_array = _str2kva(proj->pj_attr, KV_ASSIGN, KV_DELIMITER)) == NULL || kv_array->length < error) { preserve_error(gettext("warning, resource control " "assignment failed for project \"%s\" " "attribute %d"), projname, error); if (kv_array) _kva_free(kv_array); return; } preserve_error(gettext("warning, %s resource control " "assignment failed for project \"%s\""), kv_array->data[error - 1].key, projname); _kva_free(kv_array); } }