]>
Commit | Line | Data |
---|---|---|
cf1a99da KZ |
1 | /* su for Linux. Run a shell with substitute user and group IDs. |
2 | Copyright (C) 1992-2006 Free Software Foundation, Inc. | |
3 | Copyright (C) 2012 SUSE Linux Products GmbH, Nuernberg | |
4 | ||
5 | This program is free software; you can redistribute it and/or modify | |
6 | it under the terms of the GNU General Public License as published by | |
7 | the Free Software Foundation; either version 2, or (at your option) | |
8 | any later version. | |
9 | ||
10 | This program is distributed in the hope that it will be useful, | |
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of | |
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
13 | GNU General Public License for more details. | |
14 | ||
15 | You should have received a copy of the GNU General Public License | |
16 | along with this program; if not, write to the Free Software Foundation, | |
17 | Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ | |
18 | ||
19 | /* Run a shell with the real and effective UID and GID and groups | |
20 | of USER, default `root'. | |
21 | ||
22 | The shell run is taken from USER's password entry, /bin/sh if | |
23 | none is specified there. If the account has a password, su | |
24 | prompts for a password unless run by a user with real UID 0. | |
25 | ||
26 | Does not change the current directory. | |
27 | Sets `HOME' and `SHELL' from the password entry for USER, and if | |
28 | USER is not root, sets `USER' and `LOGNAME' to USER. | |
29 | The subshell is not a login shell. | |
30 | ||
31 | If one or more ARGs are given, they are passed as additional | |
32 | arguments to the subshell. | |
33 | ||
34 | Does not handle /bin/sh or other shells specially | |
35 | (setting argv[0] to "-su", passing -c only to certain shells, etc.). | |
36 | I don't see the point in doing that, and it's ugly. | |
37 | ||
9e930041 | 38 | Based on an implementation by David MacKenzie <djm@gnu.ai.mit.edu>. */ |
cf1a99da KZ |
39 | |
40 | enum | |
41 | { | |
42 | EXIT_CANNOT_INVOKE = 126, | |
43 | EXIT_ENOENT = 127 | |
44 | }; | |
45 | ||
46 | #include <config.h> | |
47 | #include <stdio.h> | |
48 | #include <getopt.h> | |
49 | #include <sys/types.h> | |
50 | #include <pwd.h> | |
51 | #include <grp.h> | |
52 | #include <security/pam_appl.h> | |
fe2c9909 WJ |
53 | #ifdef HAVE_SECURITY_PAM_MISC_H |
54 | # include <security/pam_misc.h> | |
55 | #elif defined(HAVE_SECURITY_OPENPAM_H) | |
56 | # include <security/openpam.h> | |
57 | #endif | |
cf1a99da KZ |
58 | #include <signal.h> |
59 | #include <sys/wait.h> | |
60 | #include <syslog.h> | |
b4b919fe | 61 | #include <utmpx.h> |
cf1a99da KZ |
62 | |
63 | #include "err.h" | |
64 | ||
65 | #include <stdbool.h> | |
66 | #include "c.h" | |
67 | #include "xalloc.h" | |
68 | #include "nls.h" | |
69 | #include "pathnames.h" | |
70 | #include "env.h" | |
4e183497 | 71 | #include "closestream.h" |
c74a7af1 | 72 | #include "strutils.h" |
bbc5a5ea | 73 | #include "ttyutils.h" |
cf1a99da KZ |
74 | |
75 | /* name of the pam configuration files. separate configs for su and su - */ | |
7ec6adb1 KZ |
76 | #define PAM_SRVNAME_SU "su" |
77 | #define PAM_SRVNAME_SU_L "su-l" | |
78 | ||
79 | #define PAM_SRVNAME_RUNUSER "runuser" | |
80 | #define PAM_SRVNAME_RUNUSER_L "runuser-l" | |
81 | ||
37410713 KZ |
82 | #define _PATH_LOGINDEFS_SU "/etc/default/su" |
83 | #define _PATH_LOGINDEFS_RUNUSER "/etc/default/runuser" | |
cf1a99da KZ |
84 | |
85 | #define is_pam_failure(_rc) ((_rc) != PAM_SUCCESS) | |
86 | ||
87 | #include "logindefs.h" | |
88 | #include "su-common.h" | |
89 | ||
90 | /* The shell to run if none is given in the user's passwd entry. */ | |
91 | #define DEFAULT_SHELL "/bin/sh" | |
92 | ||
93 | /* The user to become if none is specified. */ | |
94 | #define DEFAULT_USER "root" | |
95 | ||
96 | #ifndef HAVE_ENVIRON_DECL | |
97 | extern char **environ; | |
98 | #endif | |
99 | ||
100 | static void run_shell (char const *, char const *, char **, size_t) | |
101 | __attribute__ ((__noreturn__)); | |
102 | ||
103 | /* If true, pass the `-f' option to the subshell. */ | |
104 | static bool fast_startup; | |
105 | ||
106 | /* If true, simulate a login instead of just starting a shell. */ | |
107 | static bool simulate_login; | |
108 | ||
109 | /* If true, change some environment vars to indicate the user su'd to. */ | |
110 | static bool change_environment; | |
111 | ||
112 | /* If true, then don't call setsid() with a command. */ | |
113 | static int same_session = 0; | |
114 | ||
115 | /* SU_MODE_{RUNUSER,SU} */ | |
116 | static int su_mode; | |
117 | ||
fb4edda7 KZ |
118 | /* Don't print PAM info messages (Last login, etc.). */ |
119 | static int suppress_pam_info; | |
120 | ||
cf1a99da KZ |
121 | static bool _pam_session_opened; |
122 | static bool _pam_cred_established; | |
123 | static sig_atomic_t volatile caught_signal = false; | |
124 | static pam_handle_t *pamh = NULL; | |
125 | ||
7ec6adb1 KZ |
126 | static int restricted = 1; /* zero for root user */ |
127 | ||
d10028a7 | 128 | |
71f207a5 | 129 | static const struct passwd * |
d10028a7 KZ |
130 | current_getpwuid(void) |
131 | { | |
132 | uid_t ruid; | |
133 | ||
134 | /* GNU Hurd implementation has an extension where a process can exist in a | |
135 | * non-conforming environment, and thus be outside the realms of POSIX | |
136 | * process identifiers; on this platform, getuid() fails with a status of | |
137 | * (uid_t)(-1) and sets errno if a program is run from a non-conforming | |
138 | * environment. | |
139 | * | |
140 | * http://austingroupbugs.net/view.php?id=511 | |
141 | */ | |
142 | errno = 0; | |
143 | ruid = getuid (); | |
144 | ||
145 | return errno == 0 ? getpwuid (ruid) : NULL; | |
146 | } | |
147 | ||
cf1a99da KZ |
148 | /* Log the fact that someone has run su to the user given by PW; |
149 | if SUCCESSFUL is true, they gave the correct password, etc. */ | |
150 | ||
151 | static void | |
71f207a5 | 152 | log_syslog(struct passwd const * const pw, const bool successful) |
cf1a99da KZ |
153 | { |
154 | const char *new_user, *old_user, *tty; | |
155 | ||
156 | new_user = pw->pw_name; | |
157 | /* The utmp entry (via getlogin) is probably the best way to identify | |
158 | the user, especially if someone su's from a su-shell. */ | |
159 | old_user = getlogin (); | |
160 | if (!old_user) | |
161 | { | |
162 | /* getlogin can fail -- usually due to lack of utmp entry. | |
163 | Resort to getpwuid. */ | |
71f207a5 | 164 | const struct passwd *pwd = current_getpwuid(); |
d10028a7 | 165 | old_user = pwd ? pwd->pw_name : ""; |
cf1a99da | 166 | } |
bbc5a5ea | 167 | |
285c1f3a | 168 | if (get_terminal_name(NULL, &tty, NULL) != 0 || !tty) |
cf1a99da KZ |
169 | tty = "none"; |
170 | ||
171 | openlog (program_invocation_short_name, 0 , LOG_AUTH); | |
172 | syslog (LOG_NOTICE, "%s(to %s) %s on %s", | |
7ec6adb1 KZ |
173 | successful ? "" : |
174 | su_mode == RUNUSER_MODE ? "FAILED RUNUSER " : "FAILED SU ", | |
cf1a99da KZ |
175 | new_user, old_user, tty); |
176 | closelog (); | |
177 | } | |
178 | ||
c74a7af1 KZ |
179 | |
180 | /* | |
181 | * Log failed login attempts in _PATH_BTMP if that exists. | |
182 | */ | |
71f207a5 | 183 | static void log_btmp(struct passwd const * const pw) |
c74a7af1 | 184 | { |
b4b919fe | 185 | struct utmpx ut; |
c74a7af1 KZ |
186 | struct timeval tv; |
187 | const char *tty_name, *tty_num; | |
188 | ||
189 | memset(&ut, 0, sizeof(ut)); | |
190 | ||
191 | strncpy(ut.ut_user, | |
192 | pw && pw->pw_name ? pw->pw_name : "(unknown)", | |
193 | sizeof(ut.ut_user)); | |
194 | ||
285c1f3a | 195 | get_terminal_name(NULL, &tty_name, &tty_num); |
c74a7af1 KZ |
196 | if (tty_num) |
197 | xstrncpy(ut.ut_id, tty_num, sizeof(ut.ut_id)); | |
198 | if (tty_name) | |
199 | xstrncpy(ut.ut_line, tty_name, sizeof(ut.ut_line)); | |
200 | ||
c74a7af1 KZ |
201 | gettimeofday(&tv, NULL); |
202 | ut.ut_tv.tv_sec = tv.tv_sec; | |
203 | ut.ut_tv.tv_usec = tv.tv_usec; | |
c74a7af1 KZ |
204 | ut.ut_type = LOGIN_PROCESS; /* XXX doesn't matter */ |
205 | ut.ut_pid = getpid(); | |
206 | ||
b4b919fe | 207 | updwtmpx(_PATH_BTMP, &ut); |
c74a7af1 KZ |
208 | } |
209 | ||
fb4edda7 KZ |
210 | |
211 | static int su_pam_conv(int num_msg, const struct pam_message **msg, | |
212 | struct pam_response **resp, void *appdata_ptr) | |
213 | { | |
214 | if (suppress_pam_info | |
215 | && num_msg == 1 | |
216 | && msg | |
217 | && msg[0]->msg_style == PAM_TEXT_INFO) | |
218 | return PAM_SUCCESS; | |
fe2c9909 | 219 | #ifdef HAVE_SECURITY_PAM_MISC_H |
fb4edda7 | 220 | return misc_conv(num_msg, msg, resp, appdata_ptr); |
fe2c9909 WJ |
221 | #elif defined(HAVE_SECURITY_OPENPAM_H) |
222 | return openpam_ttyconv(num_msg, msg, resp, appdata_ptr); | |
223 | #endif | |
fb4edda7 KZ |
224 | } |
225 | ||
cf1a99da KZ |
226 | static struct pam_conv conv = |
227 | { | |
fb4edda7 KZ |
228 | su_pam_conv, |
229 | NULL | |
cf1a99da KZ |
230 | }; |
231 | ||
232 | static void | |
71f207a5 | 233 | cleanup_pam (const int retcode) |
cf1a99da | 234 | { |
71f207a5 | 235 | const int saved_errno = errno; |
cf1a99da KZ |
236 | |
237 | if (_pam_session_opened) | |
238 | pam_close_session (pamh, 0); | |
239 | ||
240 | if (_pam_cred_established) | |
241 | pam_setcred (pamh, PAM_DELETE_CRED | PAM_SILENT); | |
242 | ||
243 | pam_end(pamh, retcode); | |
244 | ||
245 | errno = saved_errno; | |
246 | } | |
247 | ||
248 | /* Signal handler for parent process. */ | |
249 | static void | |
8960f3ae | 250 | su_catch_sig (int sig) |
cf1a99da | 251 | { |
8960f3ae | 252 | caught_signal = sig; |
cf1a99da KZ |
253 | } |
254 | ||
255 | /* Export env variables declared by PAM modules. */ | |
256 | static void | |
257 | export_pamenv (void) | |
258 | { | |
259 | char **env; | |
260 | ||
261 | /* This is a copy but don't care to free as we exec later anyways. */ | |
262 | env = pam_getenvlist (pamh); | |
263 | while (env && *env) | |
264 | { | |
265 | if (putenv (*env) != 0) | |
266 | err (EXIT_FAILURE, NULL); | |
267 | env++; | |
268 | } | |
269 | } | |
270 | ||
271 | static void | |
272 | create_watching_parent (void) | |
273 | { | |
274 | pid_t child; | |
275 | sigset_t ourset; | |
8960f3ae | 276 | struct sigaction oldact[3]; |
cf1a99da | 277 | int status = 0; |
71f207a5 | 278 | const int retval = pam_open_session (pamh, 0); |
cf1a99da | 279 | |
cf1a99da KZ |
280 | if (is_pam_failure(retval)) |
281 | { | |
282 | cleanup_pam (retval); | |
61cebf77 | 283 | errx (EXIT_FAILURE, _("cannot open session: %s"), |
cf1a99da KZ |
284 | pam_strerror (pamh, retval)); |
285 | } | |
286 | else | |
287 | _pam_session_opened = 1; | |
288 | ||
0e8edc9f KZ |
289 | memset(oldact, 0, sizeof(oldact)); |
290 | ||
cf1a99da KZ |
291 | child = fork (); |
292 | if (child == (pid_t) -1) | |
293 | { | |
294 | cleanup_pam (PAM_ABORT); | |
295 | err (EXIT_FAILURE, _("cannot create child process")); | |
296 | } | |
297 | ||
298 | /* the child proceeds to run the shell */ | |
299 | if (child == 0) | |
300 | return; | |
301 | ||
302 | /* In the parent watch the child. */ | |
303 | ||
304 | /* su without pam support does not have a helper that keeps | |
305 | sitting on any directory so let's go to /. */ | |
306 | if (chdir ("/") != 0) | |
307 | warn (_("cannot change directory to %s"), "/"); | |
308 | ||
309 | sigfillset (&ourset); | |
310 | if (sigprocmask (SIG_BLOCK, &ourset, NULL)) | |
311 | { | |
312 | warn (_("cannot block signals")); | |
313 | caught_signal = true; | |
314 | } | |
315 | if (!caught_signal) | |
316 | { | |
317 | struct sigaction action; | |
318 | action.sa_handler = su_catch_sig; | |
319 | sigemptyset (&action.sa_mask); | |
320 | action.sa_flags = 0; | |
321 | sigemptyset (&ourset); | |
322 | if (!same_session) | |
323 | { | |
324 | if (sigaddset(&ourset, SIGINT) || sigaddset(&ourset, SIGQUIT)) | |
325 | { | |
326 | warn (_("cannot set signal handler")); | |
327 | caught_signal = true; | |
328 | } | |
329 | } | |
330 | if (!caught_signal && (sigaddset(&ourset, SIGTERM) | |
331 | || sigaddset(&ourset, SIGALRM) | |
8960f3ae | 332 | || sigaction(SIGTERM, &action, &oldact[0]) |
cf1a99da KZ |
333 | || sigprocmask(SIG_UNBLOCK, &ourset, NULL))) { |
334 | warn (_("cannot set signal handler")); | |
335 | caught_signal = true; | |
336 | } | |
8960f3ae OO |
337 | if (!caught_signal && !same_session && (sigaction(SIGINT, &action, &oldact[1]) |
338 | || sigaction(SIGQUIT, &action, &oldact[2]))) | |
cf1a99da KZ |
339 | { |
340 | warn (_("cannot set signal handler")); | |
341 | caught_signal = true; | |
342 | } | |
343 | } | |
344 | if (!caught_signal) | |
345 | { | |
346 | pid_t pid; | |
347 | for (;;) | |
348 | { | |
349 | pid = waitpid (child, &status, WUNTRACED); | |
350 | ||
351 | if (pid != (pid_t)-1 && WIFSTOPPED (status)) | |
352 | { | |
353 | kill (getpid (), SIGSTOP); | |
354 | /* once we get here, we must have resumed */ | |
355 | kill (pid, SIGCONT); | |
356 | } | |
357 | else | |
358 | break; | |
359 | } | |
360 | if (pid != (pid_t)-1) | |
8960f3ae OO |
361 | { |
362 | if (WIFSIGNALED (status)) | |
363 | { | |
1d966345 | 364 | fprintf (stderr, "%s%s\n", strsignal (WTERMSIG (status)), |
365 | WCOREDUMP (status) ? _(" (core dumped)") : ""); | |
8960f3ae | 366 | status = WTERMSIG (status) + 128; |
8960f3ae OO |
367 | } |
368 | else | |
369 | status = WEXITSTATUS (status); | |
dffab154 KZ |
370 | |
371 | /* child is gone, don't use the PID anymore */ | |
372 | child = (pid_t) -1; | |
8960f3ae OO |
373 | } |
374 | else if (caught_signal) | |
375 | status = caught_signal + 128; | |
cf1a99da | 376 | else |
8960f3ae | 377 | status = 1; |
cf1a99da KZ |
378 | } |
379 | else | |
380 | status = 1; | |
381 | ||
dffab154 | 382 | if (caught_signal && child != (pid_t)-1) |
cf1a99da KZ |
383 | { |
384 | fprintf (stderr, _("\nSession terminated, killing shell...")); | |
385 | kill (child, SIGTERM); | |
386 | } | |
387 | ||
388 | cleanup_pam (PAM_SUCCESS); | |
389 | ||
390 | if (caught_signal) | |
391 | { | |
dffab154 KZ |
392 | if (child != (pid_t)-1) |
393 | { | |
394 | sleep (2); | |
395 | kill (child, SIGKILL); | |
396 | fprintf (stderr, _(" ...killed.\n")); | |
397 | } | |
8960f3ae OO |
398 | |
399 | /* Let's terminate itself with the received signal. | |
400 | * | |
401 | * It seems that shells use WIFSIGNALED() rather than our exit status | |
402 | * value to detect situations when is necessary to cleanup (reset) | |
403 | * terminal settings (kzak -- Jun 2013). | |
404 | */ | |
405 | switch (caught_signal) { | |
406 | case SIGTERM: | |
407 | sigaction(SIGTERM, &oldact[0], NULL); | |
408 | break; | |
409 | case SIGINT: | |
410 | sigaction(SIGINT, &oldact[1], NULL); | |
411 | break; | |
412 | case SIGQUIT: | |
413 | sigaction(SIGQUIT, &oldact[2], NULL); | |
414 | break; | |
415 | default: | |
0e8edc9f KZ |
416 | /* just in case that signal stuff initialization failed and |
417 | * caught_signal = true */ | |
418 | caught_signal = SIGKILL; | |
8960f3ae OO |
419 | break; |
420 | } | |
270ab78a | 421 | kill(getpid(), caught_signal); |
cf1a99da KZ |
422 | } |
423 | exit (status); | |
424 | } | |
425 | ||
426 | static void | |
71f207a5 | 427 | authenticate (const struct passwd * const pw) |
cf1a99da | 428 | { |
c74a7af1 | 429 | const struct passwd *lpw = NULL; |
7ec6adb1 | 430 | const char *cp, *srvname = NULL; |
cf1a99da KZ |
431 | int retval; |
432 | ||
7ec6adb1 KZ |
433 | switch (su_mode) { |
434 | case SU_MODE: | |
435 | srvname = simulate_login ? PAM_SRVNAME_SU_L : PAM_SRVNAME_SU; | |
436 | break; | |
437 | case RUNUSER_MODE: | |
438 | srvname = simulate_login ? PAM_SRVNAME_RUNUSER_L : PAM_SRVNAME_RUNUSER; | |
439 | break; | |
82455ad5 KZ |
440 | default: |
441 | abort(); | |
442 | break; | |
7ec6adb1 KZ |
443 | } |
444 | ||
445 | retval = pam_start (srvname, pw->pw_name, &conv, &pamh); | |
cf1a99da KZ |
446 | if (is_pam_failure(retval)) |
447 | goto done; | |
448 | ||
449 | if (isatty (0) && (cp = ttyname (0)) != NULL) | |
450 | { | |
451 | const char *tty; | |
452 | ||
453 | if (strncmp (cp, "/dev/", 5) == 0) | |
454 | tty = cp + 5; | |
455 | else | |
456 | tty = cp; | |
457 | retval = pam_set_item (pamh, PAM_TTY, tty); | |
458 | if (is_pam_failure(retval)) | |
459 | goto done; | |
460 | } | |
461 | ||
d10028a7 | 462 | lpw = current_getpwuid (); |
cf1a99da KZ |
463 | if (lpw && lpw->pw_name) |
464 | { | |
465 | retval = pam_set_item (pamh, PAM_RUSER, (const void *) lpw->pw_name); | |
466 | if (is_pam_failure(retval)) | |
467 | goto done; | |
468 | } | |
469 | ||
7ec6adb1 KZ |
470 | if (su_mode == RUNUSER_MODE) |
471 | { | |
472 | /* | |
473 | * This is the only difference between runuser(1) and su(1). The command | |
474 | * runuser(1) does not required authentication, because user is root. | |
475 | */ | |
476 | if (restricted) | |
477 | errx(EXIT_FAILURE, _("may not be used by non-root users")); | |
478 | return; | |
479 | } | |
480 | ||
cf1a99da KZ |
481 | retval = pam_authenticate (pamh, 0); |
482 | if (is_pam_failure(retval)) | |
483 | goto done; | |
484 | ||
485 | retval = pam_acct_mgmt (pamh, 0); | |
486 | if (retval == PAM_NEW_AUTHTOK_REQD) | |
487 | { | |
488 | /* Password has expired. Offer option to change it. */ | |
489 | retval = pam_chauthtok (pamh, PAM_CHANGE_EXPIRED_AUTHTOK); | |
490 | } | |
491 | ||
492 | done: | |
493 | ||
b0332112 | 494 | log_syslog(pw, !is_pam_failure(retval)); |
cf1a99da KZ |
495 | |
496 | if (is_pam_failure(retval)) | |
497 | { | |
c74a7af1 KZ |
498 | const char *msg; |
499 | ||
500 | log_btmp(pw); | |
501 | ||
502 | msg = pam_strerror(pamh, retval); | |
cf1a99da KZ |
503 | pam_end(pamh, retval); |
504 | sleep (getlogindefs_num ("FAIL_DELAY", 1)); | |
505 | errx (EXIT_FAILURE, "%s", msg?msg:_("incorrect password")); | |
506 | } | |
507 | } | |
508 | ||
cf1a99da | 509 | static void |
71f207a5 | 510 | set_path(const struct passwd * const pw) |
cf1a99da KZ |
511 | { |
512 | int r; | |
513 | if (pw->pw_uid) | |
514 | r = logindefs_setenv("PATH", "ENV_PATH", _PATH_DEFPATH); | |
515 | ||
516 | else if ((r = logindefs_setenv("PATH", "ENV_ROOTPATH", NULL)) != 0) | |
517 | r = logindefs_setenv("PATH", "ENV_SUPATH", _PATH_DEFPATH_ROOT); | |
518 | ||
519 | if (r != 0) | |
984a6096 | 520 | err (EXIT_FAILURE, _("failed to set the %s environment variable"), "PATH"); |
cf1a99da KZ |
521 | } |
522 | ||
523 | /* Update `environ' for the new shell based on PW, with SHELL being | |
524 | the value for the SHELL environment variable. */ | |
525 | ||
526 | static void | |
71f207a5 | 527 | modify_environment (const struct passwd * const pw, const char * const shell) |
cf1a99da KZ |
528 | { |
529 | if (simulate_login) | |
530 | { | |
531 | /* Leave TERM unchanged. Set HOME, SHELL, USER, LOGNAME, PATH. | |
532 | Unset all other environment variables. */ | |
101c5838 | 533 | char *term = getenv ("TERM"); |
cf1a99da KZ |
534 | if (term) |
535 | term = xstrdup (term); | |
536 | environ = xmalloc ((6 + !!term) * sizeof (char *)); | |
537 | environ[0] = NULL; | |
101c5838 | 538 | if (term) { |
cf1a99da | 539 | xsetenv ("TERM", term, 1); |
101c5838 KZ |
540 | free(term); |
541 | } | |
cf1a99da | 542 | xsetenv ("HOME", pw->pw_dir, 1); |
7a7f9d38 KZ |
543 | if (shell) |
544 | xsetenv ("SHELL", shell, 1); | |
cf1a99da KZ |
545 | xsetenv ("USER", pw->pw_name, 1); |
546 | xsetenv ("LOGNAME", pw->pw_name, 1); | |
547 | set_path(pw); | |
548 | } | |
549 | else | |
550 | { | |
bb3e5808 | 551 | /* Set HOME, SHELL, and (if not becoming a superuser) |
cf1a99da KZ |
552 | USER and LOGNAME. */ |
553 | if (change_environment) | |
554 | { | |
555 | xsetenv ("HOME", pw->pw_dir, 1); | |
7a7f9d38 KZ |
556 | if (shell) |
557 | xsetenv ("SHELL", shell, 1); | |
cf1a99da KZ |
558 | if (getlogindefs_bool ("ALWAYS_SET_PATH", 0)) |
559 | set_path(pw); | |
705ee62f | 560 | |
cf1a99da KZ |
561 | if (pw->pw_uid) |
562 | { | |
563 | xsetenv ("USER", pw->pw_name, 1); | |
564 | xsetenv ("LOGNAME", pw->pw_name, 1); | |
565 | } | |
566 | } | |
567 | } | |
568 | ||
569 | export_pamenv (); | |
570 | } | |
571 | ||
572 | /* Become the user and group(s) specified by PW. */ | |
573 | ||
574 | static void | |
71f207a5 | 575 | init_groups (const struct passwd * const pw, const gid_t * const groups, const size_t num_groups) |
cf1a99da KZ |
576 | { |
577 | int retval; | |
578 | ||
579 | errno = 0; | |
580 | ||
581 | if (num_groups) | |
582 | retval = setgroups (num_groups, groups); | |
583 | else | |
584 | retval = initgroups (pw->pw_name, pw->pw_gid); | |
585 | ||
586 | if (retval == -1) | |
587 | { | |
588 | cleanup_pam (PAM_ABORT); | |
589 | err (EXIT_FAILURE, _("cannot set groups")); | |
590 | } | |
591 | endgrent (); | |
592 | ||
593 | retval = pam_setcred (pamh, PAM_ESTABLISH_CRED); | |
594 | if (is_pam_failure(retval)) | |
595 | errx (EXIT_FAILURE, "%s", pam_strerror (pamh, retval)); | |
596 | else | |
597 | _pam_cred_established = 1; | |
598 | } | |
599 | ||
600 | static void | |
71f207a5 | 601 | change_identity (const struct passwd * const pw) |
cf1a99da KZ |
602 | { |
603 | if (setgid (pw->pw_gid)) | |
604 | err (EXIT_FAILURE, _("cannot set group id")); | |
605 | if (setuid (pw->pw_uid)) | |
606 | err (EXIT_FAILURE, _("cannot set user id")); | |
607 | } | |
608 | ||
609 | /* Run SHELL, or DEFAULT_SHELL if SHELL is empty. | |
610 | If COMMAND is nonzero, pass it to the shell with the -c option. | |
611 | Pass ADDITIONAL_ARGS to the shell as more arguments; there | |
612 | are N_ADDITIONAL_ARGS extra arguments. */ | |
613 | ||
614 | static void | |
71f207a5 SK |
615 | run_shell (char const * const shell, char const * const command, char ** const additional_args, |
616 | const size_t n_additional_args) | |
cf1a99da | 617 | { |
71f207a5 | 618 | const size_t n_args = 1 + fast_startup + 2 * !!command + n_additional_args + 1; |
90936faf | 619 | const char **args = xcalloc (n_args, sizeof *args); |
cf1a99da KZ |
620 | size_t argno = 1; |
621 | ||
622 | if (simulate_login) | |
623 | { | |
624 | char *arg0; | |
71f207a5 | 625 | const char *shell_basename; |
cf1a99da KZ |
626 | |
627 | shell_basename = basename (shell); | |
628 | arg0 = xmalloc (strlen (shell_basename) + 2); | |
629 | arg0[0] = '-'; | |
630 | strcpy (arg0 + 1, shell_basename); | |
631 | args[0] = arg0; | |
632 | } | |
633 | else | |
634 | args[0] = basename (shell); | |
635 | if (fast_startup) | |
636 | args[argno++] = "-f"; | |
637 | if (command) | |
638 | { | |
639 | args[argno++] = "-c"; | |
640 | args[argno++] = command; | |
641 | } | |
642 | memcpy (args + argno, additional_args, n_additional_args * sizeof *args); | |
643 | args[argno + n_additional_args] = NULL; | |
644 | execv (shell, (char **) args); | |
645 | ||
646 | { | |
647 | int exit_status = (errno == ENOENT ? EXIT_ENOENT : EXIT_CANNOT_INVOKE); | |
07ff972e | 648 | warn (_("failed to execute %s"), shell); |
cf1a99da KZ |
649 | exit (exit_status); |
650 | } | |
651 | } | |
652 | ||
653 | /* Return true if SHELL is a restricted shell (one not returned by | |
654 | getusershell), else false, meaning it is a standard shell. */ | |
655 | ||
656 | static bool | |
71f207a5 | 657 | restricted_shell (const char * const shell) |
cf1a99da KZ |
658 | { |
659 | char *line; | |
660 | ||
661 | setusershell (); | |
662 | while ((line = getusershell ()) != NULL) | |
663 | { | |
664 | if (*line != '#' && !strcmp (line, shell)) | |
665 | { | |
666 | endusershell (); | |
667 | return false; | |
668 | } | |
669 | } | |
670 | endusershell (); | |
671 | return true; | |
672 | } | |
673 | ||
674 | static void __attribute__((__noreturn__)) | |
71f207a5 | 675 | usage (const int status) |
cf1a99da | 676 | { |
7a7f9d38 KZ |
677 | if (su_mode == RUNUSER_MODE) { |
678 | fputs(USAGE_HEADER, stdout); | |
281873b8 | 679 | printf (_(" %s [options] -u <user> [[--] <command>]\n"), program_invocation_short_name); |
0bd34150 | 680 | printf (_(" %s [options] [-] [<user> [<argument>...]]\n"), program_invocation_short_name); |
7a7f9d38 | 681 | fputs (_("\n" |
0bd34150 BS |
682 | "Run <command> with the effective user ID and group ID of <user>. If -u is\n" |
683 | "not given, fall back to su(1)-compatible semantics and execute standard shell.\n" | |
1b435f38 | 684 | "The options -c, -f, -l, and -s are mutually exclusive with -u.\n"), stdout); |
7a7f9d38 KZ |
685 | |
686 | fputs(USAGE_OPTIONS, stdout); | |
687 | ||
0bd34150 | 688 | fputs (_(" -u, --user <user> username\n"), stdout); |
7a7f9d38 KZ |
689 | |
690 | } else { | |
691 | fputs(USAGE_HEADER, stdout); | |
0bd34150 | 692 | printf (_(" %s [options] [-] [<user> [<argument>...]]\n"), program_invocation_short_name); |
7a7f9d38 | 693 | fputs (_("\n" |
0bd34150 BS |
694 | "Change the effective user ID and group ID to that of <user>.\n" |
695 | "A mere - implies -l. If <user> is not given, root is assumed.\n"), stdout); | |
7a7f9d38 KZ |
696 | |
697 | fputs(USAGE_OPTIONS, stdout); | |
698 | } | |
699 | ||
09f5902e SK |
700 | fputs (_(" -m, -p, --preserve-environment do not reset environment variables\n"), stdout); |
701 | fputs (_(" -g, --group <group> specify the primary group\n"), stdout); | |
702 | fputs (_(" -G, --supp-group <group> specify a supplemental group\n\n"), stdout); | |
703 | ||
704 | fputs (_(" -, -l, --login make the shell a login shell\n"), stdout); | |
705 | fputs (_(" -c, --command <command> pass a single command to the shell with -c\n"), stdout); | |
0bd34150 BS |
706 | fputs (_(" --session-command <command> pass a single command to the shell with -c\n" |
707 | " and do not create a new session\n"), stdout); | |
09f5902e | 708 | fputs (_(" -f, --fast pass -f to the shell (for csh or tcsh)\n"), stdout); |
0bd34150 | 709 | fputs (_(" -s, --shell <shell> run <shell> if /etc/shells allows it\n"), stdout); |
7a7f9d38 KZ |
710 | |
711 | fputs(USAGE_SEPARATOR, stdout); | |
712 | fputs(USAGE_HELP, stdout); | |
713 | fputs(USAGE_VERSION, stdout); | |
714 | printf(USAGE_MAN_TAIL(su_mode == SU_MODE ? "su(1)" : "runuser(1)")); | |
cf1a99da KZ |
715 | exit (status); |
716 | } | |
717 | ||
718 | static | |
719 | void load_config(void) | |
720 | { | |
7ec6adb1 KZ |
721 | switch (su_mode) { |
722 | case SU_MODE: | |
723 | logindefs_load_file(_PATH_LOGINDEFS_SU); | |
724 | break; | |
725 | case RUNUSER_MODE: | |
726 | logindefs_load_file(_PATH_LOGINDEFS_RUNUSER); | |
727 | break; | |
71f207a5 SK |
728 | default: |
729 | abort(); | |
730 | break; | |
7ec6adb1 KZ |
731 | } |
732 | ||
cf1a99da KZ |
733 | logindefs_load_file(_PATH_LOGINDEFS); |
734 | } | |
735 | ||
736 | /* | |
737 | * Returns 1 if the current user is not root | |
738 | */ | |
739 | static int | |
740 | evaluate_uid(void) | |
741 | { | |
71f207a5 SK |
742 | const uid_t ruid = getuid(); |
743 | const uid_t euid = geteuid(); | |
cf1a99da KZ |
744 | |
745 | /* if we're really root and aren't running setuid */ | |
746 | return (uid_t) 0 == ruid && ruid == euid ? 0 : 1; | |
747 | } | |
748 | ||
c619d3d1 KZ |
749 | static gid_t |
750 | add_supp_group(const char *name, gid_t **groups, size_t *ngroups) | |
751 | { | |
752 | struct group *gr; | |
753 | ||
754 | if (*ngroups >= NGROUPS_MAX) | |
755 | errx(EXIT_FAILURE, | |
756 | P_("specifying more than %d supplemental group is not possible", | |
757 | "specifying more than %d supplemental groups is not possible", | |
758 | NGROUPS_MAX - 1), NGROUPS_MAX - 1); | |
759 | ||
760 | gr = getgrnam(name); | |
761 | if (!gr) | |
762 | errx(EXIT_FAILURE, _("group %s does not exist"), name); | |
763 | ||
764 | *groups = xrealloc(*groups, sizeof(gid_t) * (*ngroups + 1)); | |
765 | (*groups)[*ngroups] = gr->gr_gid; | |
766 | (*ngroups)++; | |
767 | ||
768 | return gr->gr_gid; | |
769 | } | |
770 | ||
cf1a99da KZ |
771 | int |
772 | su_main (int argc, char **argv, int mode) | |
773 | { | |
774 | int optc; | |
7a7f9d38 | 775 | const char *new_user = DEFAULT_USER, *runuser_user = NULL; |
71f207a5 | 776 | const char *command = NULL; |
cf1a99da | 777 | int request_same_session = 0; |
71f207a5 | 778 | const char *shell = NULL; |
cf1a99da KZ |
779 | struct passwd *pw; |
780 | struct passwd pw_copy; | |
c619d3d1 KZ |
781 | |
782 | gid_t *groups = NULL; | |
783 | size_t ngroups = 0; | |
784 | bool use_supp = false; | |
785 | bool use_gid = false; | |
786 | gid_t gid = 0; | |
cf1a99da | 787 | |
bda67d64 KZ |
788 | static const struct option longopts[] = { |
789 | {"command", required_argument, NULL, 'c'}, | |
790 | {"session-command", required_argument, NULL, 'C'}, | |
791 | {"fast", no_argument, NULL, 'f'}, | |
792 | {"login", no_argument, NULL, 'l'}, | |
793 | {"preserve-environment", no_argument, NULL, 'p'}, | |
794 | {"shell", required_argument, NULL, 's'}, | |
795 | {"group", required_argument, NULL, 'g'}, | |
796 | {"supp-group", required_argument, NULL, 'G'}, | |
7a7f9d38 | 797 | {"user", required_argument, NULL, 'u'}, /* runuser only */ |
87918040 SK |
798 | {"help", no_argument, NULL, 'h'}, |
799 | {"version", no_argument, NULL, 'V'}, | |
bda67d64 KZ |
800 | {NULL, 0, NULL, 0} |
801 | }; | |
802 | ||
cf1a99da KZ |
803 | setlocale (LC_ALL, ""); |
804 | bindtextdomain (PACKAGE, LOCALEDIR); | |
805 | textdomain (PACKAGE); | |
4e183497 | 806 | atexit(close_stdout); |
cf1a99da KZ |
807 | |
808 | su_mode = mode; | |
809 | fast_startup = false; | |
810 | simulate_login = false; | |
811 | change_environment = true; | |
812 | ||
49932ad8 | 813 | while ((optc = getopt_long (argc, argv, "c:fg:G:lmps:u:hV", longopts, NULL)) != -1) |
cf1a99da KZ |
814 | { |
815 | switch (optc) | |
816 | { | |
817 | case 'c': | |
818 | command = optarg; | |
819 | break; | |
820 | ||
821 | case 'C': | |
822 | command = optarg; | |
823 | request_same_session = 1; | |
824 | break; | |
825 | ||
826 | case 'f': | |
827 | fast_startup = true; | |
828 | break; | |
829 | ||
830 | case 'g': | |
c619d3d1 KZ |
831 | use_gid = true; |
832 | gid = add_supp_group(optarg, &groups, &ngroups); | |
cf1a99da KZ |
833 | break; |
834 | ||
835 | case 'G': | |
c619d3d1 KZ |
836 | use_supp = true; |
837 | add_supp_group(optarg, &groups, &ngroups); | |
cf1a99da KZ |
838 | break; |
839 | ||
840 | case 'l': | |
841 | simulate_login = true; | |
842 | break; | |
843 | ||
844 | case 'm': | |
845 | case 'p': | |
846 | change_environment = false; | |
847 | break; | |
848 | ||
849 | case 's': | |
850 | shell = optarg; | |
851 | break; | |
852 | ||
7a7f9d38 KZ |
853 | case 'u': |
854 | if (su_mode != RUNUSER_MODE) | |
855 | usage (EXIT_FAILURE); | |
856 | runuser_user = optarg; | |
857 | break; | |
858 | ||
cf1a99da KZ |
859 | case 'h': |
860 | usage(0); | |
861 | ||
862 | case 'V': | |
863 | printf(UTIL_LINUX_VERSION); | |
864 | exit(EXIT_SUCCESS); | |
865 | ||
866 | default: | |
677ec86c | 867 | errtryhelp(EXIT_FAILURE); |
cf1a99da KZ |
868 | } |
869 | } | |
870 | ||
871 | restricted = evaluate_uid (); | |
872 | ||
873 | if (optind < argc && !strcmp (argv[optind], "-")) | |
874 | { | |
875 | simulate_login = true; | |
876 | ++optind; | |
877 | } | |
7a7f9d38 | 878 | |
3e5c0a2d | 879 | if (simulate_login && !change_environment) { |
09af3db4 | 880 | warnx(_("ignoring --preserve-environment, it's mutually exclusive with --login")); |
3e5c0a2d KZ |
881 | change_environment = true; |
882 | } | |
883 | ||
360e0887 KZ |
884 | switch (su_mode) { |
885 | case RUNUSER_MODE: | |
886 | if (runuser_user) { | |
887 | /* runuser -u <user> <command> */ | |
888 | new_user = runuser_user; | |
889 | if (shell || fast_startup || command || simulate_login) { | |
890 | errx(EXIT_FAILURE, | |
7a7f9d38 | 891 | _("options --{shell,fast,command,session-command,login} and " |
09af3db4 | 892 | "--user are mutually exclusive")); |
360e0887 KZ |
893 | } |
894 | if (optind == argc) | |
09af3db4 | 895 | errx(EXIT_FAILURE, _("no command was specified")); |
360e0887 KZ |
896 | |
897 | break; | |
7a7f9d38 | 898 | } |
360e0887 KZ |
899 | /* fallthrough if -u <user> is not specified, then follow |
900 | * traditional su(1) behavior | |
901 | */ | |
b1557fe9 | 902 | /* fallthrough */ |
360e0887 KZ |
903 | case SU_MODE: |
904 | if (optind < argc) | |
905 | new_user = argv[optind++]; | |
906 | break; | |
71f207a5 SK |
907 | default: |
908 | abort(); | |
909 | break; | |
7a7f9d38 | 910 | } |
cf1a99da | 911 | |
c619d3d1 | 912 | if ((use_supp || use_gid) && restricted) |
cf1a99da KZ |
913 | errx(EXIT_FAILURE, _("only root can specify alternative groups")); |
914 | ||
915 | logindefs_load_defaults = load_config; | |
916 | ||
917 | pw = getpwnam (new_user); | |
918 | if (! (pw && pw->pw_name && pw->pw_name[0] && pw->pw_dir && pw->pw_dir[0] | |
919 | && pw->pw_passwd)) | |
920 | errx (EXIT_FAILURE, _("user %s does not exist"), new_user); | |
921 | ||
922 | /* Make a copy of the password information and point pw at the local | |
923 | copy instead. Otherwise, some systems (e.g. Linux) would clobber | |
924 | the static data through the getlogin call from log_su. | |
925 | Also, make sure pw->pw_shell is a nonempty string. | |
926 | It may be NULL when NEW_USER is a username that is retrieved via NIS (YP), | |
927 | but that doesn't have a default shell listed. */ | |
928 | pw_copy = *pw; | |
929 | pw = &pw_copy; | |
930 | pw->pw_name = xstrdup (pw->pw_name); | |
931 | pw->pw_passwd = xstrdup (pw->pw_passwd); | |
932 | pw->pw_dir = xstrdup (pw->pw_dir); | |
933 | pw->pw_shell = xstrdup (pw->pw_shell && pw->pw_shell[0] | |
934 | ? pw->pw_shell | |
935 | : DEFAULT_SHELL); | |
936 | endpwent (); | |
937 | ||
c619d3d1 | 938 | if (use_supp && !use_gid) |
cf1a99da | 939 | pw->pw_gid = groups[0]; |
c619d3d1 KZ |
940 | else if (use_gid) |
941 | pw->pw_gid = gid; | |
cf1a99da KZ |
942 | |
943 | authenticate (pw); | |
944 | ||
945 | if (request_same_session || !command || !pw->pw_uid) | |
946 | same_session = 1; | |
947 | ||
7a7f9d38 KZ |
948 | /* initialize shell variable only if "-u <user>" not specified */ |
949 | if (runuser_user) { | |
950 | shell = NULL; | |
951 | } else { | |
952 | if (!shell && !change_environment) | |
953 | shell = getenv ("SHELL"); | |
954 | if (shell && getuid () != 0 && restricted_shell (pw->pw_shell)) | |
955 | { | |
956 | /* The user being su'd to has a nonstandard shell, and so is | |
957 | probably a uucp account or has restricted access. Don't | |
958 | compromise the account by allowing access with a standard | |
959 | shell. */ | |
960 | warnx (_("using restricted shell %s"), pw->pw_shell); | |
961 | shell = NULL; | |
962 | } | |
963 | shell = xstrdup (shell ? shell : pw->pw_shell); | |
964 | } | |
cf1a99da | 965 | |
c619d3d1 | 966 | init_groups (pw, groups, ngroups); |
cf1a99da | 967 | |
fb4edda7 KZ |
968 | if (!simulate_login || command) |
969 | suppress_pam_info = 1; /* don't print PAM info messages */ | |
970 | ||
cf1a99da KZ |
971 | create_watching_parent (); |
972 | /* Now we're in the child. */ | |
973 | ||
974 | change_identity (pw); | |
975 | if (!same_session) | |
976 | setsid (); | |
977 | ||
978 | /* Set environment after pam_open_session, which may put KRB5CCNAME | |
979 | into the pam_env, etc. */ | |
980 | ||
981 | modify_environment (pw, shell); | |
982 | ||
983 | if (simulate_login && chdir (pw->pw_dir) != 0) | |
984 | warn (_("warning: cannot change directory to %s"), pw->pw_dir); | |
985 | ||
7a7f9d38 KZ |
986 | if (shell) |
987 | run_shell (shell, command, argv + optind, max (0, argc - optind)); | |
988 | else { | |
989 | execvp(argv[optind], &argv[optind]); | |
07ff972e | 990 | err(EXIT_FAILURE, _("failed to execute %s"), argv[optind]); |
7a7f9d38 | 991 | } |
cf1a99da KZ |
992 | } |
993 | ||
994 | // vim: sw=2 cinoptions=>4,n-2,{2,^-2,\:2,=2,g0,h2,p5,t0,+2,(0,u0,w1,m1 |