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