diff options
Diffstat (limited to 'login-utils')
45 files changed, 12595 insertions, 0 deletions
diff --git a/login-utils/Makemodule.am b/login-utils/Makemodule.am new file mode 100644 index 0000000..aafbea3 --- /dev/null +++ b/login-utils/Makemodule.am @@ -0,0 +1,247 @@ + +if BUILD_LAST +usrbin_exec_PROGRAMS += last +dist_man_MANS += \ + login-utils/last.1 \ + login-utils/lastb.1 +last_SOURCES = login-utils/last.c lib/monotonic.c +last_LDADD = $(LDADD) libcommon.la $(REALTIME_LIBS) + +install-exec-hook-last: + cd $(DESTDIR)$(usrbin_execdir) && ln -sf last lastb + +INSTALL_EXEC_HOOKS += install-exec-hook-last +endif + +if BUILD_SULOGIN +sbin_PROGRAMS += sulogin +dist_man_MANS += login-utils/sulogin.8 +sulogin_SOURCES = \ + login-utils/sulogin.c \ + login-utils/sulogin-consoles.c \ + login-utils/sulogin-consoles.h +sulogin_LDADD = $(LDADD) libcommon.la + +if HAVE_LIBCRYPT +sulogin_LDADD += -lcrypt +endif +if HAVE_SELINUX +sulogin_LDADD += -lselinux +endif + +check_PROGRAMS += test_consoles +test_consoles_SOURCES = login-utils/sulogin-consoles.c +test_consoles_CFLAGS = $(AM_CFLAGS) -DTEST_PROGRAM +test_consoles_LDADD = $(LDADD) libcommon.la +endif # BUILD_SULOGIN + + +if BUILD_LOGIN +bin_PROGRAMS += login +dist_man_MANS += login-utils/login.1 +login_SOURCES = \ + login-utils/login.c \ + login-utils/logindefs.c \ + login-utils/logindefs.h +login_LDADD = $(LDADD) libcommon.la -lpam +if HAVE_LINUXPAM +login_LDADD += -lpam_misc +endif +if HAVE_AUDIT +login_LDADD += -laudit +endif +if HAVE_SELINUX +login_LDADD += -lselinux +endif +endif # BUILD_LOGIN + + +if BUILD_NOLOGIN +sbin_PROGRAMS += nologin +dist_man_MANS += login-utils/nologin.8 +nologin_SOURCES = login-utils/nologin.c +endif + + +if BUILD_UTMPDUMP +usrbin_exec_PROGRAMS += utmpdump +dist_man_MANS += login-utils/utmpdump.1 +utmpdump_SOURCES = login-utils/utmpdump.c +utmpdump_LDADD = $(LDADD) libcommon.la +endif + + +if BUILD_CHFN_CHSH +usrbin_exec_PROGRAMS += chfn chsh +dist_man_MANS += \ + login-utils/chfn.1 \ + login-utils/chsh.1 + +chfn_chsh_sources = \ + login-utils/ch-common.h \ + login-utils/ch-common.c +chfn_chsh_cflags = $(SUID_CFLAGS) $(AM_CFLAGS) +chfn_chsh_ldflags = $(SUID_LDFLAGS) $(AM_LDFLAGS) +chfn_chsh_ldadd = libcommon.la $(READLINE_LIBS) + +if CHFN_CHSH_PASSWORD +chfn_chsh_ldadd += -lpam +if HAVE_LINUXPAM +chfn_chsh_ldadd += -lpam_misc +endif +chfn_chsh_sources += \ + login-utils/auth.c \ + login-utils/auth.h +endif # CHFN_CHSH_PASSWORD + +if HAVE_USER +chfn_chsh_ldflags += $(LIBUSER_LIBS) +chfn_chsh_cflags += $(LIBUSER_CFLAGS) +chfn_chsh_sources+= \ + login-utils/libuser.c \ + login-utils/libuser.h +else +chfn_chsh_sources += \ + login-utils/islocal.c \ + login-utils/islocal.h \ + login-utils/setpwnam.c \ + login-utils/setpwnam.h +endif + +if HAVE_SELINUX +chfn_chsh_sources += \ + login-utils/selinux_utils.c \ + login-utils/selinux_utils.h +chfn_chsh_ldadd += -lselinux +endif + +chfn_SOURCES = \ + login-utils/chfn.c \ + login-utils/logindefs.c \ + login-utils/logindefs.h \ + $(chfn_chsh_sources) +chfn_CFLAGS = $(chfn_chsh_cflags) +chfn_LDFLAGS = $(chfn_chsh_ldflags) +chfn_LDADD = $(LDADD) $(chfn_chsh_ldadd) + +chsh_SOURCES = login-utils/chsh.c $(chfn_chsh_sources) +chsh_CFLAGS = $(chfn_chsh_cflags) +chsh_LDFLAGS = $(chfn_chsh_ldflags) +chsh_LDADD = $(LDADD) $(chfn_chsh_ldadd) +endif # BUILD_CHFN_CHSH + + +if BUILD_SU +bin_PROGRAMS += su +dist_man_MANS += login-utils/su.1 +su_SOURCES = \ + login-utils/su.c \ + login-utils/su-common.c \ + login-utils/su-common.h \ + login-utils/logindefs.c \ + login-utils/logindefs.h +su_CFLAGS = $(SUID_CFLAGS) $(AM_CFLAGS) +su_LDFLAGS = $(SUID_LDFLAGS) $(AM_LDFLAGS) +su_LDADD = $(LDADD) libcommon.la -lpam +if HAVE_LINUXPAM +su_LDADD += -lpam_misc +endif +if HAVE_UTIL +su_LDADD += -lutil +endif +endif # BUILD_SU + + +if BUILD_RUNUSER +sbin_PROGRAMS += runuser +dist_man_MANS += login-utils/runuser.1 +runuser_SOURCES = \ + login-utils/runuser.c \ + login-utils/su-common.c \ + login-utils/su-common.h \ + login-utils/logindefs.c \ + login-utils/logindefs.h +runuser_LDADD = $(LDADD) libcommon.la -lpam +if HAVE_LINUXPAM +runuser_LDADD += -lpam_misc +endif +if HAVE_UTIL +runuser_LDADD += -lutil +endif +endif # BUILD_RUNUSER + + +if BUILD_NEWGRP +usrbin_exec_PROGRAMS += newgrp +dist_man_MANS += login-utils/newgrp.1 +newgrp_SOURCES = login-utils/newgrp.c +newgrp_CFLAGS = $(SUID_CFLAGS) $(AM_CFLAGS) +newgrp_LDFLAGS = $(SUID_LDFLAGS) $(AM_LDFLAGS) +newgrp_LDADD = $(LDADD) +if HAVE_LIBCRYPT +newgrp_LDADD += -lcrypt +endif +endif # BUILD_NEWGRP + +if BUILD_LSLOGINS +usrbin_exec_PROGRAMS += lslogins +dist_man_MANS += login-utils/lslogins.1 +lslogins_SOURCES = \ + login-utils/lslogins.c \ + login-utils/logindefs.c \ + login-utils/logindefs.h +lslogins_LDADD = $(LDADD) libcommon.la libsmartcols.la +lslogins_CFLAGS = $(AM_CFLAGS) -I$(ul_libsmartcols_incdir) +if HAVE_SELINUX +lslogins_LDADD += -lselinux +endif +if HAVE_SYSTEMD +lslogins_LDADD += $(SYSTEMD_LIBS) $(SYSTEMD_JOURNAL_LIBS) +lslogins_CFLAGS += $(SYSTEMD_CFLAGS) $(SYSTEMD_JOURNAL_CFLAGS) +endif +endif # BUILD_LSLOGINS + +if BUILD_VIPW +usrsbin_exec_PROGRAMS += vipw +dist_man_MANS += \ + login-utils/vigr.8 \ + login-utils/vipw.8 +vipw_SOURCES = \ + login-utils/vipw.c \ + login-utils/setpwnam.h +vipw_LDADD = $(LDADD) libcommon.la +if HAVE_SELINUX +vipw_LDADD += -lselinux +endif +install-exec-hook-vipw:: + cd $(DESTDIR)$(usrsbin_execdir) && ln -sf vipw vigr + +INSTALL_EXEC_HOOKS += install-exec-hook-vipw +endif # BUILD_VIPW + + +check_PROGRAMS += \ + test_islocal \ + test_logindefs + +test_islocal_SOURCES = login-utils/islocal.c +test_islocal_CPPFLAGS = -DTEST_PROGRAM $(AM_CPPFLAGS) + +test_logindefs_SOURCES = \ + login-utils/logindefs.c \ + login-utils/logindefs.h +test_logindefs_CPPFLAGS = -DTEST_PROGRAM $(AM_CPPFLAGS) + + +install-exec-hook: +if BUILD_SU +if MAKEINSTALL_DO_CHOWN + chown root:root $(DESTDIR)$(bindir)/su +endif +if MAKEINSTALL_DO_SETUID + chmod 4755 $(DESTDIR)$(bindir)/su +endif +endif +if BUILD_VIPW + cd $(DESTDIR)$(usrsbin_execdir) && ln -sf vipw vigr +endif diff --git a/login-utils/auth.c b/login-utils/auth.c new file mode 100644 index 0000000..fdeb12b --- /dev/null +++ b/login-utils/auth.c @@ -0,0 +1,65 @@ +/* + * auth.c -- PAM authorization code, common between chsh and chfn + * (c) 2012 by Cody Maloney <cmaloney@theoreticalchaos.com> + * + * this program is free software. you can redistribute it and + * modify it under the terms of the gnu general public license. + * there is no warranty. + * + */ + +#include <security/pam_appl.h> +#ifdef HAVE_SECURITY_PAM_MISC_H +# include <security/pam_misc.h> +#elif defined(HAVE_SECURITY_OPENPAM_H) +# include <security/openpam.h> +#endif + +#include "c.h" +#include "auth.h" + +static int pam_fail_check(pam_handle_t *pamh, int retcode) +{ + if (retcode == PAM_SUCCESS) + return 0; + warnx("%s", pam_strerror(pamh, retcode)); + pam_end(pamh, retcode); + return 1; +} + +int auth_pam(const char *service_name, uid_t uid, const char *username) +{ + if (uid != 0) { + pam_handle_t *pamh = NULL; +#ifdef HAVE_SECURITY_PAM_MISC_H + struct pam_conv conv = { misc_conv, NULL }; +#elif defined(HAVE_SECURITY_OPENPAM_H) + struct pam_conv conv = { openpam_ttyconv, NULL }; +#endif + int retcode; + + retcode = pam_start(service_name, username, &conv, &pamh); + if (pam_fail_check(pamh, retcode)) + return FALSE; + + retcode = pam_authenticate(pamh, 0); + if (pam_fail_check(pamh, retcode)) + return FALSE; + + retcode = pam_acct_mgmt(pamh, 0); + if (retcode == PAM_NEW_AUTHTOK_REQD) + retcode = + pam_chauthtok(pamh, PAM_CHANGE_EXPIRED_AUTHTOK); + if (pam_fail_check(pamh, retcode)) + return FALSE; + + retcode = pam_setcred(pamh, 0); + if (pam_fail_check(pamh, retcode)) + return FALSE; + + pam_end(pamh, 0); + /* no need to establish a session; this isn't a + * session-oriented activity... */ + } + return TRUE; +} diff --git a/login-utils/auth.h b/login-utils/auth.h new file mode 100644 index 0000000..bf7c369 --- /dev/null +++ b/login-utils/auth.h @@ -0,0 +1,13 @@ +/* + * auth.h -- PAM authorization code, common between chsh and chfn + * (c) 2012 by Cody Maloney <cmaloney@theoreticalchaos.com> + * + * this program is free software. you can redistribute it and + * modify it under the terms of the gnu general public license. + * there is no warranty. + * + */ + +#include <sys/types.h> + +extern int auth_pam(const char *service_name, uid_t uid, const char *username); diff --git a/login-utils/ch-common.c b/login-utils/ch-common.c new file mode 100644 index 0000000..34b09f3 --- /dev/null +++ b/login-utils/ch-common.c @@ -0,0 +1,34 @@ +/* + * chfn and chsh shared functions + * + * this program is free software. you can redistribute it and + * modify it under the terms of the gnu general public license. + * there is no warranty. + */ + +#include <ctype.h> +#include <string.h> + +#include "c.h" +#include "nls.h" + +#include "ch-common.h" + +/* + * illegal_passwd_chars () - + * check whether a string contains illegal characters + */ +int illegal_passwd_chars(const char *str) +{ + const char illegal[] = ",:=\"\n"; + const size_t len = strlen(str); + size_t i; + + if (strpbrk(str, illegal)) + return 1; + for (i = 0; i < len; i++) { + if (iscntrl(str[i])) + return 1; + } + return 0; +} diff --git a/login-utils/ch-common.h b/login-utils/ch-common.h new file mode 100644 index 0000000..7f70e50 --- /dev/null +++ b/login-utils/ch-common.h @@ -0,0 +1,6 @@ +#ifndef UTIL_LINUX_CH_COMMON_H +#define UTIL_LINUX_CH_COMMON_H + +extern int illegal_passwd_chars(const char *str); + +#endif /* UTIL_LINUX_CH_COMMON */ diff --git a/login-utils/chfn.1 b/login-utils/chfn.1 new file mode 100644 index 0000000..c7130c4 --- /dev/null +++ b/login-utils/chfn.1 @@ -0,0 +1,108 @@ +.\" +.\" chfn.1 -- change your finger information +.\" (c) 1994 by salvatore valente <svalente@athena.mit.edu> +.\" +.\" This program is free software. You can redistribute it and +.\" modify it under the terms of the GNU General Public License. +.\" There is no warranty. +.\" +.TH CHFN 1 "November 2015" "util-linux" "User Commands" +.SH NAME +chfn \- change your finger information +.SH SYNOPSIS +.B chfn +.RB [ \-f +.IR full-name ] +.RB [ \-o +.IR office ] +.RB [ \-p +.IR office-phone ] +.RB [ \-h +.IR home-phone ] +.RB [ \-u ] +.RB [ \-v ] +.RI [ username ] +.SH DESCRIPTION +.B chfn +is used to change your finger information. This information is +stored in the +.I /etc/passwd +file, and is displayed by the +.B finger +program. The Linux +.B finger +command will display four pieces of information that can be changed by +.BR chfn : +your real name, your work room and phone, and your home phone. +.PP +Any of the four pieces of information can be specified on the command +line. If no information is given on the command line, +.B chfn +enters interactive mode. +.PP +In interactive mode, +.B chfn +will prompt for each field. At a prompt, you can enter the new information, +or just press return to leave the field unchanged. Enter the keyword +"none" to make the field blank. +.PP +.B chfn +supports non-local entries (kerberos, LDAP, etc.\&) if linked with libuser, +otherwise use \fBypchfn\fR, \fBlchfn\fR or any other implementation for +non-local entries. +.SH OPTIONS +.TP +.BR \-f , " \-\-full\-name " \fIfull-name +Specify your real name. +.TP +.BR \-o , " \-\-office " \fIoffice +Specify your office room number. +.TP +.BR \-p , " \-\-office\-phone " \fIoffice-phone +Specify your office phone number. +.TP +.BR \-h , " \-\-home\-phone " \fIhome-phone +Specify your home phone number. +.TP +.BR \-u , " \-\-help" +Display help text and exit. +.TP +.BR \-v , " \-\-version" +Display version information and exit. +.SH CONFIG FILE ITEMS +.B chfn +reads the +.IR /etc\:/login.defs (5) +configuration file. Note that the configuration file could be +distributed with another package (e.g. shadow-utils). The following +configuration items are relevant for +.BR chfn (1): +.PP +.BI CHFN_RESTRICT " string" +.RS 4 +Indicate which fields are changeable by \fBchfn\fR. + +The boolean setting \fB"yes"\fR means that only the Office, Office Phone and +Home Phone fields are changeable, and boolean setting \fB"no"\fR means that +also the Full Name is changeable. + +Another way to specify changeable fields is by abbreviations: f = Full Name, +r = Office (room), w = Office (work) Phone, h = Home Phone. For example, +\fBCHFN_RESTRICT "wh"\fR allows changing work and home phone numbers. + +If CHFN_RESTRICT is undefined, then all finger information is read-only. +This is the default. +.RE +.PP +.SH "EXIT STATUS" +Returns 0 if operation was successful, 1 if operation failed or command syntax was not valid. +.SH "SEE ALSO" +.BR chsh (1), +.BR finger (1), +.BR login.defs (5), +.BR passwd (5) +.SH AUTHOR +Salvatore Valente <svalente@mit.edu> +.SH AVAILABILITY +The chfn command is part of the util-linux package and is available from +https://www.kernel.org/pub/linux/utils/util-linux/. diff --git a/login-utils/chfn.c b/login-utils/chfn.c new file mode 100644 index 0000000..c5312fa --- /dev/null +++ b/login-utils/chfn.c @@ -0,0 +1,492 @@ +/* + * chfn.c -- change your finger information + * (c) 1994 by salvatore valente <svalente@athena.mit.edu> + * (c) 2012 by Cody Maloney <cmaloney@theoreticalchaos.com> + * + * this program is free software. you can redistribute it and + * modify it under the terms of the gnu general public license. + * there is no warranty. + * + * $Author: aebr $ + * $Revision: 1.18 $ + * $Date: 1998/06/11 22:30:11 $ + * + * Updated Thu Oct 12 09:19:26 1995 by faith@cs.unc.edu with security + * patches from Zefram <A.Main@dcs.warwick.ac.uk> + * + * Hacked by Peter Breitenlohner, peb@mppmu.mpg.de, + * to remove trailing empty fields. Oct 5, 96. + * + * 1999-02-22 Arkadiusz Miśkiewicz <misiek@pld.ORG.PL> + * - added Native Language Support + */ + +#include <ctype.h> +#include <errno.h> +#include <getopt.h> +#include <pwd.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/types.h> +#include <unistd.h> + +#include "c.h" +#include "env.h" +#include "closestream.h" +#include "islocal.h" +#include "nls.h" +#include "setpwnam.h" +#include "strutils.h" +#include "xalloc.h" +#include "logindefs.h" + +#include "ch-common.h" + +#ifdef HAVE_LIBSELINUX +# include <selinux/selinux.h> +# include "selinux_utils.h" +#endif + +#ifdef HAVE_LIBUSER +# include <libuser/user.h> +# include "libuser.h" +#elif CHFN_CHSH_PASSWORD +# include "auth.h" +#endif + +#ifdef HAVE_LIBREADLINE +# define _FUNCTION_DEF +# include <readline/readline.h> +#endif + +struct finfo { + char *full_name; + char *office; + char *office_phone; + char *home_phone; + char *other; +}; + +struct chfn_control { + struct passwd *pw; + char *username; + /* "oldf" Contains the users original finger information. + * "newf" Contains the changed finger information, and contains + * NULL in fields that haven't been changed. + * In the end, "newf" is folded into "oldf". */ + struct finfo oldf, newf; + unsigned int + allow_fullname:1, /* The login.defs restriction */ + allow_room:1, /* see: man login.defs(5) */ + allow_work:1, /* and look for CHFN_RESTRICT */ + allow_home:1, /* keyword for these four. */ + changed:1, /* is change requested */ + interactive:1; /* whether to prompt for fields or not */ +}; + +/* we do not accept gecos field sizes longer than MAX_FIELD_SIZE */ +#define MAX_FIELD_SIZE 256 + +static void __attribute__((__noreturn__)) usage(void) +{ + FILE *fp = stdout; + fputs(USAGE_HEADER, fp); + fprintf(fp, _(" %s [options] [<username>]\n"), program_invocation_short_name); + + fputs(USAGE_SEPARATOR, fp); + fputs(_("Change your finger information.\n"), fp); + + fputs(USAGE_OPTIONS, fp); + fputs(_(" -f, --full-name <full-name> real name\n"), fp); + fputs(_(" -o, --office <office> office number\n"), fp); + fputs(_(" -p, --office-phone <phone> office phone number\n"), fp); + fputs(_(" -h, --home-phone <phone> home phone number\n"), fp); + fputs(USAGE_SEPARATOR, fp); + printf( " -u, --help %s\n", USAGE_OPTSTR_HELP); + printf( " -v, --version %s\n", USAGE_OPTSTR_VERSION); + printf(USAGE_MAN_TAIL("chfn(1)")); + exit(EXIT_SUCCESS); +} + +/* + * check_gecos_string () -- + * check that the given gecos string is legal. if it's not legal, + * output "msg" followed by a description of the problem, and return (-1). + */ +static int check_gecos_string(const char *msg, char *gecos) +{ + const size_t len = strlen(gecos); + + if (MAX_FIELD_SIZE < len) { + warnx(_("field %s is too long"), msg); + return -1; + } + if (illegal_passwd_chars(gecos)) { + warnx(_("%s: has illegal characters"), gecos); + return -1; + } + return 0; +} + +/* + * parse_argv () -- + * parse the command line arguments. + * returns true if no information beyond the username was given. + */ +static void parse_argv(struct chfn_control *ctl, int argc, char **argv) +{ + int index, c, status = 0; + static const struct option long_options[] = { + { "full-name", required_argument, NULL, 'f' }, + { "office", required_argument, NULL, 'o' }, + { "office-phone", required_argument, NULL, 'p' }, + { "home-phone", required_argument, NULL, 'h' }, + { "help", no_argument, NULL, 'u' }, + { "version", no_argument, NULL, 'v' }, + { NULL, 0, NULL, 0 }, + }; + + while ((c = getopt_long(argc, argv, "f:r:p:h:o:uv", long_options, + &index)) != -1) { + switch (c) { + case 'f': + if (!ctl->allow_fullname) + errx(EXIT_FAILURE, _("login.defs forbids setting %s"), _("Name")); + ctl->newf.full_name = optarg; + status += check_gecos_string(_("Name"), optarg); + break; + case 'o': + if (!ctl->allow_room) + errx(EXIT_FAILURE, _("login.defs forbids setting %s"), _("Office")); + ctl->newf.office = optarg; + status += check_gecos_string(_("Office"), optarg); + break; + case 'p': + if (!ctl->allow_work) + errx(EXIT_FAILURE, _("login.defs forbids setting %s"), _("Office Phone")); + ctl->newf.office_phone = optarg; + status += check_gecos_string(_("Office Phone"), optarg); + break; + case 'h': + if (!ctl->allow_home) + errx(EXIT_FAILURE, _("login.defs forbids setting %s"), _("Home Phone")); + ctl->newf.home_phone = optarg; + status += check_gecos_string(_("Home Phone"), optarg); + break; + case 'v': + printf(UTIL_LINUX_VERSION); + exit(EXIT_SUCCESS); + case 'u': + usage(); + default: + errtryhelp(EXIT_FAILURE); + } + ctl->changed = 1; + ctl->interactive = 0; + } + if (status != 0) + exit(EXIT_FAILURE); + /* done parsing arguments. check for a username. */ + if (optind < argc) { + if (optind + 1 < argc) { + warnx(_("cannot handle multiple usernames")); + errtryhelp(EXIT_FAILURE); + } + ctl->username = argv[optind]; + } + return; +} + +/* + * parse_passwd () -- + * take a struct password and fill in the fields of the struct finfo. + */ +static void parse_passwd(struct chfn_control *ctl) +{ + char *gecos; + + if (!ctl->pw) + return; + /* use pw_gecos - we take a copy since PAM destroys the original */ + gecos = xstrdup(ctl->pw->pw_gecos); + /* extract known fields */ + ctl->oldf.full_name = strsep(&gecos, ","); + ctl->oldf.office = strsep(&gecos, ","); + ctl->oldf.office_phone = strsep(&gecos, ","); + ctl->oldf.home_phone = strsep(&gecos, ","); + /* extra fields contain site-specific information, and can + * not be changed by this version of chfn. */ + ctl->oldf.other = strsep(&gecos, ","); +} + +/* + * ask_new_field () -- + * ask the user for a given field and check that the string is legal. + */ +static char *ask_new_field(struct chfn_control *ctl, const char *question, + char *def_val) +{ + int len; + char *buf; +#ifndef HAVE_LIBREADLINE + size_t dummy = 0; +#endif + + if (!def_val) + def_val = ""; + while (true) { + printf("%s [%s]: ", question, def_val); + __fpurge(stdin); +#ifdef HAVE_LIBREADLINE + rl_bind_key('\t', rl_insert); + if ((buf = readline(NULL)) == NULL) +#else + if (getline(&buf, &dummy, stdin) < 0) +#endif + errx(EXIT_FAILURE, _("Aborted.")); + /* remove white spaces from string end */ + ltrim_whitespace((unsigned char *) buf); + len = rtrim_whitespace((unsigned char *) buf); + if (len == 0) { + free(buf); + return xstrdup(def_val); + } + if (!strcasecmp(buf, "none")) { + free(buf); + ctl->changed = 1; + return xstrdup(""); + } + if (check_gecos_string(question, buf) >= 0) + break; + } + ctl->changed = 1; + return buf; +} + +/* + * get_login_defs() + * find /etc/login.defs CHFN_RESTRICT and save restrictions to run time + */ +static void get_login_defs(struct chfn_control *ctl) +{ + const char *s; + size_t i; + int broken = 0; + + /* real root does not have restrictions */ + if (geteuid() == getuid() && getuid() == 0) { + ctl->allow_fullname = ctl->allow_room = ctl->allow_work = ctl->allow_home = 1; + return; + } + s = getlogindefs_str("CHFN_RESTRICT", ""); + if (!strcmp(s, "yes")) { + ctl->allow_room = ctl->allow_work = ctl->allow_home = 1; + return; + } + if (!strcmp(s, "no")) { + ctl->allow_fullname = ctl->allow_room = ctl->allow_work = ctl->allow_home = 1; + return; + } + for (i = 0; s[i]; i++) { + switch (s[i]) { + case 'f': + ctl->allow_fullname = 1; + break; + case 'r': + ctl->allow_room = 1; + break; + case 'w': + ctl->allow_work = 1; + break; + case 'h': + ctl->allow_home = 1; + break; + default: + broken = 1; + } + } + if (broken) + warnx(_("%s: CHFN_RESTRICT has unexpected value: %s"), _PATH_LOGINDEFS, s); + if (!ctl->allow_fullname && !ctl->allow_room && !ctl->allow_work && !ctl->allow_home) + errx(EXIT_FAILURE, _("%s: CHFN_RESTRICT does not allow any changes"), _PATH_LOGINDEFS); + return; +} + +/* + * ask_info () -- + * prompt the user for the finger information and store it. + */ +static void ask_info(struct chfn_control *ctl) +{ + if (ctl->allow_fullname) + ctl->newf.full_name = ask_new_field(ctl, _("Name"), ctl->oldf.full_name); + if (ctl->allow_room) + ctl->newf.office = ask_new_field(ctl, _("Office"), ctl->oldf.office); + if (ctl->allow_work) + ctl->newf.office_phone = ask_new_field(ctl, _("Office Phone"), ctl->oldf.office_phone); + if (ctl->allow_home) + ctl->newf.home_phone = ask_new_field(ctl, _("Home Phone"), ctl->oldf.home_phone); + putchar('\n'); +} + +/* + * find_field () -- + * find field value in uninteractive mode; can be new, old, or blank + */ +static char *find_field(char *nf, char *of) +{ + if (nf) + return nf; + if (of) + return of; + return xstrdup(""); +} + +/* + * add_missing () -- + * add not supplied field values when in uninteractive mode + */ +static void add_missing(struct chfn_control *ctl) +{ + ctl->newf.full_name = find_field(ctl->newf.full_name, ctl->oldf.full_name); + ctl->newf.office = find_field(ctl->newf.office, ctl->oldf.office); + ctl->newf.office_phone = find_field(ctl->newf.office_phone, ctl->oldf.office_phone); + ctl->newf.home_phone = find_field(ctl->newf.home_phone, ctl->oldf.home_phone); + ctl->newf.other = find_field(ctl->newf.other, ctl->oldf.other); + printf("\n"); +} + +/* + * save_new_data () -- + * save the given finger info in /etc/passwd. + * return zero on success. + */ +static int save_new_data(struct chfn_control *ctl) +{ + char *gecos; + int len; + + /* create the new gecos string */ + len = xasprintf(&gecos, "%s,%s,%s,%s,%s", + ctl->newf.full_name, + ctl->newf.office, + ctl->newf.office_phone, + ctl->newf.home_phone, + ctl->newf.other); + + /* remove trailing empty fields (but not subfields of ctl->newf.other) */ + if (!ctl->newf.other) { + while (len > 0 && gecos[len - 1] == ',') + len--; + gecos[len] = 0; + } + +#ifdef HAVE_LIBUSER + if (set_value_libuser("chfn", ctl->username, ctl->pw->pw_uid, + LU_GECOS, gecos) < 0) { +#else /* HAVE_LIBUSER */ + /* write the new struct passwd to the passwd file. */ + ctl->pw->pw_gecos = gecos; + if (setpwnam(ctl->pw, ".chfn") < 0) { + warn("setpwnam failed"); +#endif + printf(_ + ("Finger information *NOT* changed. Try again later.\n")); + return -1; + } + free(gecos); + printf(_("Finger information changed.\n")); + return 0; +} + +int main(int argc, char **argv) +{ + uid_t uid; + struct chfn_control ctl = { + .interactive = 1 + }; + + sanitize_env(); + setlocale(LC_ALL, ""); /* both for messages and for iscntrl() below */ + bindtextdomain(PACKAGE, LOCALEDIR); + textdomain(PACKAGE); + atexit(close_stdout); + uid = getuid(); + + /* check /etc/login.defs CHFN_RESTRICT */ + get_login_defs(&ctl); + + parse_argv(&ctl, argc, argv); + if (!ctl.username) { + ctl.pw = getpwuid(uid); + if (!ctl.pw) + errx(EXIT_FAILURE, _("you (user %d) don't exist."), + uid); + ctl.username = ctl.pw->pw_name; + } else { + ctl.pw = getpwnam(ctl.username); + if (!ctl.pw) + errx(EXIT_FAILURE, _("user \"%s\" does not exist."), + ctl.username); + } + parse_passwd(&ctl); +#ifndef HAVE_LIBUSER + if (!(is_local(ctl.username))) + errx(EXIT_FAILURE, _("can only change local entries")); +#endif + +#ifdef HAVE_LIBSELINUX + if (is_selinux_enabled() > 0) { + if (uid == 0) { + access_vector_t av = get_access_vector("passwd", "chfn"); + + if (selinux_check_passwd_access(av) != 0) { + security_context_t user_context; + if (getprevcon(&user_context) < 0) + user_context = NULL; + errx(EXIT_FAILURE, + _("%s is not authorized to change " + "the finger info of %s"), + user_context ? : _("Unknown user context"), + ctl.username); + } + } + if (setupDefaultContext(_PATH_PASSWD)) + errx(EXIT_FAILURE, + _("can't set default context for %s"), _PATH_PASSWD); + } +#endif + +#ifdef HAVE_LIBUSER + /* If we're setuid and not really root, disallow the password change. */ + if (geteuid() != getuid() && uid != ctl.pw->pw_uid) { +#else + if (uid != 0 && uid != ctl.pw->pw_uid) { +#endif + errno = EACCES; + err(EXIT_FAILURE, _("running UID doesn't match UID of user we're " + "altering, change denied")); + } + + printf(_("Changing finger information for %s.\n"), ctl.username); + +#if !defined(HAVE_LIBUSER) && defined(CHFN_CHSH_PASSWORD) + if (!auth_pam("chfn", uid, ctl.username)) { + return EXIT_FAILURE; + } +#endif + + if (ctl.interactive) + ask_info(&ctl); + + add_missing(&ctl); + + if (!ctl.changed) { + printf(_("Finger information not changed.\n")); + return EXIT_SUCCESS; + } + + return save_new_data(&ctl) == 0 ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/login-utils/chsh.1 b/login-utils/chsh.1 new file mode 100644 index 0000000..c955b05 --- /dev/null +++ b/login-utils/chsh.1 @@ -0,0 +1,65 @@ +.\" +.\" chsh.1 -- change your login shell +.\" (c) 1994 by salvatore valente <svalente@athena.mit.edu> +.\" +.\" This program is free software. You can redistribute it and +.\" modify it under the terms of the GNU General Public License. +.\" There is no warranty. +.\" +.TH CHSH 1 "July 2014" "util-linux" "User Commands" +.SH NAME +chsh \- change your login shell +.SH SYNOPSIS +.B chsh +.RB [ \-s +.IR shell ] +.RB [ \-l ] +.RB [ \-h ] +.RB [ \-v ] +.RI [ username ] +.SH DESCRIPTION +.B chsh +is used to change your login shell. +If a shell is not given on the command line, +.B chsh +prompts for one. + +.B chsh +supports non-local entries (kerberos, LDAP, etc.\&) if linked with libuser, +otherwise use \fBypchsh\fR, \fBlchsh\fR or any other implementation for +non-local entries. +.SH OPTIONS +.TP +.BR \-s , " \-\-shell " \fIshell +Specify your login shell. +.TP +.BR \-l , " \-\-list\-shells" +Print the list of shells listed in +.I /etc/shells +and exit. +.TP +.BR \-h , " \-\-help" +Display help text and exit. +.TP +.BR \-v , " \-\-version" +Display version information and exit. +.SH "VALID SHELLS" +.B chsh +will accept the full pathname of any executable file on the system. +However, it will issue a warning if the shell is not listed in the +.I /etc/shells +file. +On the other hand, it can also be configured such that it will +only accept shells listed in this file, unless you are root. +.SH "EXIT STATUS" +Returns 0 if operation was successful, 1 if operation failed or command syntax was not valid. +.SH "SEE ALSO" +.BR login (1), +.BR login.defs (5), +.BR passwd (5), +.BR shells (5) +.SH AUTHOR +Salvatore Valente <svalente@mit.edu> +.SH AVAILABILITY +The chsh command is part of the util-linux package and is available from +https://www.kernel.org/pub/linux/utils/util-linux/. diff --git a/login-utils/chsh.c b/login-utils/chsh.c new file mode 100644 index 0000000..c267c72 --- /dev/null +++ b/login-utils/chsh.c @@ -0,0 +1,365 @@ +/* + * chsh.c -- change your login shell + * (c) 1994 by salvatore valente <svalente@athena.mit.edu> + * (c) 2012 by Cody Maloney <cmaloney@theoreticalchaos.com> + * + * this program is free software. you can redistribute it and + * modify it under the terms of the gnu general public license. + * there is no warranty. + * + * $Author: aebr $ + * $Revision: 1.19 $ + * $Date: 1998/06/11 22:30:14 $ + * + * Updated Thu Oct 12 09:33:15 1995 by faith@cs.unc.edu with security + * patches from Zefram <A.Main@dcs.warwick.ac.uk> + * + * Updated Mon Jul 1 18:46:22 1996 by janl@math.uio.no with security + * suggestion from Zefram. Disallowing users with shells not in /etc/shells + * from changing their shell. + * + * 1999-02-22 Arkadiusz Miśkiewicz <misiek@pld.ORG.PL> + * - added Native Language Support + */ + +#include <ctype.h> +#include <errno.h> +#include <getopt.h> +#include <pwd.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/types.h> +#include <unistd.h> + +#include "c.h" +#include "env.h" +#include "closestream.h" +#include "islocal.h" +#include "nls.h" +#include "pathnames.h" +#include "setpwnam.h" +#include "strutils.h" +#include "xalloc.h" + +#include "ch-common.h" + +#ifdef HAVE_LIBSELINUX +# include <selinux/selinux.h> +# include "selinux_utils.h" +#endif + + +#ifdef HAVE_LIBUSER +# include <libuser/user.h> +# include "libuser.h" +#elif CHFN_CHSH_PASSWORD +# include "auth.h" +#endif + +#ifdef HAVE_LIBREADLINE +# define _FUNCTION_DEF +# include <readline/readline.h> +#endif + +struct sinfo { + char *username; + char *shell; +}; + +static void __attribute__((__noreturn__)) usage(void) +{ + FILE *fp = stdout; + fputs(USAGE_HEADER, fp); + fprintf(fp, _(" %s [options] [<username>]\n"), program_invocation_short_name); + + fputs(USAGE_SEPARATOR, fp); + fputs(_("Change your login shell.\n"), fp); + + fputs(USAGE_OPTIONS, fp); + fputs(_(" -s, --shell <shell> specify login shell\n"), fp); + fputs(_(" -l, --list-shells print list of shells and exit\n"), fp); + fputs(USAGE_SEPARATOR, fp); + printf( " -u, --help %s\n", USAGE_OPTSTR_HELP); + printf( " -v, --version %s\n", USAGE_OPTSTR_VERSION); + printf(USAGE_MAN_TAIL("chsh(1)")); + exit(EXIT_SUCCESS); +} + +/* + * is_known_shell() -- if the given shell appears in /etc/shells, + * return true. if not, return false. + */ +static int is_known_shell(const char *shell_name) +{ + char *s, ret = 0; + + if (!shell_name) + return 0; + + setusershell(); + while ((s = getusershell())) { + if (strcmp(shell_name, s) == 0) { + ret = 1; + break; + } + } + endusershell(); + return ret; +} + +/* + * print_shells () -- /etc/shells is outputted to stdout. + */ +static void print_shells(void) +{ + char *s; + + while ((s = getusershell())) + printf("%s\n", s); + endusershell(); +} + +#ifdef HAVE_LIBREADLINE +static char *shell_name_generator(const char *text, int state) +{ + static size_t len; + char *s; + + if (!state) { + setusershell(); + len = strlen(text); + } + + while ((s = getusershell())) { + if (strncmp(s, text, len) == 0) + return xstrdup(s); + } + return NULL; +} + +static char **shell_name_completion(const char *text, + int start __attribute__((__unused__)), + int end __attribute__((__unused__))) +{ + rl_attempted_completion_over = 1; + return rl_completion_matches(text, shell_name_generator); +} +#endif + +/* + * parse_argv () -- + * parse the command line arguments, and fill in "pinfo" with any + * information from the command line. + */ +static void parse_argv(int argc, char **argv, struct sinfo *pinfo) +{ + static const struct option long_options[] = { + {"shell", required_argument, NULL, 's'}, + {"list-shells", no_argument, NULL, 'l'}, + {"help", no_argument, NULL, 'h'}, + {"version", no_argument, NULL, 'v'}, + {NULL, 0, NULL, 0}, + }; + int c; + + while ((c = getopt_long(argc, argv, "s:lhuv", long_options, NULL)) != -1) { + switch (c) { + case 'v': + printf(UTIL_LINUX_VERSION); + exit(EXIT_SUCCESS); + case 'u': /* deprecated */ + case 'h': + usage(); + case 'l': + print_shells(); + exit(EXIT_SUCCESS); + case 's': + pinfo->shell = optarg; + break; + default: + errtryhelp(EXIT_FAILURE); + } + } + /* done parsing arguments. check for a username. */ + if (optind < argc) { + if (optind + 1 < argc) { + errx(EXIT_FAILURE, _("cannot handle multiple usernames")); + } + pinfo->username = argv[optind]; + } +} + +/* + * ask_new_shell () -- + * ask the user for a shell and return it. + */ +static char *ask_new_shell(char *question, char *oldshell) +{ + int len; + char *ans = NULL; +#ifdef HAVE_LIBREADLINE + rl_attempted_completion_function = shell_name_completion; +#else + size_t dummy = 0; +#endif + if (!oldshell) + oldshell = ""; + printf("%s [%s]\n", question, oldshell); +#ifdef HAVE_LIBREADLINE + if ((ans = readline("> ")) == NULL) +#else + if (getline(&ans, &dummy, stdin) < 0) +#endif + return NULL; + /* remove the newline at the end of ans. */ + ltrim_whitespace((unsigned char *) ans); + len = rtrim_whitespace((unsigned char *) ans); + if (len == 0) + return NULL; + return ans; +} + +/* + * check_shell () -- if the shell is completely invalid, print + * an error and exit. + */ +static void check_shell(const char *shell) +{ + if (*shell != '/') + errx(EXIT_FAILURE, _("shell must be a full path name")); + if (access(shell, F_OK) < 0) + errx(EXIT_FAILURE, _("\"%s\" does not exist"), shell); + if (access(shell, X_OK) < 0) + errx(EXIT_FAILURE, _("\"%s\" is not executable"), shell); + if (illegal_passwd_chars(shell)) + errx(EXIT_FAILURE, _("%s: has illegal characters"), shell); + if (!is_known_shell(shell)) { +#ifdef ONLY_LISTED_SHELLS + if (!getuid()) + warnx(_("Warning: \"%s\" is not listed in %s."), shell, + _PATH_SHELLS); + else + errx(EXIT_FAILURE, + _("\"%s\" is not listed in %s.\n" + "Use %s -l to see list."), shell, _PATH_SHELLS, + program_invocation_short_name); +#else + warnx(_("\"%s\" is not listed in %s.\n" + "Use %s -l to see list."), shell, _PATH_SHELLS, + program_invocation_short_name); +#endif + } +} + +int main(int argc, char **argv) +{ + char *oldshell; + int nullshell = 0; + const uid_t uid = getuid(); + struct sinfo info = { NULL }; + struct passwd *pw; + + sanitize_env(); + setlocale(LC_ALL, ""); + bindtextdomain(PACKAGE, LOCALEDIR); + textdomain(PACKAGE); + atexit(close_stdout); + + parse_argv(argc, argv, &info); + if (!info.username) { + pw = getpwuid(uid); + if (!pw) + errx(EXIT_FAILURE, _("you (user %d) don't exist."), + uid); + } else { + pw = getpwnam(info.username); + if (!pw) + errx(EXIT_FAILURE, _("user \"%s\" does not exist."), + info.username); + } + +#ifndef HAVE_LIBUSER + if (!(is_local(pw->pw_name))) + errx(EXIT_FAILURE, _("can only change local entries")); +#endif + +#ifdef HAVE_LIBSELINUX + if (is_selinux_enabled() > 0) { + if (uid == 0) { + access_vector_t av = get_access_vector("passwd", "chsh"); + + if (selinux_check_passwd_access(av) != 0) { + security_context_t user_context; + if (getprevcon(&user_context) < 0) + user_context = + (security_context_t) NULL; + + errx(EXIT_FAILURE, + _("%s is not authorized to change the shell of %s"), + user_context ? : _("Unknown user context"), + pw->pw_name); + } + } + if (setupDefaultContext(_PATH_PASSWD) != 0) + errx(EXIT_FAILURE, + _("can't set default context for %s"), _PATH_PASSWD); + } +#endif + + oldshell = pw->pw_shell; + if (oldshell == NULL || *oldshell == '\0') { + oldshell = _PATH_BSHELL; /* default */ + nullshell = 1; + } + + /* reality check */ +#ifdef HAVE_LIBUSER + /* If we're setuid and not really root, disallow the password change. */ + if (geteuid() != getuid() && uid != pw->pw_uid) { +#else + if (uid != 0 && uid != pw->pw_uid) { +#endif + errno = EACCES; + err(EXIT_FAILURE, + _("running UID doesn't match UID of user we're " + "altering, shell change denied")); + } + if (uid != 0 && !is_known_shell(oldshell)) { + errno = EACCES; + err(EXIT_FAILURE, _("your shell is not in %s, " + "shell change denied"), _PATH_SHELLS); + } + + printf(_("Changing shell for %s.\n"), pw->pw_name); + +#if !defined(HAVE_LIBUSER) && defined(CHFN_CHSH_PASSWORD) + if (!auth_pam("chsh", uid, pw->pw_name)) { + return EXIT_FAILURE; + } +#endif + if (!info.shell) { + info.shell = ask_new_shell(_("New shell"), oldshell); + if (!info.shell) + return EXIT_SUCCESS; + } + + check_shell(info.shell); + + if (!nullshell && strcmp(oldshell, info.shell) == 0) + errx(EXIT_SUCCESS, _("Shell not changed.")); + +#ifdef HAVE_LIBUSER + if (set_value_libuser("chsh", pw->pw_name, uid, + LU_LOGINSHELL, info.shell) < 0) + errx(EXIT_FAILURE, _("Shell *NOT* changed. Try again later.")); +#else + pw->pw_shell = info.shell; + if (setpwnam(pw, ".chsh") < 0) + err(EXIT_FAILURE, _("setpwnam failed\n" + "Shell *NOT* changed. Try again later.")); +#endif + + printf(_("Shell changed.\n")); + return EXIT_SUCCESS; +} diff --git a/login-utils/islocal.c b/login-utils/islocal.c new file mode 100644 index 0000000..7c4e775 --- /dev/null +++ b/login-utils/islocal.c @@ -0,0 +1,112 @@ +/* + * islocal.c - returns true if user is registered in the local + * /etc/passwd file. Written by Álvaro Martínez Echevarria, + * alvaro@enano.etsit.upm.es, to allow peaceful coexistence with yp. Nov 94. + * + * Hacked a bit by poe@daimi.aau.dk + * See also ftp://ftp.daimi.aau.dk/pub/linux/poe/admutil* + * + * Hacked by Peter Breitenlohner, peb@mppmu.mpg.de, + * to distinguish user names where one is a prefix of the other, + * and to use "pathnames.h". Oct 5, 96. + * + * 1999-02-22 Arkadiusz Miśkiewicz <misiek@pld.ORG.PL> + * - added Native Language Support + * + * 2008-04-06 James Youngman, jay@gnu.org + * - Completely rewritten to remove assumption that /etc/passwd + * lines are < 1024 characters long. Also added unit tests. + */ + +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> + +#include "closestream.h" +#include "islocal.h" +#include "nls.h" +#include "pathnames.h" + +static int is_local_in_file(const char *user, const char *filename) +{ + int local = 0; + size_t match; + int chin, skip; + FILE *f; + + if (NULL == (f = fopen(filename, "r"))) + return -1; + + match = 0u; + skip = 0; + while ((chin = getc(f)) != EOF) { + if (skip) { + /* Looking for the start of the next line. */ + if ('\n' == chin) { + /* Start matching username at the next char. */ + skip = 0; + match = 0u; + } + } else { + if (':' == chin) { + if (0 == user[match]) { + /* Success. */ + local = 1; + /* next line has no test coverage, + * but it is just an optimisation + * anyway. */ + break; + } else { + /* we read a whole username, but it + * is the wrong user. Skip to the + * next line. */ + skip = 1; + } + } else if ('\n' == chin) { + /* This line contains no colon; it's + * malformed. No skip since we are already + * at the start of the next line. */ + match = 0u; + } else if (chin != user[match]) { + /* username does not match. */ + skip = 1; + } else { + ++match; + } + } + } + fclose(f); + return local; +} + +int is_local(const char *user) +{ + int rv; + + if ((rv = is_local_in_file(user, _PATH_PASSWD)) < 0) + err(EXIT_FAILURE, _("cannot open %s"), _PATH_PASSWD); + return rv; +} + +#ifdef TEST_PROGRAM +int main(int argc, char *argv[]) +{ + atexit(close_stdout); + if (argc <= 2) { + fprintf(stderr, _("Usage: %s <passwordfile> <username>...\n"), + argv[0]); + return 1; + } else { + int i; + for (i = 2; i < argc; i++) { + const int rv = is_local_in_file(argv[i], argv[1]); + if (rv < 0) { + perror(argv[1]); + return 2; + } + printf("%d:%s\n", rv, argv[i]); + } + return 0; + } +} +#endif diff --git a/login-utils/islocal.h b/login-utils/islocal.h new file mode 100644 index 0000000..2c58799 --- /dev/null +++ b/login-utils/islocal.h @@ -0,0 +1 @@ +extern int is_local(const char *user); diff --git a/login-utils/last.1 b/login-utils/last.1 new file mode 100644 index 0000000..619be3f --- /dev/null +++ b/login-utils/last.1 @@ -0,0 +1,198 @@ +.\" Copyright (C) 1998-2004 Miquel van Smoorenburg. +.\" +.\" This program is free software; you can redistribute it and/or modify +.\" it under the terms of the GNU General Public License as published by +.\" the Free Software Foundation; either version 2 of the License, or +.\" (at your option) any later version. +.\" +.\" This program is distributed in the hope that it will be useful, +.\" but WITHOUT ANY WARRANTY; without even the implied warranty of +.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +.\" GNU General Public License for more details. +.\" +.\" You should have received a copy of the GNU General Public License +.\" along with this program; if not, write to the Free Software +.\" Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +.\" +.TH "LAST, LASTB" "1" "October 2013" "util-linux" "User Commands" +.SH NAME +last, lastb \- show a listing of last logged in users +.SH SYNOPSIS +.B last +[options] +.RI [ username "...] [" tty ...] +.br +.B lastb +[options] +.RI [ username "...] [" tty ...] +.SH DESCRIPTION +.B last +searches back through the +.I /var/log/wtmp +file (or the file designated by the +.B \-f +option) and displays a list of all users logged in (and out) since that +file was created. One or more +.IR usernames " and/or " ttys +can be given, in which case +.B last +will show only the entries matching those arguments. Names of +.I ttys +can be abbreviated, thus +.B last 0 +is the same as +.BR "last tty0" . +.PP +When catching a SIGINT signal (generated by the interrupt key, usually +control-C) or a SIGQUIT signal, +.B last +will show how far it has searched through the file; in the case of the +SIGINT signal +.B last +will then terminate. +.PP +The pseudo user +.B reboot +logs in each time the system is rebooted. Thus +.B last reboot +will show a log of all the reboots since the log file was created. +.PP +.B lastb +is the same as +.BR last , +except that by default it shows a log of the +.I /var/log/btmp +file, which contains all the bad login attempts. +.SH OPTIONS +.TP +.BR \-a , " \-\-hostlast" +Display the hostname in the last column. Useful in combination with the +.B \-\-dns +option. +.TP +.BR \-d , " \-\-dns" +For non-local logins, Linux stores not only the host name of the remote +host, but its IP number as well. This option translates the IP number +back into a hostname. +.TP +.BR \-f , " \-\-file " \fIfile\fR +Tell +.B last +to use a specific \fIfile\fR instead of +.IR /var/log/wtmp . +The +.B \-\-file +option can be given multiple times, and all of the specified files will be +processed. +.TP +.BR \-F , " \-\-fulltimes" +Print full login and logout times and dates. +.TP +.BR \-i , " \-\-ip" +Like +.B \-\-dns , +but displays the host's IP number instead of the name. +.TP +.BI \- number +.TQ +.BR \-n , " -\-limit " \fInumber\fR +Tell +.B last +how many lines to show. +.TP +.BR \-p , " \-\-present " \fItime\fR +Display the users who were present at the specified time. This is +like using the options +.BR \-\-since " and " \-\-until +together with the same \fItime\fR. +.TP +.BR \-R , " \-\-nohostname" +Suppresses the display of the hostname field. +.TP +.BR \-s , " \-\-since " \fItime\fR +Display the state of logins since the specified +.IR time . +This is useful, e.g., to easily determine who was logged in at a +particular time. The option is often combined with +.BR \-\-until . +.TP +.BR \-t , " \-\-until " \fItime\fR +Display the state of logins until the specified +.IR time . +.TP +.BI \-\-time\-format " format" +Define the output timestamp +.I format +to be one of +.IR notime , +.IR short , +.IR full , +or +.IR iso . +The +.I notime +variant will not print any timestamps at all, +.I short +is the default, and +.I full +is the same as the +.B \-\-fulltimes +option. The +.I iso +variant will display the timestamp in ISO-8601 format. The ISO format +contains timezone information, making it preferable when printouts are +investigated outside of the system. +.TP +.BR \-w , " \-\-fullnames" +Display full user names and domain names in the output. +.TP +.BR \-x , " \-\-system" +Display the system shutdown entries and run level changes. +.SH TIME FORMATS +The options that take the +.I time +argument understand the following formats: +.TS +l2 l. +YYYYMMDDhhmmss +YYYY-MM-DD hh:mm:ss +YYYY-MM-DD hh:mm (seconds will be set to 00) +YYYY-MM-DD (time will be set to 00:00:00) +hh:mm:ss (date will be set to today) +hh:mm (date will be set to today, seconds to 00) +now +yesterday (time is set to 00:00:00) +today (time is set to 00:00:00) +tomorrow (time is set to 00:00:00) ++5min +-5days +.TE +.SH NOTES +The files +.I wtmp +and +.I btmp +might not be found. The system only logs information in these files if +they are present. This is a local configuration issue. If you want the +files to be used, they can be created with a simple +.BR touch (1) +command (for example, +.IR "touch /var/log/wtmp" ). +.SH FILES +/var/log/wtmp +.br +/var/log/btmp +.SH AUTHOR +.MT miquels@cistron.nl +Miquel van Smoorenburg +.ME +.SH AVAILABILITY +The last command is part of the util-linux package and is available from +.UR https://\:www.kernel.org\:/pub\:/linux\:/utils\:/util-linux/ +Linux Kernel Archive +.UE . +.SH "SEE ALSO" +.BR login (1), +.BR wtmp (5), +.BR init (8), +.BR shutdown (8) diff --git a/login-utils/last.c b/login-utils/last.c new file mode 100644 index 0000000..6b25efd --- /dev/null +++ b/login-utils/last.c @@ -0,0 +1,1032 @@ +/* + * last(1) from sysvinit project, merged into util-linux in Aug 2013. + * + * Copyright (C) 1991-2004 Miquel van Smoorenburg. + * Copyright (C) 2013 Ondrej Oprala <ooprala@redhat.com> + * Karel Zak <kzak@redhat.com> + * + * Re-implementation of the 'last' command, this time for Linux. Yes I know + * there is BSD last, but I just felt like writing this. No thanks :-). Also, + * this version gives lots more info (especially with -x) + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <time.h> +#include <stdio.h> +#include <ctype.h> +#include <utmpx.h> +#include <pwd.h> +#include <stdlib.h> +#include <unistd.h> +#include <string.h> +#include <signal.h> +#include <getopt.h> +#include <netinet/in.h> +#include <netdb.h> +#include <arpa/inet.h> +#include <libgen.h> + +#include "c.h" +#include "nls.h" +#include "optutils.h" +#include "pathnames.h" +#include "xalloc.h" +#include "closestream.h" +#include "carefulputc.h" +#include "strutils.h" +#include "timeutils.h" +#include "monotonic.h" + +#ifndef SHUTDOWN_TIME +# define SHUTDOWN_TIME 254 +#endif + +#ifndef LAST_LOGIN_LEN +# define LAST_LOGIN_LEN 8 +#endif + +#ifndef LAST_DOMAIN_LEN +# define LAST_DOMAIN_LEN 16 +#endif + +#ifndef LAST_TIMESTAMP_LEN +# define LAST_TIMESTAMP_LEN 32 +#endif + +#define UCHUNKSIZE 16384 /* How much we read at once. */ + +struct last_control { + unsigned int lastb :1, /* Is this command 'lastb' */ + extended :1, /* Lots of info */ + showhost :1, /* Show hostname */ + altlist :1, /* Hostname at the end */ + usedns :1, /* Use DNS to lookup the hostname */ + useip :1; /* Print IP address in number format */ + + unsigned int name_len; /* Number of login name characters to print */ + unsigned int domain_len; /* Number of domain name characters to print */ + unsigned int maxrecs; /* Maximum number of records to list */ + + char **show; /* Match search list */ + + struct timeval boot_time; /* system boot time */ + time_t since; /* at what time to start displaying the file */ + time_t until; /* at what time to stop displaying the file */ + time_t present; /* who where present at time_t */ + unsigned int time_fmt; /* time format */ +}; + +/* Double linked list of struct utmp's */ +struct utmplist { + struct utmpx ut; + struct utmplist *next; + struct utmplist *prev; +}; + +/* Types of listing */ +enum { + R_CRASH = 1, /* No logout record, system boot in between */ + R_DOWN, /* System brought down in decent way */ + R_NORMAL, /* Normal */ + R_NOW, /* Still logged in */ + R_REBOOT, /* Reboot record. */ + R_PHANTOM, /* No logout record but session is stale. */ + R_TIMECHANGE /* NEW_TIME or OLD_TIME */ +}; + +enum { + LAST_TIMEFTM_NONE = 0, + LAST_TIMEFTM_SHORT, + LAST_TIMEFTM_CTIME, + LAST_TIMEFTM_ISO8601, + + LAST_TIMEFTM_HHMM, /* non-public */ +}; + +struct last_timefmt { + const char *name; + int in_len; /* log-in */ + int in_fmt; + int out_len; /* log-out */ + int out_fmt; +}; + +static struct last_timefmt timefmts[] = { + [LAST_TIMEFTM_NONE] = { .name = "notime" }, + [LAST_TIMEFTM_SHORT] = { + .name = "short", + .in_len = 16, + .out_len = 7, + .in_fmt = LAST_TIMEFTM_CTIME, + .out_fmt = LAST_TIMEFTM_HHMM + }, + [LAST_TIMEFTM_CTIME] = { + .name = "full", + .in_len = 24, + .out_len = 26, + .in_fmt = LAST_TIMEFTM_CTIME, + .out_fmt = LAST_TIMEFTM_CTIME + }, + [LAST_TIMEFTM_ISO8601] = { + .name = "iso", + .in_len = 25, + .out_len = 27, + .in_fmt = LAST_TIMEFTM_ISO8601, + .out_fmt = LAST_TIMEFTM_ISO8601 + } +}; + +/* Global variables */ +static unsigned int recsdone; /* Number of records listed */ +static time_t lastdate; /* Last date we've seen */ +static time_t currentdate; /* date when we started processing the file */ + +/* --time-format=option parser */ +static int which_time_format(const char *s) +{ + size_t i; + + for (i = 0; i < ARRAY_SIZE(timefmts); i++) { + if (strcmp(timefmts[i].name, s) == 0) + return i; + } + errx(EXIT_FAILURE, _("unknown time format: %s"), s); +} + +/* + * Read one utmp entry, return in new format. + * Automatically reposition file pointer. + */ +static int uread(FILE *fp, struct utmpx *u, int *quit, const char *filename) +{ + static int utsize; + static char buf[UCHUNKSIZE]; + char tmp[1024]; + static off_t fpos; + static int bpos; + off_t o; + + if (quit == NULL && u != NULL) { + /* + * Normal read. + */ + return fread(u, sizeof(struct utmpx), 1, fp); + } + + if (u == NULL) { + /* + * Initialize and position. + */ + utsize = sizeof(struct utmpx); + fseeko(fp, 0, SEEK_END); + fpos = ftello(fp); + if (fpos == 0) + return 0; + o = ((fpos - 1) / UCHUNKSIZE) * UCHUNKSIZE; + if (fseeko(fp, o, SEEK_SET) < 0) { + warn(_("seek on %s failed"), filename); + return 0; + } + bpos = (int)(fpos - o); + if (fread(buf, bpos, 1, fp) != 1) { + warn(_("cannot read %s"), filename); + return 0; + } + fpos = o; + return 1; + } + + /* + * Read one struct. From the buffer if possible. + */ + bpos -= utsize; + if (bpos >= 0) { + memcpy(u, buf + bpos, sizeof(struct utmpx)); + return 1; + } + + /* + * Oops we went "below" the buffer. We should be able to + * seek back UCHUNKSIZE bytes. + */ + fpos -= UCHUNKSIZE; + if (fpos < 0) + return 0; + + /* + * Copy whatever is left in the buffer. + */ + memcpy(tmp + (-bpos), buf, utsize + bpos); + if (fseeko(fp, fpos, SEEK_SET) < 0) { + warn(_("seek on %s failed"), filename); + return 0; + } + + /* + * Read another UCHUNKSIZE bytes. + */ + if (fread(buf, UCHUNKSIZE, 1, fp) != 1) { + warn(_("cannot read %s"), filename); + return 0; + } + + /* + * The end of the UCHUNKSIZE byte buffer should be the first + * few bytes of the current struct utmp. + */ + memcpy(tmp, buf + UCHUNKSIZE + bpos, -bpos); + bpos += UCHUNKSIZE; + + memcpy(u, tmp, sizeof(struct utmpx)); + + return 1; +} + +/* + * Print a short date. + */ +static char *showdate(void) +{ + char *s = ctime(&lastdate); + s[16] = 0; + return s; +} + +/* + * SIGINT handler + */ +static void int_handler(int sig __attribute__((unused))) +{ + errx(EXIT_FAILURE, _("Interrupted %s"), showdate()); +} + +/* + * SIGQUIT handler + */ +static void quit_handler(int sig __attribute__((unused))) +{ + warnx(_("Interrupted %s"), showdate()); + signal(SIGQUIT, quit_handler); +} + +/* + * Lookup a host with DNS. + */ +static int dns_lookup(char *result, int size, int useip, int32_t *a) +{ + struct sockaddr_in sin; + struct sockaddr_in6 sin6; + struct sockaddr *sa; + int salen, flags; + int mapped = 0; + + flags = useip ? NI_NUMERICHOST : 0; + + /* + * IPv4 or IPv6 ? + * 1. If last 3 4bytes are 0, must be IPv4 + * 2. If IPv6 in IPv4, handle as IPv4 + * 3. Anything else is IPv6 + * + * Ugly. + */ + if (a[0] == 0 && a[1] == 0 && a[2] == (int32_t)htonl (0xffff)) + mapped = 1; + + if (mapped || (a[1] == 0 && a[2] == 0 && a[3] == 0)) { + /* IPv4 */ + sin.sin_family = AF_INET; + sin.sin_port = 0; + sin.sin_addr.s_addr = mapped ? a[3] : a[0]; + sa = (struct sockaddr *)&sin; + salen = sizeof(sin); + } else { + /* IPv6 */ + memset(&sin6, 0, sizeof(sin6)); + sin6.sin6_family = AF_INET6; + sin6.sin6_port = 0; + memcpy(sin6.sin6_addr.s6_addr, a, 16); + sa = (struct sockaddr *)&sin6; + salen = sizeof(sin6); + } + + return getnameinfo(sa, salen, result, size, NULL, 0, flags); +} + +static int time_formatter(int fmt, char *dst, size_t dlen, time_t *when) +{ + int ret = 0; + + switch (fmt) { + case LAST_TIMEFTM_NONE: + *dst = 0; + break; + case LAST_TIMEFTM_HHMM: + { + struct tm *tm = localtime(when); + if (!snprintf(dst, dlen, "%02d:%02d", tm->tm_hour, tm->tm_min)) + ret = -1; + break; + } + case LAST_TIMEFTM_CTIME: + snprintf(dst, dlen, "%s", ctime(when)); + ret = rtrim_whitespace((unsigned char *) dst); + break; + case LAST_TIMEFTM_ISO8601: + ret = strtime_iso(when, ISO_TIMESTAMP_T, dst, dlen); + break; + default: + abort(); + } + return ret; +} + +/* + * Remove trailing spaces from a string. + */ +static void trim_trailing_spaces(char *s) +{ + char *p; + + for (p = s; *p; ++p) + continue; + while (p > s && isspace(*--p)) + continue; + if (p > s) + ++p; + *p++ = '\n'; + *p = '\0'; +} + +/* + * Show one line of information on screen + */ +static int list(const struct last_control *ctl, struct utmpx *p, time_t logout_time, int what) +{ + time_t secs, utmp_time; + char logintime[LAST_TIMESTAMP_LEN]; + char logouttime[LAST_TIMESTAMP_LEN]; + char length[LAST_TIMESTAMP_LEN]; + char final[512]; + char utline[sizeof(p->ut_line) + 1]; + char domain[256]; + char *s; + int mins, hours, days; + int r, len; + struct last_timefmt *fmt; + + /* + * uucp and ftp have special-type entries + */ + utline[0] = 0; + strncat(utline, p->ut_line, sizeof(utline) - 1); + if (strncmp(utline, "ftp", 3) == 0 && isdigit(utline[3])) + utline[3] = 0; + if (strncmp(utline, "uucp", 4) == 0 && isdigit(utline[4])) + utline[4] = 0; + + /* + * Is this something we want to show? + */ + if (ctl->show) { + char **walk; + for (walk = ctl->show; *walk; walk++) { + if (strncmp(p->ut_user, *walk, sizeof(p->ut_user)) == 0 || + strcmp(utline, *walk) == 0 || + (strncmp(utline, "tty", 3) == 0 && + strcmp(utline + 3, *walk) == 0)) break; + } + if (*walk == NULL) return 0; + } + + /* + * Calculate times + */ + fmt = &timefmts[ctl->time_fmt]; + + utmp_time = p->ut_tv.tv_sec; + + if (ctl->present) { + if (ctl->present < utmp_time) + return 0; + if (0 < logout_time && logout_time < ctl->present) + return 0; + } + + /* log-in time */ + if (time_formatter(fmt->in_fmt, logintime, + sizeof(logintime), &utmp_time) < 0) + errx(EXIT_FAILURE, _("preallocation size exceeded")); + + /* log-out time */ + secs = logout_time - utmp_time; /* Under strange circumstances, secs < 0 can happen */ + mins = (secs / 60) % 60; + hours = (secs / 3600) % 24; + days = secs / 86400; + + strcpy(logouttime, "- "); + if (time_formatter(fmt->out_fmt, logouttime + 2, + sizeof(logouttime) - 2, &logout_time) < 0) + errx(EXIT_FAILURE, _("preallocation size exceeded")); + + if (logout_time == currentdate) { + if (ctl->time_fmt > LAST_TIMEFTM_SHORT) { + sprintf(logouttime, " still running"); + length[0] = 0; + } else { + sprintf(logouttime, " still"); + sprintf(length, "running"); + } + } else if (days) { + sprintf(length, "(%d+%02d:%02d)", days, abs(hours), abs(mins)); /* hours and mins always shown as positive (w/o minus sign!) even if secs < 0 */ + } else if (hours) { + sprintf(length, " (%02d:%02d)", hours, abs(mins)); /* mins always shown as positive (w/o minus sign!) even if secs < 0 */ + } else if (secs >= 0) { + sprintf(length, " (%02d:%02d)", hours, mins); + } else { + sprintf(length, " (-00:%02d)", abs(mins)); /* mins always shown as positive (w/o minus sign!) even if secs < 0 */ + } + + switch(what) { + case R_CRASH: + sprintf(logouttime, "- crash"); + break; + case R_DOWN: + sprintf(logouttime, "- down "); + break; + case R_NOW: + if (ctl->time_fmt > LAST_TIMEFTM_SHORT) { + sprintf(logouttime, " still logged in"); + length[0] = 0; + } else { + sprintf(logouttime, " still"); + sprintf(length, "logged in"); + } + break; + case R_PHANTOM: + if (ctl->time_fmt > LAST_TIMEFTM_SHORT) { + sprintf(logouttime, " gone - no logout"); + length[0] = 0; + } else if (ctl->time_fmt == LAST_TIMEFTM_SHORT) { + sprintf(logouttime, " gone"); + sprintf(length, "- no logout"); + } else { + logouttime[0] = 0; + sprintf(length, "no logout"); + } + break; + case R_TIMECHANGE: + logouttime[0] = 0; + length[0] = 0; + break; + case R_NORMAL: + case R_REBOOT: + break; + default: + abort(); + } + + /* + * Look up host with DNS if needed. + */ + r = -1; + if (ctl->usedns || ctl->useip) + r = dns_lookup(domain, sizeof(domain), ctl->useip, (int32_t*)p->ut_addr_v6); + if (r < 0) + mem2strcpy(domain, p->ut_host, sizeof(p->ut_host), sizeof(domain)); + + if (ctl->showhost) { + if (!ctl->altlist) { + len = snprintf(final, sizeof(final), + "%-8.*s %-12.12s %-16.*s %-*.*s %-*.*s %s\n", + ctl->name_len, p->ut_user, utline, + ctl->domain_len, domain, + fmt->in_len, fmt->in_len, logintime, fmt->out_len, fmt->out_len, + logouttime, length); + } else { + len = snprintf(final, sizeof(final), + "%-8.*s %-12.12s %-*.*s %-*.*s %-12.12s %s\n", + ctl->name_len, p->ut_user, utline, + fmt->in_len, fmt->in_len, logintime, fmt->out_len, fmt->out_len, + logouttime, length, domain); + } + } else + len = snprintf(final, sizeof(final), + "%-8.*s %-12.12s %-*.*s %-*.*s %s\n", + ctl->name_len, p->ut_user, utline, + fmt->in_len, fmt->in_len, logintime, fmt->out_len, fmt->out_len, + logouttime, length); + +#if defined(__GLIBC__) +# if (__GLIBC__ == 2) && (__GLIBC_MINOR__ == 0) + final[sizeof(final)-1] = '\0'; +# endif +#endif + + trim_trailing_spaces(final); + /* + * Print out "final" string safely. + */ + for (s = final; *s; s++) + fputc_careful(*s, stdout, '*'); + + if (len < 0 || (size_t)len >= sizeof(final)) + putchar('\n'); + + recsdone++; + if (ctl->maxrecs && ctl->maxrecs <= recsdone) + return 1; + + return 0; +} + + +static void __attribute__((__noreturn__)) usage(const struct last_control *ctl) +{ + FILE *out = stdout; + fputs(USAGE_HEADER, out); + fprintf(out, _( + " %s [options] [<username>...] [<tty>...]\n"), program_invocation_short_name); + + fputs(USAGE_SEPARATOR, out); + fputs(_("Show a listing of last logged in users.\n"), out); + + fputs(USAGE_OPTIONS, out); + fputs(_(" -<number> how many lines to show\n"), out); + fputs(_(" -a, --hostlast display hostnames in the last column\n"), out); + fputs(_(" -d, --dns translate the IP number back into a hostname\n"), out); + fprintf(out, + _(" -f, --file <file> use a specific file instead of %s\n"), ctl->lastb ? _PATH_BTMP : _PATH_WTMP); + fputs(_(" -F, --fulltimes print full login and logout times and dates\n"), out); + fputs(_(" -i, --ip display IP numbers in numbers-and-dots notation\n"), out); + fputs(_(" -n, --limit <number> how many lines to show\n"), out); + fputs(_(" -R, --nohostname don't display the hostname field\n"), out); + fputs(_(" -s, --since <time> display the lines since the specified time\n"), out); + fputs(_(" -t, --until <time> display the lines until the specified time\n"), out); + fputs(_(" -p, --present <time> display who were present at the specified time\n"), out); + fputs(_(" -w, --fullnames display full user and domain names\n"), out); + fputs(_(" -x, --system display system shutdown entries and run level changes\n"), out); + fputs(_(" --time-format <format> show timestamps in the specified <format>:\n" + " notime|short|full|iso\n"), out); + + fputs(USAGE_SEPARATOR, out); + printf(USAGE_HELP_OPTIONS(22)); + printf(USAGE_MAN_TAIL("last(1)")); + + exit(out == stderr ? EXIT_FAILURE : EXIT_SUCCESS); +} + +static int is_phantom(const struct last_control *ctl, struct utmpx *ut) +{ + struct passwd *pw; + char path[sizeof(ut->ut_line) + 16]; + int ret = 0; + + if (ut->ut_tv.tv_sec < ctl->boot_time.tv_sec) + return 1; + pw = getpwnam(ut->ut_user); + if (!pw) + return 1; + sprintf(path, "/proc/%u/loginuid", ut->ut_pid); + if (access(path, R_OK) == 0) { + unsigned int loginuid; + FILE *f = NULL; + + if (!(f = fopen(path, "r"))) + return 1; + if (fscanf(f, "%u", &loginuid) != 1) + ret = 1; + fclose(f); + if (!ret && pw->pw_uid != loginuid) + return 1; + } else { + struct stat st; + + sprintf(path, "/dev/%s", ut->ut_line); + if (stat(path, &st)) + return 1; + if (pw->pw_uid != st.st_uid) + return 1; + } + return ret; +} + +static void process_wtmp_file(const struct last_control *ctl, + const char *filename) +{ + FILE *fp; /* File pointer of wtmp file */ + + struct utmpx ut; /* Current utmp entry */ + struct utmplist *ulist = NULL; /* All entries */ + struct utmplist *p; /* Pointer into utmplist */ + struct utmplist *next; /* Pointer into utmplist */ + + time_t lastboot = 0; /* Last boottime */ + time_t lastrch = 0; /* Last run level change */ + time_t lastdown; /* Last downtime */ + time_t begintime; /* When wtmp begins */ + int whydown = 0; /* Why we went down: crash or shutdown */ + + int c, x; /* Scratch */ + struct stat st; /* To stat the [uw]tmp file */ + int quit = 0; /* Flag */ + int down = 0; /* Down flag */ + + time(&lastdown); + /* + * Fill in 'lastdate' + */ + lastdate = currentdate = lastrch = lastdown; + + /* + * Install signal handlers + */ + signal(SIGINT, int_handler); + signal(SIGQUIT, quit_handler); + + /* + * Open the utmp file + */ + if ((fp = fopen(filename, "r")) == NULL) + err(EXIT_FAILURE, _("cannot open %s"), filename); + + /* + * Optimize the buffer size. + */ + setvbuf(fp, NULL, _IOFBF, UCHUNKSIZE); + + /* + * Read first structure to capture the time field + */ + if (uread(fp, &ut, NULL, filename) == 1) + begintime = ut.ut_tv.tv_sec; + else { + if (fstat(fileno(fp), &st) != 0) + err(EXIT_FAILURE, _("stat of %s failed"), filename); + begintime = st.st_ctime; + quit = 1; + } + + /* + * Go to end of file minus one structure + * and/or initialize utmp reading code. + */ + uread(fp, NULL, NULL, filename); + + /* + * Read struct after struct backwards from the file. + */ + while (!quit) { + + if (uread(fp, &ut, &quit, filename) != 1) + break; + + if (ctl->since && ut.ut_tv.tv_sec < ctl->since) + continue; + + if (ctl->until && ctl->until < ut.ut_tv.tv_sec) + continue; + + lastdate = ut.ut_tv.tv_sec; + + if (ctl->lastb) { + quit = list(ctl, &ut, ut.ut_tv.tv_sec, R_NORMAL); + continue; + } + + /* + * Set ut_type to the correct type. + */ + if (strncmp(ut.ut_line, "~", 1) == 0) { + if (strncmp(ut.ut_user, "shutdown", 8) == 0) + ut.ut_type = SHUTDOWN_TIME; + else if (strncmp(ut.ut_user, "reboot", 6) == 0) + ut.ut_type = BOOT_TIME; + else if (strncmp(ut.ut_user, "runlevel", 8) == 0) + ut.ut_type = RUN_LVL; + } +#if 1 /*def COMPAT*/ + /* + * For stupid old applications that don't fill in + * ut_type correctly. + */ + else { + if (ut.ut_type != DEAD_PROCESS && + ut.ut_user[0] && ut.ut_line[0] && + strcmp(ut.ut_user, "LOGIN") != 0) + ut.ut_type = USER_PROCESS; + /* + * Even worse, applications that write ghost + * entries: ut_type set to USER_PROCESS but + * empty ut_user... + */ + if (ut.ut_user[0] == 0) + ut.ut_type = DEAD_PROCESS; + + /* + * Clock changes. + */ + if (strcmp(ut.ut_user, "date") == 0) { + if (ut.ut_line[0] == '|') + ut.ut_type = OLD_TIME; + if (ut.ut_line[0] == '{') + ut.ut_type = NEW_TIME; + } + } +#endif + switch (ut.ut_type) { + case SHUTDOWN_TIME: + if (ctl->extended) { + strcpy(ut.ut_line, "system down"); + quit = list(ctl, &ut, lastboot, R_NORMAL); + } + lastdown = lastrch = ut.ut_tv.tv_sec; + down = 1; + break; + case OLD_TIME: + case NEW_TIME: + if (ctl->extended) { + strcpy(ut.ut_line, + ut.ut_type == NEW_TIME ? "new time" : + "old time"); + quit = list(ctl, &ut, lastdown, R_TIMECHANGE); + } + break; + case BOOT_TIME: + strcpy(ut.ut_line, "system boot"); + quit = list(ctl, &ut, lastdown, R_REBOOT); + lastboot = ut.ut_tv.tv_sec; + down = 1; + break; + case RUN_LVL: + x = ut.ut_pid & 255; + if (ctl->extended) { + sprintf(ut.ut_line, "(to lvl %c)", x); + quit = list(ctl, &ut, lastrch, R_NORMAL); + } + if (x == '0' || x == '6') { + lastdown = ut.ut_tv.tv_sec; + down = 1; + ut.ut_type = SHUTDOWN_TIME; + } + lastrch = ut.ut_tv.tv_sec; + break; + + case USER_PROCESS: + /* + * This was a login - show the first matching + * logout record and delete all records with + * the same ut_line. + */ + c = 0; + for (p = ulist; p; p = next) { + next = p->next; + if (strncmp(p->ut.ut_line, ut.ut_line, + sizeof(ut.ut_line)) == 0) { + /* Show it */ + if (c == 0) { + quit = list(ctl, &ut, p->ut.ut_tv.tv_sec, R_NORMAL); + c = 1; + } + if (p->next) + p->next->prev = p->prev; + if (p->prev) + p->prev->next = p->next; + else + ulist = p->next; + free(p); + } + } + /* + * Not found? Then crashed, down, still + * logged in, or missing logout record. + */ + if (c == 0) { + if (!lastboot) { + c = R_NOW; + /* Is process still alive? */ + if (is_phantom(ctl, &ut)) + c = R_PHANTOM; + } else + c = whydown; + quit = list(ctl, &ut, lastboot, c); + } + /* fallthrough */ + + case DEAD_PROCESS: + /* + * Just store the data if it is + * interesting enough. + */ + if (ut.ut_line[0] == 0) + break; + p = xmalloc(sizeof(struct utmplist)); + memcpy(&p->ut, &ut, sizeof(struct utmpx)); + p->next = ulist; + p->prev = NULL; + if (ulist) + ulist->prev = p; + ulist = p; + break; + + case EMPTY: + case INIT_PROCESS: + case LOGIN_PROCESS: +#ifdef ACCOUNTING + case ACCOUNTING: +#endif + /* ignored ut_types */ + break; + + default: + warnx("unrecognized ut_type: %d", ut.ut_type); + } + + /* + * If we saw a shutdown/reboot record we can remove + * the entire current ulist. + */ + if (down) { + lastboot = ut.ut_tv.tv_sec; + whydown = (ut.ut_type == SHUTDOWN_TIME) ? R_DOWN : R_CRASH; + for (p = ulist; p; p = next) { + next = p->next; + free(p); + } + ulist = NULL; + down = 0; + } + } + + if (ctl->time_fmt != LAST_TIMEFTM_NONE) { + struct last_timefmt *fmt; + char timestr[LAST_TIMESTAMP_LEN]; + char *tmp = xstrdup(filename); + + fmt = &timefmts[ctl->time_fmt]; + if (time_formatter(fmt->in_fmt, timestr, + sizeof(timestr), &begintime) < 0) + errx(EXIT_FAILURE, _("preallocation size exceeded")); + printf(_("\n%s begins %s\n"), basename(tmp), timestr); + free(tmp); + } + + fclose(fp); + + for (p = ulist; p; p = next) { + next = p->next; + free(p); + } +} + +int main(int argc, char **argv) +{ + struct last_control ctl = { + .showhost = TRUE, + .name_len = LAST_LOGIN_LEN, + .time_fmt = LAST_TIMEFTM_SHORT, + .domain_len = LAST_DOMAIN_LEN + }; + char **files = NULL; + size_t i, nfiles = 0; + int c; + usec_t p; + + enum { + OPT_TIME_FORMAT = CHAR_MAX + 1 + }; + static const struct option long_opts[] = { + { "limit", required_argument, NULL, 'n' }, + { "help", no_argument, NULL, 'h' }, + { "file", required_argument, NULL, 'f' }, + { "nohostname", no_argument, NULL, 'R' }, + { "version", no_argument, NULL, 'V' }, + { "hostlast", no_argument, NULL, 'a' }, + { "since", required_argument, NULL, 's' }, + { "until", required_argument, NULL, 't' }, + { "present", required_argument, NULL, 'p' }, + { "system", no_argument, NULL, 'x' }, + { "dns", no_argument, NULL, 'd' }, + { "ip", no_argument, NULL, 'i' }, + { "fulltimes", no_argument, NULL, 'F' }, + { "fullnames", no_argument, NULL, 'w' }, + { "time-format", required_argument, NULL, OPT_TIME_FORMAT }, + { NULL, 0, NULL, 0 } + }; + static const ul_excl_t excl[] = { /* rows and cols in ASCII order */ + { 'F', OPT_TIME_FORMAT }, /* fulltime, time-format */ + { 0 } + }; + int excl_st[ARRAY_SIZE(excl)] = UL_EXCL_STATUS_INIT; + + setlocale(LC_ALL, ""); + bindtextdomain(PACKAGE, LOCALEDIR); + textdomain(PACKAGE); + atexit(close_stdout); + /* + * Which file do we want to read? + */ + ctl.lastb = strcmp(program_invocation_short_name, "lastb") == 0 ? 1 : 0; + while ((c = getopt_long(argc, argv, + "hVf:n:RxadFit:p:s:0123456789w", long_opts, NULL)) != -1) { + + err_exclusive_options(c, long_opts, excl, excl_st); + + switch(c) { + case 'h': + usage(&ctl); + break; + case 'V': + printf(UTIL_LINUX_VERSION); + return EXIT_SUCCESS; + case 'R': + ctl.showhost = 0; + break; + case 'x': + ctl.extended = 1; + break; + case 'n': + ctl.maxrecs = strtos32_or_err(optarg, _("failed to parse number")); + break; + case 'f': + if (!files) + files = xmalloc(sizeof(char *) * argc); + files[nfiles++] = xstrdup(optarg); + break; + case 'd': + ctl.usedns = 1; + break; + case 'i': + ctl.useip = 1; + break; + case 'a': + ctl.altlist = 1; + break; + case 'F': + ctl.time_fmt = LAST_TIMEFTM_CTIME; + break; + case 'p': + if (parse_timestamp(optarg, &p) < 0) + errx(EXIT_FAILURE, _("invalid time value \"%s\""), optarg); + ctl.present = (time_t) (p / 1000000); + break; + case 's': + if (parse_timestamp(optarg, &p) < 0) + errx(EXIT_FAILURE, _("invalid time value \"%s\""), optarg); + ctl.since = (time_t) (p / 1000000); + break; + case 't': + if (parse_timestamp(optarg, &p) < 0) + errx(EXIT_FAILURE, _("invalid time value \"%s\""), optarg); + ctl.until = (time_t) (p / 1000000); + break; + case 'w': + if (ctl.name_len < sizeof(((struct utmpx *) 0)->ut_user)) + ctl.name_len = sizeof(((struct utmpx *) 0)->ut_user); + if (ctl.domain_len < sizeof(((struct utmpx *) 0)->ut_host)) + ctl.domain_len = sizeof(((struct utmpx *) 0)->ut_host); + break; + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + ctl.maxrecs = 10 * ctl.maxrecs + c - '0'; + break; + case OPT_TIME_FORMAT: + ctl.time_fmt = which_time_format(optarg); + break; + default: + errtryhelp(EXIT_FAILURE); + } + } + + if (optind < argc) + ctl.show = argv + optind; + + if (!files) { + files = xmalloc(sizeof(char *)); + files[nfiles++] = xstrdup(ctl.lastb ? _PATH_BTMP : _PATH_WTMP); + } + + for (i = 0; i < nfiles; i++) { + get_boot_time(&ctl.boot_time); + process_wtmp_file(&ctl, files[i]); + free(files[i]); + } + free(files); + return EXIT_SUCCESS; +} diff --git a/login-utils/lastb.1 b/login-utils/lastb.1 new file mode 100644 index 0000000..f57c02a --- /dev/null +++ b/login-utils/lastb.1 @@ -0,0 +1 @@ +.so man1/last.1 diff --git a/login-utils/libuser.c b/login-utils/libuser.c new file mode 100644 index 0000000..b11fadc --- /dev/null +++ b/login-utils/libuser.c @@ -0,0 +1,72 @@ +/* + * libuser.c -- Utilize libuser to set a user attribute + * (c) 2012 by Cody Maloney <cmaloney@theoreticalchaos.com> + * + * this program is free software. you can redistribute it and + * modify it under the terms of the gnu general public license. + * there is no warranty. + * + */ + +#include "libuser.h" + +#include <grp.h> +#include <libuser/user.h> +#include <unistd.h> + +#include "auth.h" +#include "c.h" +#include "nls.h" + +static int auth_lu(const char *service_name, struct lu_context *ctx, uid_t uid, + const char *username); + +static int auth_lu(const char *service_name, struct lu_context *ctx, uid_t uid, + const char *username) { + if (!lu_uses_elevated_privileges(ctx)) { + /* Drop privileges */ + if (setegid(getgid()) == -1) + err(EXIT_FAILURE, _("Couldn't drop group privileges")); + if (seteuid(getuid()) == -1) + err(EXIT_FAILURE, _("Couldn't drop group privileges")); + return TRUE; + } + + return auth_pam(service_name, uid, username); +} + +int set_value_libuser(const char *service_name, const char *username, uid_t uid, + const char *attr, const char *val) { + struct lu_context *ctx; + struct lu_error *error = NULL; + struct lu_ent *ent; + + ctx = lu_start(username, lu_user, NULL, NULL, lu_prompt_console_quiet, + NULL, &error); + if (ctx == NULL) + errx(EXIT_FAILURE, _("libuser initialization failed: %s."), + lu_strerror(error)); + + if (!auth_lu(service_name, ctx, uid, username)) { + errno = EACCES; + err(EXIT_FAILURE, _("changing user attribute failed")); + } + + /* Look up the user's record. */ + ent = lu_ent_new(); + if (lu_user_lookup_name(ctx, username, ent, &error) == FALSE) { + lu_end(ctx); + errx(EXIT_FAILURE, _("user \"%s\" does not exist."), username); + } + + lu_ent_set_string(ent, attr, val); + if (!lu_user_modify(ctx, ent, &error)) { + lu_ent_free(ent); + lu_end(ctx); + errx(EXIT_FAILURE, _("user attribute not changed: %s"), lu_strerror(error)); + } + lu_ent_free(ent); + lu_end(ctx); + + return 0; +} diff --git a/login-utils/libuser.h b/login-utils/libuser.h new file mode 100644 index 0000000..7454b99 --- /dev/null +++ b/login-utils/libuser.h @@ -0,0 +1,14 @@ +/* + * libuser.h -- Utilize libuser to set a user attribute + * (c) 2012 by Cody Maloney <cmaloney@theoreticalchaos.com> + * + * this program is free software. you can redistribute it and + * modify it under the terms of the gnu general public license. + * there is no warranty. + * + */ + +#include <sys/types.h> + +extern int set_value_libuser(const char *service_name, const char *username, + uid_t uid, const char *attr, const char *val); diff --git a/login-utils/login.1 b/login-utils/login.1 new file mode 100644 index 0000000..cb8adde --- /dev/null +++ b/login-utils/login.1 @@ -0,0 +1,350 @@ +.\" Copyright 1993 Rickard E. Faith (faith@cs.unc.edu) +.\" May be distributed under the GNU General Public License +.TH LOGIN "1" "June 2012" "util-linux" "User Commands" +.SH NAME +login \- begin session on the system +.SH SYNOPSIS +.B login +[ +.B \-p +] [ +.B \-h +.I host +] [ +.B \-H +] [ +.B \-f +.I username +| +.I username +] +.SH DESCRIPTION +.B login +is used when signing onto a system. If no argument is given, +.B login +prompts for the username. +.PP +The user is then prompted for a password, where appropriate. Echoing +is disabled to prevent revealing the password. Only a small number +of password failures are permitted before +.B login +exits and the communications link is severed. +.PP +If password aging has been enabled for the account, the user may be +prompted for a new password before proceeding. He will be forced to +provide his old password and the new password before continuing. +Please refer to +.BR passwd (1) +for more information. +.PP +The user and group ID will be set according to their values in the +.I /etc/passwd +file. There is one exception if the user ID is zero: in this case, +only the primary group ID of the account is set. This should allow +the system administrator to login even in case of network problems. +The value for +.BR $HOME , +.BR $USER , +.BR $SHELL , +.BR $PATH , +.BR $LOGNAME , +and +.B $MAIL +are set according to the appropriate fields in the password entry. +.B $PATH +defaults to +.I /usr\:/local\:/bin:\:/bin:\:/usr\:/bin +for normal users, and to +.I /usr\:/local\:/sbin:\:/usr\:/local\:/bin:\:/sbin:\:/bin:\:/usr\:/sbin:\:/usr\:/bin +for root, if not otherwise configured. +.P +The environment variable +.B $TERM +will be preserved, if it exists (other environment variables are +preserved if the +.B \-p +option is given), else it will be initialized to the terminal type on your tty. +.PP +Then the user's shell is started. If no shell is specified for the +user in +.IR /etc\:/passwd , +then +.I /bin\:/sh +is used. If there is no directory specified in +.IR /etc\:/passwd , +then +.I / +is used (the home directory is checked for the +.I .hushlogin +file described below). +.PP +If the file +.I .hushlogin +exists, then a "quiet" login is performed (this disables the checking +of mail and the printing of the last login time and message of the +day). Otherwise, if +.I /var\:/log\:/lastlog +exists, the last login time is printed (and the current login is +recorded). +.SH OPTIONS +.TP +.B \-p +Used by +.BR getty (8) +to tell +.B login +not to destroy the environment. +.TP +.B \-f +Used to skip a second login authentication. This specifically does +.B not +work for root, and does not appear to work well under Linux. +.TP +.B \-h +Used by other servers (i.e., +.BR telnetd (8)) +to pass the name of the remote host to +.B login +so that it may be placed in utmp and wtmp. Only the superuser may +use this option. +.IP +Note that the +.B \-h +option has impact on the +.B PAM service +.BR name . +The standard service name is +.IR login , +with the +.B \-h +option the name is +.IR remote . +It is necessary to create proper PAM config files (e.g. +.I /etc\:/pam.d\:/login +and +.IR /etc\:/pam.d\:/remote ). +.TP +.B \-H +Used by other servers (i.e., +.BR telnetd (8)) +to tell +.B login +that printing the hostname should be suppressed in the login: prompt. +See also LOGIN_PLAIN_PROMPT below if your server does not allow to configure +.B login +command line. +.TP +\fB\-\-help\fR +Display help text and exit. +.TP +\fB\-V\fR, \fB\-\-version\fR +Display version information and exit. +.SH CONFIG FILE ITEMS +.B login +reads the +.IR /etc\:/login.defs (5) +configuration file. Note that the configuration file could be +distributed with another package (e.g. shadow-utils). The following +configuration items are relevant for +.BR login (1): +.PP +.B MOTD_FILE +(string) +.RS 4 +If defined, a ":" delimited list of "message of the day" files to be +displayed upon login. The default value is +.IR /etc\:/motd . +If the +.B MOTD_FILE +item is empty or a quiet login is enabled, then the message of the day +is not displayed. Note that the same functionality is also provided +by +.BR pam_motd (8) +PAM module. +.RE +.PP +.B LOGIN_PLAIN_PROMPT +(boolean) +.RS 4 +Tell login that printing the hostname should be suppressed in the login: +prompt. This is alternative to the \fB\-H\fR command line option. The default +value is +.IR no . +.RE +.PP +.B LOGIN_TIMEOUT +(number) +.RS 4 +Max time in seconds for login. The default value is +.IR 60 . +.RE +.PP +.B LOGIN_RETRIES +(number) +.RS 4 +Maximum number of login retries in case of a bad password. The default +value is +.IR 3 . +.RE +.PP +.B FAIL_DELAY +(number) +.RS 4 +Delay in seconds before being allowed another three tries after a +login failure. The default value is +.IR 5 . +.RE +.PP +.B TTYPERM +(string) +.RS 4 +The terminal permissions. The default value is +.I 0600 +or +.I 0620 +if tty group is used. +.RE +.PP +.B TTYGROUP +(string) +.RS 4 +The login tty will be owned by the +.BR TTYGROUP . +The default value is +.IR tty . +If the +.B TTYGROUP +does not exist, then the ownership of the terminal is set to the +user\'s primary group. +.PP +The +.B TTYGROUP +can be either the name of a group or a numeric group identifier. +.RE +.PP +.B HUSHLOGIN_FILE +(string) +.RS 4 +If defined, this file can inhibit all the usual chatter during the +login sequence. If a full pathname (e.g. +.IR /etc\:/hushlogins ) +is specified, then hushed mode will be enabled if the user\'s name or +shell are found in the file. If this global hush login file is empty +then the hushed mode will be enabled for all users. +.PP +If a full pathname is not specified, then hushed mode will be enabled +if the file exists in the user\'s home directory. +.PP +The default is to check +.I /etc\:/hushlogins +and if it does not exist then +.I ~/.hushlogin +.PP +If the +.B HUSHLOGIN_FILE +item is empty, then all the checks are disabled. +.RE +.PP +.B DEFAULT_HOME +(boolean) +.RS 4 +Indicate if login is allowed if we cannot change directory to the +home directory. If set to +.IR yes , +the user will login in the root (/) directory if it is not possible +to change directory to her home. The default value is +.IR yes . +.RE +.PP +.B LOG_UNKFAIL_ENAB +(boolean) +.RS 4 +Enable display of unknown usernames when login failures are recorded. +The default value is +.IR no . +.PP +Note that logging unknown usernames may be a security issue if a +user enters her password instead of her login name. +.RE +.PP +.B ENV_PATH +(string) +.RS 4 +If set, it will be used to define the PATH environment variable when +a regular user logs in. The default value is +.I /usr\:/local\:/bin:\:/bin:\:/usr\:/bin +.RE +.PP +.B ENV_ROOTPATH +(string) +.br +.B ENV_SUPATH +(string) +.RS 4 +If set, it will be used to define the PATH environment variable when +the superuser logs in. The default value is +.I /usr\:/local\:/sbin:\:/usr\:/local\:/bin:\:/sbin:\:/bin:\:/usr\:/sbin:\:/usr\:/bin +.RE +.SH FILES +.nf +.I /var/run/utmp +.I /var/log/wtmp +.I /var/log/lastlog +.I /var/spool/mail/* +.I /etc/motd +.I /etc/passwd +.I /etc/nologin +.I /etc/pam.d/login +.I /etc/pam.d/remote +.I /etc/hushlogins +.I .hushlogin +.fi +.SH "SEE ALSO" +.BR mail (1), +.BR passwd (1), +.BR passwd (5), +.BR environ (7), +.BR getty (8), +.BR init (8), +.BR shutdown (8) +.SH BUGS +The undocumented BSD +.B \-r +option is not supported. This may be required by some +.BR rlogind (8) +programs. +.PP +A recursive login, as used to be possible in the good old days, no +longer works; for most purposes +.BR su (1) +is a satisfactory substitute. Indeed, for security reasons, login +does a vhangup() system call to remove any possible listening +processes on the tty. This is to avoid password sniffing. If one +uses the command +.BR login , +then the surrounding shell gets killed by vhangup() because it's no +longer the true owner of the tty. This can be avoided by using +.B exec login +in a top-level shell or xterm. +.SH AUTHOR +Derived from BSD login 5.40 (5/9/89) by +.MT glad@\:daimi.\:dk +Michael Glad +.ME +for HP-UX +.br +Ported to Linux 0.12: +.MT poe@\:daimi.\:aau.\:dk +Peter Orbaek +.ME +.br +Rewritten to a PAM-only version by +.MT kzak@\:redhat.\:com +Karel Zak +.ME +.SH AVAILABILITY +The login command is part of the util-linux package and is +available from +.UR https://\:www.kernel.org\:/pub\:/linux\:/utils\:/util-linux/ +Linux Kernel Archive +.UE . diff --git a/login-utils/login.c b/login-utils/login.c new file mode 100644 index 0000000..23b62b8 --- /dev/null +++ b/login-utils/login.c @@ -0,0 +1,1386 @@ +/* + * login(1) + * + * This program is derived from 4.3 BSD software and is subject to the + * copyright notice below. + * + * Copyright (C) 2011 Karel Zak <kzak@redhat.com> + * 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 MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. + */ +#include <sys/param.h> +#include <stdio.h> +#include <ctype.h> +#include <unistd.h> +#include <getopt.h> +#include <memory.h> +#include <time.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <sys/resource.h> +#include <sys/file.h> +#include <termios.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/wait.h> +#include <signal.h> +#include <errno.h> +#include <grp.h> +#include <pwd.h> +#include <utmpx.h> +#ifdef HAVE_LASTLOG_H +# include <lastlog.h> +#endif +#include <stdlib.h> +#include <sys/syslog.h> +#ifdef HAVE_LINUX_MAJOR_H +# include <linux/major.h> +#endif +#include <netdb.h> +#include <security/pam_appl.h> +#ifdef HAVE_SECURITY_PAM_MISC_H +# include <security/pam_misc.h> +#elif defined(HAVE_SECURITY_OPENPAM_H) +# include <security/openpam.h> +#endif +#include <sys/sendfile.h> + +#ifdef HAVE_LIBAUDIT +# include <libaudit.h> +#endif + +#include "c.h" +#include "setproctitle.h" +#include "pathnames.h" +#include "strutils.h" +#include "nls.h" +#include "env.h" +#include "xalloc.h" +#include "all-io.h" +#include "fileutils.h" +#include "ttyutils.h" +#include "pwdutils.h" + +#include "logindefs.h" + +#define is_pam_failure(_rc) ((_rc) != PAM_SUCCESS) + +#define LOGIN_MAX_TRIES 3 +#define LOGIN_EXIT_TIMEOUT 5 +#define LOGIN_TIMEOUT 60 + +#ifdef USE_TTY_GROUP +# define TTY_MODE 0620 +#else +# define TTY_MODE 0600 +#endif + +#define TTYGRPNAME "tty" /* name of group to own ttys */ +#define VCS_PATH_MAX 64 + +/* + * 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; + int quiet; /* 1 if hush file exists */ + + unsigned int remote:1, /* login -h */ + nohost:1, /* login -H */ + noauth:1, /* login -f */ + keep_env:1; /* login -p */ +}; + +/* + * This bounds the time given to login. Not a define, so it can + * be patched on machines where it's too small. + */ +static unsigned int timeout = LOGIN_TIMEOUT; +static int child_pid = 0; +static volatile int got_sig = 0; +static char timeout_msg[128]; + +#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); + ignore_result( write(STDERR_FILENO, timeout_msg, strlen(timeout_msg)) ); + signal(SIGALRM, SIG_IGN); + alarm(0); + timedout2(0); +} + +/* + * This handler allows 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) + kill(-child_pid, signal); + else + got_sig = 1; + if (signal == SIGTERM) + kill(-child_pid, SIGHUP); /* because the shell often ignores SIGTERM */ +} + +/* + * 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 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; +} + +/* + * Output the /etc/motd file. + * + * It determines the name of a login announcement file and outputs it to the + * user's terminal at login time. The MOTD_FILE configuration option is a + * colon-delimited list of filenames. An empty MOTD_FILE option disables + * message-of-the-day printing completely. + */ +static void motd(void) +{ + char *motdlist, *motdfile; + const char *mb; + + mb = getlogindefs_str("MOTD_FILE", _PATH_MOTDFILE); + if (!mb || !*mb) + return; + + motdlist = xstrdup(mb); + + for (motdfile = strtok(motdlist, ":"); motdfile; + motdfile = strtok(NULL, ":")) { + + struct stat st; + int fd; + + fd = open(motdfile, O_RDONLY, 0); + if (fd < 0) + continue; + if (!fstat(fd, &st) && st.st_size) + sendfile(fileno(stdout), fd, NULL, st.st_size); + close(fd); + } + + free(motdlist); +} + +/* + * 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); +} + +#define chown_err(_what, _uid, _gid) \ + syslog(LOG_ERR, _("chown (%s, %lu, %lu) failed: %m"), \ + (_what), (unsigned long) (_uid), (unsigned long) (_gid)) + +#define chmod_err(_what, _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; + + 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)) || + 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 + + 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); +} + + +/* + * 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; + time_t t; + int fd; + + if (!cxt->pwd) + 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; + + if (lseek(fd, (off_t) cxt->pwd->pw_uid * sizeof(ll), SEEK_SET) == -1) + goto done; + + /* + * Print last log message. + */ + if (!cxt->quiet) { + if (read(fd, (char *)&ll, sizeof(ll)) == sizeof(ll) && + ll.ll_time != 0) { + time_t ll_time = (time_t) ll.ll_time; + + printf(_("Last login: %.*s "), 24 - 5, ctime(&ll_time)); + if (*ll.ll_host != '\0') + printf(_("from %.*s\n"), + (int)sizeof(ll.ll_host), ll.ll_host); + else + printf(_("on %.*s\n"), + (int)sizeof(ll.ll_line), ll.ll_line); + } + if (lseek(fd, (off_t) cxt->pwd->pw_uid * sizeof(ll), SEEK_SET) == -1) + goto done; + } + + 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 (write_all(fd, (char *)&ll, 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; + struct utmpx *utp; + struct timeval tv; + + 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 <michael@stud.uni-hannover.de> + */ + 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 "<host> 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 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); + + rc = pam_set_item(pamh, PAM_TTY, cxt->tty_name); + 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; + unsigned int retries, failcount = 0; + const char *hostname = cxt->hostname ? cxt->hostname : + cxt->tty_name ? cxt->tty_name : "<unknown>"; + 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); + + /* + * 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 her password instead of her 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); + + fprintf(stderr, _("Login incorrect\n\n")); + + pam_set_item(pamh, PAM_USER, NULL); + 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); + + /* + * Grab the user information out of the password file for future use. + * 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 in %s:%d. Abort."), + __FUNCTION__, __LINE__); + 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, 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(0); + close(1); + close(2); + 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 = xmalloc(sizeof(char *)); + memset(environ, 0, 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 <host>] [-H] [[-f] <username>]\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 second login authentication")); + puts(_(" -h <host> 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); +} + +int main(int argc, char **argv) +{ + int c; + int cnt; + char *childArgv[10]; + char *buff; + int childArgc = 0; + int retcode; + struct sigaction act; + 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 + + }; + + /* the only two longopts to satisfy UL standards */ + enum { HELP_OPTION = CHAR_MAX + 1 }; + static const struct option longopts[] = { + {"help", no_argument, NULL, HELP_OPTION}, + {"version", no_argument, NULL, 'V'}, + {NULL, 0, NULL, 0} + }; + + timeout = (unsigned int)getlogindefs_num("LOGIN_TIMEOUT", LOGIN_TIMEOUT); + + setlocale(LC_ALL, ""); + bindtextdomain(PACKAGE, LOCALEDIR); + textdomain(PACKAGE); + + /* TRANSLATORS: The standard value for %u is 60. */ + snprintf(timeout_msg, sizeof(timeout_msg), + _("%s: timed out after %u seconds"), + program_invocation_short_name, timeout); + + signal(SIGALRM, timedout); + (void) 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); + initproctitle(argc, argv); + + /* + * -p is used by getty to tell login not to destroy the environment + * -f is used to skip a second login authentication + * -h is used by other servers to pass the name of the remote + * host to login so that it may be placed in utmp and wtmp + */ + 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': + printf(UTIL_LINUX_VERSION); + return 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...) */ + while (*p) + *p++ = ' '; + } + + for (cnt = get_fd_tabsize() - 1; cnt > 2; cnt--) + close(cnt); + + 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\" in %s:%d. Abort."), + cxt.username, __FUNCTION__, __LINE__); + 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. + */ + 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); + } + + /* + * Open PAM session (after successful authentication and account check). + */ + loginpam_session(&cxt); + + /* committed to login -- turn off timeout */ + alarm((unsigned int)0); + + endpwent(); + + cxt.quiet = get_hushlogin_status(pwd, 1); + + 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 ... */ + + setproctitle("login", cxt.username); + + log_syslog(&cxt); + + if (!cxt.quiet) { + 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 + } + + /* + * 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, ' ')) { + buff = xmalloc(strlen(pwd->pw_shell) + 6); + + strcpy(buff, "exec "); + strcat(buff, pwd->pw_shell); + childArgv[childArgc++] = "/bin/sh"; + childArgv[childArgc++] = "-sh"; + childArgv[childArgc++] = "-c"; + childArgv[childArgc++] = 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); + + childArgv[childArgc++] = pwd->pw_shell; + childArgv[childArgc++] = xstrdup(tbuf); + } + + childArgv[childArgc++] = NULL; + + execvp(childArgv[0], childArgv + 1); + + if (!strcmp(childArgv[0], "/bin/sh")) + warn(_("couldn't exec shell script")); + else + warn(_("no shell")); + + exit(EXIT_SUCCESS); +} diff --git a/login-utils/logindefs.c b/login-utils/logindefs.c new file mode 100644 index 0000000..ebf1a9f --- /dev/null +++ b/login-utils/logindefs.c @@ -0,0 +1,437 @@ +/* + * Copyright (C) 2003, 2004, 2005 Thorsten Kukuk + * Author: Thorsten Kukuk <kukuk@suse.de> + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain any existing copyright + * notice, and this entire permission notice in its entirety, + * including the disclaimer of warranties. + * + * 2. Redistributions in binary form must reproduce all prior and current + * copyright notices, this list of conditions, and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * 3. The name of any author may not be used to endorse or promote + * products derived from this software without their specific prior + * written permission. + */ +#include <assert.h> +#include <ctype.h> +#include <errno.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/syslog.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <pwd.h> + +#include "c.h" +#include "closestream.h" +#include "logindefs.h" +#include "nls.h" +#include "pathnames.h" +#include "xalloc.h" + +struct item { + char *name; /* name of the option. */ + char *value; /* value of the option. */ + char *path; /* name of config file for this option. */ + + struct item *next; /* pointer to next option. */ +}; + +static struct item *list = NULL; + +static void (*logindefs_loader)(void *) = NULL; +static void *logindefs_loader_data = NULL; + +void free_getlogindefs_data(void) +{ + struct item *ptr; + + ptr = list; + while (ptr) { + struct item *tmp = ptr->next; + + free(ptr->path); + free(ptr->name); + free(ptr->value); + free(ptr); + ptr = tmp; + } + + list = NULL; +} + +static void store(const char *name, const char *value, const char *path) +{ + struct item *new = xmalloc(sizeof(struct item)); + + if (!name) + abort(); + + new->name = xstrdup(name); + new->value = value && *value ? xstrdup(value) : NULL; + new->path = xstrdup(path); + new->next = list; + list = new; +} + +void logindefs_load_file(const char *filename) +{ + FILE *f; + char buf[BUFSIZ]; + + f = fopen(filename, "r"); + if (!f) + return; + + while (fgets(buf, sizeof(buf), f)) { + + char *p, *name, *data = NULL; + + if (*buf == '#' || *buf == '\n') + continue; /* only comment or empty line */ + + p = strchr(buf, '#'); + if (p) + *p = '\0'; + else { + size_t n = strlen(buf); + if (n && *(buf + n - 1) == '\n') + *(buf + n - 1) = '\0'; + } + + if (!*buf) + continue; /* empty line */ + + /* ignore space at begin of the line */ + name = buf; + while (*name && isspace((unsigned)*name)) + name++; + + /* go to the end of the name */ + data = name; + while (*data && !(isspace((unsigned)*data) || *data == '=')) + data++; + if (data > name && *data) + *data++ = '\0'; + + if (!*name || data == name) + continue; + + /* go to the begin of the value */ + while (*data + && (isspace((unsigned)*data) || *data == '=' + || *data == '"')) + data++; + + /* remove space at the end of the value */ + p = data + strlen(data); + if (p > data) + p--; + while (p > data && (isspace((unsigned)*p) || *p == '"')) + *p-- = '\0'; + + store(name, data, filename); + } + + fclose(f); +} + +void logindefs_set_loader(void (*loader)(void *data), void *data) +{ + logindefs_loader = loader; + logindefs_loader_data = data; +} + +static void load_defaults(void) +{ + if (logindefs_loader) + logindefs_loader(logindefs_loader_data); + else + logindefs_load_file(_PATH_LOGINDEFS); +} + +static struct item *search(const char *name) +{ + struct item *ptr; + + if (!list) + load_defaults(); + + ptr = list; + while (ptr != NULL) { + if (strcasecmp(name, ptr->name) == 0) + return ptr; + ptr = ptr->next; + } + + return NULL; +} + +static const char *search_config(const char *name) +{ + struct item *ptr; + + ptr = list; + while (ptr != NULL) { + if (strcasecmp(name, ptr->name) == 0) + return ptr->path; + ptr = ptr->next; + } + + return NULL; +} + +int getlogindefs_bool(const char *name, int dflt) +{ + struct item *ptr = search(name); + return ptr && ptr->value ? (strcasecmp(ptr->value, "yes") == 0) : dflt; +} + +unsigned long getlogindefs_num(const char *name, long dflt) +{ + struct item *ptr = search(name); + char *end = NULL; + unsigned long retval; + + if (!ptr || !ptr->value) + return dflt; + + errno = 0; + retval = strtoul(ptr->value, &end, 0); + if (end && *end == '\0' && !errno) + return retval; + + syslog(LOG_NOTICE, _("%s: %s contains invalid numerical value: %s"), + search_config(name), name, ptr->value); + return dflt; +} + +/* + * Returns: + * @dflt if @name not found + * "" (empty string) if found, but value not defined + * "string" if found + */ +const char *getlogindefs_str(const char *name, const char *dflt) +{ + struct item *ptr = search(name); + + if (!ptr) + return dflt; + if (!ptr->value) + return ""; + return ptr->value; +} + +/* + * For compatibility with shadow-utils we have to support additional + * syntax for environment variables in login.defs(5) file. The standard + * syntax is: + * + * ENV_FOO data + * + * but shadow-utils supports also + * + * ENV_FOO FOO=data + * + * the FOO= prefix has to be remove before we call setenv(). + */ +int logindefs_setenv(const char *name, const char *conf, const char *dflt) +{ + const char *val = getlogindefs_str(conf, dflt); + const char *p; + + if (!val) + return -1; + + p = strchr(val, '='); + if (p) { + size_t sz = strlen(name); + + if (strncmp(val, name, sz) == 0 && *(p + 1)) { + val = p + 1; + if (*val == '"') + val++; + if (!*val) + val = dflt; + } + } + + return val ? setenv(name, val, 1) : -1; +} + +/* + * We need to check the effective UID/GID. For example, $HOME could be on a + * root-squashed NFS or on an NFS with UID mapping, and access(2) uses the + * real UID/GID. Then open(2) seems as the surest solution. + * -- kzak@redhat.com (10-Apr-2009) + */ +int effective_access(const char *path, int mode) +{ + int fd = open(path, mode); + if (fd != -1) + close(fd); + return fd == -1 ? -1 : 0; +} + + +/* + * Check the per-account or the global hush-login setting. + * + * Hushed mode is enabled: + * + * a) if a global (e.g. /etc/hushlogins) hush file exists: + * 1) for ALL ACCOUNTS if the file is empty + * 2) for the current user if the username or shell is found in the file + * + * b) if a ~/.hushlogin file exists + * + * The ~/.hushlogin file is ignored if the global hush file exists. + * + * The HUSHLOGIN_FILE login.def variable overrides the default hush filename. + * + * Note that shadow-utils login(1) does not support "a1)". The "a1)" is + * necessary if you want to use PAM for "Last login" message. + * + * -- Karel Zak <kzak@redhat.com> (26-Aug-2011) + * + * + * The per-account check requires some explanation: As root we may not be able + * to read the directory of the user if it is on an NFS-mounted filesystem. We + * temporarily set our effective uid to the user-uid, making sure that we keep + * root privileges in the real uid. + * + * A portable solution would require a fork(), but we rely on Linux having the + * BSD setreuid(). + */ + +int get_hushlogin_status(struct passwd *pwd, int force_check) +{ + const char *files[] = { _PATH_HUSHLOGINS, _PATH_HUSHLOGIN, NULL }; + const char *file; + char buf[BUFSIZ]; + int i; + + file = getlogindefs_str("HUSHLOGIN_FILE", NULL); + if (file) { + if (!*file) + return 0; /* empty HUSHLOGIN_FILE defined */ + + files[0] = file; + files[1] = NULL; + } + + for (i = 0; files[i]; i++) { + int ok = 0; + + file = files[i]; + + /* global hush-file */ + if (*file == '/') { + struct stat st; + FILE *f; + + if (stat(file, &st) != 0) + continue; /* file does not exist */ + + if (st.st_size == 0) + return 1; /* for all accounts */ + + f = fopen(file, "r"); + if (!f) + continue; /* ignore errors... */ + + while (ok == 0 && fgets(buf, sizeof(buf), f)) { + if (buf[0] != '\0') + buf[strlen(buf) - 1] = '\0'; + ok = !strcmp(buf, *buf == '/' ? pwd->pw_shell : + pwd->pw_name); + } + fclose(f); + if (ok) + return 1; /* found username/shell */ + + return 0; /* ignore per-account files */ + } + + /* per-account setting */ + if (strlen(pwd->pw_dir) + sizeof(file) + 2 > sizeof(buf)) + continue; + + sprintf(buf, "%s/%s", pwd->pw_dir, file); + + if (force_check) { + uid_t ruid = getuid(); + gid_t egid = getegid(); + + if (setregid(-1, pwd->pw_gid) == 0 && + setreuid(0, pwd->pw_uid) == 0) + ok = effective_access(buf, O_RDONLY) == 0; + + if (setuid(0) != 0 || + setreuid(ruid, 0) != 0 || + setregid(-1, egid) != 0) { + syslog(LOG_ALERT, _("hush login status: restore original IDs failed")); + exit(EXIT_FAILURE); + } + if (ok) + return 1; /* enabled by user */ + } + else { + int rc; + rc = effective_access(buf, O_RDONLY); + if (rc == 0) + return 1; + else if (rc == -1 && errno == EACCES) + return -1; + } + + } + + return 0; +} +#ifdef TEST_PROGRAM +int main(int argc, char *argv[]) +{ + char *name, *type; + atexit(close_stdout); + + if (argc <= 1) + errx(EXIT_FAILURE, "usage: %s <filename> " + "[<str|num|bool> <valname>]", argv[0]); + + logindefs_load_file(argv[1]); + + if (argc != 4) { /* list all */ + struct item *ptr; + + for (ptr = list; ptr; ptr = ptr->next) + printf("%s: $%s: '%s'\n", ptr->path, ptr->name, + ptr->value); + + return EXIT_SUCCESS; + } + + type = argv[2]; + name = argv[3]; + + if (strcmp(type, "str") == 0) + printf("$%s: '%s'\n", name, getlogindefs_str(name, "DEFAULT")); + else if (strcmp(type, "num") == 0) + printf("$%s: '%ld'\n", name, getlogindefs_num(name, 0)); + else if (strcmp(type, "bool") == 0) + printf("$%s: '%s'\n", name, + getlogindefs_bool(name, 0) ? "Y" : "N"); + + return EXIT_SUCCESS; +} +#endif diff --git a/login-utils/logindefs.h b/login-utils/logindefs.h new file mode 100644 index 0000000..064737c --- /dev/null +++ b/login-utils/logindefs.h @@ -0,0 +1,14 @@ +#ifndef UTIL_LINUX_LOGINDEFS_H +#define UTIL_LINUX_LOGINDEFS_H + +extern void logindefs_load_file(const char *filename); +extern void logindefs_set_loader(void (*loader)(void *data), void *data); +extern int getlogindefs_bool(const char *name, int dflt); +extern unsigned long getlogindefs_num(const char *name, long dflt); +extern const char *getlogindefs_str(const char *name, const char *dflt); +extern void free_getlogindefs_data(void); +extern int logindefs_setenv(const char *name, const char *conf, const char *dflt); +extern int effective_access(const char *path, int mode); +extern int get_hushlogin_status(struct passwd *pwd, int force_check); + +#endif /* UTIL_LINUX_LOGINDEFS_H */ diff --git a/login-utils/lslogins.1 b/login-utils/lslogins.1 new file mode 100644 index 0000000..5aa14c7 --- /dev/null +++ b/login-utils/lslogins.1 @@ -0,0 +1,152 @@ +.\" Copyright 2014 Ondrej Oprala (ondrej.oprala@gmail.com) +.\" May be distributed under the GNU General Public License +.TH LSLOGINS "1" "April 2014" "util-linux" "User Commands" +.SH NAME +lslogins \- display information about known users in the system +.SH SYNOPSIS +.B lslogins +[options] +.RB [ \-s | \-u [ =\fIUID ]] +.RB [ \-g " \fIgroups\fR]" +.RB [ \-l " \fIlogins\fR]" +.RB [\fIusername\fR] +.SH DESCRIPTION +.PP +Examine the wtmp and btmp logs, /etc/shadow (if necessary) and /etc/passwd +and output the desired data. + +The optional argument \fIusername\fR forces +.BR lslogins +to print all available details about the specified user only. In this case the +output format is different than in case of \fB\-l\fR or \fB\-g\fR and unknown +is \fIusername\fR reported as an error. + +.PP +The default action is to list info about all the users in the system. +.SH OPTIONS +Mandatory arguments to long options are mandatory for short options too. +.TP +\fB\-a\fR, \fB\-\-acc\-expiration\fR +Display data about the date of last password change and the account expiration +date (see \fBshadow\fR(5) for more info). (Requires root privileges.) +.TP +\fB\-\-btmp\-file \fIpath\fP +Alternate path for btmp. +.TP +\fB\-c\fR, \fB\-\-colon\-separate\fR +Separate info about each user with a colon instead of a newline. +.TP +\fB\-e\fR, \fB\-\-export\fR +Output data in the format of NAME=VALUE. +.TP +\fB\-f\fR, \fB\-\-failed\fR +Display data about the users' last failed login attempts. +.TP +\fB\-G\fR, \fB\-\-supp\-groups\fR +Show information about supplementary groups. +.TP +\fB\-g\fR, \fB\-\-groups\fR=\fIgroups\fR +Only show data of users belonging to \fIgroups\fR. More than one group +may be specified; the list has to be comma-separated. The unknown group +names are ignored. + +Note that relation between user and group may be invisible for primary group if +the user is not explicitly specify as group member (e.g. in /etc/group). If the +command lslogins scans for groups than it uses groups database only, and user +database with primary GID is not used at all. +.TP +\fB\-h\fR, \fB\-\-help\fR +Display help information and exit. +.TP +\fB\-L\fR, \fB\-\-last\fR +Display data containing information about the users' last login sessions. +.TP +\fB\-l\fR, \fB\-\-logins\fR=\fIlogins\fR +Only show data of users with a login specified in \fIlogins\fR (user names or user +IDS). More than one login may be specified; the list has to be comma-separated. +The unknown login names are ignored. +.TP +\fB\-n\fR, \fB\-\-newline\fR +Display each piece of information on a separate line. +.TP +\fB\-\-noheadings\fR +Do not print a header line. +.TP +\fB\-\-notruncate\fR +Don't truncate output. +.TP +\fB\-o\fR, \fB\-\-output \fIlist\fP +Specify which output columns to print. +The default list of columns may be extended if \fIlist\fP is +specified in the format \fI+list\fP. +.TP +.B \-\-output\-all +Output all available columns. +.B \-\-help +to get a list of all supported columns. +.TP +\fB\-p\fR, \fB\-\-pwd\fR +Display information related to login by password (see also \fB\-afL). +.TP +\fB\-r\fR, \fB\-\-raw\fR +Raw output (no columnation). +.TP +\fB\-s\fR, \fB\-\-system\-accs\fR +Show system accounts. These are by default all accounts with a UID below 1000 +(non-inclusive), with the exception of either nobody or nfsnobody (UID 65534). +This hardcoded default maybe overwritten by parameters SYS_UID_MIN and SYS_UID_MAX in +the file /etc/login.defs. +.TP +\fB\-\-time\-format\fR \fItype\fP +Display dates in short, full or iso format. The default is short, this time +format is designed to be space efficient and human readable. +.TP +\fB\-u\fR, \fB\-\-user\-accs\fR +Show user accounts. These are by default all accounts with UID above 1000 +(inclusive), with the exception of either nobody or nfsnobody (UID 65534). +This hardcoded default maybe overwritten by parameters UID_MIN and UID_MAX in +the file /etc/login.defs. +.TP +\fB\-V\fR, \fB\-\-version\fR +Display version information and exit. +.TP +\fB\-\-wtmp\-file \fIpath\fP +Alternate path for wtmp. +.TP +\fB\-Z\fR, \fB\-\-context\fR +Display the users' security context. +.TP +\fB\-z\fR, \fB\-\-print0\fR +Delimit user entries with a nul character, instead of a newline. + +.SH NOTES +The default UID thresholds are read from /etc/login.defs. + +.SH EXIT STATUS +.TP +0 +if OK, +.TP +1 +if incorrect arguments specified, +.TP +2 +if a serious error occurs (e.g. a corrupt log). +.SH SEE ALSO +\fBgroup\fP(5), \fBpasswd\fP(5), \fBshadow\fP(5), \fButmp\fP(5) +.SH HISTORY +The \fBlslogins\fP utility is inspired by the \fBlogins\fP utility, which first appeared in FreeBSD 4.10. +.SH AUTHORS +.MT ooprala@redhat.com +Ondrej Oprala +.ME +.br +.MT kzak@redhat.com +Karel Zak +.ME + +.SH AVAILABILITY +The lslogins command is part of the util-linux package and is available from +.UR https://\:www.kernel.org\:/pub\:/linux\:/utils\:/util-linux/ +Linux Kernel Archive +.UE . diff --git a/login-utils/lslogins.c b/login-utils/lslogins.c new file mode 100644 index 0000000..e2a0d43 --- /dev/null +++ b/login-utils/lslogins.c @@ -0,0 +1,1595 @@ +/* + * lslogins - List information about users on the system + * + * Copyright (C) 2014 Ondrej Oprala <ooprala@redhat.com> + * Copyright (C) 2014 Karel Zak <kzak@redhat.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it would be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <getopt.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/syslog.h> +#include <pwd.h> +#include <grp.h> +#include <shadow.h> +#include <paths.h> +#include <time.h> +#include <utmpx.h> +#include <signal.h> +#include <err.h> +#include <limits.h> +#include <search.h> + +#include <libsmartcols.h> +#ifdef HAVE_LIBSELINUX +# include <selinux/selinux.h> +#endif + +#ifdef HAVE_LIBSYSTEMD +# include <systemd/sd-journal.h> +#endif + +#include "c.h" +#include "nls.h" +#include "closestream.h" +#include "xalloc.h" +#include "list.h" +#include "strutils.h" +#include "optutils.h" +#include "pathnames.h" +#include "logindefs.h" +#include "procutils.h" +#include "timeutils.h" + +/* + * column description + */ +struct lslogins_coldesc { + const char *name; + const char *help; + const char *pretty_name; + + double whint; /* width hint */ + long flag; +}; + +static int lslogins_flag; + +#define UL_UID_MIN 1000 +#define UL_UID_MAX 60000 +#define UL_SYS_UID_MIN 201 +#define UL_SYS_UID_MAX 999 + +/* we use the value of outmode to determine + * appropriate flags for the libsmartcols table + * (e.g., a value of out_newline would imply a raw + * table with the column separator set to '\n'). + */ +static int outmode; +/* + * output modes + */ +enum { + OUT_COLON = 1, + OUT_EXPORT, + OUT_NEWLINE, + OUT_RAW, + OUT_NUL, + OUT_PRETTY +}; + +struct lslogins_user { + char *login; + uid_t uid; + char *group; + gid_t gid; + char *gecos; + + int pwd_empty; + int nologin; + int pwd_lock; + int pwd_deny; + + gid_t *sgroups; + size_t nsgroups; + + char *pwd_ctime; + char *pwd_warn; + char *pwd_expire; + char *pwd_ctime_min; + char *pwd_ctime_max; + const char *pwd_method; + + char *last_login; + char *last_tty; + char *last_hostname; + + char *failed_login; + char *failed_tty; + +#ifdef HAVE_LIBSELINUX + security_context_t context; +#endif + char *homedir; + char *shell; + char *pwd_status; + int hushed; + char *nprocs; + +}; + +/* + * time modes + * */ +enum { + TIME_INVALID = 0, + TIME_SHORT, + TIME_FULL, + TIME_ISO, + TIME_ISO_SHORT, +}; + +/* + * flags + */ +enum { + F_SYSAC = (1 << 3), + F_USRAC = (1 << 4), +}; + +/* + * IDs + */ +enum { + COL_USER = 0, + COL_UID, + COL_GECOS, + COL_HOME, + COL_SHELL, + COL_NOLOGIN, + COL_PWDLOCK, + COL_PWDEMPTY, + COL_PWDDENY, + COL_PWDMETHOD, + COL_GROUP, + COL_GID, + COL_SGROUPS, + COL_SGIDS, + COL_LAST_LOGIN, + COL_LAST_TTY, + COL_LAST_HOSTNAME, + COL_FAILED_LOGIN, + COL_FAILED_TTY, + COL_HUSH_STATUS, + COL_PWD_WARN, + COL_PWD_CTIME, + COL_PWD_CTIME_MIN, + COL_PWD_CTIME_MAX, + COL_PWD_EXPIR, + COL_SELINUX, + COL_NPROCS, +}; + +#define is_wtmp_col(x) ((x) == COL_LAST_LOGIN || \ + (x) == COL_LAST_TTY || \ + (x) == COL_LAST_HOSTNAME) + +#define is_btmp_col(x) ((x) == COL_FAILED_LOGIN || \ + (x) == COL_FAILED_TTY) + +enum { + STATUS_FALSE = 0, + STATUS_TRUE, + STATUS_UNKNOWN +}; + +static const char *const status[] = { + [STATUS_FALSE] = "0", + [STATUS_TRUE] = "1", + [STATUS_UNKNOWN]= NULL +}; + +static const char *const pretty_status[] = { + [STATUS_FALSE] = N_("no"), + [STATUS_TRUE] = N_("yes"), + [STATUS_UNKNOWN]= NULL +}; + +#define get_status(x) (outmode == OUT_PRETTY ? pretty_status[(x)] : status[(x)]) + +static const struct lslogins_coldesc coldescs[] = +{ + [COL_USER] = { "USER", N_("user name"), N_("Username"), 0.1, SCOLS_FL_NOEXTREMES }, + [COL_UID] = { "UID", N_("user ID"), "UID", 1, SCOLS_FL_RIGHT}, + [COL_PWDEMPTY] = { "PWD-EMPTY", N_("password not required"), N_("Password not required"), 1, SCOLS_FL_RIGHT }, + [COL_PWDDENY] = { "PWD-DENY", N_("login by password disabled"), N_("Login by password disabled"), 1, SCOLS_FL_RIGHT }, + [COL_PWDLOCK] = { "PWD-LOCK", N_("password defined, but locked"), N_("Password is locked"), 1, SCOLS_FL_RIGHT }, + [COL_PWDMETHOD] = { "PWD-METHOD", N_("password encryption method"), N_("Password encryption method"), 0.1 }, + [COL_NOLOGIN] = { "NOLOGIN", N_("log in disabled by nologin(8) or pam_nologin(8)"), N_("No login"), 1, SCOLS_FL_RIGHT }, + [COL_GROUP] = { "GROUP", N_("primary group name"), N_("Primary group"), 0.1 }, + [COL_GID] = { "GID", N_("primary group ID"), "GID", 1, SCOLS_FL_RIGHT }, + [COL_SGROUPS] = { "SUPP-GROUPS", N_("supplementary group names"), N_("Supplementary groups"), 0.1 }, + [COL_SGIDS] = { "SUPP-GIDS", N_("supplementary group IDs"), N_("Supplementary group IDs"), 0.1 }, + [COL_HOME] = { "HOMEDIR", N_("home directory"), N_("Home directory"), 0.1 }, + [COL_SHELL] = { "SHELL", N_("login shell"), N_("Shell"), 0.1 }, + [COL_GECOS] = { "GECOS", N_("full user name"), N_("Gecos field"), 0.1, SCOLS_FL_TRUNC }, + [COL_LAST_LOGIN] = { "LAST-LOGIN", N_("date of last login"), N_("Last login"), 0.1, SCOLS_FL_RIGHT }, + [COL_LAST_TTY] = { "LAST-TTY", N_("last tty used"), N_("Last terminal"), 0.05 }, + [COL_LAST_HOSTNAME] = { "LAST-HOSTNAME",N_("hostname during the last session"), N_("Last hostname"), 0.1}, + [COL_FAILED_LOGIN] = { "FAILED-LOGIN", N_("date of last failed login"), N_("Failed login"), 0.1 }, + [COL_FAILED_TTY] = { "FAILED-TTY", N_("where did the login fail?"), N_("Failed login terminal"), 0.05 }, + [COL_HUSH_STATUS] = { "HUSHED", N_("user's hush settings"), N_("Hushed"), 1, SCOLS_FL_RIGHT }, + [COL_PWD_WARN] = { "PWD-WARN", N_("days user is warned of password expiration"), N_("Password expiration warn interval"), 0.1, SCOLS_FL_RIGHT }, + [COL_PWD_EXPIR] = { "PWD-EXPIR", N_("password expiration date"), N_("Password expiration"), 0.1, SCOLS_FL_RIGHT }, + [COL_PWD_CTIME] = { "PWD-CHANGE", N_("date of last password change"), N_("Password changed"), 0.1, SCOLS_FL_RIGHT}, + [COL_PWD_CTIME_MIN] = { "PWD-MIN", N_("number of days required between changes"), N_("Minimum change time"), 0.1, SCOLS_FL_RIGHT }, + [COL_PWD_CTIME_MAX] = { "PWD-MAX", N_("max number of days a password may remain unchanged"), N_("Maximum change time"), 0.1, SCOLS_FL_RIGHT }, + [COL_SELINUX] = { "CONTEXT", N_("the user's security context"), N_("Selinux context"), 0.1 }, + [COL_NPROCS] = { "PROC", N_("number of processes run by the user"), N_("Running processes"), 1, SCOLS_FL_RIGHT }, +}; + +struct lslogins_control { + struct utmpx *wtmp; + size_t wtmp_size; + + struct utmpx *btmp; + size_t btmp_size; + + void *usertree; + + uid_t uid; + uid_t UID_MIN; + uid_t UID_MAX; + + uid_t SYS_UID_MIN; + uid_t SYS_UID_MAX; + + char **ulist; + size_t ulsiz; + + unsigned int time_mode; + + const char *journal_path; + + unsigned int selinux_enabled : 1, + fail_on_unknown : 1, /* fail if user does not exist */ + ulist_on : 1, + noheadings : 1, + notrunc : 1; +}; + +/* these have to remain global since there's no other reasonable way to pass + * them for each call of fill_table() via twalk() */ +static struct libscols_table *tb; + +/* columns[] array specifies all currently wanted output column. The columns + * are defined by coldescs[] array and you can specify (on command line) each + * column twice. That's enough, dynamically allocated array of the columns is + * unnecessary overkill and over-engineering in this case */ +static int columns[ARRAY_SIZE(coldescs) * 2]; +static size_t ncolumns; + +static inline size_t err_columns_index(size_t arysz, size_t idx) +{ + if (idx >= arysz) + errx(EXIT_FAILURE, _("too many columns specified, " + "the limit is %zu columns"), + arysz - 1); + return idx; +} + +#define add_column(ary, n, id) \ + ((ary)[ err_columns_index(ARRAY_SIZE(ary), (n)) ] = (id)) + +static int column_name_to_id(const char *name, size_t namesz) +{ + size_t i; + + for (i = 0; i < ARRAY_SIZE(coldescs); i++) { + const char *cn = coldescs[i].name; + + if (!strncasecmp(name, cn, namesz) && !*(cn + namesz)) + return i; + } + warnx(_("unknown column: %s"), name); + return -1; +} + +static struct timeval now; + +static char *make_time(int mode, time_t time) +{ + int rc = 0; + char buf[64] = {0}; + + switch(mode) { + case TIME_FULL: + { + char *s; + struct tm tm; + localtime_r(&time, &tm); + + asctime_r(&tm, buf); + if (*(s = buf + strlen(buf) - 1) == '\n') + *s = '\0'; + rc = 0; + break; + } + case TIME_SHORT: + rc = strtime_short(&time, &now, UL_SHORTTIME_THISYEAR_HHMM, + buf, sizeof(buf)); + break; + case TIME_ISO: + rc = strtime_iso(&time, ISO_TIMESTAMP_T, buf, sizeof(buf)); + break; + case TIME_ISO_SHORT: + rc = strtime_iso(&time, ISO_DATE, buf, sizeof(buf)); + break; + default: + errx(EXIT_FAILURE, _("unsupported time type")); + } + + if (rc) + errx(EXIT_FAILURE, _("failed to compose time string")); + + return xstrdup(buf); +} + + +static char *uidtostr(uid_t uid) +{ + char *str_uid = NULL; + xasprintf(&str_uid, "%u", uid); + return str_uid; +} + +static char *gidtostr(gid_t gid) +{ + char *str_gid = NULL; + xasprintf(&str_gid, "%u", gid); + return str_gid; +} + +static char *build_sgroups_string(gid_t *sgroups, size_t nsgroups, int want_names) +{ + size_t n = 0, maxlen, len; + char *res, *p; + + if (!nsgroups) + return NULL; + + len = maxlen = nsgroups * 10; + res = p = xmalloc(maxlen); + + while (n < nsgroups) { + int x; +again: + if (!want_names) + x = snprintf(p, len, "%u,", sgroups[n]); + else { + struct group *grp = getgrgid(sgroups[n]); + if (!grp) { + free(res); + return NULL; + } + x = snprintf(p, len, "%s,", grp->gr_name); + } + + if (x < 0 || (size_t) x >= len) { + size_t cur = p - res; + + maxlen *= 2; + res = xrealloc(res, maxlen); + p = res + cur; + len = maxlen - cur; + goto again; + } + + len -= x; + p += x; + ++n; + } + + if (p > res) + *(p - 1) = '\0'; + + return res; +} + +static struct utmpx *get_last_wtmp(struct lslogins_control *ctl, const char *username) +{ + size_t n = 0; + + if (!username) + return NULL; + + n = ctl->wtmp_size - 1; + do { + if (!strncmp(username, ctl->wtmp[n].ut_user, + sizeof(ctl->wtmp[0].ut_user))) + return ctl->wtmp + n; + } while (n--); + return NULL; + +} + +static int require_wtmp(void) +{ + size_t i; + for (i = 0; i < ncolumns; i++) + if (is_wtmp_col(columns[i])) + return 1; + return 0; +} + +static int require_btmp(void) +{ + size_t i; + for (i = 0; i < ncolumns; i++) + if (is_btmp_col(columns[i])) + return 1; + return 0; +} + +static struct utmpx *get_last_btmp(struct lslogins_control *ctl, const char *username) +{ + size_t n = 0; + + if (!username) + return NULL; + + n = ctl->btmp_size - 1; + do { + if (!strncmp(username, ctl->btmp[n].ut_user, + sizeof(ctl->wtmp[0].ut_user))) + return ctl->btmp + n; + }while (n--); + return NULL; + +} + +static int read_utmp(char const *file, size_t *nents, struct utmpx **res) +{ + size_t n_read = 0, n_alloc = 0; + struct utmpx *utmp = NULL, *u; + + if (utmpxname(file) < 0) + return -errno; + + setutxent(); + errno = 0; + + while ((u = getutxent()) != NULL) { + if (n_read == n_alloc) { + n_alloc += 32; + utmp = xrealloc(utmp, n_alloc * sizeof (struct utmpx)); + } + utmp[n_read++] = *u; + } + if (!u && errno) { + free(utmp); + return -errno; + } + + endutxent(); + + *nents = n_read; + *res = utmp; + + return 0; +} + +static int parse_wtmp(struct lslogins_control *ctl, char *path) +{ + int rc = 0; + + rc = read_utmp(path, &ctl->wtmp_size, &ctl->wtmp); + if (rc < 0 && errno != EACCES) + err(EXIT_FAILURE, "%s", path); + return rc; +} + +static int parse_btmp(struct lslogins_control *ctl, char *path) +{ + int rc = 0; + + rc = read_utmp(path, &ctl->btmp_size, &ctl->btmp); + if (rc < 0 && errno != EACCES) + err(EXIT_FAILURE, "%s", path); + return rc; +} + +static int get_sgroups(gid_t **list, size_t *len, struct passwd *pwd) +{ + size_t n = 0; + int ngroups = 0; + + *len = 0; + *list = NULL; + + /* first let's get a supp. group count */ + getgrouplist(pwd->pw_name, pwd->pw_gid, *list, &ngroups); + if (!ngroups) + return -1; + + *list = xcalloc(1, ngroups * sizeof(gid_t)); + + /* now for the actual list of GIDs */ + if (-1 == getgrouplist(pwd->pw_name, pwd->pw_gid, *list, &ngroups)) + return -1; + + *len = (size_t) ngroups; + + /* getgroups also returns the user's primary GID - dispose of it */ + while (n < *len) { + if ((*list)[n] == pwd->pw_gid) + break; + ++n; + } + + if (*len) + (*list)[n] = (*list)[--(*len)]; + + return 0; +} + +static int get_nprocs(const uid_t uid) +{ + int nprocs = 0; + pid_t pid; + struct proc_processes *proc = proc_open_processes(); + + proc_processes_filter_by_uid(proc, uid); + + while (!proc_next_pid(proc, &pid)) + ++nprocs; + + proc_close_processes(proc); + return nprocs; +} + +static const char *get_pwd_method(const char *str, const char **next, unsigned int *sz) +{ + const char *p = str; + const char *res = NULL; + + if (!p || *p++ != '$') + return NULL; + + if (sz) + *sz = 0; + + switch (*p) { + case '1': + res = "MD5"; + if (sz) + *sz = 22; + break; + case '2': + p++; + if (*p == 'a' || *p == 'y') + res = "Blowfish"; + break; + case '5': + res = "SHA-256"; + if (sz) + *sz = 43; + break; + case '6': + res = "SHA-512"; + if (sz) + *sz = 86; + break; + default: + return NULL; + } + p++; + + if (*p != '$') + return NULL; + if (next) + *next = ++p; + return res; +} + +#define is_valid_pwd_char(x) (isalnum((unsigned char) (x)) || (x) == '.' || (x) == '/') + +static int valid_pwd(const char *str) +{ + const char *p = str; + unsigned int sz = 0, n; + + /* $id$ */ + if (get_pwd_method(str, &p, &sz) == NULL) + return 0; + if (!*p) + return 0; + + /* salt$ */ + for (; p && *p; p++) { + if (*p == '$') { + p++; + break; + } + if (!is_valid_pwd_char(*p)) + return 0; + } + if (!*p) + return 0; + + /* encrypted */ + for (n = 0; p && *p; p++, n++) { + if (!is_valid_pwd_char(*p)) + return 0; + } + + if (sz && n != sz) + return 0; + return 1; +} + +static struct lslogins_user *get_user_info(struct lslogins_control *ctl, const char *username) +{ + struct lslogins_user *user; + struct passwd *pwd; + struct group *grp; + struct spwd *shadow; + struct utmpx *user_wtmp = NULL, *user_btmp = NULL; + size_t n = 0; + time_t time; + uid_t uid; + errno = 0; + + errno = 0; + pwd = username ? getpwnam(username) : getpwent(); + if (!pwd) + return NULL; + + ctl->uid = uid = pwd->pw_uid; + + /* nfsnobody is an exception to the UID_MAX limit. This is "nobody" on + * some systems; the decisive point is the UID - 65534 */ + if ((lslogins_flag & F_USRAC) && + strcmp("nfsnobody", pwd->pw_name) != 0 && + uid != 0) { + if (uid < ctl->UID_MIN || uid > ctl->UID_MAX) { + errno = EAGAIN; + return NULL; + } + + } else if ((lslogins_flag & F_SYSAC) && + (uid < ctl->SYS_UID_MIN || uid > ctl->SYS_UID_MAX)) { + errno = EAGAIN; + return NULL; + } + + errno = 0; + grp = getgrgid(pwd->pw_gid); + if (!grp) + return NULL; + + user = xcalloc(1, sizeof(struct lslogins_user)); + + if (ctl->wtmp) + user_wtmp = get_last_wtmp(ctl, pwd->pw_name); + if (ctl->btmp) + user_btmp = get_last_btmp(ctl, pwd->pw_name); + + lckpwdf(); + shadow = getspnam(pwd->pw_name); + ulckpwdf(); + + /* required by tseach() stuff */ + user->uid = pwd->pw_uid; + + while (n < ncolumns) { + switch (columns[n++]) { + case COL_USER: + user->login = xstrdup(pwd->pw_name); + break; + case COL_UID: + user->uid = pwd->pw_uid; + break; + case COL_GROUP: + user->group = xstrdup(grp->gr_name); + break; + case COL_GID: + user->gid = pwd->pw_gid; + break; + case COL_SGROUPS: + case COL_SGIDS: + if (get_sgroups(&user->sgroups, &user->nsgroups, pwd)) + err(EXIT_FAILURE, _("failed to get supplementary groups")); + break; + case COL_HOME: + user->homedir = xstrdup(pwd->pw_dir); + break; + case COL_SHELL: + user->shell = xstrdup(pwd->pw_shell); + break; + case COL_GECOS: + user->gecos = xstrdup(pwd->pw_gecos); + break; + case COL_LAST_LOGIN: + if (user_wtmp) { + time = user_wtmp->ut_tv.tv_sec; + user->last_login = make_time(ctl->time_mode, time); + } + break; + case COL_LAST_TTY: + if (user_wtmp) + user->last_tty = xstrdup(user_wtmp->ut_line); + break; + case COL_LAST_HOSTNAME: + if (user_wtmp) + user->last_hostname = xstrdup(user_wtmp->ut_host); + break; + case COL_FAILED_LOGIN: + if (user_btmp) { + time = user_btmp->ut_tv.tv_sec; + user->failed_login = make_time(ctl->time_mode, time); + } + break; + case COL_FAILED_TTY: + if (user_btmp) + user->failed_tty = xstrdup(user_btmp->ut_line); + break; + case COL_HUSH_STATUS: + user->hushed = get_hushlogin_status(pwd, 0); + if (user->hushed == -1) + user->hushed = STATUS_UNKNOWN; + break; + case COL_PWDEMPTY: + if (shadow) { + if (!*shadow->sp_pwdp) /* '\0' */ + user->pwd_empty = STATUS_TRUE; + } else + user->pwd_empty = STATUS_UNKNOWN; + break; + case COL_PWDDENY: + if (shadow) { + if ((*shadow->sp_pwdp == '!' || + *shadow->sp_pwdp == '*') && + !valid_pwd(shadow->sp_pwdp + 1)) + user->pwd_deny = STATUS_TRUE; + } else + user->pwd_deny = STATUS_UNKNOWN; + break; + case COL_PWDLOCK: + if (shadow) { + if (*shadow->sp_pwdp == '!' && valid_pwd(shadow->sp_pwdp + 1)) + user->pwd_lock = STATUS_TRUE; + } else + user->pwd_lock = STATUS_UNKNOWN; + break; + case COL_PWDMETHOD: + if (shadow) { + const char *p = shadow->sp_pwdp; + + if (*p == '!' || *p == '*') + p++; + user->pwd_method = get_pwd_method(p, NULL, NULL); + } else + user->pwd_method = NULL; + break; + case COL_NOLOGIN: + if (strstr(pwd->pw_shell, "nologin")) + user->nologin = 1; + else if (pwd->pw_uid) + user->nologin = access(_PATH_NOLOGIN, F_OK) == 0 || + access(_PATH_VAR_NOLOGIN, F_OK) == 0; + break; + case COL_PWD_WARN: + if (shadow && shadow->sp_warn >= 0) + xasprintf(&user->pwd_warn, "%ld", shadow->sp_warn); + break; + case COL_PWD_EXPIR: + if (shadow && shadow->sp_expire >= 0) + user->pwd_expire = make_time(ctl->time_mode == TIME_ISO ? + TIME_ISO_SHORT : ctl->time_mode, + shadow->sp_expire * 86400); + break; + case COL_PWD_CTIME: + /* sp_lstchg is specified in days, showing hours + * (especially in non-GMT timezones) would only serve + * to confuse */ + if (shadow) + user->pwd_ctime = make_time(ctl->time_mode == TIME_ISO ? + TIME_ISO_SHORT : ctl->time_mode, + shadow->sp_lstchg * 86400); + break; + case COL_PWD_CTIME_MIN: + if (shadow && shadow->sp_min > 0) + xasprintf(&user->pwd_ctime_min, "%ld", shadow->sp_min); + break; + case COL_PWD_CTIME_MAX: + if (shadow && shadow->sp_max > 0) + xasprintf(&user->pwd_ctime_max, "%ld", shadow->sp_max); + break; + case COL_SELINUX: +#ifdef HAVE_LIBSELINUX + if (ctl->selinux_enabled) { + /* typedefs and pointers are pure evil */ + security_context_t con = NULL; + if (getcon(&con) == 0) + user->context = con; + } +#endif + break; + case COL_NPROCS: + xasprintf(&user->nprocs, "%d", get_nprocs(pwd->pw_uid)); + break; + default: + /* something went very wrong here */ + err(EXIT_FAILURE, "fatal: unknown error"); + break; + } + } + + return user; +} + +static int str_to_uint(char *s, unsigned int *ul) +{ + char *end; + if (!s || !*s) + return -1; + *ul = strtoul(s, &end, 0); + if (!*end) + return 0; + return 1; +} + +/* get a definitive list of users we want info about... */ +static int get_ulist(struct lslogins_control *ctl, char *logins, char *groups) +{ + char *u, *g; + size_t i = 0, n = 0, *arsiz; + struct group *grp; + struct passwd *pwd; + char ***ar; + uid_t uid; + gid_t gid; + + ar = &ctl->ulist; + arsiz = &ctl->ulsiz; + + /* an arbitrary starting value */ + *arsiz = 32; + *ar = xcalloc(1, sizeof(char *) * (*arsiz)); + + if (logins) { + while ((u = strtok(logins, ","))) { + logins = NULL; + + /* user specified by UID? */ + if (!str_to_uint(u, &uid)) { + pwd = getpwuid(uid); + if (!pwd) + continue; + u = pwd->pw_name; + } + (*ar)[i++] = xstrdup(u); + + if (i == *arsiz) + *ar = xrealloc(*ar, sizeof(char *) * (*arsiz += 32)); + } + ctl->ulist_on = 1; + } + + if (groups) { + /* FIXME: this might lead to duplicate entries, although not visible + * in output, crunching a user's info multiple times is very redundant */ + while ((g = strtok(groups, ","))) { + n = 0; + groups = NULL; + + /* user specified by GID? */ + if (!str_to_uint(g, &gid)) + grp = getgrgid(gid); + else + grp = getgrnam(g); + + if (!grp) + continue; + + while ((u = grp->gr_mem[n++])) { + (*ar)[i++] = xstrdup(u); + + if (i == *arsiz) + *ar = xrealloc(*ar, sizeof(char *) * (*arsiz += 32)); + } + } + ctl->ulist_on = 1; + } + *arsiz = i; + return 0; +} + +static void free_ctl(struct lslogins_control *ctl) +{ + size_t n = 0; + + free(ctl->wtmp); + free(ctl->btmp); + + while (n < ctl->ulsiz) + free(ctl->ulist[n++]); + + free(ctl->ulist); + free(ctl); +} + +static struct lslogins_user *get_next_user(struct lslogins_control *ctl) +{ + struct lslogins_user *u; + errno = 0; + while (!(u = get_user_info(ctl, NULL))) { + /* no "false" errno-s here, iff we're unable to + * get a valid user entry for any reason, quit */ + if (errno == EAGAIN) + continue; + return NULL; + } + return u; +} + +/* some UNIX implementations set errno iff a passwd/grp/... + * entry was not found. The original UNIX logins(1) utility always + * ignores invalid login/group names, so we're going to as well.*/ +#define IS_REAL_ERRNO(e) !((e) == ENOENT || (e) == ESRCH || \ + (e) == EBADF || (e) == EPERM || (e) == EAGAIN) + +static int get_user(struct lslogins_control *ctl, struct lslogins_user **user, + const char *username) +{ + *user = get_user_info(ctl, username); + if (!*user && IS_REAL_ERRNO(errno)) + return -1; + return 0; +} + +static int cmp_uid(const void *a, const void *b) +{ + uid_t x = ((const struct lslogins_user *)a)->uid; + uid_t z = ((const struct lslogins_user *)b)->uid; + return x > z ? 1 : (x < z ? -1 : 0); +} + +static int create_usertree(struct lslogins_control *ctl) +{ + struct lslogins_user *user = NULL; + size_t n = 0; + + if (ctl->ulist_on) { + for (n = 0; n < ctl->ulsiz; n++) { + int rc = get_user(ctl, &user, ctl->ulist[n]); + + if (ctl->fail_on_unknown && !user) { + warnx(_("cannot found '%s'"), ctl->ulist[n]); + return -1; + } + if (rc || !user) + continue; + + tsearch(user, &ctl->usertree, cmp_uid); + } + } else { + while ((user = get_next_user(ctl))) + tsearch(user, &ctl->usertree, cmp_uid); + } + return 0; +} + +static struct libscols_table *setup_table(struct lslogins_control *ctl) +{ + struct libscols_table *table = scols_new_table(); + size_t n = 0; + + if (!table) + err(EXIT_FAILURE, _("failed to allocate output table")); + if (ctl->noheadings) + scols_table_enable_noheadings(table, 1); + + switch(outmode) { + case OUT_COLON: + scols_table_enable_raw(table, 1); + scols_table_set_column_separator(table, ":"); + break; + case OUT_NEWLINE: + scols_table_set_column_separator(table, "\n"); + /* fallthrough */ + case OUT_EXPORT: + scols_table_enable_export(table, 1); + break; + case OUT_NUL: + scols_table_set_line_separator(table, "\0"); + /* fallthrough */ + case OUT_RAW: + scols_table_enable_raw(table, 1); + break; + case OUT_PRETTY: + scols_table_enable_noheadings(table, 1); + default: + break; + } + + while (n < ncolumns) { + int flags = coldescs[columns[n]].flag; + + if (ctl->notrunc) + flags &= ~SCOLS_FL_TRUNC; + + if (!scols_table_new_column(table, + coldescs[columns[n]].name, + coldescs[columns[n]].whint, + flags)) + goto fail; + ++n; + } + + return table; +fail: + scols_unref_table(table); + return NULL; +} + +static void fill_table(const void *u, const VISIT which, const int depth __attribute__((unused))) +{ + struct libscols_line *ln; + const struct lslogins_user *user = *(struct lslogins_user * const *)u; + size_t n = 0; + + if (which == preorder || which == endorder) + return; + + ln = scols_table_new_line(tb, NULL); + if (!ln) + err(EXIT_FAILURE, _("failed to allocate output line")); + + while (n < ncolumns) { + int rc = 0; + + switch (columns[n]) { + case COL_USER: + rc = scols_line_set_data(ln, n, user->login); + break; + case COL_UID: + rc = scols_line_refer_data(ln, n, uidtostr(user->uid)); + break; + case COL_PWDEMPTY: + rc = scols_line_set_data(ln, n, get_status(user->pwd_empty)); + break; + case COL_NOLOGIN: + rc = scols_line_set_data(ln, n, get_status(user->nologin)); + break; + case COL_PWDLOCK: + rc = scols_line_set_data(ln, n, get_status(user->pwd_lock)); + break; + case COL_PWDDENY: + rc = scols_line_set_data(ln, n, get_status(user->pwd_deny)); + break; + case COL_PWDMETHOD: + rc = scols_line_set_data(ln, n, user->pwd_method); + break; + case COL_GROUP: + rc = scols_line_set_data(ln, n, user->group); + break; + case COL_GID: + rc = scols_line_refer_data(ln, n, gidtostr(user->gid)); + break; + case COL_SGROUPS: + rc = scols_line_refer_data(ln, n, + build_sgroups_string(user->sgroups, + user->nsgroups, + TRUE)); + break; + case COL_SGIDS: + rc = scols_line_refer_data(ln, n, + build_sgroups_string(user->sgroups, + user->nsgroups, + FALSE)); + break; + case COL_HOME: + rc = scols_line_set_data(ln, n, user->homedir); + break; + case COL_SHELL: + rc = scols_line_set_data(ln, n, user->shell); + break; + case COL_GECOS: + rc = scols_line_set_data(ln, n, user->gecos); + break; + case COL_LAST_LOGIN: + rc = scols_line_set_data(ln, n, user->last_login); + break; + case COL_LAST_TTY: + rc = scols_line_set_data(ln, n, user->last_tty); + break; + case COL_LAST_HOSTNAME: + rc = scols_line_set_data(ln, n, user->last_hostname); + break; + case COL_FAILED_LOGIN: + rc = scols_line_set_data(ln, n, user->failed_login); + break; + case COL_FAILED_TTY: + rc = scols_line_set_data(ln, n, user->failed_tty); + break; + case COL_HUSH_STATUS: + rc = scols_line_set_data(ln, n, get_status(user->hushed)); + break; + case COL_PWD_WARN: + rc = scols_line_set_data(ln, n, user->pwd_warn); + break; + case COL_PWD_EXPIR: + rc = scols_line_set_data(ln, n, user->pwd_expire); + break; + case COL_PWD_CTIME: + rc = scols_line_set_data(ln, n, user->pwd_ctime); + break; + case COL_PWD_CTIME_MIN: + rc = scols_line_set_data(ln, n, user->pwd_ctime_min); + break; + case COL_PWD_CTIME_MAX: + rc = scols_line_set_data(ln, n, user->pwd_ctime_max); + break; + case COL_SELINUX: +#ifdef HAVE_LIBSELINUX + rc = scols_line_set_data(ln, n, user->context); +#endif + break; + case COL_NPROCS: + rc = scols_line_set_data(ln, n, user->nprocs); + break; + default: + /* something went very wrong here */ + err(EXIT_FAILURE, _("internal error: unknown column")); + } + + if (rc) + err(EXIT_FAILURE, _("failed to add output data")); + ++n; + } + return; +} +#ifdef HAVE_LIBSYSTEMD +static void print_journal_tail(const char *journal_path, uid_t uid, size_t len, int time_mode) +{ + sd_journal *j; + char *match, *timestamp; + uint64_t x; + time_t t; + const char *identifier, *pid, *message; + size_t identifier_len, pid_len, message_len; + + if (journal_path) + sd_journal_open_directory(&j, journal_path, 0); + else + sd_journal_open(&j, SD_JOURNAL_LOCAL_ONLY); + + xasprintf(&match, "_UID=%d", uid); + + sd_journal_add_match(j, match, 0); + sd_journal_seek_tail(j); + sd_journal_previous_skip(j, len); + + do { + if (0 > sd_journal_get_data(j, "SYSLOG_IDENTIFIER", + (const void **) &identifier, &identifier_len)) + goto done; + if (0 > sd_journal_get_data(j, "_PID", + (const void **) &pid, &pid_len)) + goto done; + if (0 > sd_journal_get_data(j, "MESSAGE", + (const void **) &message, &message_len)) + goto done; + + sd_journal_get_realtime_usec(j, &x); + t = x / 1000000; + timestamp = make_time(time_mode, t); + /* Get rid of journal entry field identifiers */ + identifier = strchr(identifier, '=') + 1; + pid = strchr(pid, '=') + 1; + message = strchr(message, '=') + 1; + + fprintf(stdout, "%s %s[%s]: %s\n", timestamp, identifier, pid, + message); + free(timestamp); + } while (sd_journal_next(j)); + +done: + free(match); + sd_journal_flush_matches(j); + sd_journal_close(j); +} +#endif + +static int print_pretty(struct libscols_table *table) +{ + struct libscols_iter *itr = scols_new_iter(SCOLS_ITER_FORWARD); + struct libscols_column *col; + struct libscols_cell *data; + struct libscols_line *ln; + const char *hstr, *dstr; + int n = 0; + + ln = scols_table_get_line(table, 0); + while (!scols_table_next_column(table, itr, &col)) { + + data = scols_line_get_cell(ln, n); + + hstr = _(coldescs[columns[n]].pretty_name); + dstr = scols_cell_get_data(data); + + if (dstr) + printf("%s:%*c%-36s\n", hstr, 35 - (int)strlen(hstr), ' ', dstr); + ++n; + } + + scols_free_iter(itr); + return 0; + +} + +static int print_user_table(struct lslogins_control *ctl) +{ + tb = setup_table(ctl); + if (!tb) + return -1; + + twalk(ctl->usertree, fill_table); + if (outmode == OUT_PRETTY) { + print_pretty(tb); +#ifdef HAVE_LIBSYSTEMD + fprintf(stdout, _("\nLast logs:\n")); + print_journal_tail(ctl->journal_path, ctl->uid, 3, ctl->time_mode); + fputc('\n', stdout); +#endif + } else + scols_print_table(tb); + return 0; +} + +static void free_user(void *f) +{ + struct lslogins_user *u = f; + free(u->login); + free(u->group); + free(u->gecos); + free(u->sgroups); + free(u->pwd_ctime); + free(u->pwd_warn); + free(u->pwd_ctime_min); + free(u->pwd_ctime_max); + free(u->last_login); + free(u->last_tty); + free(u->last_hostname); + free(u->failed_login); + free(u->failed_tty); + free(u->homedir); + free(u->shell); + free(u->pwd_status); +#ifdef HAVE_LIBSELINUX + freecon(u->context); +#endif + free(u); +} + +static int parse_time_mode(const char *s) +{ + struct lslogins_timefmt { + const char *name; + const int val; + }; + static const struct lslogins_timefmt timefmts[] = { + {"iso", TIME_ISO}, + {"full", TIME_FULL}, + {"short", TIME_SHORT}, + }; + size_t i; + + for (i = 0; i < ARRAY_SIZE(timefmts); i++) { + if (strcmp(timefmts[i].name, s) == 0) + return timefmts[i].val; + } + errx(EXIT_FAILURE, _("unknown time format: %s"), s); +} + +static void __attribute__((__noreturn__)) usage(void) +{ + FILE *out = stdout; + size_t i; + + fputs(USAGE_HEADER, out); + fprintf(out, _(" %s [options] [<username>]\n"), program_invocation_short_name); + + fputs(USAGE_SEPARATOR, out); + fputs(_("Display information about known users in the system.\n"), out); + + fputs(USAGE_OPTIONS, out); + fputs(_(" -a, --acc-expiration display info about passwords expiration\n"), out); + fputs(_(" -c, --colon-separate display data in a format similar to /etc/passwd\n"), out); + fputs(_(" -e, --export display in an export-able output format\n"), out); + fputs(_(" -f, --failed display data about the users' last failed logins\n"), out); + fputs(_(" -G, --supp-groups display information about groups\n"), out); + fputs(_(" -g, --groups=<groups> display users belonging to a group in <groups>\n"), out); + fputs(_(" -L, --last show info about the users' last login sessions\n"), out); + fputs(_(" -l, --logins=<logins> display only users from <logins>\n"), out); + fputs(_(" -n, --newline display each piece of information on a new line\n"), out); + fputs(_(" --noheadings don't print headings\n"), out); + fputs(_(" --notruncate don't truncate output\n"), out); + fputs(_(" -o, --output[=<list>] define the columns to output\n"), out); + fputs(_(" --output-all output all columns\n"), out); + fputs(_(" -p, --pwd display information related to login by password.\n"), out); + fputs(_(" -r, --raw display in raw mode\n"), out); + fputs(_(" -s, --system-accs display system accounts\n"), out); + fputs(_(" --time-format=<type> display dates in short, full or iso format\n"), out); + fputs(_(" -u, --user-accs display user accounts\n"), out); + fputs(_(" -Z, --context display SELinux contexts\n"), out); + fputs(_(" -z, --print0 delimit user entries with a nul character\n"), out); + fputs(_(" --wtmp-file <path> set an alternate path for wtmp\n"), out); + fputs(_(" --btmp-file <path> set an alternate path for btmp\n"), out); + fputs(USAGE_SEPARATOR, out); + printf(USAGE_HELP_OPTIONS(26)); + + fputs(USAGE_COLUMNS, out); + for (i = 0; i < ARRAY_SIZE(coldescs); i++) + fprintf(out, " %14s %s\n", coldescs[i].name, _(coldescs[i].help)); + + printf(USAGE_MAN_TAIL("lslogins(1)")); + + exit(EXIT_SUCCESS); +} + +int main(int argc, char *argv[]) +{ + int c; + char *logins = NULL, *groups = NULL, *outarg = NULL; + char *path_wtmp = _PATH_WTMP, *path_btmp = _PATH_BTMP; + struct lslogins_control *ctl = xcalloc(1, sizeof(struct lslogins_control)); + size_t i; + + /* long only options. */ + enum { + OPT_WTMP = CHAR_MAX + 1, + OPT_BTMP, + OPT_NOTRUNC, + OPT_NOHEAD, + OPT_TIME_FMT, + OPT_OUTPUT_ALL, + }; + + static const struct option longopts[] = { + { "acc-expiration", no_argument, 0, 'a' }, + { "colon-separate", no_argument, 0, 'c' }, + { "export", no_argument, 0, 'e' }, + { "failed", no_argument, 0, 'f' }, + { "groups", required_argument, 0, 'g' }, + { "help", no_argument, 0, 'h' }, + { "logins", required_argument, 0, 'l' }, + { "supp-groups", no_argument, 0, 'G' }, + { "newline", no_argument, 0, 'n' }, + { "notruncate", no_argument, 0, OPT_NOTRUNC }, + { "noheadings", no_argument, 0, OPT_NOHEAD }, + { "output", required_argument, 0, 'o' }, + { "output-all", no_argument, 0, OPT_OUTPUT_ALL }, + { "last", no_argument, 0, 'L', }, + { "raw", no_argument, 0, 'r' }, + { "system-accs", no_argument, 0, 's' }, + { "time-format", required_argument, 0, OPT_TIME_FMT }, + { "user-accs", no_argument, 0, 'u' }, + { "version", no_argument, 0, 'V' }, + { "pwd", no_argument, 0, 'p' }, + { "print0", no_argument, 0, 'z' }, + { "wtmp-file", required_argument, 0, OPT_WTMP }, + { "btmp-file", required_argument, 0, OPT_BTMP }, +#ifdef HAVE_LIBSELINUX + { "context", no_argument, 0, 'Z' }, +#endif + { NULL, 0, 0, 0 } + }; + + static const ul_excl_t excl[] = { /* rows and cols in ASCII order */ + { 'G', 'o' }, + { 'L', 'o' }, + { 'Z', 'o' }, + { 'a', 'o' }, + { 'c','n','r','z' }, + { 'o', 'p' }, + { 0 } + }; + int excl_st[ARRAY_SIZE(excl)] = UL_EXCL_STATUS_INIT; + + setlocale(LC_ALL, ""); + bindtextdomain(PACKAGE, LOCALEDIR); + textdomain(PACKAGE); + atexit(close_stdout); + + ctl->time_mode = TIME_SHORT; + + /* very basic default */ + add_column(columns, ncolumns++, COL_UID); + add_column(columns, ncolumns++, COL_USER); + + while ((c = getopt_long(argc, argv, "acefGg:hLl:no:prsuVzZ", + longopts, NULL)) != -1) { + + err_exclusive_options(c, longopts, excl, excl_st); + + switch (c) { + case 'a': + add_column(columns, ncolumns++, COL_PWD_WARN); + add_column(columns, ncolumns++, COL_PWD_CTIME_MIN); + add_column(columns, ncolumns++, COL_PWD_CTIME_MAX); + add_column(columns, ncolumns++, COL_PWD_CTIME); + add_column(columns, ncolumns++, COL_PWD_EXPIR); + break; + case 'c': + outmode = OUT_COLON; + break; + case 'e': + outmode = OUT_EXPORT; + break; + case 'f': + add_column(columns, ncolumns++, COL_FAILED_LOGIN); + add_column(columns, ncolumns++, COL_FAILED_TTY); + break; + case 'G': + add_column(columns, ncolumns++, COL_GID); + add_column(columns, ncolumns++, COL_GROUP); + add_column(columns, ncolumns++, COL_SGIDS); + add_column(columns, ncolumns++, COL_SGROUPS); + break; + case 'g': + groups = optarg; + break; + case 'h': + usage(); + break; + case 'L': + add_column(columns, ncolumns++, COL_LAST_TTY); + add_column(columns, ncolumns++, COL_LAST_HOSTNAME); + add_column(columns, ncolumns++, COL_LAST_LOGIN); + break; + case 'l': + logins = optarg; + break; + case 'n': + outmode = OUT_NEWLINE; + break; + case 'o': + if (*optarg == '=') + optarg++; + outarg = optarg; + break; + case OPT_OUTPUT_ALL: + for (ncolumns = 0; ncolumns < ARRAY_SIZE(coldescs); ncolumns++) + columns[ncolumns] = ncolumns; + break; + case 'r': + outmode = OUT_RAW; + break; + case 's': + ctl->SYS_UID_MIN = getlogindefs_num("SYS_UID_MIN", UL_SYS_UID_MIN); + ctl->SYS_UID_MAX = getlogindefs_num("SYS_UID_MAX", UL_SYS_UID_MAX); + lslogins_flag |= F_SYSAC; + break; + case 'u': + ctl->UID_MIN = getlogindefs_num("UID_MIN", UL_UID_MIN); + ctl->UID_MAX = getlogindefs_num("UID_MAX", UL_UID_MAX); + lslogins_flag |= F_USRAC; + break; + case 'p': + add_column(columns, ncolumns++, COL_PWDEMPTY); + add_column(columns, ncolumns++, COL_PWDLOCK); + add_column(columns, ncolumns++, COL_PWDDENY); + add_column(columns, ncolumns++, COL_NOLOGIN); + add_column(columns, ncolumns++, COL_HUSH_STATUS); + add_column(columns, ncolumns++, COL_PWDMETHOD); + break; + case 'z': + outmode = OUT_NUL; + break; + case OPT_WTMP: + path_wtmp = optarg; + break; + case OPT_BTMP: + path_btmp = optarg; + break; + case OPT_NOTRUNC: + ctl->notrunc = 1; + break; + case OPT_NOHEAD: + ctl->noheadings = 1; + break; + case OPT_TIME_FMT: + ctl->time_mode = parse_time_mode(optarg); + break; + case 'V': + printf(UTIL_LINUX_VERSION); + return EXIT_SUCCESS; + case 'Z': + { +#ifdef HAVE_LIBSELINUX + int sl = is_selinux_enabled(); + if (sl < 0) + warn(_("failed to request selinux state")); + else + ctl->selinux_enabled = sl == 1; +#endif + add_column(columns, ncolumns++, COL_SELINUX); + break; + } + default: + errtryhelp(EXIT_FAILURE); + } + } + + if (argc - optind == 1) { + if (strchr(argv[optind], ',')) + errx(EXIT_FAILURE, _("Only one user may be specified. Use -l for multiple users.")); + logins = argv[optind]; + outmode = OUT_PRETTY; + ctl->fail_on_unknown = 1; + } else if (argc != optind) + errx(EXIT_FAILURE, _("Only one user may be specified. Use -l for multiple users.")); + + scols_init_debug(0); + + /* lslogins -u -s == lslogins */ + if (lslogins_flag & F_USRAC && lslogins_flag & F_SYSAC) + lslogins_flag &= ~(F_USRAC | F_SYSAC); + + if (outmode == OUT_PRETTY) { + /* all columns for lslogins <username> */ + for (ncolumns = 0, i = 0; i < ARRAY_SIZE(coldescs); i++) + columns[ncolumns++] = i; + + } else if (ncolumns == 2) { + /* default colummns */ + add_column(columns, ncolumns++, COL_NPROCS); + add_column(columns, ncolumns++, COL_PWDLOCK); + add_column(columns, ncolumns++, COL_PWDDENY); + add_column(columns, ncolumns++, COL_LAST_LOGIN); + add_column(columns, ncolumns++, COL_GECOS); + } + + if (outarg && string_add_to_idarray(outarg, columns, ARRAY_SIZE(columns), + &ncolumns, column_name_to_id) < 0) + return EXIT_FAILURE; + + if (require_wtmp()) + parse_wtmp(ctl, path_wtmp); + if (require_btmp()) + parse_btmp(ctl, path_btmp); + + if (logins || groups) + get_ulist(ctl, logins, groups); + + if (create_usertree(ctl)) + return EXIT_FAILURE; + + print_user_table(ctl); + + scols_unref_table(tb); + tdestroy(ctl->usertree, free_user); + free_ctl(ctl); + + return EXIT_SUCCESS; +} diff --git a/login-utils/newgrp.1 b/login-utils/newgrp.1 new file mode 100644 index 0000000..f85a8d3 --- /dev/null +++ b/login-utils/newgrp.1 @@ -0,0 +1,34 @@ +.\" Original author unknown. This man page is in the public domain. +.\" Modified Sat Oct 9 17:46:48 1993 by faith@cs.unc.edu +.TH NEWGRP 1 "October 1993" "util-linux" "User Commands" +.SH NAME +newgrp \- log in to a new group +.SH SYNOPSIS +.B newgrp +.RI [ group ] +.SH DESCRIPTION +.B newgrp +changes the group identification of its caller, analogously to +.BR login (1). +The same person remains logged in, and the current directory +is unchanged, but calculations of access permissions to files are performed +with respect to the new group ID. +.LP +If no group is specified, the GID is changed to the login GID. +.LP +.SH FILES +.I /etc/group +.br +.I /etc/passwd + +.SH "SEE ALSO" +.BR login (1), +.BR group (5) + +.SH AUTHOR +Originally by Michael Haardt. Currently maintained by +Peter Orbaek (poe@daimi.aau.dk). + +.SH AVAILABILITY +The newgrp command is part of the util-linux package and is available from +https://www.kernel.org/pub/linux/utils/util-linux/. diff --git a/login-utils/newgrp.c b/login-utils/newgrp.c new file mode 100644 index 0000000..4538831 --- /dev/null +++ b/login-utils/newgrp.c @@ -0,0 +1,240 @@ +/* setgrp.c - by Michael Haardt. Set the gid if possible + * Added a bit more error recovery/reporting - poe + * Vesa Roukonen added code for asking password */ + +/* 1999-02-22 Arkadiusz Miśkiewicz <misiek@pld.ORG.PL> + * - added Native Language Support + */ + +#include <errno.h> +#include <getopt.h> +#include <grp.h> +#include <pwd.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <termios.h> +#include <unistd.h> + +#ifdef HAVE_CRYPT_H +# include <crypt.h> +#endif + +#ifdef HAVE_GETSGNAM +# include <gshadow.h> +#endif + +#include "c.h" +#include "closestream.h" +#include "nls.h" +#include "pathnames.h" +#include "xalloc.h" + +static char *xgetpass(FILE *input, const char *prompt) +{ + char *pass = NULL; + struct termios saved, no_echo; + const int fd = fileno(input); + size_t dummy = 0; + ssize_t len; + + fputs(prompt, stdout); + if (isatty(fd)) { + /* disable echo */ + tcgetattr(fd, &saved); + no_echo = saved; + no_echo.c_lflag &= ~ECHO; + no_echo.c_lflag |= ECHONL; + if (tcsetattr(fd, TCSANOW, &no_echo)) + err(EXIT_FAILURE, _("could not set terminal attributes")); + } + len = getline(&pass, &dummy, input); + if (isatty(fd)) + /* restore terminal */ + if (tcsetattr(fd, TCSANOW, &saved)) + err(EXIT_FAILURE, _("could not set terminal attributes")); + if (len < 0) + err(EXIT_FAILURE, _("getline() failed")); + if (0 < len && *(pass + len - 1) == '\n') + *(pass + len - 1) = '\0'; + return pass; +} + +#ifndef HAVE_EXPLICIT_BZERO +/* Ensure memory is set to value c without compiler optimization getting + * into way that could happen with memset(3). */ +static int xmemset_s(void *v, size_t sz, const int c) +{ + volatile unsigned char *p = v; + + if (v == NULL) + return EINVAL; + while (sz--) + *p++ = c; + return 0; +} +#endif + +/* try to read password from gshadow */ +static char *get_gshadow_pwd(const char *groupname) +{ +#ifdef HAVE_GETSGNAM + struct sgrp *sgrp; + + sgrp = getsgnam(groupname); + return sgrp ? xstrdup(sgrp->sg_passwd) : NULL; +#else + char buf[BUFSIZ]; + char *pwd = NULL; + FILE *f; + + if (groupname == NULL || *groupname == '\0') + return NULL; + + f = fopen(_PATH_GSHADOW, "r"); + if (!f) + return NULL; + + while (fgets(buf, sizeof buf, f)) { + char *cp = strchr(buf, ':'); + if (!cp) + /* any junk in gshadow? */ + continue; + *cp = '\0'; + if (strcmp(buf, groupname) == 0) { + if (cp - buf >= BUFSIZ) + /* only group name on line */ + break; + pwd = cp + 1; + if ((cp = strchr(pwd, ':')) && pwd == cp + 1) + /* empty password */ + pwd = NULL; + else if (cp) + *cp = '\0'; + break; + } + } + fclose(f); + return pwd ? xstrdup(pwd) : NULL; +#endif /* HAVE_GETSGNAM */ +} + +static int allow_setgid(const struct passwd *pe, const struct group *ge) +{ + char **look; + int notfound = 1; + char *pwd, *xpwd; + + if (getuid() == 0) + /* root may do anything */ + return TRUE; + if (ge->gr_gid == pe->pw_gid) + /* You can switch back to your default group */ + return TRUE; + + look = ge->gr_mem; + while (*look && (notfound = strcmp(*look++, pe->pw_name))) ; + + if (!notfound) + /* member of group => OK */ + return TRUE; + + /* Ask for password. Often there is no password in /etc/group, so + * contrary to login et al. we let an empty password mean the same + * as in /etc/passwd */ + + /* check /etc/gshadow */ + if (!(pwd = get_gshadow_pwd(ge->gr_name))) + pwd = ge->gr_passwd; + + if (pwd && *pwd && (xpwd = xgetpass(stdin, _("Password: ")))) { + char *cbuf = crypt(xpwd, pwd); + +#ifdef HAVE_EXPLICIT_BZERO + explicit_bzero(xpwd, strlen(xpwd)); +#else + xmemset_s(xpwd, strlen(xpwd), 0); +#endif + free(xpwd); + if (!cbuf) + warn(_("crypt failed")); + else if (strcmp(pwd, cbuf) == 0) + return TRUE; + } + + /* default to denial */ + return FALSE; +} + +static void __attribute__((__noreturn__)) usage(void) +{ + FILE *out = stdout; + fputs(USAGE_HEADER, out); + fprintf(out, _(" %s <group>\n"), program_invocation_short_name); + + fputs(USAGE_SEPARATOR, out); + fputs(_("Log in to a new group.\n"), out); + + fputs(USAGE_OPTIONS, out); + printf(USAGE_HELP_OPTIONS(16)); + printf(USAGE_MAN_TAIL("newgrp(1)")); + exit(EXIT_SUCCESS); +} + +int main(int argc, char *argv[]) +{ + struct passwd *pw_entry; + struct group *gr_entry; + char *shell; + int ch; + static const struct option longopts[] = { + {"version", no_argument, NULL, 'V'}, + {"help", no_argument, NULL, 'h'}, + {NULL, 0, NULL, 0} + }; + + setlocale(LC_ALL, ""); + bindtextdomain(PACKAGE, LOCALEDIR); + textdomain(PACKAGE); + atexit(close_stdout); + + while ((ch = getopt_long(argc, argv, "Vh", longopts, NULL)) != -1) + switch (ch) { + case 'V': + printf(UTIL_LINUX_VERSION); + return EXIT_SUCCESS; + case 'h': + usage(); + default: + errtryhelp(EXIT_FAILURE); + } + + if (!(pw_entry = getpwuid(getuid()))) + err(EXIT_FAILURE, _("who are you?")); + + if (argc < 2) { + if (setgid(pw_entry->pw_gid) < 0) + err(EXIT_FAILURE, _("setgid failed")); + } else { + errno = 0; + if (!(gr_entry = getgrnam(argv[1]))) { + if (errno) + err(EXIT_FAILURE, _("no such group")); + else + errx(EXIT_FAILURE, _("no such group")); + } + if (!allow_setgid(pw_entry, gr_entry)) + errx(EXIT_FAILURE, _("permission denied")); + if (setgid(gr_entry->gr_gid) < 0) + err(EXIT_FAILURE, _("setgid failed")); + } + + if (setuid(getuid()) < 0) + err(EXIT_FAILURE, _("setuid failed")); + + fflush(NULL); + shell = (pw_entry->pw_shell && *pw_entry->pw_shell ? + pw_entry->pw_shell : _PATH_BSHELL); + execl(shell, shell, (char *)0); + errexec(shell); +} diff --git a/login-utils/nologin.8 b/login-utils/nologin.8 new file mode 100644 index 0000000..ee59484 --- /dev/null +++ b/login-utils/nologin.8 @@ -0,0 +1,52 @@ +.TH NOLOGIN 8 "September 2013" "util-linux" "System Administration" +.SH NAME +nologin \- politely refuse a login +.SH SYNOPSIS +.B nologin +.RB [ \-V ] +.RB [ \-h ] +.SH DESCRIPTION +.B nologin +displays a message that an account is not available and exits non-zero. It is +intended as a replacement shell field to deny login access to an account. +.PP +If the file /etc/nologin.txt exists, nologin displays its contents to the +user instead of the default message. +.PP +The exit code returned by +.B nologin +is always 1. +.PP +.SH OPTIONS +.IP "\fB\-h, \-\-help\fP" +Display help text and exit. +.IP "\fB-V, \-\-version" +Display version information and exit. +.SH NOTES +.B nologin +is a per-account way to disable login (usually used for system accounts like http or ftp). +.BR nologin (8) +uses /etc/nologin.txt as an optional source for a non-default message, the login +access is always refused independently of the file. +.PP +.BR pam_nologin (8) +PAM module usually prevents all non-root users from logging into the system. +.BR pam_nologin (8) +functionality is controlled by /var/run/nologin or the /etc/nologin file. +.SH AUTHORS +.UR kzak@redhat.com +Karel Zak +.UE +.SH SEE ALSO +.BR login (1), +.BR passwd (5), +.BR pam_nologin (8) +.SH HISTORY +The +.B nologin +command appeared in 4.4BSD. +.SH AVAILABILITY +The nologin command is part of the util-linux package and is available from +.UR https://\:www.kernel.org\:/pub\:/linux\:/utils\:/util-linux/ +Linux Kernel Archive +.UE . diff --git a/login-utils/nologin.c b/login-utils/nologin.c new file mode 100644 index 0000000..b0b6a72 --- /dev/null +++ b/login-utils/nologin.c @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2013 Karel Zak <kzak@redhat.com> + */ + +#include <stdio.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <fcntl.h> +#include <string.h> +#include <unistd.h> +#include <stdlib.h> +#include <getopt.h> + +#include "c.h" +#include "nls.h" +#include "pathnames.h" + +/* + * Always return EXIT_FAILURE (1), don't try to be smart! + */ + +static void __attribute__((__noreturn__)) usage(void) +{ + FILE *out = stdout; + fputs(USAGE_HEADER, out); + fprintf(out, + _(" %s [options]\n"), program_invocation_short_name); + + fputs(USAGE_SEPARATOR, out); + fputs(_("Politely refuse a login.\n"), out); + + fputs(USAGE_OPTIONS, out); + printf(USAGE_HELP_OPTIONS(16)); + + printf(USAGE_MAN_TAIL("nologin(8)")); + exit(EXIT_FAILURE); +} + +int main(int argc, char *argv[]) +{ + int c, fd = -1; + struct stat st; + static const struct option longopts[] = { + { "help", 0, NULL, 'h' }, + { "version", 0, NULL, 'V' }, + { NULL, 0, NULL, 0 } + }; + + setlocale(LC_ALL, ""); + bindtextdomain(PACKAGE, LOCALEDIR); + textdomain(PACKAGE); + + while ((c = getopt_long(argc, argv, "hV", longopts, NULL)) != -1) { + switch (c) { + case 'h': + usage(); + break; + case 'V': + printf(UTIL_LINUX_VERSION); + return EXIT_FAILURE; + default: + errtryhelp(EXIT_FAILURE); + } + } + + fd = open(_PATH_NOLOGIN_TXT, O_RDONLY); + if (fd < 0) + goto dflt; + + c = fstat(fd, &st); + if (c < 0 || !S_ISREG(st.st_mode)) + goto dflt; + else { + char buf[BUFSIZ]; + ssize_t rd; + + while ((rd = read(fd, buf, sizeof(buf))) > 0) + ignore_result( write(STDOUT_FILENO, buf, rd) ); + + close(fd); + return EXIT_FAILURE; + } + +dflt: + if (fd >= 0) + close(fd); + fprintf(stdout, _("This account is currently not available.\n")); + return EXIT_FAILURE; +} diff --git a/login-utils/runuser.1 b/login-utils/runuser.1 new file mode 100644 index 0000000..bf0d024 --- /dev/null +++ b/login-utils/runuser.1 @@ -0,0 +1,254 @@ +.TH RUNUSER 1 "July 2014" "util-linux" "User Commands" +.SH NAME +runuser \- run a command with substitute user and group ID +.SH SYNOPSIS +.BR runuser " [options] " \-u +.I user +.RI "[[\-\-] " command " ["argument "...]]" +.LP +.BR runuser " [options] [" \- ] +.RI [ user " [" argument "...]]" +.SH DESCRIPTION +.B runuser +allows to run commands with a substitute user and group ID. +If the option \fB\-u\fR is not given, it falls back to +.BR su -compatible +semantics and a shell is executed. +The difference between the commands +.B runuser +and +.B su +is that +.B runuser +does not ask for a password (because it may be executed by the root user only) and +it uses a different PAM configuration. +The command +.B runuser +does not have to be installed with set-user-ID permissions. +.PP +If the PAM session is not required then recommended solution is to use +.BR setpriv (1) +command. +.PP +When called without arguments, +.B runuser +defaults to running an interactive shell as +.IR root . +.PP +For backward compatibility, +.B runuser +defaults to not change the current directory and to only set the +environment variables +.B HOME +and +.B SHELL +(plus +.B USER +and +.B LOGNAME +if the target +.I user +is not root). +This version of +.B runuser +uses PAM for session management. +.SH OPTIONS +.TP +.BR \-c , " \-\-command" = \fIcommand +Pass +.I command +to the shell with the +.B \-c +option. +.TP +.BR \-f , " \-\-fast" +Pass +.B \-f +to the shell, which may or may not be useful depending on the +shell. +.TP +.BR \-g , " \-\-group" = \fIgroup +The primary group to be used. This option is allowed for the root user only. +.TP +.BR \-G , " \-\-supp\-group" = \fIgroup +Specify a supplemental group. This option is available to the root user only. The first specified +supplementary group is also used as a primary group if the option \fB\-\-group\fR is unspecified. +.TP +.BR \- , " \-l" , " \-\-login" +Start the shell as a login shell with an environment similar to a real +login: +.RS 10 +.TP +o +clears all the environment variables except for +.B TERM +and variables specified by \fB\-\-whitelist\-environment\fR +.TP +o +initializes the environment variables +.BR HOME , +.BR SHELL , +.BR USER , +.BR LOGNAME , +.B PATH +.TP +o +changes to the target user's home directory +.TP +o +sets argv[0] of the shell to +.RB ' \- ' +in order to make the shell a login shell +.RE +.TP +.BR \-m , " \-p" , " \-\-preserve\-environment" +Preserve the entire environment, i.e. it does not set +.BR HOME , +.BR SHELL , +.B USER +nor +.BR LOGNAME . +The option is ignored if the option \fB\-\-login\fR is specified. +.TP +.BR \-s , " \-\-shell" = \fIshell +Run the specified \fIshell\fR instead of the default. The shell to run is +selected according to the following rules, in order: +.RS 10 +.TP +o +the shell specified with +.B \-\-shell +.TP +o +the shell specified in the environment variable +.B SHELL +if the +.B \-\-preserve\-environment +option is used +.TP +o +the shell listed in the passwd entry of the target user +.TP +o +/bin/sh +.RE +.IP +If the target user has a restricted shell (i.e. not listed in +/etc/shells) the +.B \-\-shell +option and the +.B SHELL +environment variables are ignored unless the calling user is root. +.TP +.BI \-\-session\-command= command +Same as +.B \-c , +but do not create a new session. (Discouraged.) +.TP +.BR \-w , " \-\-whitelist\-environment" = \fIlist +Don't reset environment variables specified in comma separated \fIlist\fR when clears +environment for \fB\-\-login\fR. The whitelist is ignored for the environment variables +.BR HOME , +.BR SHELL , +.BR USER , +.BR LOGNAME ", and" +.BR PATH "." +.TP +.BR \-V , " \-\-version" +Display version information and exit. +.TP +.BR \-h , " \-\-help" +Display help text and exit. +.SH CONFIG FILES +.B runuser +reads the +.I /etc/default/runuser +and +.I /etc/login.defs +configuration files. The following configuration items are relevant +for +.BR runuser : +.PP +.B ENV_PATH +(string) +.RS 4 +Defines the PATH environment variable for a regular user. The +default value is +.IR /usr/local/bin:\:/bin:\:/usr/bin . +.RE +.PP +.B ENV_ROOTPATH +(string) +.br +.B ENV_SUPATH +(string) +.RS 4 +Defines the PATH environment variable for root. The default value is +.IR /usr/local/sbin:\:/usr/local/bin:\:/sbin:\:/bin:\:/usr/sbin:\:/usr/bin . +.RE +.PP +.B ALWAYS_SET_PATH +(boolean) +.RS 4 +If set to +.I yes +and \-\-login and \-\-preserve\-environment were not specified +.B runuser +initializes +.BR PATH . +.RE +.sp +The environment variable PATH may be different on systems where /bin and /sbin +are merged into /usr. +.SH EXIT STATUS +.B runuser +normally returns the exit status of the command it executed. If the +command was killed by a signal, +.B runuser +returns the number of the signal plus 128. +.PP +Exit status generated by +.B runuser +itself: +.RS 10 +.TP +1 +Generic error before executing the requested command +.TP +126 +The requested command could not be executed +.TP +127 +The requested command was not found +.RE +.SH FILES +.PD 0 +.TP 17 +/etc/pam.d/runuser +default PAM configuration file +.TP +/etc/pam.d/runuser-l +PAM configuration file if \-\-login is specified +.TP +/etc/default/runuser +runuser specific logindef config file +.TP +/etc/login.defs +global logindef config file +.PD 1 +.SH "SEE ALSO" +.BR setpriv (1), +.BR su (1), +.BR login.defs (5), +.BR shells (5), +.BR pam (8) +.SH HISTORY +This \fB runuser\fR command was +derived from coreutils' \fBsu\fR, which was based on an implementation by +David MacKenzie, and the Fedora \fBrunuser\fR command by Dan Walsh. +.SH AVAILABILITY +The runuser command is part of the util-linux package and is +available from +.UR https://\:www.kernel.org\:/pub\:/linux\:/utils\:/util-linux/ +Linux Kernel Archive +.UE . diff --git a/login-utils/runuser.c b/login-utils/runuser.c new file mode 100644 index 0000000..d4f37f4 --- /dev/null +++ b/login-utils/runuser.c @@ -0,0 +1,7 @@ + +#include "su-common.h" + +int main(int argc, char **argv) +{ + return su_main(argc, argv, RUNUSER_MODE); +} diff --git a/login-utils/selinux_utils.c b/login-utils/selinux_utils.c new file mode 100644 index 0000000..dfd696f --- /dev/null +++ b/login-utils/selinux_utils.c @@ -0,0 +1,29 @@ +#include <selinux/context.h> +#include <selinux/selinux.h> +#include <stdio.h> +#include <string.h> +#include <sys/types.h> + +#include "selinux_utils.h" + +access_vector_t get_access_vector(const char *tclass, const char *op) +{ + security_class_t tc = string_to_security_class(tclass); + + return tc ? string_to_av_perm(tc, op) : 0; +} + +int setupDefaultContext(char *orig_file) +{ + if (is_selinux_enabled() > 0) { + security_context_t scontext; + if (getfilecon(orig_file, &scontext) < 0) + return 1; + if (setfscreatecon(scontext) < 0) { + freecon(scontext); + return 1; + } + freecon(scontext); + } + return 0; +} diff --git a/login-utils/selinux_utils.h b/login-utils/selinux_utils.h new file mode 100644 index 0000000..cf0ed66 --- /dev/null +++ b/login-utils/selinux_utils.h @@ -0,0 +1,7 @@ +#ifndef UTIL_LINUX_SELINUX_UTILS_H +#define UTIL_LINUX_SELINUX_UTILS_H + +extern access_vector_t get_access_vector(const char *tclass, const char *op); +extern int setupDefaultContext(char *orig_file); + +#endif diff --git a/login-utils/setpwnam.c b/login-utils/setpwnam.c new file mode 100644 index 0000000..0616c79 --- /dev/null +++ b/login-utils/setpwnam.c @@ -0,0 +1,218 @@ +/* + * setpwnam.c -- edit an entry in a password database. + * + * (c) 1994 Salvatore Valente <svalente@mit.edu> + * This file is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * Edited 11/10/96 (DD/MM/YY ;-) by Nicolai Langfeldt (janl@math.uio.no) + * to read /etc/passwd directly so that passwd, chsh and chfn can work on + * machines that run NIS (previously YP). Changes will not be made to + * usernames starting with +. + * + * This file is distributed with no warranty. + * + * Usage: + * 1) get a struct passwd * from getpwnam(). + * You should assume a struct passwd has an infinite number of fields, so + * you should not try to create one from scratch. + * 2) edit the fields you want to edit. + * 3) call setpwnam() with the edited struct passwd. + * + * A _normal user_ program should never directly manipulate etc/passwd but + * /use getpwnam() and (family, as well as) setpwnam(). + * + * But, setpwnam was made to _edit_ the password file. For use by chfn, + * chsh and passwd. _I_ _HAVE_ to read and write /etc/passwd directly. Let + * those who say nay be forever silent and think about how getpwnam (and + * family) works on a machine running YP. + * + * Added checks for failure of malloc() and removed error reporting to + * stderr, this is a library function and should not print on the screen, + * but return appropriate error codes. + * 27-Jan-97 - poe@daimi.aau.dk + * + * Thanks to "two guys named Ian". + * + * $Author: poer $ + * $Revision: 1.13 $ + * $Date: 1997/06/23 08:26:29 $ + */ + +#undef DEBUG + +#include <errno.h> +#include <fcntl.h> +#include <paths.h> +#include <pwd.h> +#include <shadow.h> +#include <signal.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/resource.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include "c.h" +#include "fileutils.h" +#include "closestream.h" +#include "setpwnam.h" + +static void pw_init(void); + +/* + * setpwnam () -- + * takes a struct passwd in which every field is filled in and valid. + * If the given username exists in the passwd file, the entry is + * replaced with the given entry. + */ +int setpwnam(struct passwd *pwd, const char *prefix) +{ + FILE *fp = NULL, *pwf = NULL; + int save_errno; + int found; + int namelen; + int buflen = 256; + int contlen, rc; + char *linebuf = NULL; + char *tmpname = NULL; + + pw_init(); + + if ((fp = xfmkstemp(&tmpname, "/etc", prefix)) == NULL) + return -1; + + /* ptmp should be owned by root.root or root.wheel */ + if (fchown(fileno(fp), (uid_t) 0, (gid_t) 0) < 0) + goto fail; + + /* acquire exclusive lock */ + if (lckpwdf() < 0) + goto fail; + pwf = fopen(PASSWD_FILE, "r"); + if (!pwf) + goto fail; + + namelen = strlen(pwd->pw_name); + + linebuf = malloc(buflen); + if (!linebuf) + goto fail; + + /* parse the passwd file */ + found = false; + + /* Do you wonder why I don't use getpwent? Read comments at top of + * file */ + while (fgets(linebuf, buflen, pwf) != NULL) { + contlen = strlen(linebuf); + while (linebuf[contlen - 1] != '\n' && !feof(pwf)) { + char *tmp; + /* Extend input buffer if it failed getting the whole line, + * so now we double the buffer size */ + buflen *= 2; + tmp = realloc(linebuf, buflen); + if (tmp == NULL) + goto fail; + linebuf = tmp; + /* And fill the rest of the buffer */ + if (fgets(&linebuf[contlen], buflen / 2, pwf) == NULL) + break; + contlen = strlen(linebuf); + /* That was a lot of work for nothing. Gimme perl! */ + } + + /* Is this the username we were sent to change? */ + if (!found && linebuf[namelen] == ':' && + !strncmp(linebuf, pwd->pw_name, namelen)) { + /* Yes! So go forth in the name of the Lord and + * change it! */ + if (putpwent(pwd, fp) < 0) + goto fail; + found = true; + continue; + } + /* Nothing in particular happened, copy input to output */ + fputs(linebuf, fp); + } + + /* xfmkstemp is too restrictive by default for passwd file */ + if (fchmod(fileno(fp), 0644) < 0) + goto fail; + rc = close_stream(fp); + fp = NULL; + if (rc != 0) + goto fail; + + fclose(pwf); /* I don't think I want to know if this failed */ + pwf = NULL; + + if (!found) { + errno = ENOENT; /* give me something better */ + goto fail; + } + + /* we don't care if we can't remove the backup file */ + unlink(PASSWD_FILE ".OLD"); + /* we don't care if we can't create the backup file */ + ignore_result(link(PASSWD_FILE, PASSWD_FILE ".OLD")); + /* we DO care if we can't rename to the passwd file */ + if (rename(tmpname, PASSWD_FILE) < 0) + goto fail; + /* finally: success */ + ulckpwdf(); + free(linebuf); + return 0; + + fail: + save_errno = errno; + ulckpwdf(); + if (fp != NULL) + fclose(fp); + if (tmpname != NULL) + unlink(tmpname); + free(tmpname); + if (pwf != NULL) + fclose(pwf); + free(linebuf); + errno = save_errno; + return -1; +} + +/* Set up the limits so that we're not foiled */ +static void pw_init(void) +{ + struct rlimit rlim; + + /* Unlimited resource limits. */ + rlim.rlim_cur = rlim.rlim_max = RLIM_INFINITY; + setrlimit(RLIMIT_CPU, &rlim); + setrlimit(RLIMIT_FSIZE, &rlim); + setrlimit(RLIMIT_STACK, &rlim); + setrlimit(RLIMIT_DATA, &rlim); + setrlimit(RLIMIT_RSS, &rlim); + +#ifndef DEBUG + /* Don't drop core (not really necessary, but GP's). */ + rlim.rlim_cur = rlim.rlim_max = 0; + setrlimit(RLIMIT_CORE, &rlim); +#endif + + /* Turn off signals. */ + signal(SIGALRM, SIG_IGN); + signal(SIGHUP, SIG_IGN); + signal(SIGINT, SIG_IGN); + signal(SIGPIPE, SIG_IGN); + signal(SIGQUIT, SIG_IGN); + signal(SIGTERM, SIG_IGN); + signal(SIGTSTP, SIG_IGN); + signal(SIGTTOU, SIG_IGN); + + /* Create with exact permissions. */ + umask(0); +} diff --git a/login-utils/setpwnam.h b/login-utils/setpwnam.h new file mode 100644 index 0000000..9578592 --- /dev/null +++ b/login-utils/setpwnam.h @@ -0,0 +1,33 @@ +/* + * setpwnam.h -- + * define several paths + * + * (c) 1994 Martin Schulze <joey@infodrom.north.de> + * This file is based on setpwnam.c which is + * (c) 1994 Salvatore Valente <svalente@mit.edu> + * + * This file is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + */ +#ifndef UTIL_LINUX_SETPWNAM_H +#define UTIL_LINUX_SETPWNAM_H + +#include "pathnames.h" + +#ifndef DEBUG +# define PASSWD_FILE _PATH_PASSWD +# define GROUP_FILE _PATH_GROUP +# define SHADOW_FILE _PATH_SHADOW_PASSWD +# define SGROUP_FILE _PATH_GSHADOW +#else +# define PASSWD_FILE "/tmp/passwd" +# define GROUP_FILE "/tmp/group" +# define SHADOW_FILE "/tmp/shadow" +# define SGROUP_FILE "/tmp/gshadow" +#endif + +extern int setpwnam (struct passwd *pwd, const char *prefix); + +#endif /* UTIL_LINUX_SETPWNAM_H */ diff --git a/login-utils/su-common.c b/login-utils/su-common.c new file mode 100644 index 0000000..e0604e2 --- /dev/null +++ b/login-utils/su-common.c @@ -0,0 +1,1520 @@ +/* + * su(1) for Linux. Run a shell with substitute user and group IDs. + * + * Copyright (C) 1992-2006 Free Software Foundation, Inc. + * Copyright (C) 2012 SUSE Linux Products GmbH, Nuernberg + * Copyright (C) 2016-2017 Karel Zak <kzak@redhat.com> + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. You should have received a copy of the GNU General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * + * Based on an implementation by David MacKenzie <djm@gnu.ai.mit.edu>. + */ +#include <stdio.h> +#include <getopt.h> +#include <sys/types.h> +#include <pwd.h> +#include <grp.h> +#include <security/pam_appl.h> +#ifdef HAVE_SECURITY_PAM_MISC_H +# include <security/pam_misc.h> +#elif defined(HAVE_SECURITY_OPENPAM_H) +# include <security/openpam.h> +#endif +#include <signal.h> +#include <sys/wait.h> +#include <syslog.h> +#include <utmpx.h> + +#if defined(HAVE_LIBUTIL) && defined(HAVE_PTY_H) && defined(HAVE_SYS_SIGNALFD_H) +# include <pty.h> +# include <poll.h> +# include <sys/signalfd.h> +# include "all-io.h" +# define USE_PTY +#endif + +#include "err.h" + +#include <stdbool.h> + +#include "c.h" +#include "xalloc.h" +#include "nls.h" +#include "pathnames.h" +#include "env.h" +#include "closestream.h" +#include "strv.h" +#include "strutils.h" +#include "ttyutils.h" +#include "pwdutils.h" +#include "optutils.h" + +#include "logindefs.h" +#include "su-common.h" + +#include "debug.h" + +UL_DEBUG_DEFINE_MASK(su); +UL_DEBUG_DEFINE_MASKNAMES(su) = UL_DEBUG_EMPTY_MASKNAMES; + +#define SU_DEBUG_INIT (1 << 1) +#define SU_DEBUG_PAM (1 << 2) +#define SU_DEBUG_PARENT (1 << 3) +#define SU_DEBUG_TTY (1 << 4) +#define SU_DEBUG_LOG (1 << 5) +#define SU_DEBUG_MISC (1 << 6) +#define SU_DEBUG_SIG (1 << 7) +#define SU_DEBUG_PTY (1 << 8) +#define SU_DEBUG_ALL 0xFFFF + +#define DBG(m, x) __UL_DBG(su, SU_DEBUG_, m, x) +#define ON_DBG(m, x) __UL_DBG_CALL(su, SU_DEBUG_, m, x) + + +/* name of the pam configuration files. separate configs for su and su - */ +#define PAM_SRVNAME_SU "su" +#define PAM_SRVNAME_SU_L "su-l" + +#define PAM_SRVNAME_RUNUSER "runuser" +#define PAM_SRVNAME_RUNUSER_L "runuser-l" + +#define _PATH_LOGINDEFS_SU "/etc/default/su" +#define _PATH_LOGINDEFS_RUNUSER "/etc/default/runuser" + +#define is_pam_failure(_rc) ((_rc) != PAM_SUCCESS) + +/* The shell to run if none is given in the user's passwd entry. */ +#define DEFAULT_SHELL "/bin/sh" + +/* The user to become if none is specified. */ +#define DEFAULT_USER "root" + +#ifndef HAVE_ENVIRON_DECL +extern char **environ; +#endif + +enum { + SIGTERM_IDX = 0, + SIGINT_IDX, + SIGQUIT_IDX, + + SIGNALS_IDX_COUNT +}; + +/* + * su/runuser control struct + */ +struct su_context { + pam_handle_t *pamh; /* PAM handler */ + struct pam_conv conv; /* PAM conversation */ + + struct passwd *pwd; /* new user info */ + char *pwdbuf; /* pwd strings */ + + const char *tty_name; /* tty_path without /dev prefix */ + const char *tty_number; /* end of the tty_path */ + + char *new_user; /* wanted user */ + char *old_user; /* original user */ + + pid_t child; /* fork() baby */ + int childstatus; /* wait() status */ + + char **env_whitelist_names; /* environment whitelist */ + char **env_whitelist_vals; + + struct sigaction oldact[SIGNALS_IDX_COUNT]; /* original sigactions indexed by SIG*_IDX */ + +#ifdef USE_PTY + struct termios stdin_attrs; /* stdin and slave terminal runtime attributes */ + int pty_master; + int pty_slave; + int pty_sigfd; /* signalfd() */ + int poll_timeout; + struct winsize win; /* terminal window size */ + sigset_t oldsig; /* original signal mask */ +#endif + unsigned int runuser :1, /* flase=su, true=runuser */ + runuser_uopt :1, /* runuser -u specified */ + isterm :1, /* is stdin terminal? */ + fast_startup :1, /* pass the `-f' option to the subshell. */ + simulate_login :1, /* simulate a login instead of just starting a shell. */ + change_environment :1, /* change some environment vars to indicate the user su'd to.*/ + same_session :1, /* don't call setsid() with a command. */ + suppress_pam_info:1, /* don't print PAM info messages (Last login, etc.). */ + pam_has_session :1, /* PAM session opened */ + pam_has_cred :1, /* PAM cred established */ + pty :1, /* create pseudo-terminal */ + restricted :1; /* false for root user */ +}; + + +static sig_atomic_t volatile caught_signal = false; + +/* Signal handler for parent process. */ +static void +su_catch_sig(int sig) +{ + caught_signal = sig; +} + +static void su_init_debug(void) +{ + __UL_INIT_DEBUG_FROM_ENV(su, SU_DEBUG_, 0, SU_DEBUG); +} + +static void init_tty(struct su_context *su) +{ + su->isterm = isatty(STDIN_FILENO) ? 1 : 0; + DBG(TTY, ul_debug("initialize [is-term=%s]", su->isterm ? "true" : "false")); + if (su->isterm) + get_terminal_name(NULL, &su->tty_name, &su->tty_number); +} + +/* + * Note, this function has to be possible call more than once. If the child is + * already dead than it returns saved result from the previous call. + */ +static int wait_for_child(struct su_context *su) +{ + pid_t pid = (pid_t) -1;; + int status = 0; + + if (su->child == (pid_t) -1) + return su->childstatus; + + if (su->child != (pid_t) -1) { + /* + * The "su" parent process spends all time here in waitpid(), + * but "su --pty" uses pty_proxy_master() and waitpid() is only + * called to pick up child status or to react to SIGSTOP. + */ + DBG(SIG, ul_debug("waiting for child [%d]...", su->child)); + for (;;) { + pid = waitpid(su->child, &status, WUNTRACED); + + if (pid != (pid_t) - 1 && WIFSTOPPED(status)) { + DBG(SIG, ul_debug(" child got SIGSTOP -- stop all session")); + kill(getpid(), SIGSTOP); + /* once we get here, we must have resumed */ + kill(pid, SIGCONT); + DBG(SIG, ul_debug(" session resumed -- continue")); +#ifdef USE_PTY + /* Let's go back to pty_proxy_master() */ + if (su->pty_sigfd != -1) { + DBG(SIG, ul_debug(" leaving on child SIGSTOP")); + return 0; + } +#endif + } else + break; + } + } + if (pid != (pid_t) -1) { + if (WIFSIGNALED(status)) { + fprintf(stderr, "%s%s\n", + strsignal(WTERMSIG(status)), + WCOREDUMP(status) ? _(" (core dumped)") + : ""); + status = WTERMSIG(status) + 128; + } else + status = WEXITSTATUS(status); + + DBG(SIG, ul_debug("child %d is dead", su->child)); + su->child = (pid_t) -1; /* Don't use the PID anymore! */ + su->childstatus = status; + } else if (caught_signal) + status = caught_signal + 128; + else + status = 1; + + DBG(SIG, ul_debug("child status=%d", status)); + return status; +} + + +#ifdef USE_PTY +static void pty_init_slave(struct su_context *su) +{ + DBG(PTY, ul_debug("initialize slave")); + + ioctl(su->pty_slave, TIOCSCTTY, 1); + close(su->pty_master); + + dup2(su->pty_slave, STDIN_FILENO); + dup2(su->pty_slave, STDOUT_FILENO); + dup2(su->pty_slave, STDERR_FILENO); + + close(su->pty_slave); + close(su->pty_sigfd); + + su->pty_slave = -1; + su->pty_master = -1; + su->pty_sigfd = -1; + + sigprocmask(SIG_SETMASK, &su->oldsig, NULL); + + DBG(PTY, ul_debug("... initialize slave done")); +} + +static void pty_create(struct su_context *su) +{ + struct termios slave_attrs; + int rc; + + if (su->isterm) { + DBG(PTY, ul_debug("create for terminal")); + struct winsize win; + + /* original setting of the current terminal */ + if (tcgetattr(STDIN_FILENO, &su->stdin_attrs) != 0) + err(EXIT_FAILURE, _("failed to get terminal attributes")); + + /* reuse the current terminal setting for slave */ + slave_attrs = su->stdin_attrs; + cfmakeraw(&slave_attrs); + + ioctl(STDIN_FILENO, TIOCGWINSZ, (char *)&su->win); + + /* create master+slave */ + rc = openpty(&su->pty_master, &su->pty_slave, NULL, &slave_attrs, &win); + + } else { + DBG(PTY, ul_debug("create for non-terminal")); + rc = openpty(&su->pty_master, &su->pty_slave, NULL, NULL, NULL); + + /* set slave attributes */ + if (!rc) { + tcgetattr(su->pty_slave, &slave_attrs); + cfmakeraw(&slave_attrs); + tcsetattr(su->pty_slave, TCSANOW, &slave_attrs); + } + } + + if (rc < 0) + err(EXIT_FAILURE, _("failed to create pseudo-terminal")); + + DBG(PTY, ul_debug("pty setup done [master=%d, slave=%d]", su->pty_master, su->pty_slave)); +} + +static void pty_cleanup(struct su_context *su) +{ + if (su->pty_master == -1) + return; + + DBG(PTY, ul_debug("cleanup")); + if (su->isterm) + tcsetattr(STDIN_FILENO, TCSADRAIN, &su->stdin_attrs); +} + +static int write_output(char *obuf, ssize_t bytes) +{ + DBG(PTY, ul_debug(" writing output")); + + if (write_all(STDOUT_FILENO, obuf, bytes)) { + DBG(PTY, ul_debug(" writing output *failed*")); + warn(_("write failed")); + return -errno; + } + + return 0; +} + +static int write_to_child(struct su_context *su, + char *buf, size_t bufsz) +{ + return write_all(su->pty_master, buf, bufsz); +} + +/* + * The su(1) is usually faster than shell, so it's a good idea to wait until + * the previous message has been already read by shell from slave before we + * write to master. This is necessary especially for EOF situation when we can + * send EOF to master before shell is fully initialized, to workaround this + * problem we wait until slave is empty. For example: + * + * echo "date" | su + * + * Unfortunately, the child (usually shell) can ignore stdin at all, so we + * don't wait forever to avoid dead locks... + * + * Note that su --pty is primarily designed for interactive sessions as it + * maintains master+slave tty stuff within the session. Use pipe to write to + * su(1) and assume non-interactive (tee-like) behavior is NOT well + * supported. + */ +static void write_eof_to_child(struct su_context *su) +{ + unsigned int tries = 0; + struct pollfd fds[] = { + { .fd = su->pty_slave, .events = POLLIN } + }; + char c = DEF_EOF; + + DBG(PTY, ul_debug(" waiting for empty slave")); + while (poll(fds, 1, 10) == 1 && tries < 8) { + DBG(PTY, ul_debug(" slave is not empty")); + xusleep(250000); + tries++; + } + if (tries < 8) + DBG(PTY, ul_debug(" slave is empty now")); + + DBG(PTY, ul_debug(" sending EOF to master")); + write_to_child(su, &c, sizeof(char)); +} + +static int pty_handle_io(struct su_context *su, int fd, int *eof) +{ + char buf[BUFSIZ]; + ssize_t bytes; + + DBG(PTY, ul_debug("%d FD active", fd)); + *eof = 0; + + /* read from active FD */ + bytes = read(fd, buf, sizeof(buf)); + if (bytes < 0) { + if (errno == EAGAIN || errno == EINTR) + return 0; + return -errno; + } + + if (bytes == 0) { + *eof = 1; + return 0; + } + + /* from stdin (user) to command */ + if (fd == STDIN_FILENO) { + DBG(PTY, ul_debug(" stdin --> master %zd bytes", bytes)); + + if (write_to_child(su, buf, bytes)) { + warn(_("write failed")); + return -errno; + } + /* without sync write_output() will write both input & + * shell output that looks like double echoing */ + fdatasync(su->pty_master); + + /* from command (master) to stdout */ + } else if (fd == su->pty_master) { + DBG(PTY, ul_debug(" master --> stdout %zd bytes", bytes)); + write_output(buf, bytes); + } + + return 0; +} + +static int pty_handle_signal(struct su_context *su, int fd) +{ + struct signalfd_siginfo info; + ssize_t bytes; + + DBG(SIG, ul_debug("signal FD %d active", fd)); + + bytes = read(fd, &info, sizeof(info)); + if (bytes != sizeof(info)) { + if (bytes < 0 && (errno == EAGAIN || errno == EINTR)) + return 0; + return -errno; + } + + switch (info.ssi_signo) { + case SIGCHLD: + DBG(SIG, ul_debug(" get signal SIGCHLD")); + + /* The child terminated or stopped. Note that we ignore SIGCONT + * here, because stop/cont semantic is handled by wait_for_child() */ + if (info.ssi_code == CLD_EXITED || info.ssi_status == SIGSTOP) + wait_for_child(su); + /* The child is dead, force poll() timeout. */ + if (su->child == (pid_t) -1) + su->poll_timeout = 10; + return 0; + case SIGWINCH: + DBG(SIG, ul_debug(" get signal SIGWINCH")); + if (su->isterm) { + ioctl(STDIN_FILENO, TIOCGWINSZ, (char *)&su->win); + ioctl(su->pty_slave, TIOCSWINSZ, (char *)&su->win); + } + break; + case SIGTERM: + /* fallthrough */ + case SIGINT: + /* fallthrough */ + case SIGQUIT: + DBG(SIG, ul_debug(" get signal SIG{TERM,INT,QUIT}")); + caught_signal = info.ssi_signo; + /* Child termination is going to generate SIGCHILD (see above) */ + kill(su->child, SIGTERM); + break; + default: + abort(); + } + + return 0; +} + +static void pty_proxy_master(struct su_context *su) +{ + sigset_t ourset; + int rc = 0, ret, eof = 0; + enum { + POLLFD_SIGNAL = 0, + POLLFD_MASTER, + POLLFD_STDIN + + }; + struct pollfd pfd[] = { + [POLLFD_SIGNAL] = { .fd = -1, .events = POLLIN | POLLERR | POLLHUP }, + [POLLFD_MASTER] = { .fd = su->pty_master, .events = POLLIN | POLLERR | POLLHUP }, + [POLLFD_STDIN] = { .fd = STDIN_FILENO, .events = POLLIN | POLLERR | POLLHUP } + }; + + /* for PTY mode we use signalfd + * + * TODO: script(1) initializes this FD before fork, good or bad idea? + */ + sigfillset(&ourset); + if (sigprocmask(SIG_BLOCK, &ourset, NULL)) { + warn(_("cannot block signals")); + caught_signal = true; + return; + } + + sigemptyset(&ourset); + sigaddset(&ourset, SIGCHLD); + sigaddset(&ourset, SIGWINCH); + sigaddset(&ourset, SIGALRM); + sigaddset(&ourset, SIGTERM); + sigaddset(&ourset, SIGINT); + sigaddset(&ourset, SIGQUIT); + + if ((su->pty_sigfd = signalfd(-1, &ourset, SFD_CLOEXEC)) < 0) { + warn(("cannot create signal file descriptor")); + caught_signal = true; + return; + } + + pfd[POLLFD_SIGNAL].fd = su->pty_sigfd; + su->poll_timeout = -1; + + while (!caught_signal) { + size_t i; + int errsv; + + DBG(PTY, ul_debug("calling poll()")); + + /* wait for input or signal */ + ret = poll(pfd, ARRAY_SIZE(pfd), su->poll_timeout); + errsv = errno; + DBG(PTY, ul_debug("poll() rc=%d", ret)); + + if (ret < 0) { + if (errsv == EAGAIN) + continue; + warn(_("poll failed")); + break; + } + if (ret == 0) { + DBG(PTY, ul_debug("leaving poll() loop [timeout=%d]", su->poll_timeout)); + break; + } + + for (i = 0; i < ARRAY_SIZE(pfd); i++) { + rc = 0; + + if (pfd[i].revents == 0) + continue; + + DBG(PTY, ul_debug(" active pfd[%s].fd=%d %s %s %s", + i == POLLFD_STDIN ? "stdin" : + i == POLLFD_MASTER ? "master" : + i == POLLFD_SIGNAL ? "signal" : "???", + pfd[i].fd, + pfd[i].revents & POLLIN ? "POLLIN" : "", + pfd[i].revents & POLLHUP ? "POLLHUP" : "", + pfd[i].revents & POLLERR ? "POLLERR" : "")); + switch (i) { + case POLLFD_STDIN: + case POLLFD_MASTER: + /* data */ + if (pfd[i].revents & POLLIN) + rc = pty_handle_io(su, pfd[i].fd, &eof); + /* EOF maybe detected by two ways: + * A) poll() return POLLHUP event after close() + * B) read() returns 0 (no data) */ + if ((pfd[i].revents & POLLHUP) || eof) { + DBG(PTY, ul_debug(" ignore FD")); + pfd[i].fd = -1; + if (i == POLLFD_STDIN) { + write_eof_to_child(su); + DBG(PTY, ul_debug(" ignore STDIN")); + } + } + continue; + case POLLFD_SIGNAL: + rc = pty_handle_signal(su, pfd[i].fd); + break; + } + if (rc) + break; + } + } + + close(su->pty_sigfd); + su->pty_sigfd = -1; + DBG(PTY, ul_debug("poll() done [signal=%d, rc=%d]", caught_signal, rc)); +} +#endif /* USE_PTY */ + + +/* Log the fact that someone has run su to the user given by PW; + if SUCCESSFUL is true, they gave the correct password, etc. */ + +static void log_syslog(struct su_context *su, bool successful) +{ + DBG(LOG, ul_debug("syslog logging")); + + openlog(program_invocation_short_name, 0, LOG_AUTH); + syslog(LOG_NOTICE, "%s(to %s) %s on %s", + successful ? "" : + su->runuser ? "FAILED RUNUSER " : "FAILED SU ", + su->new_user, su->old_user ? : "", + su->tty_name ? : "none"); + closelog(); +} + +/* + * Log failed login attempts in _PATH_BTMP if that exists. + */ +static void log_btmp(struct su_context *su) +{ + struct utmpx ut; + struct timeval tv; + + DBG(LOG, ul_debug("btmp logging")); + + memset(&ut, 0, sizeof(ut)); + str2memcpy(ut.ut_user, + su->pwd && su->pwd->pw_name ? su->pwd->pw_name : "(unknown)", + sizeof(ut.ut_user)); + + if (su->tty_number) + str2memcpy(ut.ut_id, su->tty_number, sizeof(ut.ut_id)); + if (su->tty_name) + str2memcpy(ut.ut_line, su->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 = getpid(); + + updwtmpx(_PATH_BTMP, &ut); +} + +static int supam_conv( int num_msg, + const struct pam_message **msg, + struct pam_response **resp, + void *data) +{ + struct su_context *su = (struct su_context *) data; + + if (su->suppress_pam_info + && num_msg == 1 + && msg && msg[0]->msg_style == PAM_TEXT_INFO) + return PAM_SUCCESS; + +#ifdef HAVE_SECURITY_PAM_MISC_H + return misc_conv(num_msg, msg, resp, data); +#elif defined(HAVE_SECURITY_OPENPAM_H) + return openpam_ttyconv(num_msg, msg, resp, data); +#endif +} + +static void supam_cleanup(struct su_context *su, int retcode) +{ + const int errsv = errno; + + DBG(PAM, ul_debug("cleanup")); + + if (su->pam_has_session) + pam_close_session(su->pamh, 0); + if (su->pam_has_cred) + pam_setcred(su->pamh, PAM_DELETE_CRED | PAM_SILENT); + pam_end(su->pamh, retcode); + errno = errsv; +} + + +static void supam_export_environment(struct su_context *su) +{ + char **env; + + DBG(PAM, ul_debug("init environ[]")); + + /* This is a copy but don't care to free as we exec later anyways. */ + env = pam_getenvlist(su->pamh); + + while (env && *env) { + if (putenv(*env) != 0) + err(EXIT_FAILURE, _("failed to modify environment")); + env++; + } +} + +static void supam_authenticate(struct su_context *su) +{ + const char *srvname = NULL; + int rc; + + srvname = su->runuser ? + (su->simulate_login ? PAM_SRVNAME_RUNUSER_L : PAM_SRVNAME_RUNUSER) : + (su->simulate_login ? PAM_SRVNAME_SU_L : PAM_SRVNAME_SU); + + DBG(PAM, ul_debug("start [name: %s]", srvname)); + + rc = pam_start(srvname, su->pwd->pw_name, &su->conv, &su->pamh); + if (is_pam_failure(rc)) + goto done; + + if (su->tty_name) { + rc = pam_set_item(su->pamh, PAM_TTY, su->tty_name); + if (is_pam_failure(rc)) + goto done; + } + if (su->old_user) { + rc = pam_set_item(su->pamh, PAM_RUSER, (const void *) su->old_user); + if (is_pam_failure(rc)) + goto done; + } + if (su->runuser) { + /* + * This is the only difference between runuser(1) and su(1). The command + * runuser(1) does not required authentication, because user is root. + */ + if (su->restricted) + errx(EXIT_FAILURE, _("may not be used by non-root users")); + return; + } + + rc = pam_authenticate(su->pamh, 0); + if (is_pam_failure(rc)) + goto done; + + /* Check password expiration and offer option to change it. */ + rc = pam_acct_mgmt(su->pamh, 0); + if (rc == PAM_NEW_AUTHTOK_REQD) + rc = pam_chauthtok(su->pamh, PAM_CHANGE_EXPIRED_AUTHTOK); + done: + log_syslog(su, !is_pam_failure(rc)); + + if (is_pam_failure(rc)) { + const char *msg; + + DBG(PAM, ul_debug("authentication failed")); + log_btmp(su); + + msg = pam_strerror(su->pamh, rc); + pam_end(su->pamh, rc); + sleep(getlogindefs_num("FAIL_DELAY", 1)); + errx(EXIT_FAILURE, "%s", msg ? msg : _("incorrect password")); + } +} + +static void supam_open_session(struct su_context *su) +{ + int rc; + + DBG(PAM, ul_debug("opening session")); + + rc = pam_open_session(su->pamh, 0); + if (is_pam_failure(rc)) { + supam_cleanup(su, rc); + errx(EXIT_FAILURE, _("cannot open session: %s"), + pam_strerror(su->pamh, rc)); + } else + su->pam_has_session = 1; +} + +static void parent_setup_signals(struct su_context *su) +{ + sigset_t ourset; + + /* + * Signals setup + * + * 1) block all signals + */ + DBG(SIG, ul_debug("initialize signals")); + + sigfillset(&ourset); + if (sigprocmask(SIG_BLOCK, &ourset, NULL)) { + warn(_("cannot block signals")); + caught_signal = true; + } + + if (!caught_signal) { + struct sigaction action; + action.sa_handler = su_catch_sig; + sigemptyset(&action.sa_mask); + action.sa_flags = 0; + + sigemptyset(&ourset); + + /* 2a) add wanted signals to the mask (for session) */ + if (!su->same_session + && (sigaddset(&ourset, SIGINT) + || sigaddset(&ourset, SIGQUIT))) { + + warn(_("cannot initialize signal mask for session")); + caught_signal = true; + } + /* 2b) add wanted generic signals to the mask */ + if (!caught_signal + && (sigaddset(&ourset, SIGTERM) + || sigaddset(&ourset, SIGALRM))) { + + warn(_("cannot initialize signal mask")); + caught_signal = true; + } + + /* 3a) set signal handlers (for session) */ + if (!caught_signal + && !su->same_session + && (sigaction(SIGINT, &action, &su->oldact[SIGINT_IDX]) + || sigaction(SIGQUIT, &action, &su->oldact[SIGQUIT_IDX]))) { + + warn(_("cannot set signal handler for session")); + caught_signal = true; + } + + /* 3b) set signal handlers */ + if (!caught_signal + && sigaction(SIGTERM, &action, &su->oldact[SIGTERM_IDX])) { + + warn(_("cannot set signal handler")); + caught_signal = true; + } + + /* 4) unblock wanted signals */ + if (!caught_signal + && sigprocmask(SIG_UNBLOCK, &ourset, NULL)) { + + warn(_("cannot set signal mask")); + caught_signal = true; + } + } +} + + +static void create_watching_parent(struct su_context *su) +{ + int status; + + DBG(MISC, ul_debug("forking...")); +#ifdef USE_PTY + /* no-op, just save original signal mask to oldsig */ + sigprocmask(SIG_BLOCK, NULL, &su->oldsig); + + if (su->pty) + pty_create(su); +#endif + fflush(stdout); /* ??? */ + + switch ((int) (su->child = fork())) { + case -1: /* error */ + supam_cleanup(su, PAM_ABORT); +#ifdef USE_PTY + if (su->pty) + pty_cleanup(su); +#endif + err(EXIT_FAILURE, _("cannot create child process")); + break; + + case 0: /* child */ + return; + + default: /* parent */ + DBG(MISC, ul_debug("child [pid=%d]", (int) su->child)); + break; + } + + /* free unnecessary stuff */ + free_getlogindefs_data(); + + /* In the parent watch the child. */ + + /* su without pam support does not have a helper that keeps + sitting on any directory so let's go to /. */ + if (chdir("/") != 0) + warn(_("cannot change directory to %s"), "/"); +#ifdef USE_PTY + if (su->pty) + pty_proxy_master(su); + else +#endif + parent_setup_signals(su); + + /* + * Wait for child + */ + if (!caught_signal) + status = wait_for_child(su); + else + status = 1; + + DBG(SIG, ul_debug("final child status=%d", status)); + + if (caught_signal && su->child != (pid_t)-1) { + fprintf(stderr, _("\nSession terminated, killing shell...")); + kill(su->child, SIGTERM); + } + + supam_cleanup(su, PAM_SUCCESS); + + if (caught_signal) { + if (su->child != (pid_t)-1) { + DBG(SIG, ul_debug("killing child")); + sleep(2); + kill(su->child, SIGKILL); + fprintf(stderr, _(" ...killed.\n")); + } + + /* Let's terminate itself with the received signal. + * + * It seems that shells use WIFSIGNALED() rather than our exit status + * value to detect situations when is necessary to cleanup (reset) + * terminal settings (kzak -- Jun 2013). + */ + DBG(SIG, ul_debug("restore signals setting")); + switch (caught_signal) { + case SIGTERM: + sigaction(SIGTERM, &su->oldact[SIGTERM_IDX], NULL); + break; + case SIGINT: + sigaction(SIGINT, &su->oldact[SIGINT_IDX], NULL); + break; + case SIGQUIT: + sigaction(SIGQUIT, &su->oldact[SIGQUIT_IDX], NULL); + break; + default: + /* just in case that signal stuff initialization failed and + * caught_signal = true */ + caught_signal = SIGKILL; + break; + } + DBG(SIG, ul_debug("self-send %d signal", caught_signal)); + kill(getpid(), caught_signal); + } + +#ifdef USE_PTY + if (su->pty) + pty_cleanup(su); +#endif + DBG(MISC, ul_debug("exiting [rc=%d]", status)); + exit(status); +} + +/* Adds @name from the current environment to the whitelist. If @name is not + * set then nothing is added to the whitelist and returns 1. + */ +static int env_whitelist_add(struct su_context *su, const char *name) +{ + const char *env = getenv(name); + + if (!env) + return 1; + if (strv_extend(&su->env_whitelist_names, name)) + err_oom(); + if (strv_extend(&su->env_whitelist_vals, env)) + err_oom(); + return 0; +} + +static int env_whitelist_setenv(struct su_context *su, int overwrite) +{ + char **one; + size_t i = 0; + int rc; + + STRV_FOREACH(one, su->env_whitelist_names) { + rc = setenv(*one, su->env_whitelist_vals[i], overwrite); + if (rc) + return rc; + i++; + } + + return 0; +} + +/* Creates (add to) whitelist from comma delimited string */ +static int env_whitelist_from_string(struct su_context *su, const char *str) +{ + char **all = strv_split(str, ","); + char **one; + + if (!all) { + if (errno == ENOMEM) + err_oom(); + return -EINVAL; + } + + STRV_FOREACH(one, all) + env_whitelist_add(su, *one); + strv_free(all); + return 0; +} + +static void setenv_path(const struct passwd *pw) +{ + int rc; + + DBG(MISC, ul_debug("setting PATH")); + + if (pw->pw_uid) + rc = logindefs_setenv("PATH", "ENV_PATH", _PATH_DEFPATH); + + else if ((rc = logindefs_setenv("PATH", "ENV_ROOTPATH", NULL)) != 0) + rc = logindefs_setenv("PATH", "ENV_SUPATH", _PATH_DEFPATH_ROOT); + + if (rc) + err(EXIT_FAILURE, _("failed to set the PATH environment variable")); +} + +static void modify_environment(struct su_context *su, const char *shell) +{ + const struct passwd *pw = su->pwd; + + + DBG(MISC, ul_debug("modify environ[]")); + + /* Leave TERM unchanged. Set HOME, SHELL, USER, LOGNAME, PATH. + * + * Unset all other environment variables, but follow + * --whitelist-environment if specified. + */ + if (su->simulate_login) { + /* leave TERM unchanged */ + env_whitelist_add(su, "TERM"); + + /* Note that original su(1) has allocated environ[] by malloc + * to the number of expected variables. This seems unnecessary + * optimization as libc later realloc(current_size+2) and for + * empty environ[] the curren_size is zero. It seems better to + * keep all logic around environment in glibc's hands. + * --kzak [Aug 2018] + */ +#ifdef HAVE_CLEARENV + clearenv(); +#else + environ = NULL; +#endif + /* always reset */ + if (shell) + xsetenv("SHELL", shell, 1); + + setenv_path(pw); + + xsetenv("HOME", pw->pw_dir, 1); + xsetenv("USER", pw->pw_name, 1); + xsetenv("LOGNAME", pw->pw_name, 1); + + /* apply all from whitelist, but no overwrite */ + env_whitelist_setenv(su, 0); + + /* Set HOME, SHELL, and (if not becoming a superuser) USER and LOGNAME. + */ + } else if (su->change_environment) { + xsetenv("HOME", pw->pw_dir, 1); + if (shell) + xsetenv("SHELL", shell, 1); + + if (getlogindefs_bool("ALWAYS_SET_PATH", 0)) + setenv_path(pw); + + if (pw->pw_uid) { + xsetenv("USER", pw->pw_name, 1); + xsetenv("LOGNAME", pw->pw_name, 1); + } + } + + supam_export_environment(su); +} + +static void init_groups(struct su_context *su, gid_t *groups, size_t ngroups) +{ + int rc; + + DBG(MISC, ul_debug("initialize groups")); + + errno = 0; + if (ngroups) + rc = setgroups(ngroups, groups); + else + rc = initgroups(su->pwd->pw_name, su->pwd->pw_gid); + + if (rc == -1) { + supam_cleanup(su, PAM_ABORT); + err(EXIT_FAILURE, _("cannot set groups")); + } + endgrent(); + + rc = pam_setcred(su->pamh, PAM_ESTABLISH_CRED); + if (is_pam_failure(rc)) + errx(EXIT_FAILURE, _("failed to user credentials: %s"), + pam_strerror(su->pamh, rc)); + su->pam_has_cred = 1; +} + +static void change_identity(const struct passwd *pw) +{ + DBG(MISC, ul_debug("changing identity [GID=%d, UID=%d]", pw->pw_gid, pw->pw_uid)); + + if (setgid(pw->pw_gid)) + err(EXIT_FAILURE, _("cannot set group id")); + if (setuid(pw->pw_uid)) + err(EXIT_FAILURE, _("cannot set user id")); +} + +/* Run SHELL, if COMMAND is nonzero, pass it to the shell with the -c option. + * Pass ADDITIONAL_ARGS to the shell as more arguments; there are + * N_ADDITIONAL_ARGS extra arguments. + */ +static void run_shell( + struct su_context *su, + char const *shell, char const *command, char **additional_args, + size_t n_additional_args) +{ + size_t n_args = 1 + su->fast_startup + 2 * ! !command + n_additional_args + 1; + const char **args = xcalloc(n_args, sizeof *args); + size_t argno = 1; + + DBG(MISC, ul_debug("starting shell [shell=%s, command=\"%s\"%s%s]", + shell, command, + su->simulate_login ? " login" : "", + su->fast_startup ? " fast-start" : "")); + + if (su->simulate_login) { + char *arg0; + char *shell_basename; + + shell_basename = basename(shell); + arg0 = xmalloc(strlen(shell_basename) + 2); + arg0[0] = '-'; + strcpy(arg0 + 1, shell_basename); + args[0] = arg0; + } else + args[0] = basename(shell); + + if (su->fast_startup) + args[argno++] = "-f"; + if (command) { + args[argno++] = "-c"; + args[argno++] = command; + } + + memcpy(args + argno, additional_args, n_additional_args * sizeof *args); + args[argno + n_additional_args] = NULL; + execv(shell, (char **)args); + errexec(shell); +} + +/* Return true if SHELL is a restricted shell (one not returned by + * getusershell), else false, meaning it is a standard shell. + */ +static bool is_restricted_shell(const char *shell) +{ + char *line; + + setusershell(); + while ((line = getusershell()) != NULL) { + if (*line != '#' && !strcmp(line, shell)) { + endusershell(); + return false; + } + } + endusershell(); + + DBG(MISC, ul_debug("%s is restricted shell (not in /etc/shells)", shell)); + return true; +} + +static void usage_common(void) +{ + fputs(_(" -m, -p, --preserve-environment do not reset environment variables\n"), stdout); + fputs(_(" -w, --whitelist-environment <list> don't reset specified variables\n"), stdout); + fputs(USAGE_SEPARATOR, stdout); + + fputs(_(" -g, --group <group> specify the primary group\n"), stdout); + fputs(_(" -G, --supp-group <group> specify a supplemental group\n"), stdout); + fputs(USAGE_SEPARATOR, stdout); + + fputs(_(" -, -l, --login make the shell a login shell\n"), stdout); + fputs(_(" -c, --command <command> pass a single command to the shell with -c\n"), stdout); + fputs(_(" --session-command <command> pass a single command to the shell with -c\n" + " and do not create a new session\n"), stdout); + fputs(_(" -f, --fast pass -f to the shell (for csh or tcsh)\n"), stdout); + fputs(_(" -s, --shell <shell> run <shell> if /etc/shells allows it\n"), stdout); + fputs(_(" -P, --pty create a new pseudo-terminal\n"), stdout); + + fputs(USAGE_SEPARATOR, stdout); + printf(USAGE_HELP_OPTIONS(33)); +} + +static void usage_runuser(void) +{ + fputs(USAGE_HEADER, stdout); + fprintf(stdout, + _(" %1$s [options] -u <user> [[--] <command>]\n" + " %1$s [options] [-] [<user> [<argument>...]]\n"), + program_invocation_short_name); + + fputs(USAGE_SEPARATOR, stdout); + fputs(_("Run <command> with the effective user ID and group ID of <user>. If -u is\n" + "not given, fall back to su(1)-compatible semantics and execute standard shell.\n" + "The options -c, -f, -l, and -s are mutually exclusive with -u.\n"), stdout); + + fputs(USAGE_OPTIONS, stdout); + fputs(_(" -u, --user <user> username\n"), stdout); + usage_common(); + fputs(USAGE_SEPARATOR, stdout); + + fprintf(stdout, USAGE_MAN_TAIL("runuser(1)")); +} + +static void usage_su(void) +{ + fputs(USAGE_HEADER, stdout); + fprintf(stdout, + _(" %s [options] [-] [<user> [<argument>...]]\n"), + program_invocation_short_name); + + fputs(USAGE_SEPARATOR, stdout); + fputs(_("Change the effective user ID and group ID to that of <user>.\n" + "A mere - implies -l. If <user> is not given, root is assumed.\n"), stdout); + + fputs(USAGE_OPTIONS, stdout); + usage_common(); + + fprintf(stdout, USAGE_MAN_TAIL("su(1)")); +} + +static void __attribute__((__noreturn__)) usage(int mode) +{ + if (mode == SU_MODE) + usage_su(); + else + usage_runuser(); + + exit(EXIT_SUCCESS); +} + +static void load_config(void *data) +{ + struct su_context *su = (struct su_context *) data; + + DBG(MISC, ul_debug("loading logindefs")); + logindefs_load_file(su->runuser ? _PATH_LOGINDEFS_RUNUSER : _PATH_LOGINDEFS_SU); + logindefs_load_file(_PATH_LOGINDEFS); +} + +/* + * Returns 1 if the current user is not root + */ +static int is_not_root(void) +{ + const uid_t ruid = getuid(); + const uid_t euid = geteuid(); + + /* if we're really root and aren't running setuid */ + return (uid_t) 0 == ruid && ruid == euid ? 0 : 1; +} + +static gid_t add_supp_group(const char *name, gid_t **groups, size_t *ngroups) +{ + struct group *gr; + + if (*ngroups >= NGROUPS_MAX) + errx(EXIT_FAILURE, + P_("specifying more than %d supplemental group is not possible", + "specifying more than %d supplemental groups is not possible", + NGROUPS_MAX - 1), NGROUPS_MAX - 1); + + gr = getgrnam(name); + if (!gr) + errx(EXIT_FAILURE, _("group %s does not exist"), name); + + DBG(MISC, ul_debug("add %s group [name=%s, GID=%d]", name, gr->gr_name, (int) gr->gr_gid)); + + *groups = xrealloc(*groups, sizeof(gid_t) * (*ngroups + 1)); + (*groups)[*ngroups] = gr->gr_gid; + (*ngroups)++; + + return gr->gr_gid; +} + +int su_main(int argc, char **argv, int mode) +{ + struct su_context _su = { + .conv = { supam_conv, NULL }, + .runuser = (mode == RUNUSER_MODE ? 1 : 0), + .change_environment = 1, + .new_user = DEFAULT_USER, +#ifdef USE_PTY + .pty_master = -1, + .pty_slave = -1, + .pty_sigfd = -1, +#endif + }, *su = &_su; + + int optc; + char *command = NULL; + int request_same_session = 0; + char *shell = NULL; + + gid_t *groups = NULL; + size_t ngroups = 0; + bool use_supp = false; + bool use_gid = false; + gid_t gid = 0; + + static const struct option longopts[] = { + {"command", required_argument, NULL, 'c'}, + {"session-command", required_argument, NULL, 'C'}, + {"fast", no_argument, NULL, 'f'}, + {"login", no_argument, NULL, 'l'}, + {"preserve-environment", no_argument, NULL, 'p'}, + {"pty", no_argument, NULL, 'P'}, + {"shell", required_argument, NULL, 's'}, + {"group", required_argument, NULL, 'g'}, + {"supp-group", required_argument, NULL, 'G'}, + {"user", required_argument, NULL, 'u'}, /* runuser only */ + {"whitelist-environment", required_argument, NULL, 'w'}, + {"help", no_argument, 0, 'h'}, + {"version", no_argument, 0, 'V'}, + {NULL, 0, NULL, 0} + }; + static const ul_excl_t excl[] = { /* rows and cols in ASCII order */ + { 'm', 'w' }, /* preserve-environment, whitelist-environment */ + { 'p', 'w' }, /* preserve-environment, whitelist-environment */ + { 0 } + }; + int excl_st[ARRAY_SIZE(excl)] = UL_EXCL_STATUS_INIT; + + setlocale(LC_ALL, ""); + bindtextdomain(PACKAGE, LOCALEDIR); + textdomain(PACKAGE); + atexit(close_stdout); + + su_init_debug(); + su->conv.appdata_ptr = (void *) su; + + while ((optc = + getopt_long(argc, argv, "c:fg:G:lmpPs:u:hVw:", longopts, + NULL)) != -1) { + + err_exclusive_options(optc, longopts, excl, excl_st); + + switch (optc) { + case 'c': + command = optarg; + break; + + case 'C': + command = optarg; + request_same_session = 1; + break; + + case 'f': + su->fast_startup = true; + break; + + case 'g': + use_gid = true; + gid = add_supp_group(optarg, &groups, &ngroups); + break; + + case 'G': + use_supp = true; + add_supp_group(optarg, &groups, &ngroups); + break; + + case 'l': + su->simulate_login = true; + break; + + case 'm': + case 'p': + su->change_environment = false; + break; + + case 'w': + env_whitelist_from_string(su, optarg); + break; + + case 'P': +#ifdef USE_PTY + su->pty = 1; +#else + errx(EXIT_FAILURE, _("--pty is not supported for your system")); +#endif + break; + + case 's': + shell = optarg; + break; + + case 'u': + if (!su->runuser) + errtryhelp(EXIT_FAILURE); + su->runuser_uopt = 1; + su->new_user = optarg; + break; + + case 'h': + usage(mode); + + case 'V': + printf(UTIL_LINUX_VERSION); + exit(EXIT_SUCCESS); + + default: + errtryhelp(EXIT_FAILURE); + } + } + + su->restricted = is_not_root(); + + if (optind < argc && !strcmp(argv[optind], "-")) { + su->simulate_login = true; + ++optind; + } + + if (su->simulate_login && !su->change_environment) { + warnx(_ + ("ignoring --preserve-environment, it's mutually exclusive with --login")); + su->change_environment = true; + } + + switch (mode) { + case RUNUSER_MODE: + /* runuser -u <user> <command> + * + * If -u <user> is not specified, then follow traditional su(1) behavior and + * fallthrough + */ + if (su->runuser_uopt) { + if (shell || su->fast_startup || command || su->simulate_login) + errx(EXIT_FAILURE, + _("options --{shell,fast,command,session-command,login} and " + "--user are mutually exclusive")); + if (optind == argc) + errx(EXIT_FAILURE, _("no command was specified")); + break; + } + /* fallthrough */ + case SU_MODE: + if (optind < argc) + su->new_user = argv[optind++]; + break; + } + + if ((use_supp || use_gid) && su->restricted) + errx(EXIT_FAILURE, + _("only root can specify alternative groups")); + + logindefs_set_loader(load_config, (void *) su); + init_tty(su); + + su->pwd = xgetpwnam(su->new_user, &su->pwdbuf); + if (!su->pwd + || !su->pwd->pw_passwd + || !su->pwd->pw_name || !*su->pwd->pw_name + || !su->pwd->pw_dir || !*su->pwd->pw_dir) + errx(EXIT_FAILURE, _("user %s does not exist"), su->new_user); + + su->new_user = su->pwd->pw_name; + su->old_user = xgetlogin(); + + if (!su->pwd->pw_shell || !*su->pwd->pw_shell) + su->pwd->pw_shell = DEFAULT_SHELL; + + if (use_supp && !use_gid) + su->pwd->pw_gid = groups[0]; + else if (use_gid) + su->pwd->pw_gid = gid; + + supam_authenticate(su); + + if (request_same_session || !command || !su->pwd->pw_uid) + su->same_session = 1; + + /* initialize shell variable only if "-u <user>" not specified */ + if (su->runuser_uopt) { + shell = NULL; + } else { + if (!shell && !su->change_environment) + shell = getenv("SHELL"); + + if (shell + && getuid() != 0 + && is_restricted_shell(su->pwd->pw_shell)) { + /* The user being su'd to has a nonstandard shell, and + * so is probably a uucp account or has restricted + * access. Don't compromise the account by allowing + * access with a standard shell. + */ + warnx(_("using restricted shell %s"), su->pwd->pw_shell); + shell = NULL; + } + shell = xstrdup(shell ? shell : su->pwd->pw_shell); + } + + init_groups(su, groups, ngroups); + + if (!su->simulate_login || command) + su->suppress_pam_info = 1; /* don't print PAM info messages */ + + supam_open_session(su); + + create_watching_parent(su); + /* Now we're in the child. */ + + change_identity(su->pwd); + if (!su->same_session || su->pty) { + DBG(MISC, ul_debug("call setsid()")); + setsid(); + } +#ifdef USE_PTY + if (su->pty) + pty_init_slave(su); +#endif + /* Set environment after pam_open_session, which may put KRB5CCNAME + into the pam_env, etc. */ + + modify_environment(su, shell); + + if (su->simulate_login && chdir(su->pwd->pw_dir) != 0) + warn(_("warning: cannot change directory to %s"), su->pwd->pw_dir); + + if (shell) + run_shell(su, shell, command, argv + optind, max(0, argc - optind)); + + execvp(argv[optind], &argv[optind]); + err(EXIT_FAILURE, _("failed to execute %s"), argv[optind]); +} diff --git a/login-utils/su-common.h b/login-utils/su-common.h new file mode 100644 index 0000000..7cf3769 --- /dev/null +++ b/login-utils/su-common.h @@ -0,0 +1,11 @@ +#ifndef UTIL_LINUX_SU_COMMON_H +#define UTIL_LINUX_SU_COMMON_H + +enum { + SU_MODE, + RUNUSER_MODE +}; + +extern int su_main(int argc, char **argv, int mode); + +#endif /* UTIL_LINUX_SU_COMMON */ diff --git a/login-utils/su.1 b/login-utils/su.1 new file mode 100644 index 0000000..d6a064f --- /dev/null +++ b/login-utils/su.1 @@ -0,0 +1,294 @@ +.TH SU 1 "July 2014" "util-linux" "User Commands" +.SH NAME +su \- run a command with substitute user and group ID +.SH SYNOPSIS +.BR su " [options] [" \- ] +.RI [ user " [" argument ...]] +.SH DESCRIPTION +.B su +allows to run commands with a substitute user and group ID. +.PP +When called without arguments, +.B su +defaults to running an interactive shell as +.IR root . +.PP +For backward compatibility, +.B su +defaults to not change the current directory and to only set the +environment variables +.B HOME +and +.B SHELL +(plus +.B USER +and +.B LOGNAME +if the target +.I user +is not root). It is recommended to always use the +.B \-\-login +option (instead of its shortcut +.BR \- ) +to avoid side effects caused by mixing environments. +.PP +This version of +.B su +uses PAM for authentication, account and session management. Some +configuration options found in other +.B su +implementations, such as support for a wheel group, have to be +configured via PAM. +.PP +.B su +is mostly designed for unprivileged users, the recommended solution for +privileged users (e.g. scripts executed by root) is to use +non-set-user-ID command +.BR runuser (1) +that does not require authentication and provide separate PAM configuration. If +the PAM session is not required at all then the recommend solution is to use +command +.BR setpriv (1). + +.SH OPTIONS +.TP +.BR \-c , " \-\-command" = \fIcommand +Pass +.I command +to the shell with the +.B \-c +option. +.TP +.BR \-f , " \-\-fast" +Pass +.B \-f +to the shell, which may or may not be useful, depending on the shell. +.TP +.BR \-g , " \-\-group" = \fIgroup +Specify the primary group. This option is available to the root user only. +.TP +.BR \-G , " \-\-supp\-group" = \fIgroup +Specify a supplemental group. This option is available to the root user only. The first specified +supplementary group is also used as a primary group if the option \fB\-\-group\fR is unspecified. +.TP +.BR \- , " \-l" , " \-\-login" +Start the shell as a login shell with an environment similar to a real +login: +.RS 10 +.TP +o +clears all the environment variables except +.B TERM +and variables specified by \fB\-\-whitelist\-environment\fR +.TP +o +initializes the environment variables +.BR HOME , +.BR SHELL , +.BR USER , +.BR LOGNAME ", and" +.B PATH +.TP +o +changes to the target user's home directory +.TP +o +sets argv[0] of the shell to +.RB ' \- ' +in order to make the shell a login shell +.RE +.TP +.BR \-m , " \-p" , " \-\-preserve\-environment" +Preserve the entire environment, i.e. it does not set +.BR HOME , +.BR SHELL , +.B USER +nor +.BR LOGNAME . +This option is ignored if the option \fB\-\-login\fR is specified. +.TP +.BR \-P , " \-\-pty" +Create pseudo-terminal for the session. The independent terminal provides +better security as user does not share terminal with the original +session. This allow to avoid TIOCSTI ioctl terminal injection and another +security attacks against terminal file descriptors. The all session is also +possible to move to background (e.g. "su --pty - username -c +application &"). If the pseudo-terminal is enabled then su command works +as a proxy between the sessions (copy stdin and stdout). + +This feature is EXPERIMENTAL for now and may be removed in the next releases. + +.TP +.BR \-s , " \-\-shell" = \fIshell +Run the specified \fIshell\fR instead of the default. The shell to run is +selected according to the following rules, in order: +.RS 10 +.TP +o +the shell specified with +.B \-\-shell +.TP +o +the shell specified in the environment variable +.BR SHELL , +if the +.B \-\-preserve\-environment +option is used +.TP +o +the shell listed in the passwd entry of the target user +.TP +o +/bin/sh +.RE +.IP +If the target user has a restricted shell (i.e. not listed in +/etc/shells), the +.B \-\-shell +option and the +.B SHELL +environment variables are ignored unless the calling user is root. +.TP +.BI \-\-session\-command= command +Same as +.B \-c +but do not create a new session. (Discouraged.) +.TP +.BR \-w , " \-\-whitelist\-environment" = \fIlist +Don't reset environment variables specified in comma separated \fIlist\fR when clears +environment for \fB\-\-login\fR. The whitelist is ignored for the environment variables +.BR HOME , +.BR SHELL , +.BR USER , +.BR LOGNAME ", and" +.BR PATH "." +.TP +.BR \-V , " \-\-version" +Display version information and exit. +.TP +.BR \-h , " \-\-help" +Display help text and exit. +.SH SIGNALS +Upon receiving either +.BR SIGINT , +.B SIGQUIT +or +.BR SIGTERM , +.B su +terminates its child and afterwards terminates itself with the received signal. +The child is terminated by SIGTERM, after unsuccessful attempt and 2 seconds of +delay the child is killed by SIGKILL. +.SH CONFIG FILES +.B su +reads the +.I /etc/default/su +and +.I /etc/login.defs +configuration files. The following configuration items are relevant +for +.BR su (1): +.PP +.B FAIL_DELAY +(number) +.RS 4 +Delay in seconds in case of an authentication failure. The number must be +a non-negative integer. +.RE +.PP +.B ENV_PATH +(string) +.RS 4 +Defines the PATH environment variable for a regular user. The +default value is +.IR /usr/local/bin:\:/bin:\:/usr/bin . +.RE +.PP +.B ENV_ROOTPATH +(string) +.br +.B ENV_SUPATH +(string) +.RS 4 +Defines the PATH environment variable for root. The default value is +.IR /usr/local/sbin:\:/usr/local/bin:\:/sbin:\:/bin:\:/usr/sbin:\:/usr/bin . +.RE +.PP +.B ALWAYS_SET_PATH +(boolean) +.RS 4 +If set to +.I yes +and \-\-login and \-\-preserve\-environment were not specified +.B su +initializes +.BR PATH . +.RE +.sp +The environment variable PATH may be different on systems where /bin and /sbin +are merged into /usr. +.SH EXIT STATUS +.B su +normally returns the exit status of the command it executed. If the +command was killed by a signal, +.B su +returns the number of the signal plus 128. +.PP +Exit status generated by +.B su +itself: +.RS 10 +.TP +1 +Generic error before executing the requested command +.TP +126 +The requested command could not be executed +.TP +127 +The requested command was not found +.RE +.SH FILES +.PD 0 +.TP 17 +/etc/pam.d/su +default PAM configuration file +.TP +/etc/pam.d/su-l +PAM configuration file if \-\-login is specified +.TP +/etc/default/su +command specific logindef config file +.TP +/etc/login.defs +global logindef config file +.PD 1 +.SH NOTES +For security reasons +.B su +always logs failed log-in attempts to the btmp file, but it does not write to +the lastlog file at all. This solution allows to control +.B su +behavior by PAM configuration. If you want to use the pam_lastlog module to +print warning message about failed log-in attempts then the pam_lastlog has to +be configured to update the lastlog file as well. For example by: + +.RS +.br +session required pam_lastlog.so nowtmp +.RE +.SH "SEE ALSO" +.BR setpriv (1), +.BR login.defs (5), +.BR shells (5), +.BR pam (8), +.BR runuser (8) +.SH HISTORY +This \fBsu\fR command was +derived from coreutils' \fBsu\fR, which was based on an implementation by +David MacKenzie. The util-linux has been refactored by Karel Zak. +.SH AVAILABILITY +The su command is part of the util-linux package and is +available from +.UR https://\:www.kernel.org\:/pub\:/linux\:/utils\:/util-linux/ +Linux Kernel Archive +.UE . diff --git a/login-utils/su.c b/login-utils/su.c new file mode 100644 index 0000000..d9c67f3 --- /dev/null +++ b/login-utils/su.c @@ -0,0 +1,8 @@ + +#include "su-common.h" + +int main(int argc, char **argv) +{ + return su_main(argc, argv, SU_MODE); +} + diff --git a/login-utils/sulogin-consoles.c b/login-utils/sulogin-consoles.c new file mode 100644 index 0000000..2c0eed3 --- /dev/null +++ b/login-utils/sulogin-consoles.c @@ -0,0 +1,828 @@ +/* + * consoles.c Routines to detect the system consoles + * + * Copyright (c) 2011 SuSE LINUX Products GmbH, All rights reserved. + * Copyright (C) 2012 Karel Zak <kzak@redhat.com> + * Copyright (C) 2012 Werner Fink <werner@suse.de> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program (see the file COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * Author: Werner Fink <werner@suse.de> + */ + +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/ioctl.h> +#ifdef __linux__ +# include <sys/vt.h> +# include <sys/kd.h> +# include <linux/serial.h> +# include <linux/major.h> +#endif +#include <dirent.h> +#include <errno.h> +#include <fcntl.h> +#include <unistd.h> + +#ifdef USE_SULOGIN_EMERGENCY_MOUNT +# include <sys/mount.h> +# include <linux/fs.h> +# include <linux/magic.h> +# ifndef MNT_DETACH +# define MNT_DETACH 2 +# endif +#endif + +#include "c.h" +#include "canonicalize.h" +#include "sulogin-consoles.h" + +#if !defined(__STDC_VERSION__) || (__STDC_VERSION__ < 199901L) +# ifndef typeof +# define typeof __typeof__ +# endif +# ifndef restrict +# define restrict __restrict__ +# endif +#endif + +#define alignof(type) ((sizeof(type)+(sizeof(void*)-1)) & ~(sizeof(void*)-1)) +#define strsize(string) (strlen((string))+1) + +static int consoles_debug; +#define DBG(x) do { \ + if (consoles_debug) { \ + fputs("consoles debug: ", stderr); \ + x; \ + } \ + } while (0) + +static inline void __attribute__ ((__format__ (__printf__, 1, 2))) +dbgprint(const char * const mesg, ...) +{ + va_list ap; + va_start(ap, mesg); + vfprintf(stderr, mesg, ap); + va_end(ap); + fputc('\n', stderr); +} + +#ifdef USE_SULOGIN_EMERGENCY_MOUNT +/* + * Make C library standard calls such like ttyname(3) work + * even if the system does not show any of the standard + * directories. + */ + +static uint32_t emergency_flags; +# define MNT_PROCFS 0x0001 +# define MNT_DEVTMPFS 0x0002 + +void emergency_do_umounts(void) +{ + if (emergency_flags & MNT_DEVTMPFS) + umount2("/dev", MNT_DETACH); + if (emergency_flags & MNT_PROCFS) + umount2("/proc", MNT_DETACH); +} + +void emergency_do_mounts(void) +{ + struct stat rt, xt; + + if (emergency_flags) { + emergency_flags = 0; + return; + } + + if (stat("/", &rt) != 0) { + warn("cannot get file status of root file system\n"); + return; + } + + if (stat("/proc", &xt) == 0 + && rt.st_dev == xt.st_dev + && mount("proc", "/proc", "proc", MS_RELATIME, NULL) == 0) + emergency_flags |= MNT_PROCFS; + + if (stat("/dev", &xt) == 0 + && rt.st_dev == xt.st_dev + && mount("devtmpfs", "/dev", "devtmpfs", + MS_RELATIME, "mode=0755,nr_inodes=0") == 0) { + + emergency_flags |= MNT_DEVTMPFS; + mknod("/dev/console", S_IFCHR|S_IRUSR|S_IWUSR, + makedev(TTYAUX_MAJOR, 1)); + + if (symlink("/proc/self/fd", "/dev/fd") == 0) { + ignore_result( symlink("fd/0", "/dev/stdin") ); + ignore_result( symlink("fd/1", "/dev/stdout") ); + ignore_result( symlink("fd/2", "/dev/stderr") ); + } + } +} + +#else /* !USE_SULOGIN_EMERGENCY_MOUNT */ + +void emergency_do_umounts(void) { } +void emergency_do_mounts(void) { } + +#endif /* USE_SULOGIN_EMERGENCY_MOUNT */ + +/* + * Read and allocate one line from file, + * the caller has to free the result + */ +static __attribute__((__nonnull__)) +char *oneline(const char * const file) +{ + FILE *fp; + char *ret = NULL; + size_t dummy = 0; + ssize_t len; + + DBG(dbgprint("reading %s", file)); + + if (!(fp = fopen(file, "r" UL_CLOEXECSTR))) + return NULL; + len = getline(&ret, &dummy, fp); + if (len >= 0) { + char *nl; + + if (len) + ret[len-1] = '\0'; + if ((nl = strchr(ret, '\n'))) + *nl = '\0'; + } + + fclose(fp); + return ret; +} + +#ifdef __linux__ +/* + * Read and determine active attribute for tty below + * /sys/class/tty, the caller has to free the result. + */ +static __attribute__((__malloc__)) +char *actattr(const char * const tty) +{ + char *ret, *path; + + if (!tty || !*tty) + return NULL; + if (asprintf(&path, "/sys/class/tty/%s/active", tty) < 0) + return NULL; + + ret = oneline(path); + free(path); + return ret; +} + +/* + * Read and determine device attribute for tty below + * /sys/class/tty. + */ +static +dev_t devattr(const char * const tty) +{ + dev_t dev = 0; + char *path, *value; + + if (!tty || !*tty) + return 0; + if (asprintf(&path, "/sys/class/tty/%s/dev", tty) < 0) + return 0; + + value = oneline(path); + if (value) { + unsigned int maj, min; + + if (sscanf(value, "%u:%u", &maj, &min) == 2) + dev = makedev(maj, min); + free(value); + } + + free(path); + return dev; +} +#endif /* __linux__ */ + +/* + * Search below /dev for the character device in `dev_t comparedev' variable. + * Note that realpath(3) is used here to avoid not existent devices due the + * strdup(3) used in our canonicalize_path()! + */ +static +#ifdef __GNUC__ +__attribute__((__nonnull__,__malloc__,__hot__)) +#endif +char* scandev(DIR *dir, const dev_t comparedev) +{ + char path[PATH_MAX]; + char *name = NULL; + const struct dirent *dent; + int len, fd; + + DBG(dbgprint("scanning /dev for %u:%u", major(comparedev), minor(comparedev))); + + /* + * Try udev links on character devices first. + */ + if ((len = snprintf(path, sizeof(path), + "/dev/char/%u:%u", major(comparedev), minor(comparedev))) > 0 && + (size_t)len < sizeof(path)) { + + name = realpath(path, NULL); + if (name) + goto out; + } + + fd = dirfd(dir); + rewinddir(dir); + while ((dent = readdir(dir))) { + struct stat st; + +#ifdef _DIRENT_HAVE_D_TYPE + if (dent->d_type != DT_UNKNOWN && dent->d_type != DT_CHR) + continue; +#endif + if (fstatat(fd, dent->d_name, &st, 0) < 0) + continue; + if (!S_ISCHR(st.st_mode)) + continue; + if (comparedev != st.st_rdev) + continue; + if ((len = snprintf(path, sizeof(path), "/dev/%s", dent->d_name)) < 0 || + (size_t)len >= sizeof(path)) + continue; + + name = realpath(path, NULL); + if (name) + goto out; + } + +#ifdef USE_SULOGIN_EMERGENCY_MOUNT + /* + * There was no /dev mounted hence and no device was found hence we create our own. + */ + if (!name && (emergency_flags & MNT_DEVTMPFS)) { + + if ((len = snprintf(path, sizeof(path), + "/dev/tmp-%u:%u", major(comparedev), minor(comparedev))) < 0 || + (size_t)len >= sizeof(path)) + goto out; + + if (mknod(path, S_IFCHR|S_IRUSR|S_IWUSR, comparedev) < 0 && errno != EEXIST) + goto out; + + name = realpath(path, NULL); + } +#endif +out: + return name; +} + +/* + * Default control characters for an unknown terminal line. + */ + +/* + * Allocate an aligned `struct console' memory area, + * initialize its default values, and append it to + * the global linked list. + */ +static +#ifdef __GNUC__ +__attribute__((__hot__)) +#endif +int append_console(struct list_head *consoles, const char * const name) +{ + struct console *restrict tail; + const struct console *last = NULL; + + DBG(dbgprint("appenging %s", name)); + + if (!list_empty(consoles)) + last = list_last_entry(consoles, struct console, entry); + + if (posix_memalign((void *) &tail, sizeof(void *), + alignof(struct console) + strsize(name)) != 0) + return -ENOMEM; + + INIT_LIST_HEAD(&tail->entry); + INIT_CHARDATA(&tail->cp); + + list_add_tail(&tail->entry, consoles); + tail->tty = ((char *) tail) + alignof(struct console); + strcpy(tail->tty, name); + + tail->file = (FILE*)0; + tail->flags = 0; + tail->fd = -1; + tail->id = last ? last->id + 1 : 0; + tail->pid = -1; + memset(&tail->tio, 0, sizeof(tail->tio)); + + return 0; +} + +#ifdef __linux__ +/* + * return codes: + * < 0 - fatal error (no mem or so... ) + * 0 - success + * 1 - recoverable error + * 2 - detection not available + */ +static int detect_consoles_from_proc(struct list_head *consoles) +{ + char fbuf[16 + 1]; + DIR *dir = NULL; + FILE *fc = NULL; + int maj, min, rc = 1, matches; + + DBG(dbgprint("trying /proc")); + + fc = fopen("/proc/consoles", "r" UL_CLOEXECSTR); + if (!fc) { + rc = 2; + goto done; + } + dir = opendir("/dev"); + if (!dir) + goto done; + + while ((matches = fscanf(fc, "%*s %*s (%16[^)]) %d:%d", fbuf, &maj, &min)) >= 1) { + char *name; + dev_t comparedev; + + if (matches != 3) + continue; + if (!strchr(fbuf, 'E')) + continue; + comparedev = makedev(maj, min); + name = scandev(dir, comparedev); + if (!name) + continue; + rc = append_console(consoles, name); + free(name); + if (rc < 0) + goto done; + } + + rc = list_empty(consoles) ? 1 : 0; +done: + if (dir) + closedir(dir); + if (fc) + fclose(fc); + DBG(dbgprint("[/proc rc=%d]", rc)); + return rc; +} + +/* + * return codes: + * < 0 - fatal error (no mem or so... ) + * 0 - success + * 1 - recoverable error + * 2 - detection not available + */ +static int detect_consoles_from_sysfs(struct list_head *consoles) +{ + char *attrib = NULL, *words, *token; + DIR *dir = NULL; + int rc = 1; + + DBG(dbgprint("trying /sys")); + + attrib = actattr("console"); + if (!attrib) { + rc = 2; + goto done; + } + + words = attrib; + + dir = opendir("/dev"); + if (!dir) + goto done; + + while ((token = strsep(&words, " \t\r\n"))) { + char *name; + dev_t comparedev; + + if (*token == '\0') + continue; + + comparedev = devattr(token); + if (comparedev == makedev(TTY_MAJOR, 0)) { + char *tmp = actattr(token); + if (!tmp) + continue; + comparedev = devattr(tmp); + free(tmp); + } + + name = scandev(dir, comparedev); + if (!name) + continue; + rc = append_console(consoles, name); + free(name); + if (rc < 0) + goto done; + } + + rc = list_empty(consoles) ? 1 : 0; +done: + free(attrib); + if (dir) + closedir(dir); + DBG(dbgprint("[/sys rc=%d]", rc)); + return rc; +} + + +static int detect_consoles_from_cmdline(struct list_head *consoles) +{ + char *cmdline, *words, *token; + dev_t comparedev; + DIR *dir = NULL; + int rc = 1, fd; + + DBG(dbgprint("trying kernel cmdline")); + + cmdline = oneline("/proc/cmdline"); + if (!cmdline) { + rc = 2; + goto done; + } + + words= cmdline; + dir = opendir("/dev"); + if (!dir) + goto done; + + while ((token = strsep(&words, " \t\r\n"))) { +#ifdef TIOCGDEV + unsigned int devnum; +#else + struct vt_stat vt; + struct stat st; +#endif + char *colon, *name; + + if (*token != 'c') + continue; + if (strncmp(token, "console=", 8) != 0) + continue; + token += 8; + + if (strcmp(token, "brl") == 0) + token += 4; + if ((colon = strchr(token, ','))) + *colon = '\0'; + + if (asprintf(&name, "/dev/%s", token) < 0) + continue; + if ((fd = open(name, O_RDWR|O_NONBLOCK|O_NOCTTY|O_CLOEXEC)) < 0) { + free(name); + continue; + } + free(name); +#ifdef TIOCGDEV + if (ioctl (fd, TIOCGDEV, &devnum) < 0) { + close(fd); + continue; + } + comparedev = (dev_t) devnum; +#else + if (fstat(fd, &st) < 0) { + close(fd); + continue; + } + comparedev = st.st_rdev; + if (comparedev == makedev(TTY_MAJOR, 0)) { + if (ioctl(fd, VT_GETSTATE, &vt) < 0) { + close(fd); + continue; + } + comparedev = makedev(TTY_MAJOR, (int)vt.v_active); + } +#endif + close(fd); + + name = scandev(dir, comparedev); + if (!name) + continue; + rc = append_console(consoles, name); + free(name); + if (rc < 0) + goto done; + } + + rc = list_empty(consoles) ? 1 : 0; +done: + if (dir) + closedir(dir); + free(cmdline); + DBG(dbgprint("[kernel cmdline rc=%d]", rc)); + return rc; +} + +#ifdef TIOCGDEV +static int detect_consoles_from_tiocgdev(struct list_head *consoles, + const int fallback, + const char *device) +{ + unsigned int devnum; + char *name; + int rc = 1, fd = -1; + dev_t comparedev; + DIR *dir = NULL; + struct console *console; + + DBG(dbgprint("trying tiocgdev")); + + if (!device || !*device) + fd = dup(fallback); + else + fd = open(device, O_RDWR|O_NONBLOCK|O_NOCTTY|O_CLOEXEC); + + if (fd < 0) + goto done; + if (ioctl (fd, TIOCGDEV, &devnum) < 0) + goto done; + + comparedev = (dev_t) devnum; + dir = opendir("/dev"); + if (!dir) + goto done; + + name = scandev(dir, comparedev); + closedir(dir); + + if (!name) { + name = (char *) (device && *device ? device : ttyname(fallback)); + if (!name) + name = "/dev/tty1"; + + name = strdup(name); + if (!name) { + rc = -ENOMEM; + goto done; + } + } + rc = append_console(consoles, name); + free(name); + if (rc < 0) + goto done; + if (list_empty(consoles)) { + rc = 1; + goto done; + } + console = list_last_entry(consoles, struct console, entry); + if (console && (!device || !*device)) + console->fd = fallback; +done: + if (fd >= 0) + close(fd); + DBG(dbgprint("[tiocgdev rc=%d]", rc)); + return rc; +} +#endif /* TIOCGDEV */ +#endif /* __linux__ */ + +/* + * Try to detect the real device(s) used for the system console + * /dev/console if but only if /dev/console is used. On Linux + * this can be more than one device, e.g. a serial line as well + * as a virtual console as well as a simple printer. + * + * Returns 1 if stdout and stderr should be reconnected and 0 + * otherwise or less than zero on error. + */ +int detect_consoles(const char *device, const int fallback, struct list_head *consoles) +{ + int fd, reconnect = 0, rc; + dev_t comparedev = 0; + + consoles_debug = getenv("CONSOLES_DEBUG") ? 1 : 0; + + if (!device || !*device) + fd = fallback >= 0 ? dup(fallback) : - 1; + else { + fd = open(device, O_RDWR|O_NONBLOCK|O_NOCTTY|O_CLOEXEC); + reconnect = 1; + } + + DBG(dbgprint("detection started [device=%s, fallback=%d]", + device, fallback)); + + if (fd >= 0) { + DIR *dir; + char *name; + struct stat st; +#ifdef TIOCGDEV + unsigned int devnum; +#endif +#ifdef __GNU__ + /* + * The Hurd always gives st_rdev as 0, which causes this + * method to select the first terminal it finds. + */ + close(fd); + goto fallback; +#endif + DBG(dbgprint("trying device/fallback file descriptor")); + + if (fstat(fd, &st) < 0) { + close(fd); + goto fallback; + } + comparedev = st.st_rdev; + + if (reconnect && + (fstat(fallback, &st) < 0 || comparedev != st.st_rdev)) + dup2(fd, fallback); +#ifdef __linux__ + /* + * Check if the device detection for Linux system console should be used. + */ + if (comparedev == makedev(TTYAUX_MAJOR, 0)) { /* /dev/tty */ + close(fd); + device = "/dev/tty"; + goto fallback; + } + if (comparedev == makedev(TTYAUX_MAJOR, 1)) { /* /dev/console */ + close(fd); + goto console; + } + if (comparedev == makedev(TTYAUX_MAJOR, 2)) { /* /dev/ptmx */ + close(fd); + device = "/dev/tty"; + goto fallback; + } + if (comparedev == makedev(TTY_MAJOR, 0)) { /* /dev/tty0 */ + struct vt_stat vt; + if (ioctl(fd, VT_GETSTATE, &vt) < 0) { + close(fd); + goto fallback; + } + comparedev = makedev(TTY_MAJOR, (int)vt.v_active); + } +#endif +#ifdef TIOCGDEV + if (ioctl (fd, TIOCGDEV, &devnum) < 0) { + close(fd); + goto fallback; + } + comparedev = (dev_t)devnum; +#endif + close(fd); + dir = opendir("/dev"); + if (!dir) + goto fallback; + name = scandev(dir, comparedev); + closedir(dir); + + if (name) { + rc = append_console(consoles, name); + free(name); + if (rc < 0) + return rc; + } + if (list_empty(consoles)) + goto fallback; + + DBG(dbgprint("detection success [rc=%d]", reconnect)); + return reconnect; + } +#ifdef __linux__ +console: + /* + * Detection of devices used for Linux system console using + * the /proc/consoles API with kernel 2.6.38 and higher. + */ + rc = detect_consoles_from_proc(consoles); + if (rc == 0) + return reconnect; /* success */ + if (rc < 0) + return rc; /* fatal error */ + + /* + * Detection of devices used for Linux system console using + * the sysfs /sys/class/tty/ API with kernel 2.6.37 and higher. + */ + rc = detect_consoles_from_sysfs(consoles); + if (rc == 0) + return reconnect; /* success */ + if (rc < 0) + return rc; /* fatal error */ + + /* + * Detection of devices used for Linux system console using + * kernel parameter on the kernels command line. + */ + rc = detect_consoles_from_cmdline(consoles); + if (rc == 0) + return reconnect; /* success */ + if (rc < 0) + return rc; /* fatal error */ + + /* + * Detection of the device used for Linux system console using + * the ioctl TIOCGDEV if available (e.g. official 2.6.38). + */ +#ifdef TIOCGDEV + rc = detect_consoles_from_tiocgdev(consoles, fallback, device); + if (rc == 0) + return reconnect; /* success */ + if (rc < 0) + return rc; /* fatal error */ +#endif + if (!list_empty(consoles)) { + DBG(dbgprint("detection success [rc=%d]", reconnect)); + return reconnect; + } + +#endif /* __linux __ */ + +fallback: + if (fallback >= 0) { + const char *name; + char *n; + struct console *console; + + if (device && *device != '\0') + name = device; + else name = ttyname(fallback); + + if (!name) + name = "/dev/tty"; + + n = strdup(name); + if (!n) + return -ENOMEM; + rc = append_console(consoles, n); + free(n); + if (rc < 0) + return rc; + if (list_empty(consoles)) + return 1; + console = list_last_entry(consoles, struct console, entry); + if (console) + console->fd = fallback; + } + + DBG(dbgprint("detection done by fallback [rc=%d]", reconnect)); + return reconnect; +} + + +#ifdef TEST_PROGRAM +int main(int argc, char *argv[]) +{ + char *name = NULL; + int fd, re; + struct list_head *p, consoles; + + if (argc == 2) { + name = argv[1]; + fd = open(name, O_RDWR); + } else { + name = ttyname(STDIN_FILENO); + fd = STDIN_FILENO; + } + + if (!name) + errx(EXIT_FAILURE, "usage: %s [<tty>]\n", program_invocation_short_name); + + INIT_LIST_HEAD(&consoles); + re = detect_consoles(name, fd, &consoles); + + list_for_each(p, &consoles) { + struct console *c = list_entry(p, struct console, entry); + printf("%s: id=%d %s\n", c->tty, c->id, re ? "(reconnect) " : ""); + } + + return 0; +} +#endif diff --git a/login-utils/sulogin-consoles.h b/login-utils/sulogin-consoles.h new file mode 100644 index 0000000..f762e87 --- /dev/null +++ b/login-utils/sulogin-consoles.h @@ -0,0 +1,54 @@ +/* + * consoles.h Header file for routines to detect the system consoles + * + * Copyright (c) 2011 SuSE LINUX Products GmbH, All rights reserved. + * Copyright (c) 2012 Werner Fink <werner@suse.de> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program (see the file COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * Author: Werner Fink <werner@suse.de> + */ +#ifndef UTIL_LINUX_SULOGIN_CONSOLES_H +#define UTIL_LINUX_SULOGIN_CONSOLES_H + +#include <sys/types.h> +#include <stdint.h> +#include <stdio.h> +#include <termios.h> + +#include "list.h" +#include "ttyutils.h" + +struct console { + struct list_head entry; + char *tty; + FILE *file; + uint32_t flags; + int fd, id; +#define CON_SERIAL 0x0001 +#define CON_NOTTY 0x0002 + pid_t pid; + struct chardata cp; + struct termios tio; +}; + +extern int detect_consoles(const char *device, int fallback, + struct list_head *consoles); + +extern void emergency_do_umounts(void); +extern void emergency_do_mounts(void); + +#endif /* UTIL_LINUX_SULOGIN_CONSOLES_H */ diff --git a/login-utils/sulogin.8 b/login-utils/sulogin.8 new file mode 100644 index 0000000..7998b4a --- /dev/null +++ b/login-utils/sulogin.8 @@ -0,0 +1,95 @@ +.\" Copyright (C) 1998-2006 Miquel van Smoorenburg. +.\" Copyright (C) 2012 Karel Zak <kzak@redhat.com> +.\" +.\" This program is free software; you can redistribute it and/or modify +.\" it under the terms of the GNU General Public License as published by +.\" the Free Software Foundation; either version 2 of the License, or +.\" (at your option) any later version. +.\" +.\" This program is distributed in the hope that it will be useful, +.\" but WITHOUT ANY WARRANTY; without even the implied warranty of +.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +.\" GNU General Public License for more details. +.\" +.\" You should have received a copy of the GNU General Public License +.\" along with this program; if not, write to the Free Software +.\" Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +.\" +.TH SULOGIN "8" "July 2014" "util-linux" "System Administration" +.SH NAME +sulogin \- single-user login +.SH SYNOPSIS +.BR sulogin " [options]" +.RI [ tty ] +.SH DESCRIPTION +.B sulogin +is invoked by +.B init +when the system goes into single-user mode. +.PP +The user is prompted: +.IP "" .5i +Give root password for system maintenance +.br +(or type Control\-D for normal startup): +.PP +If the root account is locked and --force is specified, no password is required. +.PP +.B sulogin +will be connected to the current terminal, or to the optional \fItty\fR device that +can be specified on the command line (typically +.IR /dev/console ). +.PP +When the user exits from the single-user shell, or presses control\-D at the +prompt, the system will continue to boot. +.SH OPTIONS +.IP "\fB\-e\fR, \fB\-\-force\fP" +If the default method of obtaining the root password from the system via +.BR getpwnam (3) +fails, then examine +.I /etc/passwd +and +.I /etc/shadow +to get the password. If these files are damaged or nonexistent, or when +root account is locked by '!' or '*' at the begin of the password then +.B sulogin +will \fBstart a root shell without asking for a password\fP. +.PP +.IP +Only use the +.B \-e +option if you are sure the console is physically protected against +unauthorized access. +.IP "\fB\-p\fR, \fB\-\-login\-shell\fP" +Specifying this option causes +.B sulogin +to start the shell process as a login shell. +.IP "\fB\-t\fR, \fB\-\-timeout \fIseconds\fP" +Specify the maximum amount of time to wait for user input. By default, +.B sulogin +will wait forever. +.IP "\fB\-h\fR, \fB\-\-help\fP" +Display help text and exit. +.IP "\fB\-V\fR, \fB\-\-version\fP" +Display version information and exit. +.SH ENVIRONMENT VARIABLES +.B sulogin +looks for the environment variable +.B SUSHELL +or +.B sushell +to determine what shell to start. If the environment variable is not set, it +will try to execute root's shell from +.IR /etc/passwd . +If that fails, it +will fall back to +.IR /bin/sh . +.SH AUTHOR +.B sulogin +was written by Miquel van Smoorenburg for sysvinit and later ported +to util-linux by Dave Reisner and Karel Zak. +.SH AVAILABILITY +The sulogin command is part of the util-linux package and is available from +.UR https://\:www.kernel.org\:/pub\:/linux\:/utils\:/util-linux/ +Linux Kernel Archive +.UE . diff --git a/login-utils/sulogin.c b/login-utils/sulogin.c new file mode 100644 index 0000000..5f09bd4 --- /dev/null +++ b/login-utils/sulogin.c @@ -0,0 +1,1110 @@ +/* + * sulogin + * + * This program gives Linux machines a reasonable secure way to boot single + * user. It forces the user to supply the root password before a shell is + * started. If there is a shadow password file and the encrypted root password + * is "x" the shadow password will be used. + * + * Copyright (C) 1998-2003 Miquel van Smoorenburg. + * Copyright (C) 2012 Karel Zak <kzak@redhat.com> + * Copyright (C) 2012 Werner Fink <werner@suse.de> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +#include <sys/mman.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/wait.h> +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <unistd.h> +#include <fcntl.h> +#include <signal.h> +#include <pwd.h> +#include <shadow.h> +#include <termios.h> +#include <errno.h> +#include <getopt.h> +#include <sys/ioctl.h> +#ifdef HAVE_CRYPT_H +# include <crypt.h> +#endif + +#ifdef HAVE_LIBSELINUX +# include <selinux/selinux.h> +# include <selinux/get_context_list.h> +#endif + +#ifdef __linux__ +# include <sys/kd.h> +# include <sys/param.h> +#endif + +#include "c.h" +#include "closestream.h" +#include "env.h" +#include "nls.h" +#include "pathnames.h" +#ifdef USE_PLYMOUTH_SUPPORT +# include "plymouth-ctrl.h" +#endif +#include "strutils.h" +#include "ttyutils.h" +#include "sulogin-consoles.h" +#define CONMAX 16 + +static unsigned int timeout; +static int profile; +static volatile uint32_t openfd; /* Remember higher file descriptors */ + +static struct sigaction saved_sigint; +static struct sigaction saved_sigtstp; +static struct sigaction saved_sigquit; +static struct sigaction saved_sighup; +static struct sigaction saved_sigchld; + +static volatile sig_atomic_t alarm_rised; +static volatile sig_atomic_t sigchild; + +#ifndef IUCLC +# define IUCLC 0 +#endif + +#ifndef WEXITED +# warning "WEXITED is missing, sulogin may not work as expected" +# define WEXITED 0 +#endif + +static int locked_account_password(const char * const passwd) +{ + if (passwd && (*passwd == '*' || *passwd == '!')) + return 1; + return 0; +} + +/* + * Fix the tty modes and set reasonable defaults. + */ +static void tcinit(struct console *con) +{ + int flags = 0, mode = 0; + struct termios *tio = &con->tio; + const int fd = con->fd; +#ifdef USE_PLYMOUTH_SUPPORT + struct termios lock; + int i = (plymouth_command(MAGIC_PING)) ? PLYMOUTH_TERMIOS_FLAGS_DELAY : 0; + if (i) + plymouth_command(MAGIC_QUIT); + while (i-- > 0) { + /* + * With plymouth the termios flags become changed after this + * function had changed the termios. + */ + memset(&lock, 0, sizeof(struct termios)); + if (ioctl(fd, TIOCGLCKTRMIOS, &lock) < 0) + break; + if (!lock.c_iflag && !lock.c_oflag && !lock.c_cflag && !lock.c_lflag) + break; + sleep(1); + } + memset(&lock, 0, sizeof(struct termios)); + ioctl(fd, TIOCSLCKTRMIOS, &lock); +#endif + errno = 0; + + if (tcgetattr(fd, tio) < 0) { + warn(_("tcgetattr failed")); + con->flags |= CON_NOTTY; + return; + } + + /* Handle lines other than virtual consoles here */ +#if defined(KDGKBMODE) + if (ioctl(fd, KDGKBMODE, &mode) < 0) +#endif + { + speed_t ispeed, ospeed; + struct winsize ws; + errno = 0; + + /* this is a modem line */ + con->flags |= CON_SERIAL; + + /* Flush input and output queues on modem lines */ + tcflush(fd, TCIOFLUSH); + + ispeed = cfgetispeed(tio); + ospeed = cfgetospeed(tio); + + if (!ispeed) ispeed = TTYDEF_SPEED; + if (!ospeed) ospeed = TTYDEF_SPEED; + + tio->c_cflag = CREAD | CS8 | HUPCL | (tio->c_cflag & CLOCAL); + tio->c_iflag = 0; + tio->c_lflag = 0; + tio->c_oflag &= OPOST | ONLCR; + + cfsetispeed(tio, ispeed); + cfsetospeed(tio, ospeed); + +#ifdef HAVE_STRUCT_TERMIOS_C_LINE + tio->c_line = 0; +#endif + tio->c_cc[VTIME] = 0; + tio->c_cc[VMIN] = 1; + + if (ioctl(fd, TIOCGWINSZ, &ws) == 0) { + int set = 0; + if (ws.ws_row == 0) { + ws.ws_row = 24; + set++; + } + if (ws.ws_col == 0) { + ws.ws_col = 80; + set++; + } + if (set) + ignore_result( ioctl(fd, TIOCSWINSZ, &ws) ); + } + + setlocale(LC_CTYPE, "POSIX"); + goto setattr; + } +#if defined(IUTF8) && defined(KDGKBMODE) + /* Handle mode of current keyboard setup, e.g. for UTF-8 */ + switch(mode) { + case K_UNICODE: + setlocale(LC_CTYPE, "C.UTF-8"); + flags |= UL_TTY_UTF8; + break; + case K_RAW: + case K_MEDIUMRAW: + case K_XLATE: + default: + setlocale(LC_CTYPE, "POSIX"); + break; + } +#else + setlocale(LC_CTYPE, "POSIX"); +#endif + reset_virtual_console(tio, flags); +setattr: + if (tcsetattr(fd, TCSANOW, tio)) + warn(_("tcsetattr failed")); + + /* Enable blocking mode for read and write */ + if ((flags = fcntl(fd, F_GETFL, 0)) != -1) + ignore_result( fcntl(fd, F_SETFL, flags & ~O_NONBLOCK) ); +} + +/* + * Finalize the tty modes on modem lines. + */ +static void tcfinal(struct console *con) +{ + struct termios *tio = &con->tio; + const int fd = con->fd; + + if ((con->flags & CON_SERIAL) == 0) { + xsetenv("TERM", "linux", 1); + return; + } + if (con->flags & CON_NOTTY) { + xsetenv("TERM", "dumb", 1); + return; + } + +#if defined (__s390__) || defined (__s390x__) + xsetenv("TERM", "dumb", 1); +#else + xsetenv("TERM", "vt102", 1); +#endif + tio->c_iflag |= (IXON | IXOFF); + tio->c_lflag |= (ICANON | ISIG | ECHO|ECHOE|ECHOK|ECHOKE); + tio->c_oflag |= OPOST; + + tio->c_cc[VINTR] = CINTR; + tio->c_cc[VQUIT] = CQUIT; + tio->c_cc[VERASE] = con->cp.erase; + tio->c_cc[VKILL] = con->cp.kill; + tio->c_cc[VEOF] = CEOF; +#ifdef VSWTC + tio->c_cc[VSWTC] = _POSIX_VDISABLE; +#elif defined(VSWTCH) + tio->c_cc[VSWTCH] = _POSIX_VDISABLE; +#endif + tio->c_cc[VSTART] = CSTART; + tio->c_cc[VSTOP] = CSTOP; + tio->c_cc[VSUSP] = CSUSP; + tio->c_cc[VEOL] = _POSIX_VDISABLE; + + if (con->cp.eol == CR) { + tio->c_iflag |= ICRNL; + tio->c_iflag &= ~(INLCR|IGNCR); + tio->c_oflag |= ONLCR; + tio->c_oflag &= ~(OCRNL|ONLRET); + } + + switch (con->cp.parity) { + default: + case 0: + tio->c_cflag &= ~(PARODD | PARENB); + tio->c_iflag &= ~(INPCK | ISTRIP); + break; + case 1: /* odd parity */ + tio->c_cflag |= PARODD; + /* fallthrough */ + case 2: /* even parity */ + tio->c_cflag |= PARENB; + tio->c_iflag |= (INPCK | ISTRIP); + /* fallthrough */ + case (1 | 2): /* no parity bit */ + tio->c_cflag &= ~CSIZE; + tio->c_cflag |= CS7; + break; + } + + /* Set line attributes */ + tcsetattr(fd, TCSANOW, tio); +} + +/* + * Called at timeout. + */ +static void alrm_handler(int sig __attribute__((unused))) +{ + /* Timeout expired */ + alarm_rised++; +} + +static void chld_handler(int sig __attribute__((unused))) +{ + sigchild++; +} + +static void mask_signal(int signal, void (*handler)(int), + struct sigaction *origaction) +{ + struct sigaction newaction; + + newaction.sa_handler = handler; + sigemptyset(&newaction.sa_mask); + newaction.sa_flags = 0; + + sigaction(signal, &newaction, origaction); +} + +static void unmask_signal(int signal, struct sigaction *sa) +{ + sigaction(signal, sa, NULL); +} + +/* + * See if an encrypted password is valid. The encrypted password is checked for + * traditional-style DES and FreeBSD-style MD5 encryption. + */ +static int valid(const char *pass) +{ + const char *s; + char id[5]; + size_t len; + off_t off; + + if (pass[0] == 0) + return 1; + if (pass[0] != '$') + goto check_des; + + /* + * up to 4 bytes for the signature e.g. $1$ + */ + for (s = pass+1; *s && *s != '$'; s++); + + if (*s++ != '$') + return 0; + + if ((off = (off_t)(s-pass)) > 4 || off < 3) + return 0; + + memset(id, '\0', sizeof(id)); + strncpy(id, pass, off); + + /* + * up to 16 bytes for the salt + */ + for (; *s && *s != '$'; s++); + + if (*s++ != '$') + return 0; + + if ((off_t)(s-pass) > 16) + return 0; + + len = strlen(s); + + /* + * the MD5 hash (128 bits or 16 bytes) encoded in base64 = 22 bytes + */ + if ((strcmp(id, "$1$") == 0) && (len < 22 || len > 24)) + return 0; + + /* + * the SHA-256 hash 43 bytes + */ + if ((strcmp(id, "$5$") == 0) && (len < 42 || len > 44)) + return 0; + + /* + * the SHA-512 hash 86 bytes + */ + if ((strcmp(id, "$6$") == 0) && (len < 85 || len > 87)) + return 0; + + /* + * e.g. Blowfish hash + */ + return 1; +check_des: + if (strlen(pass) != 13) + return 0; + + for (s = pass; *s; s++) { + if ((*s < '0' || *s > '9') && + (*s < 'a' || *s > 'z') && + (*s < 'A' || *s > 'Z') && + *s != '.' && *s != '/') + return 0; + } + return 1; +} + +/* + * Set a variable if the value is not NULL. + */ +static inline void set(char **var, char *val) +{ + if (val) + *var = val; +} + +/* + * Get the root password entry. + */ +static struct passwd *getrootpwent(int try_manually) +{ + static struct passwd pwd; + struct passwd *pw; + struct spwd *spw; + FILE *fp; + static char line[2 * BUFSIZ]; + static char sline[2 * BUFSIZ]; + char *p; + + /* + * First, we try to get the password the standard way using normal + * library calls. + */ + if ((pw = getpwnam("root")) && + !strcmp(pw->pw_passwd, "x") && + (spw = getspnam("root"))) + pw->pw_passwd = spw->sp_pwdp; + + if (pw || !try_manually) + return pw; + + /* + * If we come here, we could not retrieve the root password through + * library calls and we try to read the password and shadow files + * manually. + */ + pwd.pw_name = "root"; + pwd.pw_passwd = ""; + pwd.pw_gecos = "Super User"; + pwd.pw_dir = "/"; + pwd.pw_shell = ""; + pwd.pw_uid = 0; + pwd.pw_gid = 0; + + if ((fp = fopen(_PATH_PASSWD, "r")) == NULL) { + warn(_("cannot open %s"), _PATH_PASSWD); + return &pwd; + } + + /* + * Find root in the password file. + */ + while ((p = fgets(line, sizeof(line), fp)) != NULL) { + if (strncmp(line, "root:", 5) != 0) + continue; + p += 5; + set(&pwd.pw_passwd, strsep(&p, ":")); + strsep(&p, ":"); + strsep(&p, ":"); + set(&pwd.pw_gecos, strsep(&p, ":")); + set(&pwd.pw_dir, strsep(&p, ":")); + set(&pwd.pw_shell, strsep(&p, "\n")); + p = line; + break; + } + fclose(fp); + + /* + * If the encrypted password is valid or not found, return. + */ + if (p == NULL) { + warnx(_("%s: no entry for root\n"), _PATH_PASSWD); + return &pwd; + } + if (valid(pwd.pw_passwd)) + return &pwd; + + /* + * The password is invalid. If there is a shadow password, try it. + */ + *pwd.pw_passwd = '\0'; + if ((fp = fopen(_PATH_SHADOW_PASSWD, "r")) == NULL) { + warn(_("cannot open %s"), _PATH_PASSWD); + return &pwd; + } + while ((p = fgets(sline, sizeof(sline), fp)) != NULL) { + if (strncmp(sline, "root:", 5) != 0) + continue; + p += 5; + set(&pwd.pw_passwd, strsep(&p, ":")); + break; + } + fclose(fp); + + /* + * If the password is still invalid, NULL it, and return. + */ + if (p == NULL) { + warnx(_("%s: no entry for root"), _PATH_SHADOW_PASSWD); + *pwd.pw_passwd = '\0'; + } + /* locked account passwords are valid too */ + if (!locked_account_password(pwd.pw_passwd) && !valid(pwd.pw_passwd)) { + warnx(_("%s: root password garbled"), _PATH_SHADOW_PASSWD); + *pwd.pw_passwd = '\0'; + } + return &pwd; +} + +/* + * Ask by prompt for the password. + */ +static void doprompt(const char *crypted, struct console *con, int deny) +{ + struct termios tty; + + if (con->flags & CON_SERIAL) { + tty = con->tio; + /* + * For prompting: map NL in output to CR-NL + * otherwise we may see stairs in the output. + */ + tty.c_oflag |= (ONLCR | OPOST); + tcsetattr(con->fd, TCSADRAIN, &tty); + } + if (!con->file) { + con->file = fdopen(con->fd, "r+"); + if (!con->file) + goto err; + } + + if (deny) + fprintf(con->file, _("\nCannot open access to console, the root account is locked.\n" + "See sulogin(8) man page for more details.\n\n" + "Press Enter to continue.\n")); + else { +#if defined(USE_ONELINE) + if (crypted[0] && !locked_account_password(crypted)) + fprintf(con->file, _("Give root password for login: ")); + else + fprintf(con->file, _("Press Enter for login: ")); +#else + if (crypted[0] && !locked_account_password(crypted)) + fprintf(con->file, _("Give root password for maintenance\n")); + else + fprintf(con->file, _("Press Enter for maintenance\n")); + fprintf(con->file, _("(or press Control-D to continue): ")); +#endif + } + fflush(con->file); +err: + if (con->flags & CON_SERIAL) + tcsetattr(con->fd, TCSADRAIN, &con->tio); +} + +/* + * Make sure to have an own session and controlling terminal + */ +static void setup(struct console *con) +{ + int fd = con->fd; + const pid_t pid = getpid(), pgrp = getpgid(0), ppgrp = + getpgid(getppid()), ttypgrp = tcgetpgrp(fd); + + if (con->flags & CON_NOTTY) + return; + + /* + * Only go through this trouble if the new + * tty doesn't fall in this process group. + */ + if (pgrp != ttypgrp && ppgrp != ttypgrp) { + if (pid != getsid(0)) { + if (pid == getpgid(0)) + setpgid(0, getpgid(getppid())); + setsid(); + } + + mask_signal(SIGHUP, SIG_IGN, &saved_sighup); + if (ttypgrp > 0) + ioctl(STDIN_FILENO, TIOCNOTTY, (char *)1); + unmask_signal(SIGHUP, &saved_sighup); + if (fd > STDIN_FILENO) close(STDIN_FILENO); + if (fd > STDOUT_FILENO) close(STDOUT_FILENO); + if (fd > STDERR_FILENO) close(STDERR_FILENO); + + ioctl(fd, TIOCSCTTY, (char *)1); + tcsetpgrp(fd, ppgrp); + } + dup2(fd, STDIN_FILENO); + dup2(fd, STDOUT_FILENO); + dup2(fd, STDERR_FILENO); + con->fd = STDIN_FILENO; + + for (fd = STDERR_FILENO+1; fd < 32; fd++) { + if (openfd & (1<<fd)) { + close(fd); + openfd &= ~(1<<fd); + } + } +} + +/* + * Ask for the password. Note that there is no default timeout as we normally + * skip this during boot. + */ +static const char *getpasswd(struct console *con) +{ + struct sigaction sa; + struct termios tty; + static char pass[128], *ptr; + struct chardata *cp; + const char *ret = pass; + unsigned char tc; + char c, ascval; + int eightbit; + const int fd = con->fd; + + if (con->flags & CON_NOTTY) + goto out; + cp = &con->cp; + tty = con->tio; + + tty.c_iflag &= ~(IUCLC|IXON|IXOFF|IXANY); + tty.c_lflag &= ~(ECHO|ECHOE|ECHOK|ECHONL|TOSTOP|ISIG); + tc = (tcsetattr(fd, TCSAFLUSH, &tty) == 0); + + sigemptyset(&sa.sa_mask); + sa.sa_handler = alrm_handler; + sa.sa_flags = 0; + sigaction(SIGALRM, &sa, NULL); + + if (timeout) + alarm(timeout); + + ptr = &pass[0]; + cp->eol = *ptr = '\0'; + + eightbit = ((con->flags & CON_SERIAL) == 0 || (tty.c_cflag & (PARODD|PARENB)) == 0); + while (cp->eol == '\0') { + if (read(fd, &c, 1) < 1) { + if (errno == EINTR || errno == EAGAIN) { + if (alarm_rised) { + ret = NULL; + goto quit; + } + xusleep(250000); + continue; + } + ret = NULL; + switch (errno) { + case 0: + case EIO: + case ESRCH: + case EINVAL: + case ENOENT: + break; + default: + warn(_("cannot read %s"), con->tty); + break; + } + goto quit; + } + + if (eightbit) + ascval = c; + else if (c != (ascval = (c & 0177))) { + uint32_t bits, mask; + for (bits = 1, mask = 1; mask & 0177; mask <<= 1) { + if (mask & ascval) + bits++; + } + cp->parity |= ((bits & 1) ? 1 : 2); + } + + switch (ascval) { + case 0: + *ptr = '\0'; + goto quit; + case CR: + case NL: + *ptr = '\0'; + cp->eol = ascval; + break; + case BS: + case CERASE: + cp->erase = ascval; + if (ptr > &pass[0]) + ptr--; + break; + case CKILL: + cp->kill = ascval; + while (ptr > &pass[0]) + ptr--; + break; + case CEOF: + ret = NULL; + goto quit; + default: + if ((size_t)(ptr - &pass[0]) >= (sizeof(pass) -1 )) { + fprintf(stderr, "sulogin: input overrun at %s\n\r", con->tty); + ret = NULL; + goto quit; + } + *ptr++ = ascval; + break; + } + } +quit: + alarm(0); + if (tc) + tcsetattr(fd, TCSAFLUSH, &con->tio); + tcfinal(con); + printf("\r\n"); +out: + return ret; +} + +/* + * Password was OK, execute a shell. + */ +static void sushell(struct passwd *pwd) +{ + char shell[PATH_MAX]; + char home[PATH_MAX]; + char const *p; + char const *su_shell; + + /* + * Set directory and shell. + */ + if (chdir(pwd->pw_dir) != 0) { + warn(_("%s: change directory failed"), pwd->pw_dir); + printf(_("Logging in with home = \"/\".\n")); + + if (chdir("/") != 0) + warn(_("change directory to system root failed")); + } + + if ((p = getenv("SUSHELL")) != NULL) + su_shell = p; + else if ((p = getenv("sushell")) != NULL) + su_shell = p; + else { + if (pwd->pw_shell[0]) + su_shell = pwd->pw_shell; + else + su_shell = "/bin/sh"; + } + if ((p = strrchr(su_shell, '/')) == NULL) + p = su_shell; + else + p++; + + snprintf(shell, sizeof(shell), profile ? "-%s" : "%s", p); + + /* + * Set some important environment variables. + */ + if (getcwd(home, sizeof(home)) == NULL) + strcpy(home, "/"); + + xsetenv("HOME", home, 1); + xsetenv("LOGNAME", "root", 1); + xsetenv("USER", "root", 1); + if (!profile) + xsetenv("SHLVL","0",1); + + /* + * Try to execute a shell. + */ + xsetenv("SHELL", su_shell, 1); + unmask_signal(SIGINT, &saved_sigint); + unmask_signal(SIGTSTP, &saved_sigtstp); + unmask_signal(SIGQUIT, &saved_sigquit); + mask_signal(SIGHUP, SIG_DFL, NULL); + +#ifdef HAVE_LIBSELINUX + if (is_selinux_enabled() > 0) { + security_context_t scon=NULL; + char *seuser=NULL; + char *level=NULL; + if (getseuserbyname("root", &seuser, &level) == 0) { + if (get_default_context_with_level(seuser, level, 0, &scon) == 0) { + if (setexeccon(scon) != 0) + warnx(_("setexeccon failed")); + freecon(scon); + } + } + free(seuser); + free(level); + } +#endif + execl(su_shell, shell, NULL); + warn(_("failed to execute %s"), su_shell); + + xsetenv("SHELL", "/bin/sh", 1); + execl("/bin/sh", profile ? "-sh" : "sh", NULL); + warn(_("failed to execute %s"), "/bin/sh"); +} + +static void usage(void) +{ + FILE *out = stdout; + fputs(USAGE_HEADER, out); + fprintf(out, _( + " %s [options] [tty device]\n"), program_invocation_short_name); + + fputs(USAGE_SEPARATOR, out); + fputs(_("Single-user login.\n"), out); + + fputs(USAGE_OPTIONS, out); + fputs(_(" -p, --login-shell start a login shell\n" + " -t, --timeout <seconds> max time to wait for a password (default: no limit)\n" + " -e, --force examine password files directly if getpwnam(3) fails\n"), + out); + + fputs(USAGE_SEPARATOR, out); + printf(USAGE_HELP_OPTIONS(26)); + printf(USAGE_MAN_TAIL("sulogin(8)")); +} + +int main(int argc, char **argv) +{ + struct list_head *ptr, consoles; + struct console *con; + char *tty = NULL; + struct passwd *pwd; + const struct timespec sigwait = { .tv_sec = 0, .tv_nsec = 50000000 }; + siginfo_t status = { 0 }; + sigset_t set; + int c, reconnect = 0; + int opt_e = 0; + int wait = 0; + pid_t pid; + + static const struct option longopts[] = { + { "login-shell", no_argument, NULL, 'p' }, + { "timeout", required_argument, NULL, 't' }, + { "force", no_argument, NULL, 'e' }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { NULL, 0, NULL, 0 } + }; + + INIT_LIST_HEAD(&consoles); + + /* + * If we are init we need to set up a own session. + */ + if ((pid = getpid()) == 1) { + setsid(); + ignore_result( ioctl(STDIN_FILENO, TIOCSCTTY, (char *) 1) ); + } + + setlocale(LC_ALL, ""); + bindtextdomain(PACKAGE, LOCALEDIR); + textdomain(PACKAGE); + atexit(close_stdout); /* XXX */ + + /* + * See if we have a timeout flag. + */ + while ((c = getopt_long(argc, argv, "ehpt:V", longopts, NULL)) != -1) { + switch(c) { + case 't': + timeout = strtou32_or_err(optarg, _("invalid timeout argument")); + break; + case 'p': + profile = 1; + break; + case 'e': + opt_e = 1; + break; + case 'V': + printf(UTIL_LINUX_VERSION); + return EXIT_SUCCESS; + case 'h': + usage(); + return EXIT_SUCCESS; + default: + /* Do not exit! getopt prints a warning. */ + break; + } + } + + if (geteuid() != 0) + errx(EXIT_FAILURE, _("only superuser can run this program")); + + mask_signal(SIGQUIT, SIG_IGN, &saved_sigquit); + mask_signal(SIGTSTP, SIG_IGN, &saved_sigtstp); + mask_signal(SIGINT, SIG_IGN, &saved_sigint); + mask_signal(SIGHUP, SIG_IGN, &saved_sighup); + + + emergency_do_mounts(); + atexit( emergency_do_umounts ); + + /* + * See if we need to open an other tty device. + */ + if (optind < argc) + tty = argv[optind]; + + if (!tty || *tty == '\0') + tty = getenv("CONSOLE"); + + /* + * Detect possible consoles, use stdin as fallback. + * If an optional tty is given, reconnect it to stdin. + */ + reconnect = detect_consoles(tty, STDIN_FILENO, &consoles); + + /* + * If previous stdin was not the specified tty and therefore reconnected + * to the specified tty also reconnect stdout and stderr. + */ + if (reconnect) { + if (isatty(STDOUT_FILENO) == 0) + dup2(STDOUT_FILENO, STDIN_FILENO); + if (isatty(STDERR_FILENO) == 0) + dup2(STDOUT_FILENO, STDERR_FILENO); + } + + /* + * Should not happen + */ + if (list_empty(&consoles)) { + if (!errno) + errno = ENOENT; + err(EXIT_FAILURE, _("cannot open console")); + } + + /* + * Get the root password. + */ + if ((pwd = getrootpwent(opt_e)) == NULL) { + warnx(_("cannot open password database")); + sleep(2); + return EXIT_FAILURE; + } + + /* + * Ask for the password on the consoles. + */ + list_for_each(ptr, &consoles) { + con = list_entry(ptr, struct console, entry); + if (con->id >= CONMAX) + break; + if (con->fd >= 0) { + openfd |= (1 << con->fd); + tcinit(con); + continue; + } + if ((con->fd = open(con->tty, O_RDWR | O_NOCTTY | O_NONBLOCK)) < 0) + continue; + openfd |= (1 << con->fd); + tcinit(con); + } + ptr = (&consoles)->next; + + if (ptr->next == &consoles) { + con = list_entry(ptr, struct console, entry); + goto nofork; + } + + + mask_signal(SIGCHLD, chld_handler, &saved_sigchld); + do { + con = list_entry(ptr, struct console, entry); + if (con->id >= CONMAX) + break; + + switch ((con->pid = fork())) { + case 0: + mask_signal(SIGCHLD, SIG_DFL, NULL); + nofork: + setup(con); + while (1) { + const char *passwd = pwd->pw_passwd; + const char *answer; + int doshell = 0; + int deny = !opt_e && locked_account_password(pwd->pw_passwd); + + doprompt(passwd, con, deny); + + if ((answer = getpasswd(con)) == NULL) + break; + if (deny) + exit(EXIT_FAILURE); + + /* no password or locked account */ + if (!passwd[0] || locked_account_password(passwd)) + doshell++; + else { + const char *cryptbuf; + cryptbuf = crypt(answer, passwd); + if (cryptbuf == NULL) + warn(_("crypt failed")); + else if (strcmp(cryptbuf, pwd->pw_passwd) == 0) + doshell++; + } + + if (doshell) { + /* sushell() unmask signals */ + sushell(pwd); + + mask_signal(SIGQUIT, SIG_IGN, &saved_sigquit); + mask_signal(SIGTSTP, SIG_IGN, &saved_sigtstp); + mask_signal(SIGINT, SIG_IGN, &saved_sigint); + + fprintf(stderr, _("cannot execute su shell\n\n")); + break; + } + fprintf(stderr, _("Login incorrect\n\n")); + } + if (alarm_rised) { + tcfinal(con); + warnx(_("Timed out\n\n")); + } + /* + * User pressed Control-D. + */ + exit(0); + case -1: + warn(_("fork failed")); + /* fallthrough */ + default: + break; + } + + ptr = ptr->next; + + } while (ptr != &consoles); + + do { + int ret; + + status.si_pid = 0; + ret = waitid(P_ALL, 0, &status, WEXITED); + + if (ret == 0) + break; + if (ret < 0) { + if (errno == ECHILD) + break; + if (errno == EINTR) + continue; + } + + errx(EXIT_FAILURE, _("cannot wait on su shell\n\n")); + + } while (1); + + list_for_each(ptr, &consoles) { + con = list_entry(ptr, struct console, entry); + + if (con->fd < 0) + continue; + if (con->pid < 0) + continue; + if (con->pid == status.si_pid) + con->pid = -1; + else { + kill(con->pid, SIGTERM); + wait++; + } + } + + sigemptyset(&set); + sigaddset(&set, SIGCHLD); + + do { + int signum, ret; + + if (!wait) + break; + + status.si_pid = 0; + ret = waitid(P_ALL, 0, &status, WEXITED|WNOHANG); + + if (ret < 0) { + if (errno == ECHILD) + break; + if (errno == EINTR) + continue; + } + + if (!ret && status.si_pid > 0) { + list_for_each(ptr, &consoles) { + con = list_entry(ptr, struct console, entry); + + if (con->fd < 0) + continue; + if (con->pid < 0) + continue; + if (con->pid == status.si_pid) { + con->pid = -1; + wait--; + } + } + continue; + } + + signum = sigtimedwait(&set, NULL, &sigwait); + if (signum != SIGCHLD && signum < 0 && errno == EAGAIN) + break; + + } while (1); + + mask_signal(SIGCHLD, SIG_DFL, NULL); + return EXIT_SUCCESS; +} diff --git a/login-utils/utmpdump.1 b/login-utils/utmpdump.1 new file mode 100644 index 0000000..1f458e8 --- /dev/null +++ b/login-utils/utmpdump.1 @@ -0,0 +1,93 @@ +.\" Copyright (C) 2010 Michael Krapp +.\" +.\" This program is free software; you can redistribute it and/or modify +.\" it under the terms of the GNU General Public License as published by +.\" the Free Software Foundation; either version 2 of the License, or +.\" (at your option) any later version. +.\" +.\" This program is distributed in the hope that it will be useful, +.\" but WITHOUT ANY WARRANTY; without even the implied warranty of +.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +.\" GNU General Public License for more details. +.\" +.\" You should have received a copy of the GNU General Public License +.\" along with this program; if not, write to the Free Software +.\" Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +.\" +.TH UTMPDUMP 1 "July 2014" "util-linux" "User Commands" +.SH NAME +utmpdump \- dump UTMP and WTMP files in raw format +.SH SYNOPSIS +.BR utmpdump " [options]" +.RI [ filename ] +.SH DESCRIPTION +.B utmpdump +is a simple program to dump UTMP and WTMP files in raw format, so they +can be examined. +.B utmpdump +reads from stdin unless a +.I filename +is passed. +.SH OPTIONS +.TP +.BR \-f , " \-\-follow" +Output appended data as the file grows. +.TP +.BR \-o , " \-\-output " \fIfile +Write command output to \fIfile\fR instead of standard output. +.TP +.BR \-r , " \-\-reverse" +Undump, write back edited login information into the utmp or wtmp files. +.TP +.BR \-V , " \-\-version" +Display version information and exit. +.TP +.BR \-h , " \-\-help" +Display help text and exit. +.SH NOTES +.B utmpdump +can be useful in cases of corrupted utmp or wtmp entries. It can dump +out utmp/wtmp to an ASCII file, which can then be edited to remove +bogus entries, and reintegrated using: +.PP +.RS +.B utmpdump -r < ascii_file > wtmp +.RE +.PP +But be warned, +.B utmpdump +was written for debugging purposes only. +.SS File formats +.PP +The only binary version of the +.BR utmp (5) +is standardised. Textual dumps may become incompatible in future. +.PP +The version 2.28 was the last one that printed text output using +.BR ctime (3) +timestamp format. Newer dumps use millisecond precision ISO-8601 timestamp +format in UTC-0 timezone. Conversion from former timestamp format can be +made to binary, although attempt to do so can lead the timestamps to drift +amount of timezone offset. +.SH BUGS +You may +.B not +use the +.B \-r +option, as the format for the utmp/wtmp files strongly depends on the input +format. This tool was +.B not +written for normal use, but for debugging only. +.SH AUTHOR +Michael Krapp +.SH "SEE ALSO" +.BR last (1), +.BR w (1), +.BR who (1), +.BR utmp (5) +.SH AVAILABILITY +The utmpdump command is part of the util-linux package and is available +from +.UR https://\:www.kernel.org\:/pub\:/linux\:/utils\:/util-linux/ +Linux Kernel Archive +.UE . diff --git a/login-utils/utmpdump.c b/login-utils/utmpdump.c new file mode 100644 index 0000000..5cc8783 --- /dev/null +++ b/login-utils/utmpdump.c @@ -0,0 +1,397 @@ +/* + * utmpdump + * + * Simple program to dump UTMP and WTMP files in raw format, so they can be + * examined. + * + * Based on utmpdump dump from sysvinit suite. + * + * Copyright (C) 1991-2000 Miquel van Smoorenburg <miquels@cistron.nl> + * + * Copyright (C) 1998 Danek Duvall <duvall@alumni.princeton.edu> + * Copyright (C) 2012 Karel Zak <kzak@redhat.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <utmpx.h> +#include <time.h> +#include <ctype.h> +#include <getopt.h> +#include <unistd.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <sys/stat.h> +#ifdef HAVE_INOTIFY_INIT +#include <sys/inotify.h> +#endif + +#include "c.h" +#include "nls.h" +#include "xalloc.h" +#include "closestream.h" +#include "timeutils.h" + +static time_t strtotime(const char *s_time) +{ + struct tm tm; + + memset(&tm, '\0', sizeof(struct tm)); + + if (s_time[0] == ' ' || s_time[0] == '\0') + return (time_t)0; + + if (isdigit(s_time[0])) { + /* [1998-09-01T01:00:00,000000+00:00] + * Subseconds are parsed with strtousec(). Timezone is + * always UTC-0 */ + strptime(s_time, "%Y-%m-%dT%H:%M:%S", &tm); + } else { + /* [Tue Sep 01 00:00:00 1998 GMT] */ + strptime(s_time, "%a %b %d %T %Y", &tm); + /* Cheesy way of checking for DST. This could be needed + * with legacy dumps that used localtime(3). */ + if (s_time[26] == 'D') + tm.tm_isdst = 1; + } + return timegm(&tm); +} + +static suseconds_t strtousec(const char *s_time) +{ + const char *s = strchr(s_time, ','); + if (s) + return (suseconds_t) atoi(s + 1); + return 0; +} + +#define cleanse(x) xcleanse(x, sizeof(x)) +static void xcleanse(char *s, int len) +{ + for ( ; *s && len-- > 0; s++) + if (!isprint(*s) || *s == '[' || *s == ']') + *s = '?'; +} + +static void print_utline(struct utmpx *ut, FILE *out) +{ + const char *addr_string; + char buffer[INET6_ADDRSTRLEN]; + char time_string[40]; + struct timeval tv; + + if (ut->ut_addr_v6[1] || ut->ut_addr_v6[2] || ut->ut_addr_v6[3]) + addr_string = inet_ntop(AF_INET6, &(ut->ut_addr_v6), buffer, sizeof(buffer)); + else + addr_string = inet_ntop(AF_INET, &(ut->ut_addr_v6), buffer, sizeof(buffer)); + + tv.tv_sec = ut->ut_tv.tv_sec; + tv.tv_usec = ut->ut_tv.tv_usec; + + if (strtimeval_iso(&tv, ISO_TIMESTAMP_COMMA_GT, time_string, + sizeof(time_string)) != 0) + return; + cleanse(ut->ut_id); + cleanse(ut->ut_user); + cleanse(ut->ut_line); + cleanse(ut->ut_host); + + /* type pid id user line host addr time */ + fprintf(out, "[%d] [%05d] [%-4.4s] [%-*.*s] [%-*.*s] [%-*.*s] [%-15s] [%s]\n", + ut->ut_type, ut->ut_pid, ut->ut_id, + 8, (int)sizeof(ut->ut_user), ut->ut_user, + 12, (int)sizeof(ut->ut_line), ut->ut_line, + 20, (int)sizeof(ut->ut_host), ut->ut_host, + addr_string, time_string); +} + +#ifdef HAVE_INOTIFY_INIT +#define EVENTS (IN_MODIFY|IN_DELETE_SELF|IN_MOVE_SELF|IN_UNMOUNT) +#define NEVENTS 4 + +static void roll_file(const char *filename, off_t *size, FILE *out) +{ + FILE *in; + struct stat st; + struct utmpx ut; + off_t pos; + + if (!(in = fopen(filename, "r"))) + err(EXIT_FAILURE, _("cannot open %s"), filename); + + if (fstat(fileno(in), &st) == -1) + err(EXIT_FAILURE, _("stat of %s failed"), filename); + + if (st.st_size == *size) + goto done; + + if (fseek(in, *size, SEEK_SET) != (off_t) -1) { + while (fread(&ut, sizeof(ut), 1, in) == 1) + print_utline(&ut, out); + } + + pos = ftello(in); + /* If we've successfully read something, use the file position, this + * avoids data duplication. If we read nothing or hit an error, + * reset to the reported size, this handles truncated files. + */ + *size = (pos != -1 && pos != *size) ? pos : st.st_size; + +done: + fclose(in); +} + +static int follow_by_inotify(FILE *in, const char *filename, FILE *out) +{ + char buf[NEVENTS * sizeof(struct inotify_event)]; + int fd, wd, event; + ssize_t length; + off_t size; + + fd = inotify_init(); + if (fd == -1) + return -1; /* probably reached any limit ... */ + + size = ftello(in); + fclose(in); + + wd = inotify_add_watch(fd, filename, EVENTS); + if (wd == -1) + err(EXIT_FAILURE, _("%s: cannot add inotify watch."), filename); + + while (wd >= 0) { + errno = 0; + length = read(fd, buf, sizeof(buf)); + + if (length < 0 && (errno == EINTR || errno == EAGAIN)) + continue; + if (length < 0) + err(EXIT_FAILURE, _("%s: cannot read inotify events"), + filename); + + for (event = 0; event < length;) { + struct inotify_event *ev = + (struct inotify_event *) &buf[event]; + + if (ev->mask & IN_MODIFY) + roll_file(filename, &size, out); + else { + close(wd); + wd = -1; + break; + } + event += sizeof(struct inotify_event) + ev->len; + } + } + + close(fd); + return 0; +} +#endif /* HAVE_INOTIFY_INIT */ + +static FILE *dump(FILE *in, const char *filename, int follow, FILE *out) +{ + struct utmpx ut; + + if (follow) + ignore_result( fseek(in, -10 * sizeof(ut), SEEK_END) ); + + while (fread(&ut, sizeof(ut), 1, in) == 1) + print_utline(&ut, out); + + if (!follow) + return in; + +#ifdef HAVE_INOTIFY_INIT + if (follow_by_inotify(in, filename, out) == 0) + return NULL; /* file already closed */ + else +#endif + /* fallback for systems without inotify or with non-free + * inotify instances */ + for (;;) { + while (fread(&ut, sizeof(ut), 1, in) == 1) + print_utline(&ut, out); + sleep(1); + } + + return in; +} + + +/* This function won't work properly if there's a ']' or a ' ' in the real + * token. Thankfully, this should never happen. */ +static int gettok(char *line, char *dest, int size, int eatspace) +{ + int bpos, epos, eaten; + + bpos = strchr(line, '[') - line; + if (bpos < 0) + errx(EXIT_FAILURE, _("Extraneous newline in file. Exiting.")); + + line += 1 + bpos; + epos = strchr(line, ']') - line; + if (epos < 0) + errx(EXIT_FAILURE, _("Extraneous newline in file. Exiting.")); + + line[epos] = '\0'; + eaten = bpos + epos + 1; + + if (eatspace) { + char *t; + if ((t = strchr(line, ' '))) + *t = 0; + } + strncpy(dest, line, size); + + return eaten + 1; +} + +static void undump(FILE *in, FILE *out) +{ + struct utmpx ut; + char s_addr[INET6_ADDRSTRLEN + 1], s_time[29], *linestart, *line; + + linestart = xmalloc(1024 * sizeof(*linestart)); + s_time[28] = 0; + + while (fgets(linestart, 1023, in)) { + line = linestart; + memset(&ut, '\0', sizeof(ut)); + sscanf(line, "[%hd] [%d] [%4c] ", &ut.ut_type, &ut.ut_pid, ut.ut_id); + + line += 19; + line += gettok(line, ut.ut_user, sizeof(ut.ut_user), 1); + line += gettok(line, ut.ut_line, sizeof(ut.ut_line), 1); + line += gettok(line, ut.ut_host, sizeof(ut.ut_host), 1); + line += gettok(line, s_addr, sizeof(s_addr) - 1, 1); + gettok(line, s_time, sizeof(s_time) - 1, 0); + if (strchr(s_addr, '.')) + inet_pton(AF_INET, s_addr, &(ut.ut_addr_v6)); + else + inet_pton(AF_INET6, s_addr, &(ut.ut_addr_v6)); + + ut.ut_tv.tv_sec = strtotime(s_time); + ut.ut_tv.tv_usec = strtousec(s_time); + + ignore_result( fwrite(&ut, sizeof(ut), 1, out) ); + } + + free(linestart); +} + +static void __attribute__((__noreturn__)) usage(void) +{ + FILE *out = stdout; + fputs(USAGE_HEADER, out); + + fprintf(out, + _(" %s [options] [filename]\n"), program_invocation_short_name); + + fputs(USAGE_SEPARATOR, out); + fputs(_("Dump UTMP and WTMP files in raw format.\n"), out); + + fputs(USAGE_OPTIONS, out); + fputs(_(" -f, --follow output appended data as the file grows\n"), out); + fputs(_(" -r, --reverse write back dumped data into utmp file\n"), out); + fputs(_(" -o, --output <file> write to file instead of standard output\n"), out); + printf(USAGE_HELP_OPTIONS(22)); + + printf(USAGE_MAN_TAIL("utmpdump(1)")); + exit(EXIT_SUCCESS); +} + +int main(int argc, char **argv) +{ + int c; + FILE *in = NULL, *out = NULL; + int reverse = 0, follow = 0; + const char *filename = NULL; + + static const struct option longopts[] = { + { "follow", no_argument, NULL, 'f' }, + { "reverse", no_argument, NULL, 'r' }, + { "output", required_argument, NULL, 'o' }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { NULL, 0, NULL, 0 } + }; + + setlocale(LC_ALL, ""); + bindtextdomain(PACKAGE, LOCALEDIR); + textdomain(PACKAGE); + atexit(close_stdout); + + while ((c = getopt_long(argc, argv, "fro:hV", longopts, NULL)) != -1) { + switch (c) { + case 'r': + reverse = 1; + break; + + case 'f': + follow = 1; + break; + + case 'o': + out = fopen(optarg, "w"); + if (!out) + err(EXIT_FAILURE, _("cannot open %s"), + optarg); + break; + + case 'h': + usage(); + break; + case 'V': + printf(UTIL_LINUX_VERSION); + return EXIT_SUCCESS; + default: + errtryhelp(EXIT_FAILURE); + } + } + + if (!out) + out = stdout; + + if (optind < argc) { + filename = argv[optind]; + in = fopen(filename, "r"); + if (!in) + err(EXIT_FAILURE, _("cannot open %s"), filename); + } else { + if (follow) + errx(EXIT_FAILURE, _("following standard input is unsupported")); + filename = "/dev/stdin"; + in = stdin; + } + + if (reverse) { + fprintf(stderr, _("Utmp undump of %s\n"), filename); + undump(in, out); + } else { + fprintf(stderr, _("Utmp dump of %s\n"), filename); + in = dump(in, filename, follow, out); + } + + if (out != stdout && close_stream(out)) + err(EXIT_FAILURE, _("write failed")); + + if (in && in != stdin) + fclose(in); + + return EXIT_SUCCESS; +} diff --git a/login-utils/vigr.8 b/login-utils/vigr.8 new file mode 100644 index 0000000..ff72d7a --- /dev/null +++ b/login-utils/vigr.8 @@ -0,0 +1 @@ +.so man8/vipw.8 diff --git a/login-utils/vipw.8 b/login-utils/vipw.8 new file mode 100644 index 0000000..41aa36e --- /dev/null +++ b/login-utils/vipw.8 @@ -0,0 +1,89 @@ +.\" Copyright (c) 1983, 1991 The Regents of the University of California. +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" 3. All advertising materials mentioning features or use of this software +.\" must display the following acknowledgement: +.\" This product includes software developed by the University of +.\" California, Berkeley and its contributors. +.\" 4. Neither the name of the University nor the names of its contributors +.\" may be used to endorse or promote products derived from this software +.\" without specific prior written permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +.\" ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +.\" SUCH DAMAGE. +.\" +.\" @(#)vipw.8 6.7 (Berkeley) 3/16/91 +.\" +.TH VIPW "8" "September 2011" "util-linux" "System Administration" +.SH NAME +vipw, vigr \- edit the password or group file +.SH SYNOPSIS +.B vipw +[options] +.br +.B vigr +[options] +.SH DESCRIPTION +.B vipw +edits the password file after setting the appropriate locks, +and does any necessary processing after the password file is unlocked. +If the password file is already locked for editing by another user, +.B vipw +will ask you +to try again later. The default editor for +.B vipw +and +.B vigr +is +.BR vi (1). +.B vigr +edits the group file in the same manner as +.B vipw +does the passwd file. +.SH ENVIRONMENT +If the following environment variable exists, it will be utilized by +.B vipw +and +.BR vigr : +.I +.TP +.B EDITOR +The editor specified by the string +.B EDITOR +will be invoked instead of the default editor +.BR vi (1). +.SH SEE ALSO +.BR vi (1), +.BR passwd (1), +.BR flock (2), +.BR passwd (5) +.SH HISTORY +The +.B vipw +command appeared in 4.0BSD. +.br +The +.B vigr +command appeared in Util-Linux 2.6. +.SH AVAILABILITY +The vigr and vipw commands are part of the util-linux package and are available from +.UR https://\:www.kernel.org\:/pub\:/linux\:/utils\:/util-linux/ +Linux Kernel Archive +.UE . diff --git a/login-utils/vipw.c b/login-utils/vipw.c new file mode 100644 index 0000000..2502fcb --- /dev/null +++ b/login-utils/vipw.c @@ -0,0 +1,373 @@ +/* + * Copyright (c) 1987 Regents of the University of California. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * This product includes software developed by the University of + * California, Berkeley and its contributors. + * 4. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * Updated Thu Oct 12 09:56:55 1995 by faith@cs.unc.edu with security + * patches from Zefram <A.Main@dcs.warwick.ac.uk> + * + * Updated Thu Nov 9 21:58:53 1995 by Martin Schulze + * <joey@finlandia.infodrom.north.de>. Support for vigr. + * + * Martin Schulze's patches adapted to Util-Linux by Nicolai Langfeldt. + * + * 1999-02-22 Arkadiusz Miśkiewicz <misiek@pld.ORG.PL> + * - added Native Language Support + * Sun Mar 21 1999 - Arnaldo Carvalho de Melo <acme@conectiva.com.br> + * - fixed strerr(errno) in gettext calls + */ + +#include <errno.h> +#include <fcntl.h> +#include <paths.h> +#include <pwd.h> +#include <shadow.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/file.h> +#include <sys/param.h> +#include <sys/resource.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> +#include <getopt.h> + +#include "c.h" +#include "fileutils.h" +#include "closestream.h" +#include "nls.h" +#include "setpwnam.h" +#include "strutils.h" +#include "xalloc.h" +#include "rpmatch.h" + +#ifdef HAVE_LIBSELINUX +# include <selinux/selinux.h> +#endif + +#define FILENAMELEN 67 + +enum { + VIPW, + VIGR +}; +static int program; +static char orig_file[FILENAMELEN]; /* original file /etc/passwd or /etc/group */ +static char *tmp_file; /* tmp file */ + +void pw_error (char *, int, int); + +static void copyfile(int from, int to) +{ + int nr, nw, off; + char buf[8 * 1024]; + + while ((nr = read(from, buf, sizeof(buf))) > 0) + for (off = 0; off < nr; nr -= nw, off += nw) + if ((nw = write(to, buf + off, nr)) < 0) + pw_error(tmp_file, 1, 1); + + if (nr < 0) + pw_error(orig_file, 1, 1); +#ifdef HAVE_EXPLICIT_BZERO + explicit_bzero(buf, sizeof(buf)); +#endif +} + +static void pw_init(void) +{ + struct rlimit rlim; + + /* Unlimited resource limits. */ + rlim.rlim_cur = rlim.rlim_max = RLIM_INFINITY; + (void)setrlimit(RLIMIT_CPU, &rlim); + (void)setrlimit(RLIMIT_FSIZE, &rlim); + (void)setrlimit(RLIMIT_STACK, &rlim); + (void)setrlimit(RLIMIT_DATA, &rlim); + (void)setrlimit(RLIMIT_RSS, &rlim); + + /* Don't drop core (not really necessary, but GP's). */ + rlim.rlim_cur = rlim.rlim_max = 0; + (void)setrlimit(RLIMIT_CORE, &rlim); + + /* Turn off signals. */ + (void)signal(SIGALRM, SIG_IGN); + (void)signal(SIGHUP, SIG_IGN); + (void)signal(SIGINT, SIG_IGN); + (void)signal(SIGPIPE, SIG_IGN); + (void)signal(SIGQUIT, SIG_IGN); + (void)signal(SIGTERM, SIG_IGN); + (void)signal(SIGTSTP, SIG_IGN); + (void)signal(SIGTTOU, SIG_IGN); + + /* Create with exact permissions. */ + (void)umask(0); +} + +static FILE * pw_tmpfile(int lockfd) +{ + FILE *fd; + char *tmpname = NULL; + + if ((fd = xfmkstemp(&tmpname, "/etc", ".vipw")) == NULL) { + ulckpwdf(); + err(EXIT_FAILURE, _("can't open temporary file")); + } + + copyfile(lockfd, fileno(fd)); + tmp_file = tmpname; + return fd; +} + +static void pw_write(void) +{ + char tmp[FILENAMELEN + 4]; + + sprintf(tmp, "%s%s", orig_file, ".OLD"); + unlink(tmp); + + if (link(orig_file, tmp)) + warn(_("%s: create a link to %s failed"), orig_file, tmp); + +#ifdef HAVE_LIBSELINUX + if (is_selinux_enabled() > 0) { + security_context_t passwd_context = NULL; + int ret = 0; + if (getfilecon(orig_file, &passwd_context) < 0) { + warnx(_("Can't get context for %s"), orig_file); + pw_error(orig_file, 1, 1); + } + ret = setfilecon(tmp_file, passwd_context); + freecon(passwd_context); + if (ret != 0) { + warnx(_("Can't set context for %s"), tmp_file); + pw_error(tmp_file, 1, 1); + } + } +#endif + + if (rename(tmp_file, orig_file) == -1) { + int errsv = errno; + errx(EXIT_FAILURE, + ("cannot write %s: %s (your changes are still in %s)"), + orig_file, strerror(errsv), tmp_file); + } + unlink(tmp_file); + free(tmp_file); + tmp_file = NULL; +} + +static void pw_edit(void) +{ + int pstat; + pid_t pid; + char *p, *editor, *tk; + + editor = getenv("EDITOR"); + editor = xstrdup(editor ? editor : _PATH_VI); + + tk = strtok(editor, " \t"); + if (tk && (p = strrchr(tk, '/')) != NULL) + ++p; + else + p = editor; + + pid = fork(); + if (pid < 0) + err(EXIT_FAILURE, _("fork failed")); + + if (!pid) { + execlp(editor, p, tmp_file, NULL); + errexec(editor); + } + for (;;) { + pid = waitpid(pid, &pstat, WUNTRACED); + if (WIFSTOPPED(pstat)) { + /* the editor suspended, so suspend us as well */ + kill(getpid(), SIGSTOP); + kill(pid, SIGCONT); + } else { + break; + } + } + if (pid == -1 || !WIFEXITED(pstat) || WEXITSTATUS(pstat) != 0) + pw_error(editor, 1, 1); + + free(editor); +} + +void __attribute__((__noreturn__)) +pw_error(char *name, int err, int eval) +{ + if (err) { + if (name) + warn("%s: ", name); + else + warn(NULL); + } + warnx(_("%s unchanged"), orig_file); + + if (tmp_file) + unlink(tmp_file); + ulckpwdf(); + exit(eval); +} + +static void edit_file(int is_shadow) +{ + struct stat begin, end; + int passwd_file, ch_ret; + FILE *tmp_fd; + + pw_init(); + + /* acquire exclusive lock */ + if (lckpwdf() < 0) + err(EXIT_FAILURE, _("cannot get lock")); + + passwd_file = open(orig_file, O_RDONLY | O_CLOEXEC, 0); + if (passwd_file < 0) + err(EXIT_FAILURE, _("cannot open %s"), orig_file); + tmp_fd = pw_tmpfile(passwd_file); + + if (fstat(fileno(tmp_fd), &begin)) + pw_error(tmp_file, 1, 1); + + pw_edit(); + + if (fstat(fileno(tmp_fd), &end)) + pw_error(tmp_file, 1, 1); + /* Some editors, such as Vim with 'writebackup' mode enabled, + * use "atomic save" in which the old file is deleted and a new + * one with the same name created in its place. */ + if (end.st_nlink == 0) { + if (close_stream(tmp_fd) != 0) + err(EXIT_FAILURE, _("write error")); + tmp_fd = fopen(tmp_file, "r" UL_CLOEXECSTR); + if (!tmp_fd) + err(EXIT_FAILURE, _("cannot open %s"), tmp_file); + if (fstat(fileno(tmp_fd), &end)) + pw_error(tmp_file, 1, 1); + } + if (begin.st_mtime == end.st_mtime) { + warnx(_("no changes made")); + pw_error((char *)NULL, 0, 0); + } + /* pw_tmpfile() will create the file with mode 600 */ + if (!is_shadow) + ch_ret = fchmod(fileno(tmp_fd), 0644); + else + ch_ret = fchmod(fileno(tmp_fd), 0400); + if (ch_ret < 0) + err(EXIT_FAILURE, "%s: %s", _("cannot chmod file"), orig_file); + if (close_stream(tmp_fd) != 0) + err(EXIT_FAILURE, _("write error")); + pw_write(); + close(passwd_file); + ulckpwdf(); +} + +static void __attribute__((__noreturn__)) usage(void) +{ + FILE *out = stdout; + fputs(USAGE_HEADER, out); + fprintf(out, " %s\n", program_invocation_short_name); + + fputs(USAGE_SEPARATOR, out); + fputs(_("Edit the password or group file.\n"), out); + + fputs(USAGE_OPTIONS, out); + printf(USAGE_HELP_OPTIONS(16)); + printf(USAGE_MAN_TAIL("vipw(8)")); + exit(EXIT_SUCCESS); +} + +int main(int argc, char *argv[]) +{ + int c; + static const struct option longopts[] = { + {"version", no_argument, NULL, 'V'}, + {"help", no_argument, NULL, 'h'}, + {NULL, 0, NULL, 0} + }; + + setlocale(LC_ALL, ""); + bindtextdomain(PACKAGE, LOCALEDIR); + textdomain(PACKAGE); + atexit(close_stdout); + + if (!strcmp(program_invocation_short_name, "vigr")) { + program = VIGR; + xstrncpy(orig_file, GROUP_FILE, sizeof(orig_file)); + } else { + program = VIPW; + xstrncpy(orig_file, PASSWD_FILE, sizeof(orig_file)); + } + + while ((c = getopt_long(argc, argv, "Vh", longopts, NULL)) != -1) + switch (c) { + case 'V': + printf(UTIL_LINUX_VERSION); + return EXIT_SUCCESS; + case 'h': + usage(); + default: + errtryhelp(EXIT_FAILURE); + } + + edit_file(0); + + if (program == VIGR) { + strncpy(orig_file, SGROUP_FILE, FILENAMELEN - 1); + } else { + strncpy(orig_file, SHADOW_FILE, FILENAMELEN - 1); + } + + if (access(orig_file, F_OK) == 0) { + char response[80]; + + fputs((program == VIGR) + ? _("You are using shadow groups on this system.\n") + : _("You are using shadow passwords on this system.\n"), stdout); + + /* TRANSLATORS: this program uses for y and n rpmatch(3), + * which means they can be translated. */ + printf(_("Would you like to edit %s now [y/n]? "), orig_file); + + if (fgets(response, sizeof(response), stdin) && + rpmatch(response) == RPMATCH_YES) + edit_file(1); + } + exit(EXIT_SUCCESS); +} |