From cfe5e3905201349e9cf3f95d52ff4bd100bde37d Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 14 Apr 2024 21:10:49 +0200 Subject: Adding upstream version 2.39.3. Signed-off-by: Daniel Baumann --- login-utils/login.c | 1547 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1547 insertions(+) create mode 100644 login-utils/login.c (limited to 'login-utils/login.c') diff --git a/login-utils/login.c b/login-utils/login.c new file mode 100644 index 0000000..1812b90 --- /dev/null +++ b/login-utils/login.c @@ -0,0 +1,1547 @@ +/* + * login(1) + * + * This program is derived from 4.3 BSD software and is subject to the + * copyright notice below. + * + * Copyright (C) 2011 Karel Zak + * Rewritten to PAM-only version. + * + * Michael Glad (glad@daimi.dk) + * Computer Science Department, Aarhus University, Denmark + * 1990-07-04 + * + * Copyright (c) 1980, 1987, 1988 The Regents of the University of California. + * All rights reserved. + * + * Redistribution and use in source and binary forms are permitted + * provided that the above copyright notice and this paragraph are + * duplicated in all such forms and that any documentation, + * advertising materials, and other materials related to such + * distribution and use acknowledge that the software was developed + * by the University of California, Berkeley. The name of the + * University may not be used to endorse or promote products derived + * from this software without specific prior written permission. + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_LASTLOG_H +# include +#endif + +#include +#include + +#ifdef HAVE_LINUX_MAJOR_H +# include +#endif + +#include +#include + +#ifdef HAVE_SECURITY_PAM_MISC_H +# include +#elif defined(HAVE_SECURITY_OPENPAM_H) +# include +#endif + +#ifdef HAVE_LIBAUDIT +# include +#endif + +#include "c.h" +#include "pathnames.h" +#include "strutils.h" +#include "nls.h" +#include "env.h" +#include "xalloc.h" +#include "all-io.h" +#include "fileutils.h" +#include "timeutils.h" +#include "ttyutils.h" +#include "pwdutils.h" + +#include "logindefs.h" + +#define LOGIN_MAX_TRIES 3 +#define LOGIN_EXIT_TIMEOUT 5 +#define LOGIN_TIMEOUT 60 + +static char **argv0; +static size_t argv_lth; + +#define VCS_PATH_MAX 64 + +#if defined(HAVE_SCANDIRAT) && defined(HAVE_OPENAT) +# include +# define MOTDDIR_SUPPORT +# define MOTDDIR_EXT ".motd" +# define MOTDDIR_EXTSIZ (sizeof(MOTDDIR_EXT) - 1) +#endif + +/* + * Login control struct + */ +struct login_context { + const char *tty_path; /* ttyname() return value */ + const char *tty_name; /* tty_path without /dev prefix */ + const char *tty_number; /* end of the tty_path */ + mode_t tty_mode; /* chmod() mode */ + + const char *username; /* points to PAM, pwd or cmd_username */ + char *cmd_username; /* username specified on command line */ + + struct passwd *pwd; /* user info */ + char *pwdbuf; /* pwd strings */ + + pam_handle_t *pamh; /* PAM handler */ + struct pam_conv conv; /* PAM conversation */ + +#ifdef LOGIN_CHOWN_VCS + char vcsn[VCS_PATH_MAX]; /* virtual console name */ + char vcsan[VCS_PATH_MAX]; +#endif + + char *thishost; /* this machine */ + char *thisdomain; /* this machine's domain */ + char *hostname; /* remote machine */ + char hostaddress[16]; /* remote address */ + + pid_t pid; + + unsigned int quiet:1, /* hush file exists */ + remote:1, /* login -h */ + nohost:1, /* login -H */ + noauth:1, /* login -f */ + keep_env:1; /* login -p */ +}; + +static pid_t child_pid = 0; +static volatile sig_atomic_t got_sig = 0; +static char *timeout_msg; + +#ifdef LOGIN_CHOWN_VCS +/* true if the filedescriptor fd is a console tty, very Linux specific */ +static int is_consoletty(int fd) +{ + struct stat stb; + + if ((fstat(fd, &stb) >= 0) + && (major(stb.st_rdev) == TTY_MAJOR) + && (minor(stb.st_rdev) < 64)) { + return 1; + } + return 0; +} +#endif + +/* + * Robert Ambrose writes: + * A couple of my users have a problem with login processes hanging around + * soaking up pts's. What they seem to hung up on is trying to write out the + * message 'Login timed out after %d seconds' when the connection has already + * been dropped. + * What I did was add a second timeout while trying to write the message, so + * the process just exits if the second timeout expires. + */ +static void __attribute__((__noreturn__)) + timedout2(int sig __attribute__((__unused__))) +{ + struct termios ti; + + /* reset echo */ + tcgetattr(0, &ti); + ti.c_lflag |= ECHO; + tcsetattr(0, TCSANOW, &ti); + _exit(EXIT_SUCCESS); /* %% */ +} + +static void timedout(int sig __attribute__((__unused__))) +{ + signal(SIGALRM, timedout2); + alarm(10); + if (timeout_msg) + ignore_result( write(STDERR_FILENO, timeout_msg, strlen(timeout_msg)) ); + signal(SIGALRM, SIG_IGN); + alarm(0); + timedout2(0); +} + +/* + * This handler can be used to inform a shell about signals to login. If you have + * (root) permissions, you can kill all login children by one signal to the + * login process. + * + * Also, a parent who is session leader is able (before setsid() in the child) + * to inform the child when the controlling tty goes away (e.g. modem hangup). + */ +static void sig_handler(int signal) +{ + if (child_pid > 0) { + kill(-child_pid, signal); + if (signal == SIGTERM) + kill(-child_pid, SIGHUP); /* because the shell often ignores SIGTERM */ + } else + got_sig = 1; +} + +/* + * Let us delay all exit() calls when the user is not authenticated + * or the session not fully initialized (loginpam_session()). + */ +static void __attribute__((__noreturn__)) sleepexit(int eval) +{ + sleep((unsigned int)getlogindefs_num("FAIL_DELAY", LOGIN_EXIT_TIMEOUT)); + exit(eval); +} + +static void process_title_init(int argc, char **argv) +{ + int i; + char **envp = environ; + + /* + * Move the environment so we can reuse the memory. + * (Code borrowed from sendmail.) + * WARNING: ugly assumptions on memory layout here; + * if this ever causes problems, #undef DO_PS_FIDDLING + */ + for (i = 0; envp[i] != NULL; i++) + continue; + + environ = xmalloc(sizeof(char *) * (i + 1)); + + for (i = 0; envp[i] != NULL; i++) + environ[i] = xstrdup(envp[i]); + environ[i] = NULL; + + if (i > 0) + argv_lth = envp[i - 1] + strlen(envp[i - 1]) - argv[0]; + else + argv_lth = argv[argc - 1] + strlen(argv[argc - 1]) - argv[0]; + if (argv_lth > 1) + argv0 = argv; +} + +static void process_title_update(const char *username) +{ + size_t i; + const char prefix[] = "login -- "; + char buf[sizeof(prefix) + LOGIN_NAME_MAX]; + + if (!argv0) + return; + + if (sizeof(buf) < (sizeof(prefix) + strlen(username) + 1)) + return; + + snprintf(buf, sizeof(buf), "%s%s", prefix, username); + + i = strlen(buf); + if (i > argv_lth - 2) { + i = argv_lth - 2; + buf[i] = '\0'; + } + memset(argv0[0], '\0', argv_lth); /* clear the memory area */ + strcpy(argv0[0], buf); + + argv0[1] = NULL; +} + +static const char *get_thishost(struct login_context *cxt, const char **domain) +{ + if (!cxt->thishost) { + cxt->thishost = xgethostname(); + if (!cxt->thishost) { + if (domain) + *domain = NULL; + return NULL; + } + cxt->thisdomain = strchr(cxt->thishost, '.'); + if (cxt->thisdomain) + *cxt->thisdomain++ = '\0'; + } + + if (domain) + *domain = cxt->thisdomain; + return cxt->thishost; +} + +#ifdef MOTDDIR_SUPPORT +static int motddir_filter(const struct dirent *d) +{ + size_t namesz; + +#ifdef _DIRENT_HAVE_D_TYPE + if (d->d_type != DT_UNKNOWN && d->d_type != DT_REG && + d->d_type != DT_LNK) + return 0; +#endif + if (*d->d_name == '.') + return 0; + + namesz = strlen(d->d_name); + if (!namesz || namesz < MOTDDIR_EXTSIZ + 1 || + strcmp(d->d_name + (namesz - MOTDDIR_EXTSIZ), MOTDDIR_EXT) != 0) + return 0; + + return 1; /* accept */ +} + +static int motddir(const char *dirname) +{ + int dd, nfiles, i, done = 0; + struct dirent **namelist = NULL; + + dd = open(dirname, O_RDONLY | O_CLOEXEC | O_DIRECTORY); + if (dd < 0) + return 0; + + nfiles = scandirat(dd, ".", &namelist, motddir_filter, versionsort); + if (nfiles <= 0) + goto done; + + for (i = 0; i < nfiles; i++) { + struct dirent *d = namelist[i]; + int fd; + + fd = openat(dd, d->d_name, O_RDONLY | O_CLOEXEC); + if (fd >= 0) { + ul_copy_file(fd, fileno(stdout)); + close(fd); + done++; + } + } + + for (i = 0; i < nfiles; i++) + free(namelist[i]); + free(namelist); +done: + close(dd); + return done; +} +#endif /* MOTDDIR_SUPPORT */ + +/* + * Output the /etc/motd file. + * + * It determines the name of a login announcement file/dir and outputs it to the + * user's terminal at login time. The MOTD_FILE configuration option is a + * colon-delimited list of filenames or directories. An empty option disables + * message-of-the-day printing completely. + */ +static void motd(void) +{ + const char *mb; + char *file, *list; + int firstonly, done = 0; + + firstonly = getlogindefs_bool("MOTD_FIRSTONLY", 0); + + mb = getlogindefs_str("MOTD_FILE", _PATH_MOTDFILE); + if (!mb || !*mb) + return; + + list = xstrdup(mb); + + for (file = strtok(list, ":"); file; file = strtok(NULL, ":")) { + struct stat st; + + if (stat(file, &st) < 0) + continue; +#ifdef MOTDDIR_SUPPORT + if (S_ISDIR(st.st_mode)) + done += motddir(file); +#endif + if (S_ISREG(st.st_mode) && st.st_size > 0) { + int fd = open(file, O_RDONLY, 0); + if (fd >= 0) { + ul_copy_file(fd, fileno(stdout)); + close(fd); + } + done++; + } + if (firstonly && done) + break; + } + free(list); +} + +/* + * Display message of the day and you have mail notifications + */ +static void display_login_messages(void) +{ + motd(); + +#ifdef LOGIN_STAT_MAIL + /* + * This turns out to be a bad idea: when the mail spool + * is NFS mounted, and the NFS connection hangs, the + * login hangs, even root cannot login. + * Checking for mail should be done from the shell. + */ + { + struct stat st; + char *mail; + + mail = getenv("MAIL"); + if (mail && stat(mail, &st) == 0 && st.st_size != 0) { + if (st.st_mtime > st.st_atime) + printf(_("You have new mail.\n")); + else + printf(_("You have mail.\n")); + } + } +#endif +} + +/* + * Nice and simple code provided by Linus Torvalds 16-Feb-93. + * Non-blocking stuff by Maciej W. Rozycki, macro@ds2.pg.gda.pl, 1999. + * + * He writes: "Login performs open() on a tty in a blocking mode. + * In some cases it may make login wait in open() for carrier infinitely, + * for example if the line is a simplistic case of a three-wire serial + * connection. I believe login should open the line in non-blocking mode, + * leaving the decision to make a connection to getty (where it actually + * belongs)." + */ +static void open_tty(const char *tty) +{ + int i, fd, flags; + + fd = open(tty, O_RDWR | O_NONBLOCK); + if (fd == -1) { + syslog(LOG_ERR, _("FATAL: can't reopen tty: %m")); + sleepexit(EXIT_FAILURE); + } + + if (!isatty(fd)) { + close(fd); + syslog(LOG_ERR, _("FATAL: %s is not a terminal"), tty); + sleepexit(EXIT_FAILURE); + } + + flags = fcntl(fd, F_GETFL); + flags &= ~O_NONBLOCK; + fcntl(fd, F_SETFL, flags); + + for (i = 0; i < fd; i++) + close(i); + for (i = 0; i < 3; i++) + if (fd != i) + dup2(fd, i); + if (fd >= 3) + close(fd); +} + +static inline void chown_err(const char *what, uid_t uid, gid_t gid) +{ + syslog(LOG_ERR, _("chown (%s, %u, %u) failed: %m"), what, uid, gid); +} + +static inline void chmod_err(const char *what, mode_t mode) +{ + syslog(LOG_ERR, _("chmod (%s, %u) failed: %m"), what, mode); +} + +static void chown_tty(struct login_context *cxt) +{ + const char *grname; + uid_t uid = cxt->pwd->pw_uid; + gid_t gid = cxt->pwd->pw_gid; + + grname = getlogindefs_str("TTYGROUP", TTYGRPNAME); + if (grname && *grname) { + struct group *gr = getgrnam(grname); + if (gr) /* group by name */ + gid = gr->gr_gid; + else /* group by ID */ + gid = (gid_t) getlogindefs_num("TTYGROUP", gid); + } + if (fchown(0, uid, gid)) /* tty */ + chown_err(cxt->tty_name, uid, gid); + if (fchmod(0, cxt->tty_mode)) + chmod_err(cxt->tty_name, cxt->tty_mode); + +#ifdef LOGIN_CHOWN_VCS + if (is_consoletty(0)) { + if (chown(cxt->vcsn, uid, gid)) /* vcs */ + chown_err(cxt->vcsn, uid, gid); + if (chmod(cxt->vcsn, cxt->tty_mode)) + chmod_err(cxt->vcsn, cxt->tty_mode); + + if (chown(cxt->vcsan, uid, gid)) /* vcsa */ + chown_err(cxt->vcsan, uid, gid); + if (chmod(cxt->vcsan, cxt->tty_mode)) + chmod_err(cxt->vcsan, cxt->tty_mode); + } +#endif +} + +/* + * Reads the current terminal path and initializes cxt->tty_* variables. + */ +static void init_tty(struct login_context *cxt) +{ + struct stat st; + struct termios tt, ttt; + struct winsize ws; + + cxt->tty_mode = (mode_t) getlogindefs_num("TTYPERM", TTY_MODE); + + get_terminal_name(&cxt->tty_path, &cxt->tty_name, &cxt->tty_number); + + /* + * In case login is suid it was possible to use a hardlink as stdin + * and exploit races for a local root exploit. (Wojciech Purczynski). + * + * More precisely, the problem is ttyn := ttyname(0); ...; chown(ttyn); + * here ttyname() might return "/tmp/x", a hardlink to a pseudotty. + * All of this is a problem only when login is suid, which it isn't. + */ + if (!cxt->tty_path || !*cxt->tty_path || + lstat(cxt->tty_path, &st) != 0 || !S_ISCHR(st.st_mode) || + (st.st_nlink > 1 && strncmp(cxt->tty_path, "/dev/", 5) != 0) || + access(cxt->tty_path, R_OK | W_OK) != 0) { + + syslog(LOG_ERR, _("FATAL: bad tty")); + sleepexit(EXIT_FAILURE); + } + +#ifdef LOGIN_CHOWN_VCS + if (cxt->tty_number) { + /* find names of Virtual Console devices, for later mode change */ + snprintf(cxt->vcsn, sizeof(cxt->vcsn), "/dev/vcs%s", cxt->tty_number); + snprintf(cxt->vcsan, sizeof(cxt->vcsan), "/dev/vcsa%s", cxt->tty_number); + } +#endif + + /* The TTY size might be reset to 0x0 by the kernel when we close the stdin/stdout/stderr file + * descriptors so let's save the size now so we can reapply it later */ + memset(&ws, 0, sizeof(struct winsize)); + if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) < 0) + syslog(LOG_WARNING, _("TIOCGWINSZ ioctl failed: %m")); + + tcgetattr(0, &tt); + ttt = tt; + ttt.c_cflag &= ~HUPCL; + + if ((fchown(0, 0, 0) || fchmod(0, cxt->tty_mode)) && errno != EROFS) { + + syslog(LOG_ERR, _("FATAL: %s: change permissions failed: %m"), + cxt->tty_path); + sleepexit(EXIT_FAILURE); + } + + /* Kill processes left on this tty */ + tcsetattr(0, TCSANOW, &ttt); + + /* + * Let's close file descriptors before vhangup + * https://lkml.org/lkml/2012/6/5/145 + */ + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); + + signal(SIGHUP, SIG_IGN); /* so vhangup() won't kill us */ + vhangup(); + signal(SIGHUP, SIG_DFL); + + /* open stdin,stdout,stderr to the tty */ + open_tty(cxt->tty_path); + + /* restore tty modes */ + tcsetattr(0, TCSAFLUSH, &tt); + + /* Restore tty size */ + if ((ws.ws_row > 0 || ws.ws_col > 0) + && ioctl(STDIN_FILENO, TIOCSWINSZ, &ws) < 0) + syslog(LOG_WARNING, _("TIOCSWINSZ ioctl failed: %m")); +} + +/* + * Logs failed login attempts in _PATH_BTMP, if it exists. + * Must be called only with username the name of an actual user. + * The most common login failure is to give password instead of username. + */ +static void log_btmp(struct login_context *cxt) +{ + struct utmpx ut; + struct timeval tv; + + memset(&ut, 0, sizeof(ut)); + + str2memcpy(ut.ut_user, + cxt->username ? cxt->username : "(unknown)", + sizeof(ut.ut_user)); + + if (cxt->tty_number) + str2memcpy(ut.ut_id, cxt->tty_number, sizeof(ut.ut_id)); + if (cxt->tty_name) + str2memcpy(ut.ut_line, cxt->tty_name, sizeof(ut.ut_line)); + + gettimeofday(&tv, NULL); + ut.ut_tv.tv_sec = tv.tv_sec; + ut.ut_tv.tv_usec = tv.tv_usec; + + ut.ut_type = LOGIN_PROCESS; /* XXX doesn't matter */ + ut.ut_pid = cxt->pid; + + if (cxt->hostname) { + str2memcpy(ut.ut_host, cxt->hostname, sizeof(ut.ut_host)); + if (*cxt->hostaddress) + memcpy(&ut.ut_addr_v6, cxt->hostaddress, + sizeof(ut.ut_addr_v6)); + } + + updwtmpx(_PATH_BTMP, &ut); +} + +#ifdef HAVE_LIBAUDIT +static void log_audit(struct login_context *cxt, int status) +{ + int audit_fd; + struct passwd *pwd = cxt->pwd; + + audit_fd = audit_open(); + if (audit_fd == -1) + return; + if (!pwd && cxt->username) + pwd = getpwnam(cxt->username); + + audit_log_acct_message(audit_fd, + AUDIT_USER_LOGIN, + NULL, + "login", + cxt->username ? cxt->username : "(unknown)", + pwd ? pwd->pw_uid : (unsigned int)-1, + cxt->hostname, + NULL, + cxt->tty_name, + status); + + close(audit_fd); +} +#else /* !HAVE_LIBAUDIT */ +# define log_audit(cxt, status) +#endif /* HAVE_LIBAUDIT */ + +static void log_lastlog(struct login_context *cxt) +{ + struct sigaction sa, oldsa_xfsz; + struct lastlog ll; + off_t offset; + time_t t; + int fd; + + if (!cxt->pwd) + return; + + if (cxt->pwd->pw_uid > (uid_t) getlogindefs_num("LASTLOG_UID_MAX", ULONG_MAX)) + return; + + /* lastlog is huge on systems with large UIDs, ignore SIGXFSZ */ + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = SIG_IGN; + sigaction(SIGXFSZ, &sa, &oldsa_xfsz); + + fd = open(_PATH_LASTLOG, O_RDWR, 0); + if (fd < 0) + goto done; + offset = cxt->pwd->pw_uid * sizeof(ll); + + /* + * Print last log message. + */ + if (!cxt->quiet) { + if ((pread(fd, (void *)&ll, sizeof(ll), offset) == sizeof(ll)) && + ll.ll_time != 0) { + char time_string[CTIME_BUFSIZ]; + char buf[sizeof(ll.ll_host) + 1]; + + time_t ll_time = (time_t)ll.ll_time; + + ctime_r(&ll_time, time_string); + printf(_("Last login: %.*s "), 24 - 5, time_string); + + if (*ll.ll_host != '\0') { + mem2strcpy(buf, ll.ll_host, sizeof(ll.ll_host), sizeof(buf)); + printf(_("from %s\n"), buf); + } else { + mem2strcpy(buf, ll.ll_line, sizeof(ll.ll_line), sizeof(buf)); + printf(_("on %s\n"), buf); + } + } + } + + memset((char *)&ll, 0, sizeof(ll)); + + time(&t); + ll.ll_time = t; /* ll_time is always 32bit */ + + if (cxt->tty_name) + str2memcpy(ll.ll_line, cxt->tty_name, sizeof(ll.ll_line)); + if (cxt->hostname) + str2memcpy(ll.ll_host, cxt->hostname, sizeof(ll.ll_host)); + + if (pwrite(fd, (void *)&ll, sizeof(ll), offset) != sizeof(ll)) + warn(_("write lastlog failed")); +done: + if (fd >= 0) + close(fd); + + sigaction(SIGXFSZ, &oldsa_xfsz, NULL); /* restore original setting */ +} + +/* + * Update wtmp and utmp logs. + */ +static void log_utmp(struct login_context *cxt) +{ + struct utmpx ut = { 0 }; + struct utmpx *utp = NULL; + struct timeval tv = { 0 }; + + utmpxname(_PATH_UTMP); + setutxent(); + + /* Find pid in utmp. + * + * login sometimes overwrites the runlevel entry in /var/run/utmp, + * confusing sysvinit. I added a test for the entry type, and the + * problem was gone. (In a runlevel entry, st_pid is not really a pid + * but some number calculated from the previous and current runlevel.) + * -- Michael Riepe + */ + while ((utp = getutxent())) + if (utp->ut_pid == cxt->pid + && utp->ut_type >= INIT_PROCESS + && utp->ut_type <= DEAD_PROCESS) + break; + + /* If we can't find a pre-existing entry by pid, try by line. + * BSD network daemons may rely on this. */ + if (utp == NULL && cxt->tty_name) { + setutxent(); + ut.ut_type = LOGIN_PROCESS; + str2memcpy(ut.ut_line, cxt->tty_name, sizeof(ut.ut_line)); + utp = getutxline(&ut); + } + + /* If we can't find a pre-existing entry by pid and line, try it by id. + * Very stupid telnetd daemons don't set up utmp at all. (kzak) */ + if (utp == NULL && cxt->tty_number) { + setutxent(); + ut.ut_type = DEAD_PROCESS; + str2memcpy(ut.ut_id, cxt->tty_number, sizeof(ut.ut_id)); + utp = getutxid(&ut); + } + + if (utp) + memcpy(&ut, utp, sizeof(ut)); + else + /* some gettys/telnetds don't initialize utmp... */ + memset(&ut, 0, sizeof(ut)); + + if (cxt->tty_number && ut.ut_id[0] == 0) + str2memcpy(ut.ut_id, cxt->tty_number, sizeof(ut.ut_id)); + if (cxt->username) + str2memcpy(ut.ut_user, cxt->username, sizeof(ut.ut_user)); + if (cxt->tty_name) + str2memcpy(ut.ut_line, cxt->tty_name, sizeof(ut.ut_line)); + + gettimeofday(&tv, NULL); + ut.ut_tv.tv_sec = tv.tv_sec; + ut.ut_tv.tv_usec = tv.tv_usec; + ut.ut_type = USER_PROCESS; + ut.ut_pid = cxt->pid; + if (cxt->hostname) { + str2memcpy(ut.ut_host, cxt->hostname, sizeof(ut.ut_host)); + if (*cxt->hostaddress) + memcpy(&ut.ut_addr_v6, cxt->hostaddress, + sizeof(ut.ut_addr_v6)); + } + + pututxline(&ut); + endutxent(); + + updwtmpx(_PATH_WTMP, &ut); +} + +static void log_syslog(struct login_context *cxt) +{ + struct passwd *pwd = cxt->pwd; + + if (!cxt->tty_name) + return; + + if (!strncmp(cxt->tty_name, "ttyS", 4)) + syslog(LOG_INFO, _("DIALUP AT %s BY %s"), + cxt->tty_name, pwd->pw_name); + + if (!pwd->pw_uid) { + if (cxt->hostname) + syslog(LOG_NOTICE, _("ROOT LOGIN ON %s FROM %s"), + cxt->tty_name, cxt->hostname); + else + syslog(LOG_NOTICE, _("ROOT LOGIN ON %s"), cxt->tty_name); + } else { + if (cxt->hostname) + syslog(LOG_INFO, _("LOGIN ON %s BY %s FROM %s"), + cxt->tty_name, pwd->pw_name, cxt->hostname); + else + syslog(LOG_INFO, _("LOGIN ON %s BY %s"), cxt->tty_name, + pwd->pw_name); + } +} + +/* encapsulate stupid "void **" pam_get_item() API */ +static int loginpam_get_username(pam_handle_t *pamh, const char **name) +{ + const void *item = (const void *)*name; + int rc; + + rc = pam_get_item(pamh, PAM_USER, &item); + *name = (const char *)item; + return rc; +} + +static void loginpam_err(pam_handle_t *pamh, int retcode) +{ + const char *msg = pam_strerror(pamh, retcode); + + if (msg) { + fprintf(stderr, "\n%s\n", msg); + syslog(LOG_ERR, "%s", msg); + } + pam_end(pamh, retcode); + sleepexit(EXIT_FAILURE); +} + +/* + * Composes " login: " string; or returns "login: " if -H is given or + * LOGIN_PLAIN_PROMPT=yes configured. + */ +static const char *loginpam_get_prompt(struct login_context *cxt) +{ + const char *host; + char *prompt, *dflt_prompt = _("login: "); + size_t sz; + + if (cxt->nohost) + return dflt_prompt; /* -H on command line */ + + if (getlogindefs_bool("LOGIN_PLAIN_PROMPT", 0) == 1) + return dflt_prompt; + + if (!(host = get_thishost(cxt, NULL))) + return dflt_prompt; + + sz = strlen(host) + 1 + strlen(dflt_prompt) + 1; + prompt = xmalloc(sz); + snprintf(prompt, sz, "%s %s", host, dflt_prompt); + + return prompt; +} + +static inline int is_pam_failure(int rc) +{ + return rc != PAM_SUCCESS; +} + +static pam_handle_t *init_loginpam(struct login_context *cxt) +{ + pam_handle_t *pamh = NULL; + int rc; + + /* + * username is initialized to NULL and if specified on the command line + * it is set. Therefore, we are safe not setting it to anything. + */ + rc = pam_start(cxt->remote ? "remote" : "login", + cxt->username, &cxt->conv, &pamh); + if (rc != PAM_SUCCESS) { + warnx(_("PAM failure, aborting: %s"), pam_strerror(pamh, rc)); + syslog(LOG_ERR, _("Couldn't initialize PAM: %s"), + pam_strerror(pamh, rc)); + sleepexit(EXIT_FAILURE); + } + + /* hostname & tty are either set to NULL or their correct values, + * depending on how much we know. */ + rc = pam_set_item(pamh, PAM_RHOST, cxt->hostname); + if (is_pam_failure(rc)) + loginpam_err(pamh, rc); + + if (cxt->tty_path) { + rc = pam_set_item(pamh, PAM_TTY, cxt->tty_path); + if (is_pam_failure(rc)) + loginpam_err(pamh, rc); + } + + /* + * Andrew.Taylor@cal.montage.ca: Provide a user prompt to PAM so that + * the "login: " prompt gets localized. Unfortunately, PAM doesn't have + * an interface to specify the "Password: " string (yet). + */ + rc = pam_set_item(pamh, PAM_USER_PROMPT, loginpam_get_prompt(cxt)); + if (is_pam_failure(rc)) + loginpam_err(pamh, rc); + + /* We don't need the original username. We have to follow PAM. */ + cxt->username = NULL; + cxt->pamh = pamh; + + return pamh; +} + +static void loginpam_auth(struct login_context *cxt) +{ + int rc, show_unknown, keep_username; + unsigned int retries, failcount = 0; + const char *hostname = cxt->hostname ? cxt->hostname : + cxt->tty_name ? cxt->tty_name : ""; + pam_handle_t *pamh = cxt->pamh; + + /* if we didn't get a user on the command line, set it to NULL */ + loginpam_get_username(pamh, &cxt->username); + + show_unknown = getlogindefs_bool("LOG_UNKFAIL_ENAB", 0); + retries = getlogindefs_num("LOGIN_RETRIES", LOGIN_MAX_TRIES); + keep_username = getlogindefs_bool("LOGIN_KEEP_USERNAME", 0); + + /* + * There may be better ways to deal with some of these conditions, but + * at least this way I don't think we'll be giving away information... + * + * Perhaps someday we can trust that all PAM modules will pay attention + * to failure count and get rid of LOGIN_MAX_TRIES? + */ + rc = pam_authenticate(pamh, 0); + + while ((++failcount < retries) && + ((rc == PAM_AUTH_ERR) || + (rc == PAM_USER_UNKNOWN) || + (rc == PAM_CRED_INSUFFICIENT) || + (rc == PAM_AUTHINFO_UNAVAIL))) { + + if (rc == PAM_USER_UNKNOWN && !show_unknown) + /* + * Logging unknown usernames may be a security issue if + * a user enters their password instead of their login name. + */ + cxt->username = NULL; + else + loginpam_get_username(pamh, &cxt->username); + + syslog(LOG_NOTICE, + _("FAILED LOGIN %u FROM %s FOR %s, %s"), + failcount, hostname, + cxt->username ? cxt->username : "(unknown)", + pam_strerror(pamh, rc)); + + log_btmp(cxt); + log_audit(cxt, 0); + + if (!keep_username || rc == PAM_USER_UNKNOWN) { + pam_set_item(pamh, PAM_USER, NULL); + fprintf(stderr, _("Login incorrect\n\n")); + } else + fprintf(stderr, _("Password incorrect\n\n")); + + rc = pam_authenticate(pamh, 0); + } + + if (is_pam_failure(rc)) { + + if (rc == PAM_USER_UNKNOWN && !show_unknown) + cxt->username = NULL; + else + loginpam_get_username(pamh, &cxt->username); + + if (rc == PAM_MAXTRIES) + syslog(LOG_NOTICE, + _("TOO MANY LOGIN TRIES (%u) FROM %s FOR %s, %s"), + failcount, hostname, + cxt->username ? cxt->username : "(unknown)", + pam_strerror(pamh, rc)); + else + syslog(LOG_NOTICE, + _("FAILED LOGIN SESSION FROM %s FOR %s, %s"), + hostname, + cxt->username ? cxt->username : "(unknown)", + pam_strerror(pamh, rc)); + + log_btmp(cxt); + log_audit(cxt, 0); + + fprintf(stderr, _("\nLogin incorrect\n")); + pam_end(pamh, rc); + sleepexit(EXIT_SUCCESS); + } +} + +static void loginpam_acct(struct login_context *cxt) +{ + int rc; + pam_handle_t *pamh = cxt->pamh; + + rc = pam_acct_mgmt(pamh, 0); + + if (rc == PAM_NEW_AUTHTOK_REQD) + rc = pam_chauthtok(pamh, PAM_CHANGE_EXPIRED_AUTHTOK); + + if (is_pam_failure(rc)) + loginpam_err(pamh, rc); + + /* + * First get the username that we are actually using, though. + */ + rc = loginpam_get_username(pamh, &cxt->username); + if (is_pam_failure(rc)) + loginpam_err(pamh, rc); + + if (!cxt->username || !*cxt->username) { + warnx(_("\nSession setup problem, abort.")); + syslog(LOG_ERR, _("NULL user name. Abort.")); + pam_end(pamh, PAM_SYSTEM_ERR); + sleepexit(EXIT_FAILURE); + } +} + +/* + * Note that the position of the pam_setcred() call is discussable: + * + * - the PAM docs recommend pam_setcred() before pam_open_session() + * - but the original RFC http://www.opengroup.org/rfc/mirror-rfc/rfc86.0.txt + * uses pam_setcred() after pam_open_session() + * + * The old login versions (before year 2011) followed the RFC. This is probably + * not optimal, because there could be a dependence between some session modules + * and the user's credentials. + * + * The best is probably to follow openssh and call pam_setcred() before and + * after pam_open_session(). -- kzak@redhat.com (18-Nov-2011) + * + */ +static void loginpam_session(struct login_context *cxt) +{ + int rc; + pam_handle_t *pamh = cxt->pamh; + + rc = pam_setcred(pamh, PAM_ESTABLISH_CRED); + if (is_pam_failure(rc)) + loginpam_err(pamh, rc); + + rc = pam_open_session(pamh, cxt->quiet ? PAM_SILENT : 0); + if (is_pam_failure(rc)) { + pam_setcred(cxt->pamh, PAM_DELETE_CRED); + loginpam_err(pamh, rc); + } + + rc = pam_setcred(pamh, PAM_REINITIALIZE_CRED); + if (is_pam_failure(rc)) { + pam_close_session(pamh, 0); + loginpam_err(pamh, rc); + } +} + +/* + * Detach the controlling terminal, fork, restore syslog stuff, and create + * a new session. + */ +static void fork_session(struct login_context *cxt) +{ + struct sigaction sa, oldsa_hup, oldsa_term; + + signal(SIGALRM, SIG_DFL); + signal(SIGQUIT, SIG_DFL); + signal(SIGTSTP, SIG_IGN); + + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = SIG_IGN; + sigaction(SIGINT, &sa, NULL); + + sigaction(SIGHUP, &sa, &oldsa_hup); /* ignore when TIOCNOTTY */ + + /* + * Detach the controlling tty. + * We don't need the tty in a parent who only waits for a child. + * The child calls setsid() that detaches from the tty as well. + */ + ioctl(0, TIOCNOTTY, NULL); + + /* + * We have to beware of SIGTERM, because leaving a PAM session + * without pam_close_session() is a pretty bad thing. + */ + sa.sa_handler = sig_handler; + sigaction(SIGHUP, &sa, NULL); + sigaction(SIGTERM, &sa, &oldsa_term); + + closelog(); + + /* + * We must fork before setuid(), because we need to call + * pam_close_session() as root. + */ + child_pid = fork(); + if (child_pid < 0) { + warn(_("fork failed")); + + pam_setcred(cxt->pamh, PAM_DELETE_CRED); + pam_end(cxt->pamh, pam_close_session(cxt->pamh, 0)); + sleepexit(EXIT_FAILURE); + } + + if (child_pid) { + /* + * parent - wait for child to finish, then clean up session + */ + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); + free_getlogindefs_data(); + + sa.sa_handler = SIG_IGN; + sigaction(SIGQUIT, &sa, NULL); + sigaction(SIGINT, &sa, NULL); + + /* wait as long as any child is there */ + while (wait(NULL) == -1 && errno == EINTR) ; + openlog("login", LOG_ODELAY, LOG_AUTHPRIV); + + pam_setcred(cxt->pamh, PAM_DELETE_CRED); + pam_end(cxt->pamh, pam_close_session(cxt->pamh, 0)); + exit(EXIT_SUCCESS); + } + + /* + * child + */ + sigaction(SIGHUP, &oldsa_hup, NULL); /* restore old state */ + sigaction(SIGTERM, &oldsa_term, NULL); + if (got_sig) + exit(EXIT_FAILURE); + + /* + * Problem: if the user's shell is a shell like ash that doesn't do + * setsid() or setpgrp(), then a ctrl-\, sending SIGQUIT to every + * process in the pgrp, will kill us. + */ + + /* start new session */ + setsid(); + + /* make sure we have a controlling tty */ + open_tty(cxt->tty_path); + openlog("login", LOG_ODELAY, LOG_AUTHPRIV); /* reopen */ + + /* + * TIOCSCTTY: steal tty from other process group. + */ + if (ioctl(0, TIOCSCTTY, 1)) + syslog(LOG_ERR, _("TIOCSCTTY failed: %m")); + signal(SIGINT, SIG_DFL); +} + +/* + * Initialize $TERM, $HOME, ... + */ +static void init_environ(struct login_context *cxt) +{ + struct passwd *pwd = cxt->pwd; + char *termenv, **env; + char tmp[PATH_MAX]; + int len, i; + + termenv = getenv("TERM"); + if (termenv) + termenv = xstrdup(termenv); + + /* destroy environment unless user has requested preservation (-p) */ + if (!cxt->keep_env) + environ = xcalloc(1, sizeof(char *)); + + xsetenv("HOME", pwd->pw_dir, 0); /* legal to override */ + xsetenv("USER", pwd->pw_name, 1); + xsetenv("SHELL", pwd->pw_shell, 1); + xsetenv("TERM", termenv ? termenv : "dumb", 1); + free(termenv); + + if (pwd->pw_uid) { + if (logindefs_setenv("PATH", "ENV_PATH", _PATH_DEFPATH) != 0) + err(EXIT_FAILURE, _("failed to set the %s environment variable"), "PATH"); + + } else if (logindefs_setenv("PATH", "ENV_ROOTPATH", NULL) != 0 && + logindefs_setenv("PATH", "ENV_SUPATH", _PATH_DEFPATH_ROOT) != 0) { + err(EXIT_FAILURE, _("failed to set the %s environment variable"), "PATH"); + } + + /* mailx will give a funny error msg if you forget this one */ + len = snprintf(tmp, sizeof(tmp), "%s/%s", _PATH_MAILDIR, pwd->pw_name); + if (len > 0 && (size_t)len < sizeof(tmp)) + xsetenv("MAIL", tmp, 0); + + /* LOGNAME is not documented in login(1) but HP-UX 6.5 does it. We'll + * not allow modifying it. + */ + xsetenv("LOGNAME", pwd->pw_name, 1); + + env = pam_getenvlist(cxt->pamh); + for (i = 0; env && env[i]; i++) + putenv(env[i]); +} + +/* + * This is called for the -h option, initializes cxt->{hostname,hostaddress}. + */ +static void init_remote_info(struct login_context *cxt, char *remotehost) +{ + const char *domain; + char *p; + struct addrinfo hints, *info = NULL; + + cxt->remote = 1; + + get_thishost(cxt, &domain); + + if (domain && (p = strchr(remotehost, '.')) && + strcasecmp(p + 1, domain) == 0) + *p = '\0'; + + cxt->hostname = xstrdup(remotehost); + + memset(&hints, 0, sizeof(hints)); + hints.ai_flags = AI_ADDRCONFIG; + cxt->hostaddress[0] = 0; + + if (getaddrinfo(cxt->hostname, NULL, &hints, &info) == 0 && info) { + if (info->ai_family == AF_INET) { + struct sockaddr_in *sa = + (struct sockaddr_in *)info->ai_addr; + + memcpy(cxt->hostaddress, &(sa->sin_addr), sizeof(sa->sin_addr)); + + } else if (info->ai_family == AF_INET6) { + struct sockaddr_in6 *sa = + (struct sockaddr_in6 *)info->ai_addr; +#ifdef IN6_IS_ADDR_V4MAPPED + if (IN6_IS_ADDR_V4MAPPED(&sa->sin6_addr)) { + const uint8_t *bytes = sa->sin6_addr.s6_addr; + struct in_addr addr = { *(const in_addr_t *)(bytes + 12) }; + + memcpy(cxt->hostaddress, &addr, sizeof(struct in_addr)); + } else +#endif + memcpy(cxt->hostaddress, &(sa->sin6_addr), sizeof(sa->sin6_addr)); + } + freeaddrinfo(info); + } +} + +static void __attribute__((__noreturn__)) usage(void) +{ + fputs(USAGE_HEADER, stdout); + printf(_(" %s [-p] [-h ] [-H] [[-f] ]\n"), program_invocation_short_name); + fputs(USAGE_SEPARATOR, stdout); + fputs(_("Begin a session on the system.\n"), stdout); + + fputs(USAGE_OPTIONS, stdout); + puts(_(" -p do not destroy the environment")); + puts(_(" -f skip a login authentication")); + puts(_(" -h hostname to be used for utmp logging")); + puts(_(" -H suppress hostname in the login prompt")); + printf(" --help %s\n", USAGE_OPTSTR_HELP); + printf(" -V, --version %s\n", USAGE_OPTSTR_VERSION); + printf(USAGE_MAN_TAIL("login(1)")); + exit(EXIT_SUCCESS); +} + +static void initialize(int argc, char **argv, struct login_context *cxt) +{ + int c; + unsigned int timeout; + struct sigaction act; + + /* the only two longopts to satisfy UL standards */ + enum { HELP_OPTION = CHAR_MAX + 1 }; + const struct option longopts[] = { + {"help", no_argument, NULL, HELP_OPTION}, + {"version", no_argument, NULL, 'V'}, + {NULL, 0, NULL, 0} + }; + + /* + * This bounds the time given to login. Not a define, so it can + * be patched on machines where it's too small. + */ + timeout = (unsigned int)getlogindefs_num("LOGIN_TIMEOUT", LOGIN_TIMEOUT); + + /* TRANSLATORS: The standard value for %u is 60. */ + xasprintf(&timeout_msg, _("%s: timed out after %u seconds"), + program_invocation_short_name, timeout); + + signal(SIGALRM, timedout); + sigaction(SIGALRM, NULL, &act); + act.sa_flags &= ~SA_RESTART; + sigaction(SIGALRM, &act, NULL); + alarm(timeout); + signal(SIGQUIT, SIG_IGN); + signal(SIGINT, SIG_IGN); + + setpriority(PRIO_PROCESS, 0, 0); + process_title_init(argc, argv); + + while ((c = getopt_long(argc, argv, "fHh:pV", longopts, NULL)) != -1) + switch (c) { + case 'f': + cxt->noauth = 1; + break; + + case 'H': + cxt->nohost = 1; + break; + + case 'h': + if (getuid()) { + fprintf(stderr, + _("login: -h is for superuser only\n")); + exit(EXIT_FAILURE); + } + init_remote_info(cxt, optarg); + break; + + case 'p': + cxt->keep_env = 1; + break; + + case 'V': + print_version(EXIT_SUCCESS); + case HELP_OPTION: + usage(); + default: + errtryhelp(EXIT_FAILURE); + } + argc -= optind; + argv += optind; + + if (*argv) { + char *p = *argv; + + /* username from command line */ + cxt->cmd_username = xstrdup(p); + /* used temporary, it'll be replaced by username from PAM or/and cxt->pwd */ + cxt->username = cxt->cmd_username; + + /* Wipe the name - some people mistype their password here. */ + /* (Of course we are too late, but perhaps this helps a little...) */ +#ifdef HAVE_EXPLICIT_BZERO + explicit_bzero(p, strlen(p)); +#else + while (*p) + *p++ = ' '; +#endif + } +#ifdef HAVE_CLOSE_RANGE + if (close_range(STDERR_FILENO + 1, ~0U, 0) < 0) +#endif + ul_close_all_fds(STDERR_FILENO + 1, ~0U); +} + +int main(int argc, char **argv) +{ + char *child_argv[10]; + int child_argc = 0; + struct passwd *pwd; + struct login_context cxt = { + .tty_mode = TTY_MODE, /* tty chmod() */ + .pid = getpid(), /* PID */ +#ifdef HAVE_SECURITY_PAM_MISC_H + .conv = { misc_conv, NULL } /* Linux-PAM conversation function */ +#elif defined(HAVE_SECURITY_OPENPAM_H) + .conv = { openpam_ttyconv, NULL } /* OpenPAM conversation function */ +#endif + }; + + setlocale(LC_ALL, ""); + bindtextdomain(PACKAGE, LOCALEDIR); + textdomain(PACKAGE); + + initialize(argc, argv, &cxt); + + setpgrp(); /* set pgid to pid this means that setsid() will fail */ + init_tty(&cxt); + + openlog("login", LOG_ODELAY, LOG_AUTHPRIV); + + init_loginpam(&cxt); + + /* login -f, then the user has already been authenticated */ + cxt.noauth = cxt.noauth && getuid() == 0 ? 1 : 0; + + if (!cxt.noauth) + loginpam_auth(&cxt); + + /* + * Authentication may be skipped (for example, during krlogin, rlogin, + * etc...), but it doesn't mean that we can skip other account checks. + * The account could be disabled or the password has expired (although + * the kerberos ticket is valid). -- kzak@redhat.com (22-Feb-2006) + */ + loginpam_acct(&cxt); + + cxt.pwd = xgetpwnam(cxt.username, &cxt.pwdbuf); + if (!cxt.pwd) { + warnx(_("\nSession setup problem, abort.")); + syslog(LOG_ERR, _("Invalid user name \"%s\". Abort."), + cxt.username); + pam_end(cxt.pamh, PAM_SYSTEM_ERR); + sleepexit(EXIT_FAILURE); + } + + pwd = cxt.pwd; + cxt.username = pwd->pw_name; + + /* + * Initialize the supplementary group list. This should be done before + * pam_setcred, because PAM modules might add groups during that call. + * + * For root we don't call initgroups, instead we call setgroups with + * group 0. This avoids the need to step through the whole group file, + * which can cause problems if NIS, NIS+, LDAP or something similar + * is used and the machine has network problems. + */ + { + int retcode; + + retcode = pwd->pw_uid ? initgroups(cxt.username, pwd->pw_gid) : /* user */ + setgroups(0, NULL); /* root */ + if (retcode < 0) { + syslog(LOG_ERR, _("groups initialization failed: %m")); + warnx(_("\nSession setup problem, abort.")); + pam_end(cxt.pamh, PAM_SYSTEM_ERR); + sleepexit(EXIT_FAILURE); + } + } + + cxt.quiet = get_hushlogin_status(pwd, 1) == 1 ? 1 : 0; + + /* + * Open PAM session (after successful authentication and account check). + */ + loginpam_session(&cxt); + + /* committed to login -- turn off timeout */ + alarm((unsigned int)0); + free(timeout_msg); + timeout_msg = NULL; + + endpwent(); + + log_utmp(&cxt); + log_audit(&cxt, 1); + log_lastlog(&cxt); + + chown_tty(&cxt); + + if (setgid(pwd->pw_gid) < 0 && pwd->pw_gid) { + syslog(LOG_ALERT, _("setgid() failed")); + exit(EXIT_FAILURE); + } + + if (pwd->pw_shell == NULL || *pwd->pw_shell == '\0') + pwd->pw_shell = _PATH_BSHELL; + + init_environ(&cxt); /* init $HOME, $TERM ... */ + + process_title_update(cxt.username); + + log_syslog(&cxt); + + if (!cxt.quiet) + display_login_messages(); + + /* + * Detach the controlling terminal, fork, and create a new session + * and reinitialize syslog stuff. + */ + fork_session(&cxt); + + /* discard permissions last so we can't get killed and drop core */ + if (setuid(pwd->pw_uid) < 0 && pwd->pw_uid) { + syslog(LOG_ALERT, _("setuid() failed")); + exit(EXIT_FAILURE); + } + + /* wait until here to change directory! */ + if (chdir(pwd->pw_dir) < 0) { + warn(_("%s: change directory failed"), pwd->pw_dir); + + if (!getlogindefs_bool("DEFAULT_HOME", 1)) + exit(0); + if (chdir("/")) + exit(EXIT_FAILURE); + pwd->pw_dir = "/"; + printf(_("Logging in with home = \"/\".\n")); + } + + /* if the shell field has a space: treat it like a shell script */ + if (strchr(pwd->pw_shell, ' ')) { + char *buff; + + xasprintf(&buff, "exec %s", pwd->pw_shell); + child_argv[child_argc++] = "/bin/sh"; + child_argv[child_argc++] = "-sh"; + child_argv[child_argc++] = "-c"; + child_argv[child_argc++] = buff; + } else { + char tbuf[PATH_MAX + 2], *p; + + tbuf[0] = '-'; + xstrncpy(tbuf + 1, ((p = strrchr(pwd->pw_shell, '/')) ? + p + 1 : pwd->pw_shell), sizeof(tbuf) - 1); + + child_argv[child_argc++] = pwd->pw_shell; + child_argv[child_argc++] = xstrdup(tbuf); + } + + child_argv[child_argc++] = NULL; + + /* http://www.linux-pam.org/Linux-PAM-html/adg-interface-by-app-expected.html#adg-pam_end */ + (void) pam_end(cxt.pamh, PAM_SUCCESS|PAM_DATA_SILENT); + + execvp(child_argv[0], child_argv + 1); + + if (!strcmp(child_argv[0], "/bin/sh")) + warn(_("couldn't exec shell script")); + else + warn(_("no shell")); + + exit(EXIT_SUCCESS); +} -- cgit v1.2.3