#include "pathnames.h"
#include "env.h"
#include "closestream.h"
+#include "strv.h"
#include "strutils.h"
#include "ttyutils.h"
#include "pwdutils.h"
+#include "optutils.h"
#include "logindefs.h"
#include "su-common.h"
extern char **environ;
#endif
-enum {
- EXIT_CANNOT_INVOKE = 126,
- EXIT_ENOENT = 127
-};
-
enum {
SIGTERM_IDX = 0,
SIGINT_IDX,
const char *tty_number; /* end of the tty_path */
char *new_user; /* wanted user */
- char *old_user; /* orginal user */
+ char *old_user; /* original user */
pid_t child; /* fork() baby */
+ int childstatus; /* wait() status */
+
+ char **env_whitelist_names; /* environment whitelist */
+ char **env_whitelist_vals;
struct sigaction oldact[SIGNALS_IDX_COUNT]; /* original sigactions indexed by SIG*_IDX */
static void su_init_debug(void)
{
- __UL_INIT_DEBUG(su, SU_DEBUG_, 0, SU_DEBUG);
+ __UL_INIT_DEBUG_FROM_ENV(su, SU_DEBUG_, 0, SU_DEBUG);
}
static void init_tty(struct su_context *su)
{
su->isterm = isatty(STDIN_FILENO) ? 1 : 0;
- DBG(TTY, ul_debug("initilize [is-term=%s]", su->isterm ? "true" : "false"));
+ DBG(TTY, ul_debug("initialize [is-term=%s]", su->isterm ? "true" : "false"));
if (su->isterm)
get_terminal_name(NULL, &su->tty_name, &su->tty_number);
}
+/*
+ * Note, this function has to be possible call more than once. If the child is
+ * already dead than it returns saved result from the previous call.
+ */
static int wait_for_child(struct su_context *su)
{
pid_t pid = (pid_t) -1;;
int status = 0;
+ if (su->child == (pid_t) -1)
+ return su->childstatus;
+
if (su->child != (pid_t) -1) {
+ /*
+ * The "su" parent process spends all time here in waitpid(),
+ * but "su --pty" uses pty_proxy_master() and waitpid() is only
+ * called to pick up child status or to react to SIGSTOP.
+ */
DBG(SIG, ul_debug("waiting for child [%d]...", su->child));
for (;;) {
pid = waitpid(su->child, &status, WUNTRACED);
if (pid != (pid_t) - 1 && WIFSTOPPED(status)) {
+ DBG(SIG, ul_debug(" child got SIGSTOP -- stop all session"));
kill(getpid(), SIGSTOP);
/* once we get here, we must have resumed */
kill(pid, SIGCONT);
+ DBG(SIG, ul_debug(" session resumed -- continue"));
+#ifdef USE_PTY
+ /* Let's go back to pty_proxy_master() */
+ if (su->pty_sigfd != -1) {
+ DBG(SIG, ul_debug(" leaving on child SIGSTOP"));
+ return 0;
+ }
+#endif
} else
break;
}
DBG(SIG, ul_debug("child %d is dead", su->child));
su->child = (pid_t) -1; /* Don't use the PID anymore! */
+ su->childstatus = status;
} else if (caught_signal)
status = caught_signal + 128;
else
status = 1;
- DBG(SIG, ul_debug("status=%d", status));
+ DBG(SIG, ul_debug("child status=%d", status));
return status;
}
if (su->isterm) {
DBG(PTY, ul_debug("create for terminal"));
- struct winsize win;
/* original setting of the current terminal */
if (tcgetattr(STDIN_FILENO, &su->stdin_attrs) != 0)
err(EXIT_FAILURE, _("failed to get terminal attributes"));
-
- /* reuse the current terminal setting for slave */
- slave_attrs = su->stdin_attrs;
- cfmakeraw(&slave_attrs);
-
ioctl(STDIN_FILENO, TIOCGWINSZ, (char *)&su->win);
-
/* create master+slave */
- rc = openpty(&su->pty_master, &su->pty_slave, NULL, &slave_attrs, &win);
+ rc = openpty(&su->pty_master, &su->pty_slave, NULL, &su->stdin_attrs, &su->win);
+ /* set the current terminal to raw mode; pty_cleanup() reverses this change on exit */
+ slave_attrs = su->stdin_attrs;
+ cfmakeraw(&slave_attrs);
+ slave_attrs.c_lflag &= ~ECHO;
+ tcsetattr(STDIN_FILENO, TCSANOW, &slave_attrs);
} else {
DBG(PTY, ul_debug("create for non-terminal"));
rc = openpty(&su->pty_master, &su->pty_slave, NULL, NULL, NULL);
- /* set slave attributes */
- if (rc < 0) {
+ if (!rc) {
tcgetattr(su->pty_slave, &slave_attrs);
- cfmakeraw(&slave_attrs);
+ slave_attrs.c_lflag &= ~ECHO;
tcsetattr(su->pty_slave, TCSANOW, &slave_attrs);
}
}
static void pty_cleanup(struct su_context *su)
{
- if (su->pty_master == -1)
+ struct termios rtt;
+
+ if (su->pty_master == -1 || !su->isterm)
return;
DBG(PTY, ul_debug("cleanup"));
- if (su->isterm)
- tcsetattr(STDIN_FILENO, TCSADRAIN, &su->stdin_attrs);
+ rtt = su->stdin_attrs;
+ tcsetattr(STDIN_FILENO, TCSADRAIN, &rtt);
}
static int write_output(char *obuf, ssize_t bytes)
switch (info.ssi_signo) {
case SIGCHLD:
DBG(SIG, ul_debug(" get signal SIGCHLD"));
- wait_for_child(su);
- su->poll_timeout = 10;
+
+ /* The child terminated or stopped. Note that we ignore SIGCONT
+ * here, because stop/cont semantic is handled by wait_for_child() */
+ if (info.ssi_code == CLD_EXITED
+ || info.ssi_code == CLD_KILLED
+ || info.ssi_code == CLD_DUMPED
+ || info.ssi_status == SIGSTOP)
+ wait_for_child(su);
+ /* The child is dead, force poll() timeout. */
+ if (su->child == (pid_t) -1)
+ su->poll_timeout = 10;
return 0;
case SIGWINCH:
DBG(SIG, ul_debug(" get signal SIGWINCH"));
static void pty_proxy_master(struct su_context *su)
{
sigset_t ourset;
- int rc = 0, ret, ignore_stdin = 0, eof = 0;
+ int rc = 0, ret, eof = 0;
enum {
POLLFD_SIGNAL = 0,
POLLFD_MASTER,
- POLLFD_STDIN /* optional; keep it last, see ignore_stdin */
+ POLLFD_STDIN
};
struct pollfd pfd[] = {
DBG(PTY, ul_debug("calling poll()"));
/* wait for input or signal */
- ret = poll(pfd, ARRAY_SIZE(pfd) - ignore_stdin, su->poll_timeout);
+ ret = poll(pfd, ARRAY_SIZE(pfd), su->poll_timeout);
errsv = errno;
DBG(PTY, ul_debug("poll() rc=%d", ret));
break;
}
- for (i = 0; i < ARRAY_SIZE(pfd) - ignore_stdin; i++) {
+ for (i = 0; i < ARRAY_SIZE(pfd); i++) {
rc = 0;
if (pfd[i].revents == 0)
if ((pfd[i].revents & POLLHUP) || eof) {
DBG(PTY, ul_debug(" ignore FD"));
pfd[i].fd = -1;
- /* according to man poll() set FD to -1 can't be used to ignore
- * STDIN, so let's remove the FD from pool at all */
if (i == POLLFD_STDIN) {
- ignore_stdin = 1;
write_eof_to_child(su);
DBG(PTY, ul_debug(" ignore STDIN"));
}
}
}
+ close(su->pty_sigfd);
+ su->pty_sigfd = -1;
DBG(PTY, ul_debug("poll() done [signal=%d, rc=%d]", caught_signal, rc));
}
#endif /* USE_PTY */
DBG(LOG, ul_debug("btmp logging"));
memset(&ut, 0, sizeof(ut));
- strncpy(ut.ut_user,
+ str2memcpy(ut.ut_user,
su->pwd && su->pwd->pw_name ? su->pwd->pw_name : "(unknown)",
sizeof(ut.ut_user));
if (su->tty_number)
- xstrncpy(ut.ut_id, su->tty_number, sizeof(ut.ut_id));
+ str2memcpy(ut.ut_id, su->tty_number, sizeof(ut.ut_id));
if (su->tty_name)
- xstrncpy(ut.ut_line, su->tty_name, sizeof(ut.ut_line));
+ str2memcpy(ut.ut_line, su->tty_name, sizeof(ut.ut_line));
gettimeofday(&tv, NULL);
ut.ut_tv.tv_sec = tv.tv_sec;
break;
}
+ /* free unnecessary stuff */
+ free_getlogindefs_data();
/* In the parent watch the child. */
else
status = 1;
+ DBG(SIG, ul_debug("final child status=%d", status));
+
if (caught_signal && su->child != (pid_t)-1) {
fprintf(stderr, _("\nSession terminated, killing shell..."));
kill(su->child, SIGTERM);
exit(status);
}
+/* Adds @name from the current environment to the whitelist. If @name is not
+ * set then nothing is added to the whitelist and returns 1.
+ */
+static int env_whitelist_add(struct su_context *su, const char *name)
+{
+ const char *env = getenv(name);
+
+ if (!env)
+ return 1;
+ if (strv_extend(&su->env_whitelist_names, name))
+ err_oom();
+ if (strv_extend(&su->env_whitelist_vals, env))
+ err_oom();
+ return 0;
+}
+
+static int env_whitelist_setenv(struct su_context *su, int overwrite)
+{
+ char **one;
+ size_t i = 0;
+ int rc;
+
+ STRV_FOREACH(one, su->env_whitelist_names) {
+ rc = setenv(*one, su->env_whitelist_vals[i], overwrite);
+ if (rc)
+ return rc;
+ i++;
+ }
+
+ return 0;
+}
+
+/* Creates (add to) whitelist from comma delimited string */
+static int env_whitelist_from_string(struct su_context *su, const char *str)
+{
+ char **all = strv_split(str, ",");
+ char **one;
+
+ if (!all) {
+ if (errno == ENOMEM)
+ err_oom();
+ return -EINVAL;
+ }
+
+ STRV_FOREACH(one, all)
+ env_whitelist_add(su, *one);
+ strv_free(all);
+ return 0;
+}
+
static void setenv_path(const struct passwd *pw)
{
int rc;
if (pw->pw_uid)
rc = logindefs_setenv("PATH", "ENV_PATH", _PATH_DEFPATH);
- else if ((rc = logindefs_setenv("PATH", "ENV_ROOTPATH", NULL)) != 0)
- rc = logindefs_setenv("PATH", "ENV_SUPATH", _PATH_DEFPATH_ROOT);
+ else if ((rc = logindefs_setenv("PATH", "ENV_SUPATH", NULL)) != 0)
+ rc = logindefs_setenv("PATH", "ENV_ROOTPATH", _PATH_DEFPATH_ROOT);
if (rc)
err(EXIT_FAILURE, _("failed to set the PATH environment variable"));
DBG(MISC, ul_debug("modify environ[]"));
/* Leave TERM unchanged. Set HOME, SHELL, USER, LOGNAME, PATH.
- * Unset all other environment variables.
+ *
+ * Unset all other environment variables, but follow
+ * --whitelist-environment if specified.
*/
if (su->simulate_login) {
- char *term = getenv("TERM");
- if (term)
- term = xstrdup(term);
-
- environ = xmalloc((6 + ! !term) * sizeof(char *));
- environ[0] = NULL;
- if (term) {
- xsetenv("TERM", term, 1);
- free(term);
- }
-
- xsetenv("HOME", pw->pw_dir, 1);
+ /* leave TERM unchanged */
+ env_whitelist_add(su, "TERM");
+
+ /* Note that original su(1) has allocated environ[] by malloc
+ * to the number of expected variables. This seems unnecessary
+ * optimization as libc later realloc(current_size+2) and for
+ * empty environ[] the curren_size is zero. It seems better to
+ * keep all logic around environment in glibc's hands.
+ * --kzak [Aug 2018]
+ */
+#ifdef HAVE_CLEARENV
+ clearenv();
+#else
+ environ = NULL;
+#endif
+ /* always reset */
if (shell)
xsetenv("SHELL", shell, 1);
+
+ setenv_path(pw);
+
+ xsetenv("HOME", pw->pw_dir, 1);
xsetenv("USER", pw->pw_name, 1);
xsetenv("LOGNAME", pw->pw_name, 1);
- setenv_path(pw);
+
+ /* apply all from whitelist, but no overwrite */
+ env_whitelist_setenv(su, 0);
/* Set HOME, SHELL, and (if not becoming a superuser) USER and LOGNAME.
*/
size_t n_args = 1 + su->fast_startup + 2 * ! !command + n_additional_args + 1;
const char **args = xcalloc(n_args, sizeof *args);
size_t argno = 1;
- int rc;
DBG(MISC, ul_debug("starting shell [shell=%s, command=\"%s\"%s%s]",
shell, command,
memcpy(args + argno, additional_args, n_additional_args * sizeof *args);
args[argno + n_additional_args] = NULL;
execv(shell, (char **)args);
-
- rc = errno == ENOENT ? EXIT_ENOENT : EXIT_CANNOT_INVOKE;
- err(rc, _("failed to execute %s"), shell);
+ errexec(shell);
}
/* Return true if SHELL is a restricted shell (one not returned by
static void usage_common(void)
{
- fputs(_(" -m, -p, --preserve-environment do not reset environment variables\n"), stdout);
+ fputs(_(" -m, -p, --preserve-environment do not reset environment variables\n"), stdout);
+ fputs(_(" -w, --whitelist-environment <list> don't reset specified variables\n"), stdout);
+ fputs(USAGE_SEPARATOR, stdout);
+
fputs(_(" -g, --group <group> specify the primary group\n"), stdout);
fputs(_(" -G, --supp-group <group> specify a supplemental group\n"), stdout);
fputs(USAGE_SEPARATOR, stdout);
struct su_context *su = (struct su_context *) data;
DBG(MISC, ul_debug("loading logindefs"));
- logindefs_load_file(su->runuser ? _PATH_LOGINDEFS_RUNUSER : _PATH_LOGINDEFS_SU);
logindefs_load_file(_PATH_LOGINDEFS);
+ logindefs_load_file(su->runuser ? _PATH_LOGINDEFS_RUNUSER : _PATH_LOGINDEFS_SU);
}
/*
{"group", required_argument, NULL, 'g'},
{"supp-group", required_argument, NULL, 'G'},
{"user", required_argument, NULL, 'u'}, /* runuser only */
+ {"whitelist-environment", required_argument, NULL, 'w'},
{"help", no_argument, 0, 'h'},
{"version", no_argument, 0, 'V'},
{NULL, 0, NULL, 0}
};
+ static const ul_excl_t excl[] = { /* rows and cols in ASCII order */
+ { 'm', 'w' }, /* preserve-environment, whitelist-environment */
+ { 'p', 'w' }, /* preserve-environment, whitelist-environment */
+ { 0 }
+ };
+ int excl_st[ARRAY_SIZE(excl)] = UL_EXCL_STATUS_INIT;
setlocale(LC_ALL, "");
bindtextdomain(PACKAGE, LOCALEDIR);
su->conv.appdata_ptr = (void *) su;
while ((optc =
- getopt_long(argc, argv, "c:fg:G:lmpPs:u:hV", longopts,
+ getopt_long(argc, argv, "c:fg:G:lmpPs:u:hVw:", longopts,
NULL)) != -1) {
+
+ err_exclusive_options(optc, longopts, excl, excl_st);
+
switch (optc) {
case 'c':
command = optarg;
su->change_environment = false;
break;
+ case 'w':
+ env_whitelist_from_string(su, optarg);
+ break;
+
case 'P':
#ifdef USE_PTY
su->pty = 1;
DBG(MISC, ul_debug("call setsid()"));
setsid();
}
-
+#ifdef USE_PTY
if (su->pty)
pty_init_slave(su);
-
+#endif
/* Set environment after pam_open_session, which may put KRB5CCNAME
into the pam_env, etc. */