1 /* su for GNU. Run a shell with substitute user and group IDs.
2 Copyright (C) 1992-2006 Free Software Foundation, Inc.
4 This program is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation; either version 2, or (at your option)
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the Free Software Foundation,
16 Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */
18 /* Run a shell with the real and effective UID and GID and groups
19 of USER, default `root'.
21 The shell run is taken from USER's password entry, /bin/sh if
22 none is specified there. If the account has a password, su
23 prompts for a password unless run by a user with real UID 0.
25 Does not change the current directory.
26 Sets `HOME' and `SHELL' from the password entry for USER, and if
27 USER is not root, sets `USER' and `LOGNAME' to USER.
28 The subshell is not a login shell.
30 If one or more ARGs are given, they are passed as additional
31 arguments to the subshell.
33 Does not handle /bin/sh or other shells specially
34 (setting argv[0] to "-su", passing -c only to certain shells, etc.).
35 I don't see the point in doing that, and it's ugly.
37 This program intentionally does not support a "wheel group" that
38 restricts who can su to UID 0 accounts. RMS considers that to
43 Actually, with PAM, su has nothing to do with whether or not a
44 wheel group is enforced by su. RMS tries to restrict your access
45 to a su which implements the wheel group, but PAM considers that
46 to be fascist, and gives the user/sysadmin the opportunity to
47 enforce a wheel group by proper editing of /etc/pam.d/su
52 -DSYSLOG_SUCCESS Log successful su's (by default, to root) with syslog.
53 -DSYSLOG_FAILURE Log failed su's (by default, to root) with syslog.
55 -DSYSLOG_NON_ROOT Log all su's, not just those to root (UID 0).
56 Never logs attempted su's to nonexistent accounts.
58 Written by David MacKenzie <djm@gnu.ai.mit.edu>. */
63 #include <sys/types.h>
67 # include <security/pam_appl.h>
68 # include <security/pam_misc.h>
70 # include <sys/wait.h>
71 # include <sys/fsuid.h>
74 /* Hide any system prototype for getusershell.
75 This is necessary because some Cray systems have a conflicting
76 prototype (returning `int') in <unistd.h>. */
77 #define getusershell _getusershell_sys_proto_
84 #if HAVE_SYSLOG_H && HAVE_SYSLOG
86 # define SYSLOG_SUCCESS 1
87 # define SYSLOG_FAILURE 1
88 # define SYSLOG_NON_ROOT 1
90 # undef SYSLOG_SUCCESS
91 # undef SYSLOG_FAILURE
92 # undef SYSLOG_NON_ROOT
96 # include <sys/param.h>
100 # define endgrent() ((void) 0)
103 #ifndef HAVE_ENDPWENT
104 # define endpwent() ((void) 0)
113 /* The official name of this program (e.g., no `g' prefix). */
114 #define PROGRAM_NAME "su"
116 #define AUTHORS "David MacKenzie"
122 /* The default PATH for simulated logins to non-superuser accounts. */
123 #define DEFAULT_LOGIN_PATH "/usr/local/bin:/bin:/usr/bin"
125 /* The default PATH for simulated logins to superuser accounts. */
126 #define DEFAULT_ROOT_LOGIN_PATH "/usr/sbin:/bin:/usr/bin:/sbin"
128 /* The shell to run if none is given in the user's passwd entry. */
129 #define DEFAULT_SHELL "/bin/sh"
131 /* The user to become if none is specified. */
132 #define DEFAULT_USER "root"
137 char *getusershell ();
138 void endusershell ();
139 void setusershell ();
141 extern char **environ
;
143 static void run_shell (char const *, char const *, char **, size_t)
146 /* The name this program was run with. */
149 /* If true, pass the `-f' option to the subshell. */
150 static bool fast_startup
;
152 /* If true, simulate a login instead of just starting a shell. */
153 static bool simulate_login
;
155 /* If true, change some environment vars to indicate the user su'd to. */
156 static bool change_environment
;
159 static bool _pam_session_opened
;
160 static bool _pam_cred_established
;
163 static struct option
const longopts
[] =
165 {"command", required_argument
, NULL
, 'c'},
166 {"fast", no_argument
, NULL
, 'f'},
167 {"login", no_argument
, NULL
, 'l'},
168 {"preserve-environment", no_argument
, NULL
, 'p'},
169 {"shell", required_argument
, NULL
, 's'},
170 {GETOPT_HELP_OPTION_DECL
},
171 {GETOPT_VERSION_OPTION_DECL
},
175 /* Add NAME=VAL to the environment, checking for out of memory errors. */
178 xsetenv (char const *name
, char const *val
)
180 size_t namelen
= strlen (name
);
181 size_t vallen
= strlen (val
);
182 char *string
= xmalloc (namelen
+ 1 + vallen
+ 1);
183 strcpy (string
, name
);
184 string
[namelen
] = '=';
185 strcpy (string
+ namelen
+ 1, val
);
186 if (putenv (string
) != 0)
190 #if defined SYSLOG_SUCCESS || defined SYSLOG_FAILURE
191 /* Log the fact that someone has run su to the user given by PW;
192 if SUCCESSFUL is true, they gave the correct password, etc. */
195 log_su (struct passwd
const *pw
, bool successful
)
197 const char *new_user
, *old_user
, *tty
;
199 # ifndef SYSLOG_NON_ROOT
203 new_user
= pw
->pw_name
;
204 /* The utmp entry (via getlogin) is probably the best way to identify
205 the user, especially if someone su's from a su-shell. */
206 old_user
= getlogin ();
209 /* getlogin can fail -- usually due to lack of utmp entry.
210 Resort to getpwuid. */
211 struct passwd
*pwd
= getpwuid (getuid ());
212 old_user
= (pwd
? pwd
->pw_name
: "");
214 tty
= ttyname (STDERR_FILENO
);
217 /* 4.2BSD openlog doesn't have the third parameter. */
218 openlog (last_component (program_name
), 0
224 # ifdef SYSLOG_NON_ROOT
225 "%s(to %s) %s on %s",
229 successful
? "" : "FAILED SU ",
230 # ifdef SYSLOG_NON_ROOT
239 # define PAM_SERVICE_NAME PROGRAM_NAME
240 # define PAM_SERVICE_NAME_L PROGRAM_NAME "-l"
241 static sig_atomic_t volatile caught_signal
= false;
242 static pam_handle_t
*pamh
= NULL
;
244 static struct pam_conv conv
=
250 # define PAM_BAIL_P(a) \
253 pam_end (pamh, retval); \
258 cleanup_pam (int retcode
)
260 if (_pam_session_opened
)
261 pam_close_session (pamh
, 0);
263 if (_pam_cred_established
)
264 pam_setcred (pamh
, PAM_DELETE_CRED
| PAM_SILENT
);
266 pam_end(pamh
, retcode
);
269 /* Signal handler for parent process. */
271 su_catch_sig (int sig
)
273 caught_signal
= true;
276 /* Export env variables declared by PAM modules. */
282 /* This is a copy but don't care to free as we exec later anyways. */
283 env
= pam_getenvlist (pamh
);
286 if (putenv (*env
) != 0)
293 create_watching_parent (void)
299 retval
= pam_open_session (pamh
, 0);
300 if (retval
!= PAM_SUCCESS
)
302 cleanup_pam (retval
);
303 error (EXIT_FAILURE
, 0, _("cannot not open session: %s"),
304 pam_strerror (pamh
, retval
));
307 _pam_session_opened
= 1;
310 if (child
== (pid_t
) -1)
312 cleanup_pam (PAM_ABORT
);
313 error (EXIT_FAILURE
, errno
, _("cannot create child process"));
316 /* the child proceeds to run the shell */
320 /* In the parent watch the child. */
322 /* su without pam support does not have a helper that keeps
323 sitting on any directory so let's go to /. */
324 if (chdir ("/") != 0)
325 error (0, errno
, _("warning: cannot change directory to %s"), "/");
327 sigfillset (&ourset
);
328 if (sigprocmask (SIG_BLOCK
, &ourset
, NULL
))
330 error (0, errno
, _("cannot block signals"));
331 caught_signal
= true;
335 struct sigaction action
;
336 action
.sa_handler
= su_catch_sig
;
337 sigemptyset (&action
.sa_mask
);
339 sigemptyset (&ourset
);
340 if (sigaddset (&ourset
, SIGTERM
)
341 || sigaddset (&ourset
, SIGALRM
)
342 || sigaction (SIGTERM
, &action
, NULL
)
343 || sigprocmask (SIG_UNBLOCK
, &ourset
, NULL
))
345 error (0, errno
, _("cannot set signal handler"));
346 caught_signal
= true;
354 pid
= waitpid (child
, &status
, WUNTRACED
);
356 if (pid
!= (pid_t
)-1 && WIFSTOPPED (status
))
358 kill (getpid (), SIGSTOP
);
359 /* once we get here, we must have resumed */
365 if (pid
!= (pid_t
)-1)
366 if (WIFSIGNALED (status
))
367 status
= WTERMSIG (status
) + 128;
369 status
= WEXITSTATUS (status
);
378 fprintf (stderr
, _("\nSession terminated, killing shell..."));
379 kill (child
, SIGTERM
);
382 cleanup_pam (PAM_SUCCESS
);
387 kill (child
, SIGKILL
);
388 fprintf (stderr
, _(" ...killed.\n"));
394 /* Ask the user for a password.
395 If PAM is in use, let PAM ask for the password if necessary.
396 Return true if the user gives the correct password for entry PW,
397 false if not. Return true without asking for a password if run by UID 0
398 or if PW has an empty password. */
401 correct_password (const struct passwd
*pw
)
404 const struct passwd
*lpw
;
407 retval
= pam_start (simulate_login
? PAM_SERVICE_NAME_L
: PAM_SERVICE_NAME
,
408 pw
->pw_name
, &conv
, &pamh
);
409 PAM_BAIL_P (return false);
411 if (isatty (0) && (cp
= ttyname (0)) != NULL
)
415 if (strncmp (cp
, "/dev/", 5) == 0)
419 retval
= pam_set_item (pamh
, PAM_TTY
, tty
);
420 PAM_BAIL_P (return false);
422 # if 0 /* Manpage discourages use of getlogin. */
424 if (!(cp
&& *cp
&& (lpw
= getpwnam (cp
)) != NULL
&& lpw
->pw_uid
== getuid ()))
426 lpw
= getpwuid (getuid ());
427 if (lpw
&& lpw
->pw_name
)
429 retval
= pam_set_item (pamh
, PAM_RUSER
, (const void *) lpw
->pw_name
);
430 PAM_BAIL_P (return false);
432 retval
= pam_authenticate (pamh
, 0);
433 PAM_BAIL_P (return false);
434 retval
= pam_acct_mgmt (pamh
, 0);
435 if (retval
== PAM_NEW_AUTHTOK_REQD
)
437 /* Password has expired. Offer option to change it. */
438 retval
= pam_chauthtok (pamh
, PAM_CHANGE_EXPIRED_AUTHTOK
);
439 PAM_BAIL_P (return false);
441 PAM_BAIL_P (return false);
442 /* Must be authenticated if this point was reached. */
445 char *unencrypted
, *encrypted
, *correct
;
446 # if HAVE_GETSPNAM && HAVE_STRUCT_SPWD_SP_PWDP
447 /* Shadow passwd stuff for SVR3 and maybe other systems. */
448 const struct spwd
*sp
= getspnam (pw
->pw_name
);
452 correct
= sp
->sp_pwdp
;
455 correct
= pw
->pw_passwd
;
457 if (getuid () == 0 || !correct
|| correct
[0] == '\0')
460 unencrypted
= getpass (_("Password:"));
463 error (0, 0, _("getpass: cannot open /dev/tty"));
466 encrypted
= crypt (unencrypted
, correct
);
467 memset (unencrypted
, 0, strlen (unencrypted
));
468 return STREQ (encrypted
, correct
);
469 #endif /* !USE_PAM */
472 /* Update `environ' for the new shell based on PW, with SHELL being
473 the value for the SHELL environment variable. */
476 modify_environment (const struct passwd
*pw
, const char *shell
)
480 /* Leave TERM unchanged. Set HOME, SHELL, USER, LOGNAME, PATH.
481 Unset all other environment variables. */
482 char const *term
= getenv ("TERM");
484 term
= xstrdup (term
);
485 environ
= xmalloc ((6 + !!term
) * sizeof (char *));
488 xsetenv ("TERM", term
);
489 xsetenv ("HOME", pw
->pw_dir
);
490 xsetenv ("SHELL", shell
);
491 xsetenv ("USER", pw
->pw_name
);
492 xsetenv ("LOGNAME", pw
->pw_name
);
493 xsetenv ("PATH", (pw
->pw_uid
495 : DEFAULT_ROOT_LOGIN_PATH
));
499 /* Set HOME, SHELL, and if not becoming a super-user,
501 if (change_environment
)
503 xsetenv ("HOME", pw
->pw_dir
);
504 xsetenv ("SHELL", shell
);
507 xsetenv ("USER", pw
->pw_name
);
508 xsetenv ("LOGNAME", pw
->pw_name
);
518 /* Become the user and group(s) specified by PW. */
521 init_groups (const struct passwd
*pw
)
523 #ifdef HAVE_INITGROUPS
525 if (initgroups (pw
->pw_name
, pw
->pw_gid
) == -1)
528 cleanup_pam (PAM_ABORT
);
530 error (EXIT_FAIL
, errno
, _("cannot set groups"));
536 retval
= pam_setcred (pamh
, PAM_ESTABLISH_CRED
);
537 if (retval
!= PAM_SUCCESS
)
538 error (EXIT_FAILURE
, 0, "%s", pam_strerror (pamh
, retval
));
540 _pam_cred_established
= 1;
545 change_identity (const struct passwd
*pw
)
547 if (setgid (pw
->pw_gid
))
548 error (EXIT_FAIL
, errno
, _("cannot set group id"));
549 if (setuid (pw
->pw_uid
))
550 error (EXIT_FAIL
, errno
, _("cannot set user id"));
553 /* Run SHELL, or DEFAULT_SHELL if SHELL is empty.
554 If COMMAND is nonzero, pass it to the shell with the -c option.
555 Pass ADDITIONAL_ARGS to the shell as more arguments; there
556 are N_ADDITIONAL_ARGS extra arguments. */
559 run_shell (char const *shell
, char const *command
, char **additional_args
,
560 size_t n_additional_args
)
562 size_t n_args
= 1 + fast_startup
+ 2 * !!command
+ n_additional_args
+ 1;
563 char const **args
= xnmalloc (n_args
, sizeof *args
);
569 char *shell_basename
;
571 shell_basename
= last_component (shell
);
572 arg0
= xmalloc (strlen (shell_basename
) + 2);
574 strcpy (arg0
+ 1, shell_basename
);
578 args
[0] = last_component (shell
);
580 args
[argno
++] = "-f";
583 args
[argno
++] = "-c";
584 args
[argno
++] = command
;
586 memcpy (args
+ argno
, additional_args
, n_additional_args
* sizeof *args
);
587 args
[argno
+ n_additional_args
] = NULL
;
588 execv (shell
, (char **) args
);
591 int exit_status
= (errno
== ENOENT
? EXIT_ENOENT
: EXIT_CANNOT_INVOKE
);
592 error (0, errno
, "%s", shell
);
597 /* Return true if SHELL is a restricted shell (one not returned by
598 getusershell), else false, meaning it is a standard shell. */
601 restricted_shell (const char *shell
)
606 while ((line
= getusershell ()) != NULL
)
608 if (*line
!= '#' && STREQ (line
, shell
))
621 if (status
!= EXIT_SUCCESS
)
622 fprintf (stderr
, _("Try `%s --help' for more information.\n"),
626 printf (_("Usage: %s [OPTION]... [-] [USER [ARG]...]\n"), program_name
);
628 Change the effective user id and group id to that of USER.\n\
630 -, -l, --login make the shell a login shell\n\
631 -c, --command=COMMAND pass a single COMMAND to the shell with -c\n\
632 -f, --fast pass -f to the shell (for csh or tcsh)\n\
633 -m, --preserve-environment do not reset environment variables\n\
635 -s, --shell=SHELL run SHELL if /etc/shells allows it\n\
637 fputs (HELP_OPTION_DESCRIPTION
, stdout
);
638 fputs (VERSION_OPTION_DESCRIPTION
, stdout
);
641 A mere - implies -l. If USER not given, assume root.\n\
643 emit_bug_reporting_address ();
649 main (int argc
, char **argv
)
652 const char *new_user
= DEFAULT_USER
;
653 char *command
= NULL
;
656 struct passwd pw_copy
;
658 initialize_main (&argc
, &argv
);
659 program_name
= argv
[0];
660 setlocale (LC_ALL
, "");
661 bindtextdomain (PACKAGE
, LOCALEDIR
);
662 textdomain (PACKAGE
);
664 initialize_exit_failure (EXIT_FAIL
);
665 atexit (close_stdout
);
667 fast_startup
= false;
668 simulate_login
= false;
669 change_environment
= true;
671 while ((optc
= getopt_long (argc
, argv
, "c:flmps:", longopts
, NULL
)) != -1)
684 simulate_login
= true;
689 change_environment
= false;
696 case_GETOPT_HELP_CHAR
;
698 case_GETOPT_VERSION_CHAR (PROGRAM_NAME
, AUTHORS
);
705 if (optind
< argc
&& STREQ (argv
[optind
], "-"))
707 simulate_login
= true;
711 new_user
= argv
[optind
++];
713 pw
= getpwnam (new_user
);
714 if (! (pw
&& pw
->pw_name
&& pw
->pw_name
[0] && pw
->pw_dir
&& pw
->pw_dir
[0]
716 error (EXIT_FAIL
, 0, _("user %s does not exist"), new_user
);
718 /* Make a copy of the password information and point pw at the local
719 copy instead. Otherwise, some systems (e.g. Linux) would clobber
720 the static data through the getlogin call from log_su.
721 Also, make sure pw->pw_shell is a nonempty string.
722 It may be NULL when NEW_USER is a username that is retrieved via NIS (YP),
723 but that doesn't have a default shell listed. */
726 pw
->pw_name
= xstrdup (pw
->pw_name
);
727 pw
->pw_passwd
= xstrdup (pw
->pw_passwd
);
728 pw
->pw_dir
= xstrdup (pw
->pw_dir
);
729 pw
->pw_shell
= xstrdup (pw
->pw_shell
&& pw
->pw_shell
[0]
734 if (!correct_password (pw
))
736 #ifdef SYSLOG_FAILURE
739 error (EXIT_FAIL
, 0, _("incorrect password"));
741 #ifdef SYSLOG_SUCCESS
748 if (!shell
&& !change_environment
)
749 shell
= getenv ("SHELL");
750 if (shell
&& getuid () != 0 && restricted_shell (pw
->pw_shell
))
752 /* The user being su'd to has a nonstandard shell, and so is
753 probably a uucp account or has restricted access. Don't
754 compromise the account by allowing access with a standard
756 error (0, 0, _("using restricted shell %s"), pw
->pw_shell
);
759 shell
= xstrdup (shell
? shell
: pw
->pw_shell
);
764 create_watching_parent ();
765 /* Now we're in the child. */
768 change_identity (pw
);
770 /* Set environment after pam_open_session, which may put KRB5CCNAME
771 into the pam_env, etc. */
773 modify_environment (pw
, shell
);
775 if (simulate_login
&& chdir (pw
->pw_dir
) != 0)
776 error (0, errno
, _("warning: cannot change directory to %s"), pw
->pw_dir
);
778 run_shell (shell
, command
, argv
+ optind
, MAX (0, argc
- optind
));