]> git.ipfire.org Git - thirdparty/util-linux.git/blobdiff - login-utils/su-common.c
mkswap: be more explicit about maximal number of pages
[thirdparty/util-linux.git] / login-utils / su-common.c
index bfccb4643e446537d17b06a836848ff616eed57c..1662d21bbac3cf82fa7432e85b715d6cd303e5a5 100644 (file)
 #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"
@@ -103,11 +105,6 @@ UL_DEBUG_DEFINE_MASKNAMES(su) = UL_DEBUG_EMPTY_MASKNAMES;
 extern char **environ;
 #endif
 
-enum {
-       EXIT_CANNOT_INVOKE = 126,
-       EXIT_ENOENT = 127
-};
-
 enum {
        SIGTERM_IDX = 0,
        SIGINT_IDX,
@@ -130,9 +127,13 @@ struct su_context {
        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 */
 
@@ -171,31 +172,52 @@ su_catch_sig(int sig)
 
 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;
                }
@@ -212,12 +234,13 @@ static int wait_for_child(struct su_context *su)
 
                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;
 }
 
@@ -253,29 +276,26 @@ static void pty_create(struct su_context *su)
 
        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);
                }
        }
@@ -288,12 +308,14 @@ static void pty_create(struct su_context *su)
 
 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)
@@ -412,8 +434,17 @@ static int pty_handle_signal(struct su_context *su, int fd)
        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"));
@@ -442,11 +473,11 @@ static int pty_handle_signal(struct su_context *su, int fd)
 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[] = {
@@ -490,7 +521,7 @@ static void pty_proxy_master(struct su_context *su)
                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));
 
@@ -505,7 +536,7 @@ static void pty_proxy_master(struct su_context *su)
                        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)
@@ -531,10 +562,7 @@ static void pty_proxy_master(struct su_context *su)
                                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"));
                                        }
@@ -549,6 +577,8 @@ static void pty_proxy_master(struct su_context *su)
                }
        }
 
+       close(su->pty_sigfd);
+       su->pty_sigfd = -1;
        DBG(PTY, ul_debug("poll() done [signal=%d, rc=%d]", caught_signal, rc));
 }
 #endif /* USE_PTY */
@@ -581,14 +611,14 @@ static void log_btmp(struct su_context *su)
        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;
@@ -826,6 +856,8 @@ static void create_watching_parent(struct su_context *su)
                break;
        }
 
+       /* free unnecessary stuff */
+       free_getlogindefs_data();
 
        /* In the parent watch the child.  */
 
@@ -848,6 +880,8 @@ static void create_watching_parent(struct su_context *su)
        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);
@@ -898,6 +932,56 @@ static void create_watching_parent(struct su_context *su)
        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;
@@ -907,8 +991,8 @@ static void setenv_path(const struct passwd *pw)
        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"));
@@ -922,26 +1006,38 @@ static void modify_environment(struct su_context *su, const char *shell)
        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.
         */
@@ -1009,7 +1105,6 @@ static void run_shell(
        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,
@@ -1038,9 +1133,7 @@ static void run_shell(
        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
@@ -1065,7 +1158,10 @@ static bool is_restricted_shell(const char *shell)
 
 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);
@@ -1135,8 +1231,8 @@ static void load_config(void *data)
        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);
 }
 
 /*
@@ -1210,10 +1306,17 @@ int su_main(int argc, char **argv, int mode)
                {"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);
@@ -1224,8 +1327,11 @@ int su_main(int argc, char **argv, int mode)
        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;
@@ -1259,6 +1365,10 @@ int su_main(int argc, char **argv, int mode)
                        su->change_environment = false;
                        break;
 
+               case 'w':
+                       env_whitelist_from_string(su, optarg);
+                       break;
+
                case 'P':
 #ifdef USE_PTY
                        su->pty = 1;
@@ -1392,10 +1502,10 @@ int su_main(int argc, char **argv, int mode)
                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.  */