summaryrefslogtreecommitdiffstats
path: root/login-utils
diff options
context:
space:
mode:
Diffstat (limited to 'login-utils')
-rw-r--r--login-utils/Makemodule.am287
-rw-r--r--login-utils/auth.c65
-rw-r--r--login-utils/auth.h17
-rw-r--r--login-utils/ch-common.c34
-rw-r--r--login-utils/ch-common.h6
-rw-r--r--login-utils/chfn.1119
-rw-r--r--login-utils/chfn.1.adoc93
-rw-r--r--login-utils/chfn.c480
-rw-r--r--login-utils/chsh.197
-rw-r--r--login-utils/chsh.1.adoc76
-rw-r--r--login-utils/chsh.c304
-rw-r--r--login-utils/islocal.c111
-rw-r--r--login-utils/islocal.h6
-rw-r--r--login-utils/last.1241
-rw-r--r--login-utils/last.1.adoc139
-rw-r--r--login-utils/last.c1079
-rw-r--r--login-utils/lastb.11
-rw-r--r--login-utils/libuser.c72
-rw-r--r--login-utils/libuser.h18
-rw-r--r--login-utils/login.1223
-rw-r--r--login-utils/login.1.adoc179
-rw-r--r--login-utils/login.c1547
-rw-r--r--login-utils/lslogins.1234
-rw-r--r--login-utils/lslogins.1.adoc157
-rw-r--r--login-utils/lslogins.c1734
-rw-r--r--login-utils/meson.build63
-rw-r--r--login-utils/newgrp.170
-rw-r--r--login-utils/newgrp.1.adoc49
-rw-r--r--login-utils/newgrp.c241
-rw-r--r--login-utils/nologin.8100
-rw-r--r--login-utils/nologin.8.adoc79
-rw-r--r--login-utils/nologin.c111
-rw-r--r--login-utils/runuser.1281
-rw-r--r--login-utils/runuser.1.adoc143
-rw-r--r--login-utils/runuser.c7
-rw-r--r--login-utils/setpwnam.c216
-rw-r--r--login-utils/setpwnam.h33
-rw-r--r--login-utils/su-common.c1293
-rw-r--r--login-utils/su-common.h11
-rw-r--r--login-utils/su.1299
-rw-r--r--login-utils/su.1.adoc159
-rw-r--r--login-utils/su.c8
-rw-r--r--login-utils/sulogin-consoles.c829
-rw-r--r--login-utils/sulogin-consoles.h55
-rw-r--r--login-utils/sulogin.889
-rw-r--r--login-utils/sulogin.8.adoc80
-rw-r--r--login-utils/sulogin.c1199
-rw-r--r--login-utils/utmpdump.1100
-rw-r--r--login-utils/utmpdump.1.adoc88
-rw-r--r--login-utils/utmpdump.c414
-rw-r--r--login-utils/vigr.81
-rw-r--r--login-utils/vipw.871
-rw-r--r--login-utils/vipw.8.adoc81
-rw-r--r--login-utils/vipw.c369
54 files changed, 13858 insertions, 0 deletions
diff --git a/login-utils/Makemodule.am b/login-utils/Makemodule.am
new file mode 100644
index 0000000..1c5a11d
--- /dev/null
+++ b/login-utils/Makemodule.am
@@ -0,0 +1,287 @@
+
+if BUILD_LAST
+usrbin_exec_PROGRAMS += last
+MANPAGES += login-utils/last.1
+dist_noinst_DATA += login-utils/last.1.adoc
+MANLINKS += 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
+
+if FUZZING_ENGINE
+check_PROGRAMS += test_last_fuzz
+
+# https://google.github.io/oss-fuzz/getting-started/new-project-guide/#Requirements
+nodist_EXTRA_test_last_fuzz_SOURCES = dummy.cxx
+
+test_last_fuzz_SOURCES = login-utils/last.c
+test_last_fuzz_CFLAGS = $(AM_CFLAGS) -DFUZZ_TARGET
+test_last_fuzz_LDFLAGS = -lpthread
+test_last_fuzz_LDADD = $(LDADD) libcommon.la $(LIB_FUZZING_ENGINE)
+endif
+
+endif
+
+if BUILD_SULOGIN
+sbin_PROGRAMS += sulogin
+MANPAGES += login-utils/sulogin.8
+dist_noinst_DATA += login-utils/sulogin.8.adoc
+sulogin_SOURCES = \
+ login-utils/sulogin.c \
+ login-utils/sulogin-consoles.c \
+ login-utils/sulogin-consoles.h
+if USE_PLYMOUTH_SUPPORT
+sulogin_SOURCES += lib/plymouth-ctrl.c
+endif
+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
+MANPAGES += login-utils/login.1
+dist_noinst_DATA += login-utils/login.1.adoc
+login_SOURCES = \
+ login-utils/login.c \
+ lib/logindefs.c
+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
+if HAVE_ECONF
+login_LDADD += -leconf
+endif
+endif # BUILD_LOGIN
+
+
+if BUILD_NOLOGIN
+sbin_PROGRAMS += nologin
+MANPAGES += login-utils/nologin.8
+dist_noinst_DATA += login-utils/nologin.8.adoc
+nologin_SOURCES = login-utils/nologin.c
+nologin_LDADD = $(LDADD) libcommon.la
+endif
+
+
+if BUILD_UTMPDUMP
+usrbin_exec_PROGRAMS += utmpdump
+MANPAGES += login-utils/utmpdump.1
+dist_noinst_DATA += login-utils/utmpdump.1.adoc
+utmpdump_SOURCES = login-utils/utmpdump.c
+utmpdump_LDADD = $(LDADD) libcommon.la
+endif
+
+
+if BUILD_CHFN_CHSH
+usrbin_exec_PROGRAMS += chfn chsh
+MANPAGES += \
+ login-utils/chfn.1 \
+ login-utils/chsh.1
+dist_noinst_DATA += \
+ login-utils/chfn.1.adoc \
+ login-utils/chsh.1.adoc
+
+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
+
+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 += \
+ lib/selinux-utils.c \
+ include/selinux-utils.h
+chfn_chsh_ldadd += -lselinux
+endif
+
+chfn_SOURCES = \
+ login-utils/chfn.c \
+ lib/logindefs.c \
+ $(chfn_chsh_sources)
+chfn_CFLAGS = $(chfn_chsh_cflags)
+chfn_LDFLAGS = $(chfn_chsh_ldflags)
+chfn_LDADD = $(LDADD) $(chfn_chsh_ldadd)
+if HAVE_ECONF
+chfn_LDADD += -leconf
+endif
+
+chsh_SOURCES = login-utils/chsh.c lib/shells.c $(chfn_chsh_sources)
+chsh_CFLAGS = $(chfn_chsh_cflags)
+chsh_LDFLAGS = $(chfn_chsh_ldflags)
+chsh_LDADD = $(LDADD) $(chfn_chsh_ldadd)
+if HAVE_ECONF
+chsh_LDADD += -leconf
+endif
+endif # BUILD_CHFN_CHSH
+
+
+if BUILD_SU
+bin_PROGRAMS += su
+MANPAGES += login-utils/su.1
+dist_noinst_DATA += login-utils/su.1.adoc
+su_SOURCES = \
+ login-utils/su.c \
+ login-utils/su-common.c \
+ login-utils/su-common.h \
+ lib/logindefs.c \
+ lib/shells.c
+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_PTY
+su_SOURCES += lib/pty-session.c \
+ include/pty-session.h \
+ lib/monotonic.c
+su_LDADD += -lutil $(REALTIME_LIBS)
+endif
+if HAVE_ECONF
+su_LDADD += -leconf
+endif
+endif # BUILD_SU
+
+
+if BUILD_RUNUSER
+sbin_PROGRAMS += runuser
+MANPAGES += login-utils/runuser.1
+dist_noinst_DATA += login-utils/runuser.1.adoc
+runuser_SOURCES = \
+ login-utils/runuser.c \
+ login-utils/su-common.c \
+ login-utils/su-common.h \
+ lib/logindefs.c \
+ lib/shells.c
+runuser_LDADD = $(LDADD) libcommon.la -lpam
+if HAVE_LINUXPAM
+runuser_LDADD += -lpam_misc
+endif
+if HAVE_PTY
+runuser_SOURCES += lib/pty-session.c \
+ include/pty-session.h \
+ lib/monotonic.c
+runuser_LDADD += -lutil $(REALTIME_LIBS)
+endif
+if HAVE_ECONF
+runuser_LDADD += -leconf
+endif
+endif # BUILD_RUNUSER
+
+
+if BUILD_NEWGRP
+usrbin_exec_PROGRAMS += newgrp
+MANPAGES += login-utils/newgrp.1
+dist_noinst_DATA += login-utils/newgrp.1.adoc
+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
+MANPAGES += login-utils/lslogins.1
+dist_noinst_DATA += login-utils/lslogins.1.adoc
+lslogins_SOURCES = \
+ login-utils/lslogins.c \
+ lib/logindefs.c
+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
+if HAVE_ECONF
+lslogins_LDADD += -leconf
+endif
+endif # BUILD_LSLOGINS
+
+
+if BUILD_VIPW
+usrsbin_exec_PROGRAMS += vipw
+MANPAGES += login-utils/vipw.8
+dist_noinst_DATA += login-utils/vipw.8.adoc
+MANLINKS += login-utils/vigr.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_islocal_SOURCES = login-utils/islocal.c
+test_islocal_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
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..ee58d12
--- /dev/null
+++ b/login-utils/auth.h
@@ -0,0 +1,17 @@
+/*
+ * 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.
+ *
+ */
+#ifndef UTIL_LINUX_LOGIN_AUTH_H
+#define UTIL_LINUX_LOGIN_AUTH_H
+
+#include <sys/types.h>
+
+extern int auth_pam(const char *service_name, uid_t uid, const char *username);
+
+#endif /* UTIL_LINUX_LOGIN_AUTH_H */
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..0792231
--- /dev/null
+++ b/login-utils/chfn.1
@@ -0,0 +1,119 @@
+'\" t
+.\" Title: chfn
+.\" Author: [see the "AUTHOR(S)" section]
+.\" Generator: Asciidoctor 2.0.20
+.\" Date: 2023-11-21
+.\" Manual: User Commands
+.\" Source: util-linux 2.39.3
+.\" Language: English
+.\"
+.TH "CHFN" "1" "2023-11-21" "util\-linux 2.39.3" "User Commands"
+.ie \n(.g .ds Aq \(aq
+.el .ds Aq '
+.ss \n[.ss] 0
+.nh
+.ad l
+.de URL
+\fI\\$2\fP <\\$1>\\$3
+..
+.als MTO URL
+.if \n[.g] \{\
+. mso www.tmac
+. am URL
+. ad l
+. .
+. am MTO
+. ad l
+. .
+. LINKSTYLE blue R < >
+.\}
+.SH "NAME"
+chfn \- change your finger information
+.SH "SYNOPSIS"
+.sp
+\fBchfn\fP [\fB\-f\fP \fIfull\-name\fP] [\fB\-o\fP \fIoffice\fP] [\fB\-p\fP \fIoffice\-phone\fP] [\fB\-h\fP \fIhome\-phone\fP] [\fB\-u\fP] [\fB\-V\fP] [\fIusername\fP]
+.SH "DESCRIPTION"
+.sp
+\fBchfn\fP is used to change your finger information. This information is stored in the \fI/etc/passwd\fP file, and is displayed by the \fBfinger\fP program. The Linux \fBfinger\fP command will display four pieces of information that can be changed by \fBchfn\fP: your real name, your work room and phone, and your home phone.
+.sp
+Any of the four pieces of information can be specified on the command line. If no information is given on the command line, \fBchfn\fP enters interactive mode.
+.sp
+In interactive mode, \fBchfn\fP 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.
+.sp
+\fBchfn\fP supports non\-local entries (kerberos, LDAP, etc.) if linked with libuser, otherwise use \fBypchfn\fP(1), \fBlchfn\fP(1) or any other implementation for non\-local entries.
+.SH "OPTIONS"
+.sp
+\fB\-f\fP, \fB\-\-full\-name\fP \fIfull\-name\fP
+.RS 4
+Specify your real name.
+.RE
+.sp
+\fB\-o\fP, \fB\-\-office\fP \fIoffice\fP
+.RS 4
+Specify your office room number.
+.RE
+.sp
+\fB\-p\fP, \fB\-\-office\-phone\fP \fIoffice\-phone\fP
+.RS 4
+Specify your office phone number.
+.RE
+.sp
+\fB\-h\fP, \fB\-\-home\-phone\fP \fIhome\-phone\fP
+.RS 4
+Specify your home phone number.
+.RE
+.sp
+\fB\-u\fP, \fB\-\-help\fP
+.RS 4
+Display help text and exit.
+.RE
+.sp
+\fB\-V\fP, \fB\-\-version\fP
+.RS 4
+Print version and exit. The short options \fB\-V\fP have been used since version 2.39; old versions use
+deprecated \fB\-v\fP.
+.RE
+.sp
+\fB\-h\fP, \fB\-\-help\fP
+.RS 4
+Display help text and exit.
+.RE
+.sp
+\fB\-V\fP, \fB\-\-version\fP
+.RS 4
+Print version and exit.
+.RE
+.SH "CONFIG FILE ITEMS"
+.sp
+\fBchfn\fP reads the \fI/etc/login.defs\fP configuration file (see \fBlogin.defs\fP(5)). Note that the configuration file could be distributed with another package (e.g., shadow\-utils). The following configuration items are relevant for \fBchfn\fP:
+.sp
+\fBCHFN_RESTRICT\fP \fIstring\fP
+.RS 4
+Indicate which fields are changeable by \fBchfn\fP.
+.sp
+The boolean setting \fB"yes"\fP means that only the Office, Office Phone and Home Phone fields are changeable, and boolean setting \fB"no"\fP means that also the Full Name is changeable.
+.sp
+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"\fP allows changing work and home phone numbers.
+.sp
+If \fBCHFN_RESTRICT\fP is undefined, then all finger information is read\-only. This is the default.
+.RE
+.SH "EXIT STATUS"
+.sp
+Returns 0 if operation was successful, 1 if operation failed or command syntax was not valid.
+.SH "AUTHORS"
+.sp
+.MTO "svalente\(atmit.edu" "Salvatore Valente" ""
+.SH "SEE ALSO"
+.sp
+\fBchsh\fP(1),
+\fBfinger\fP(1),
+\fBlogin.defs\fP(5),
+\fBpasswd\fP(5)
+.SH "REPORTING BUGS"
+.sp
+For bug reports, use the issue tracker at \c
+.URL "https://github.com/util\-linux/util\-linux/issues" "" "."
+.SH "AVAILABILITY"
+.sp
+The \fBchfn\fP command is part of the util\-linux package which can be downloaded from \c
+.URL "https://www.kernel.org/pub/linux/utils/util\-linux/" "Linux Kernel Archive" "." \ No newline at end of file
diff --git a/login-utils/chfn.1.adoc b/login-utils/chfn.1.adoc
new file mode 100644
index 0000000..35a2e82
--- /dev/null
+++ b/login-utils/chfn.1.adoc
@@ -0,0 +1,93 @@
+//po4a: entry man manual
+////
+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.
+////
+= chfn(1)
+:doctype: manpage
+:man manual: User Commands
+:man source: util-linux {release-version}
+:page-layout: base
+:command: chfn
+
+== NAME
+
+chfn - change your finger information
+
+== SYNOPSIS
+
+*chfn* [*-f* _full-name_] [*-o* _office_] [*-p* _office-phone_] [*-h* _home-phone_] [*-u*] [*-V*] [_username_]
+
+== DESCRIPTION
+
+*chfn* is used to change your finger information. This information is stored in the _/etc/passwd_ file, and is displayed by the *finger* program. The Linux *finger* command will display four pieces of information that can be changed by *chfn*: your real name, your work room and phone, and your home phone.
+
+Any of the four pieces of information can be specified on the command line. If no information is given on the command line, *chfn* enters interactive mode.
+
+In interactive mode, *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.
+
+*chfn* supports non-local entries (kerberos, LDAP, etc.) if linked with libuser, otherwise use *ypchfn*(1), *lchfn*(1) or any other implementation for non-local entries.
+
+== OPTIONS
+
+*-f*, *--full-name* _full-name_::
+Specify your real name.
+
+*-o*, *--office* _office_::
+Specify your office room number.
+
+*-p*, *--office-phone* _office-phone_::
+Specify your office phone number.
+
+*-h*, *--home-phone* _home-phone_::
+Specify your home phone number.
+
+*-u*, *--help*::
+Display help text and exit.
+
+*-V*, *--version*::
+Print version and exit. The short options *-V* have been used since version 2.39; old versions use
+deprecated *-v*.
+
+include::man-common/help-version.adoc[]
+
+== CONFIG FILE ITEMS
+
+*chfn* reads the _/etc/login.defs_ configuration file (see *login.defs*(5)). Note that the configuration file could be distributed with another package (e.g., shadow-utils). The following configuration items are relevant for *chfn*:
+
+*CHFN_RESTRICT* _string_::
+
+Indicate which fields are changeable by *chfn*.
++
+The boolean setting *"yes"* means that only the Office, Office Phone and Home Phone fields are changeable, and boolean setting *"no"* 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, *CHFN_RESTRICT "wh"* allows changing work and home phone numbers.
++
+If *CHFN_RESTRICT* is undefined, then all finger information is read-only. This is the default.
+
+== EXIT STATUS
+
+Returns 0 if operation was successful, 1 if operation failed or command syntax was not valid.
+
+== AUTHORS
+
+mailto:svalente@mit.edu[Salvatore Valente]
+
+== SEE ALSO
+
+*chsh*(1),
+*finger*(1),
+*login.defs*(5),
+*passwd*(5)
+
+include::man-common/bugreports.adoc[]
+
+include::man-common/footer.adoc[]
+
+ifdef::translation[]
+include::man-common/translation.adoc[]
+endif::[]
diff --git a/login-utils/chfn.c b/login-utils/chfn.c
new file mode 100644
index 0000000..752d598
--- /dev/null
+++ b/login-utils/chfn.c
@@ -0,0 +1,480 @@
+/*
+ * 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
+
+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:uvV", 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': /* deprecated */
+ case 'V':
+ print_version(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];
+ }
+}
+
+/*
+ * 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 = NULL; /* leave initialized to NULL or getline segfaults */
+ size_t dummy = 0;
+
+ if (!def_val)
+ def_val = "";
+
+ while (true) {
+ printf("%s [%s]:", question, def_val);
+ __fpurge(stdin);
+
+ putchar(' ');
+ fflush(stdout);
+
+ if (getline(&buf, &dummy, stdin) < 0)
+ 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);
+}
+
+/*
+ * 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 || !*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);
+ close_stdout_atexit();
+
+ 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) {
+ char *user_cxt = NULL;
+
+ if (uid == 0 && !ul_selinux_has_access("passwd", "chfn", &user_cxt))
+ errx(EXIT_FAILURE,
+ _("%s is not authorized to change "
+ "the finger info of %s"),
+ user_cxt ? : _("Unknown user context"),
+ ctl.username);
+
+ if (ul_setfscreatecon_from_file(_PATH_PASSWD) != 0)
+ 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..f4f6025
--- /dev/null
+++ b/login-utils/chsh.1
@@ -0,0 +1,97 @@
+'\" t
+.\" Title: chsh
+.\" Author: [see the "AUTHOR(S)" section]
+.\" Generator: Asciidoctor 2.0.20
+.\" Date: 2023-11-21
+.\" Manual: User Commands
+.\" Source: util-linux 2.39.3
+.\" Language: English
+.\"
+.TH "CHSH" "1" "2023-11-21" "util\-linux 2.39.3" "User Commands"
+.ie \n(.g .ds Aq \(aq
+.el .ds Aq '
+.ss \n[.ss] 0
+.nh
+.ad l
+.de URL
+\fI\\$2\fP <\\$1>\\$3
+..
+.als MTO URL
+.if \n[.g] \{\
+. mso www.tmac
+. am URL
+. ad l
+. .
+. am MTO
+. ad l
+. .
+. LINKSTYLE blue R < >
+.\}
+.SH "NAME"
+chsh \- change your login shell
+.SH "SYNOPSIS"
+.sp
+\fBchsh\fP [\fB\-s\fP \fIshell\fP] [\fB\-l\fP] [\fB\-h\fP] [\fB\-V\fP] [\fIusername\fP]
+.SH "DESCRIPTION"
+.sp
+\fBchsh\fP is used to change your login shell. If a shell is not given on the command line, \fBchsh\fP prompts for one.
+.sp
+\fBchsh\fP supports non\-local entries (kerberos, LDAP, etc.) if linked with libuser, otherwise use \fBypchsh\fP(1), \fBlchsh\fP(1) or any other implementation for non\-local entries.
+.SH "OPTIONS"
+.sp
+\fB\-s\fP, \fB\-\-shell\fP \fIshell\fP
+.RS 4
+Specify your login shell.
+.RE
+.sp
+\fB\-l\fP, \fB\-\-list\-shells\fP
+.RS 4
+Print the list of shells listed in \fI/etc/shells\fP and exit.
+.RE
+.sp
+\fB\-h\fP, \fB\-\-help\fP
+.RS 4
+Display help text and exit. The short options \fB\-h\fP have been used since version 2.30; old versions use
+deprecated \fB\-u\fP.
+.RE
+.sp
+\fB\-V\fP, \fB\-\-version\fP
+.RS 4
+Print version and exit. The short options \fB\-V\fP have been used since version 2.39; old versions use
+deprecated \fB\-v\fP.
+.RE
+.sp
+\fB\-h\fP, \fB\-\-help\fP
+.RS 4
+Display help text and exit.
+.RE
+.sp
+\fB\-V\fP, \fB\-\-version\fP
+.RS 4
+Print version and exit.
+.RE
+.SH "VALID SHELLS"
+.sp
+\fBchsh\fP will accept the full pathname of any executable file on the system.
+.sp
+The default behavior for non\-root users is to accept only shells listed in the \fI/etc/shells\fP file, and issue a warning for root user. It can also be configured at compile\-time to only issue a warning for all users.
+.SH "EXIT STATUS"
+.sp
+Returns 0 if operation was successful, 1 if operation failed or command syntax was not valid.
+.SH "AUTHORS"
+.sp
+.MTO "svalente\(atmit.edu" "Salvatore Valente" ""
+.SH "SEE ALSO"
+.sp
+\fBlogin\fP(1),
+\fBlogin.defs\fP(5),
+\fBpasswd\fP(5),
+\fBshells\fP(5)
+.SH "REPORTING BUGS"
+.sp
+For bug reports, use the issue tracker at \c
+.URL "https://github.com/util\-linux/util\-linux/issues" "" "."
+.SH "AVAILABILITY"
+.sp
+The \fBchsh\fP command is part of the util\-linux package which can be downloaded from \c
+.URL "https://www.kernel.org/pub/linux/utils/util\-linux/" "Linux Kernel Archive" "." \ No newline at end of file
diff --git a/login-utils/chsh.1.adoc b/login-utils/chsh.1.adoc
new file mode 100644
index 0000000..965a9e3
--- /dev/null
+++ b/login-utils/chsh.1.adoc
@@ -0,0 +1,76 @@
+//po4a: entry man manual
+////
+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.
+////
+= chsh(1)
+:doctype: manpage
+:man manual: User Commands
+:man source: util-linux {release-version}
+:page-layout: base
+:command: chsh
+
+== NAME
+
+chsh - change your login shell
+
+== SYNOPSIS
+
+*chsh* [*-s* _shell_] [*-l*] [*-h*] [*-V*] [_username_]
+
+== DESCRIPTION
+
+*chsh* is used to change your login shell. If a shell is not given on the command line, *chsh* prompts for one.
+
+*chsh* supports non-local entries (kerberos, LDAP, etc.) if linked with libuser, otherwise use *ypchsh*(1), *lchsh*(1) or any other implementation for non-local entries.
+
+== OPTIONS
+
+*-s*, *--shell* _shell_::
+Specify your login shell.
+
+*-l*, *--list-shells*::
+Print the list of shells listed in _/etc/shells_ and exit.
+
+*-h*, *--help*::
+Display help text and exit. The short options *-h* have been used since version 2.30; old versions use
+deprecated *-u*.
+
+*-V*, *--version*::
+Print version and exit. The short options *-V* have been used since version 2.39; old versions use
+deprecated *-v*.
+
+include::man-common/help-version.adoc[]
+
+== VALID SHELLS
+
+*chsh* will accept the full pathname of any executable file on the system.
+
+The default behavior for non-root users is to accept only shells listed in the _/etc/shells_ file, and issue a warning for root user. It can also be configured at compile-time to only issue a warning for all users.
+
+== EXIT STATUS
+
+Returns 0 if operation was successful, 1 if operation failed or command syntax was not valid.
+
+== AUTHORS
+
+mailto:svalente@mit.edu[Salvatore Valente]
+
+== SEE ALSO
+
+*login*(1),
+*login.defs*(5),
+*passwd*(5),
+*shells*(5)
+
+include::man-common/bugreports.adoc[]
+
+include::man-common/footer.adoc[]
+
+ifdef::translation[]
+include::man-common/translation.adoc[]
+endif::[]
diff --git a/login-utils/chsh.c b/login-utils/chsh.c
new file mode 100644
index 0000000..31750d5
--- /dev/null
+++ b/login-utils/chsh.c
@@ -0,0 +1,304 @@
+/*
+ * 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 "pwdutils.h"
+#include "setpwnam.h"
+#include "strutils.h"
+#include "xalloc.h"
+
+#include "ch-common.h"
+#include "shells.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
+
+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(USAGE_HELP_OPTIONS(22));
+
+ printf(USAGE_MAN_TAIL("chsh(1)"));
+ exit(EXIT_SUCCESS);
+}
+
+/*
+ * print_shells () -- /etc/shells is outputted to stdout.
+ */
+static void print_shells(void)
+{
+ char *s;
+
+ while ((s = getusershell()))
+ printf("%s\n", s);
+ endusershell();
+}
+
+/*
+ * 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:lhuvV", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'v': /* deprecated */
+ case 'V':
+ print_version(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;
+ size_t dummy = 0;
+
+ if (!oldshell)
+ oldshell = "";
+ printf("%s [%s]:", question, oldshell);
+
+ putchar(' ');
+ fflush(stdout);
+
+ if (getline(&ans, &dummy, stdin) < 0)
+ 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, *pwbuf;
+ 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);
+ close_stdout_atexit();
+
+ parse_argv(argc, argv, &info);
+ if (!info.username) {
+ pw = xgetpwuid(uid, &pwbuf);
+ if (!pw)
+ errx(EXIT_FAILURE, _("you (user %d) don't exist."),
+ uid);
+ } else {
+ pw = xgetpwnam(info.username, &pwbuf);
+ 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) {
+ char *user_cxt = NULL;
+
+ if (uid == 0 && !ul_selinux_has_access("passwd", "chsh", &user_cxt))
+ errx(EXIT_FAILURE,
+ _("%s is not authorized to change the shell of %s"),
+ user_cxt ? : _("Unknown user context"),
+ pw->pw_name);
+
+ if (ul_setfscreatecon_from_file(_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..ab5c52e
--- /dev/null
+++ b/login-utils/islocal.c
@@ -0,0 +1,111 @@
+/*
+ * 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;
+ }
+ /* 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[])
+{
+ close_stdout_atexit();
+ if (argc <= 2) {
+ fprintf(stderr, _("Usage: %s <passwordfile> <username>...\n"),
+ argv[0]);
+ return 1;
+ }
+
+ 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..11a1bed
--- /dev/null
+++ b/login-utils/islocal.h
@@ -0,0 +1,6 @@
+#ifndef UTIL_LINUX_LOGIN_ISLOCAL_H
+#define UTIL_LINUX_LOGIN_ISLOCAL_H
+
+extern int is_local(const char *user);
+
+#endif /* UTIL_LINUX_LOGIN_ISLOCAL_H */
diff --git a/login-utils/last.1 b/login-utils/last.1
new file mode 100644
index 0000000..0689f62
--- /dev/null
+++ b/login-utils/last.1
@@ -0,0 +1,241 @@
+'\" t
+.\" Title: last
+.\" Author: [see the "AUTHOR(S)" section]
+.\" Generator: Asciidoctor 2.0.20
+.\" Date: 2023-12-01
+.\" Manual: User Commands
+.\" Source: util-linux 2.39.3
+.\" Language: English
+.\"
+.TH "LAST" "1" "2023-12-01" "util\-linux 2.39.3" "User Commands"
+.ie \n(.g .ds Aq \(aq
+.el .ds Aq '
+.ss \n[.ss] 0
+.nh
+.ad l
+.de URL
+\fI\\$2\fP <\\$1>\\$3
+..
+.als MTO URL
+.if \n[.g] \{\
+. mso www.tmac
+. am URL
+. ad l
+. .
+. am MTO
+. ad l
+. .
+. LINKSTYLE blue R < >
+.\}
+.SH "NAME"
+last, lastb \- show a listing of last logged in users
+.SH "SYNOPSIS"
+.sp
+\fBlast\fP [options] [\fIusername\fP...] [\fItty\fP...]
+.sp
+\fBlastb\fP [options] [\fIusername\fP...] [\fItty\fP...]
+.SH "DESCRIPTION"
+.sp
+\fBlast\fP searches back through the \fI/var/log/wtmp\fP file (or the file designated by the \fB\-f\fP option) and displays a list of all users logged in (and out) since that file was created. One or more \fIusernames\fP and/or \fIttys\fP can be given, in which case \fBlast\fP will show only the entries matching those arguments. Names of \fIttys\fP can be abbreviated, thus \fBlast 0\fP is the same as \fBlast tty0\fP.
+.sp
+When catching a \fBSIGINT\fP signal (generated by the interrupt key, usually control\-C) or a \fBSIGQUIT\fP signal, \fBlast\fP will show how far it has searched through the file; in the case of the \fBSIGINT\fP signal \fBlast\fP will then terminate.
+.sp
+The pseudo user \fBreboot\fP logs in each time the system is rebooted. Thus \fBlast reboot\fP will show a log of all the reboots since the log file was created.
+.sp
+\fBlastb\fP is the same as \fBlast\fP, except that by default it shows a log of the \fI/var/log/btmp\fP file, which contains all the bad login attempts.
+.SH "OPTIONS"
+.sp
+\fB\-a\fP, \fB\-\-hostlast\fP
+.RS 4
+Display the hostname in the last column. Useful in combination with the \fB\-\-dns\fP option.
+.RE
+.sp
+\fB\-d\fP, \fB\-\-dns\fP
+.RS 4
+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.
+.RE
+.sp
+\fB\-f\fP, \fB\-\-file\fP \fIfile\fP
+.RS 4
+Tell \fBlast\fP to use a specific \fIfile\fP instead of \fI/var/log/wtmp\fP. The \fB\-\-file\fP option can be given multiple times, and all of the specified files will be processed.
+.RE
+.sp
+\fB\-F\fP, \fB\-\-fulltimes\fP
+.RS 4
+Print full login and logout times and dates.
+.RE
+.sp
+\fB\-i\fP, \fB\-\-ip\fP
+.RS 4
+Like \fB\-\-dns ,\fP but displays the host\(cqs IP number instead of the name.
+.RE
+.sp
+\fB\-\fP\fInumber\fP; \fB\-n\fP, \fB\-\-limit\fP \fInumber\fP
+.RS 4
+Tell \fBlast\fP how many lines to show.
+.RE
+.sp
+\fB\-p\fP, \fB\-\-present\fP \fItime\fP
+.RS 4
+Display the users who were present at the specified time. This is like using the options \fB\-\-since\fP and \fB\-\-until\fP together with the same \fItime\fP.
+.RE
+.sp
+\fB\-R\fP, \fB\-\-nohostname\fP
+.RS 4
+Suppresses the display of the hostname field.
+.RE
+.sp
+\fB\-s\fP, \fB\-\-since\fP \fItime\fP
+.RS 4
+Display the state of logins since the specified \fItime\fP. This is useful, e.g., to easily determine who was logged in at a particular time. The option is often combined with \fB\-\-until\fP.
+.RE
+.sp
+\fB\-t\fP, \fB\-\-until\fP \fItime\fP
+.RS 4
+Display the state of logins until the specified \fItime\fP.
+.RE
+.sp
+\fB\-\-time\-format\fP \fIformat\fP
+.RS 4
+Define the output timestamp \fIformat\fP to be one of \fInotime\fP, \fIshort\fP, \fIfull\fP, or \fIiso\fP. The \fInotime\fP variant will not print any timestamps at all, \fIshort\fP is the default, and \fIfull\fP is the same as the \fB\-\-fulltimes\fP option. The \fIiso\fP 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.
+.RE
+.sp
+\fB\-w\fP, \fB\-\-fullnames\fP
+.RS 4
+Display full user names and domain names in the output.
+.RE
+.sp
+\fB\-x\fP, \fB\-\-system\fP
+.RS 4
+Display the system shutdown entries and run level changes.
+.RE
+.sp
+\fB\-h\fP, \fB\-\-help\fP
+.RS 4
+Display help text and exit.
+.RE
+.sp
+\fB\-V\fP, \fB\-\-version\fP
+.RS 4
+Print version and exit.
+.RE
+.SH "TIME FORMATS"
+.sp
+The options that take the \fItime\fP argument understand the following formats:
+.TS
+allbox tab(:);
+lt lt.
+T{
+.sp
+YYYYMMDDhhmmss
+T}:T{
+.sp
+
+T}
+T{
+.sp
+YYYY\-MM\-DD hh:mm:ss
+T}:T{
+.sp
+
+T}
+T{
+.sp
+YYYY\-MM\-DD hh:mm
+T}:T{
+.sp
+(seconds will be set to 00)
+T}
+T{
+.sp
+YYYY\-MM\-DD
+T}:T{
+.sp
+(time will be set to 00:00:00)
+T}
+T{
+.sp
+hh:mm:ss
+T}:T{
+.sp
+(date will be set to today)
+T}
+T{
+.sp
+hh:mm
+T}:T{
+.sp
+(date will be set to today, seconds to 00)
+T}
+T{
+.sp
+now
+T}:T{
+.sp
+
+T}
+T{
+.sp
+yesterday
+T}:T{
+.sp
+(time is set to 00:00:00)
+T}
+T{
+.sp
+today
+T}:T{
+.sp
+(time is set to 00:00:00)
+T}
+T{
+.sp
+tomorrow
+T}:T{
+.sp
+(time is set to 00:00:00)
+T}
+T{
+.sp
++5min
+T}:T{
+.sp
+
+T}
+T{
+.sp
+\-5days
+T}:T{
+.sp
+
+T}
+.TE
+.sp
+.SH "FILES"
+.sp
+\fI/var/log/wtmp\fP,
+\fI/var/log/btmp\fP
+.SH "NOTES"
+.sp
+The files \fIwtmp\fP and \fIbtmp\fP 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 \fBtouch\fP(1) command (for example, \fBtouch /var/log/wtmp\fP).
+.sp
+An empty entry is a valid type of wtmp entry. It means that an empty file or file with zeros is not interpreted as an error.
+.sp
+The utmp file format uses fixed sizes of strings, which means that very long strings are impossible to store in the file and impossible to display by \fBlast\fP. The usual limits are 32 bytes for a user and line name and 256 bytes for a hostname.
+.SH "AUTHORS"
+.sp
+.MTO "miquels\(atcistron.nl" "Miquel van Smoorenburg" ""
+.SH "SEE ALSO"
+.sp
+\fBlogin\fP(1),
+\fBwtmp\fP(5),
+\fBinit\fP(8),
+\fBshutdown\fP(8)
+.SH "REPORTING BUGS"
+.sp
+For bug reports, use the issue tracker at \c
+.URL "https://github.com/util\-linux/util\-linux/issues" "" "."
+.SH "AVAILABILITY"
+.sp
+The \fBlast\fP command is part of the util\-linux package which can be downloaded from \c
+.URL "https://www.kernel.org/pub/linux/utils/util\-linux/" "Linux Kernel Archive" "." \ No newline at end of file
diff --git a/login-utils/last.1.adoc b/login-utils/last.1.adoc
new file mode 100644
index 0000000..1d26ae1
--- /dev/null
+++ b/login-utils/last.1.adoc
@@ -0,0 +1,139 @@
+//po4a: entry man manual
+////
+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
+////
+= last(1)
+:doctype: manpage
+:man manual: User Commands
+:man source: util-linux {release-version}
+:page-layout: base
+:command: last
+
+== NAME
+
+last, lastb - show a listing of last logged in users
+
+== SYNOPSIS
+
+*last* [options] [_username_...] [_tty_...]
+
+*lastb* [options] [_username_...] [_tty_...]
+
+== DESCRIPTION
+
+*last* searches back through the _/var/log/wtmp_ file (or the file designated by the *-f* option) and displays a list of all users logged in (and out) since that file was created. One or more _usernames_ and/or _ttys_ can be given, in which case *last* will show only the entries matching those arguments. Names of _ttys_ can be abbreviated, thus *last 0* is the same as *last tty0*.
+
+When catching a *SIGINT* signal (generated by the interrupt key, usually control-C) or a *SIGQUIT* signal, *last* will show how far it has searched through the file; in the case of the *SIGINT* signal *last* will then terminate.
+
+The pseudo user *reboot* logs in each time the system is rebooted. Thus *last reboot* will show a log of all the reboots since the log file was created.
+
+*lastb* is the same as *last*, except that by default it shows a log of the _/var/log/btmp_ file, which contains all the bad login attempts.
+
+== OPTIONS
+
+*-a*, *--hostlast*::
+Display the hostname in the last column. Useful in combination with the *--dns* option.
+
+*-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.
+
+*-f*, *--file* _file_::
+Tell *last* to use a specific _file_ instead of _/var/log/wtmp_. The *--file* option can be given multiple times, and all of the specified files will be processed.
+
+*-F*, *--fulltimes*::
+Print full login and logout times and dates.
+
+*-i*, *--ip*::
+Like *--dns ,* but displays the host's IP number instead of the name.
+
+**-**__number__; *-n*, *--limit* _number_::
+Tell *last* how many lines to show.
+
+*-p*, *--present* _time_::
+Display the users who were present at the specified time. This is like using the options *--since* and *--until* together with the same _time_.
+
+*-R*, *--nohostname*::
+Suppresses the display of the hostname field.
+
+*-s*, *--since* _time_::
+Display the state of logins since the specified _time_. This is useful, e.g., to easily determine who was logged in at a particular time. The option is often combined with *--until*.
+
+*-t*, *--until* _time_::
+Display the state of logins until the specified _time_.
+
+*--time-format* _format_::
+Define the output timestamp _format_ to be one of _notime_, _short_, _full_, or _iso_. The _notime_ variant will not print any timestamps at all, _short_ is the default, and _full_ is the same as the *--fulltimes* option. The _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.
+
+*-w*, *--fullnames*::
+Display full user names and domain names in the output.
+
+*-x*, *--system*::
+Display the system shutdown entries and run level changes.
+
+include::man-common/help-version.adoc[]
+
+== TIME FORMATS
+
+The options that take the _time_ argument understand the following formats:
+
+[cols=",",]
+|===
+|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 |
+|===
+
+== FILES
+
+_/var/log/wtmp_,
+_/var/log/btmp_
+
+== NOTES
+
+The files _wtmp_ and _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 *touch*(1) command (for example, *touch /var/log/wtmp*).
+
+An empty entry is a valid type of wtmp entry. It means that an empty file or file with zeros is not interpreted as an error.
+
+The utmp file format uses fixed sizes of strings, which means that very long strings are impossible to store in the file and impossible to display by *last*. The usual limits are 32 bytes for a user and line name and 256 bytes for a hostname.
+
+== AUTHORS
+
+mailto:miquels@cistron.nl[Miquel van Smoorenburg]
+
+== SEE ALSO
+
+*login*(1),
+*wtmp*(5),
+*init*(8),
+*shutdown*(8)
+
+include::man-common/bugreports.adoc[]
+
+include::man-common/footer.adoc[]
+
+ifdef::translation[]
+include::man-common/translation.adoc[]
+endif::[]
diff --git a/login-utils/last.c b/login-utils/last.c
new file mode 100644
index 0000000..37c6abe
--- /dev/null
+++ b/login-utils/last.c
@@ -0,0 +1,1079 @@
+/*
+ * 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"
+#include "fileutils.h"
+
+#ifdef FUZZ_TARGET
+#include "fuzz.h"
+#endif
+
+#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 */
+
+#ifndef FUZZ_TARGET
+/* --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);
+}
+#endif
+
+/*
+ * 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;
+}
+
+#ifndef FUZZ_TARGET
+/*
+ * SIGINT handler
+ */
+static void int_handler(int sig __attribute__((unused)))
+{
+ ul_sig_err(EXIT_FAILURE, "Interrupted");
+}
+
+/*
+ * SIGQUIT handler
+ */
+static void quit_handler(int sig __attribute__((unused)))
+{
+ ul_sig_warn("Interrupted");
+ signal(SIGQUIT, quit_handler);
+}
+#endif
+
+/*
+ * 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_r(when, &tm);
+ if (!snprintf(dst, dlen, "%02d:%02d", tm.tm_hour, tm.tm_min))
+ ret = -1;
+ break;
+ }
+ case LAST_TIMEFTM_CTIME:
+ {
+ char buf[CTIME_BUFSIZ];
+
+ ctime_r(when, buf);
+ snprintf(dst, dlen, "%s", buf);
+ 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];
+ int mins, hours, days;
+ int r, len;
+ struct last_timefmt *fmt;
+
+ /*
+ * uucp and ftp have special-type entries
+ */
+ mem2strcpy(utline, p->ut_line, sizeof(p->ut_line), sizeof(utline));
+ 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) {
+ snprintf(logouttime, sizeof(logouttime), " still running");
+ length[0] = 0;
+ } else {
+ snprintf(logouttime, sizeof(logouttime), " still");
+ snprintf(length, sizeof(length), "running");
+ }
+ } else if (days) {
+ snprintf(length, sizeof(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) {
+ snprintf(length, sizeof(length), " (%02d:%02d)", hours, abs(mins)); /* mins always shown as positive (w/o minus sign!) even if secs < 0 */
+ } else if (secs >= 0) {
+ snprintf(length, sizeof(length), " (%02d:%02d)", hours, mins);
+ } else {
+ snprintf(length, sizeof(length), " (-00:%02d)", abs(mins)); /* mins always shown as positive (w/o minus sign!) even if secs < 0 */
+ }
+
+ switch(what) {
+ case R_CRASH:
+ snprintf(logouttime, sizeof(logouttime), "- crash");
+ break;
+ case R_DOWN:
+ snprintf(logouttime, sizeof(logouttime), "- down ");
+ break;
+ case R_NOW:
+ if (ctl->time_fmt > LAST_TIMEFTM_SHORT) {
+ snprintf(logouttime, sizeof(logouttime), " still logged in");
+ length[0] = 0;
+ } else {
+ snprintf(logouttime, sizeof(logouttime), " still");
+ snprintf(length, sizeof(length), "logged in");
+ }
+ break;
+ case R_PHANTOM:
+ if (ctl->time_fmt > LAST_TIMEFTM_SHORT) {
+ snprintf(logouttime, sizeof(logouttime), " gone - no logout");
+ length[0] = 0;
+ } else if (ctl->time_fmt == LAST_TIMEFTM_SHORT) {
+ snprintf(logouttime, sizeof(logouttime), " gone");
+ snprintf(length, sizeof(length), "- no logout");
+ } else {
+ logouttime[0] = 0;
+ snprintf(length, sizeof(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.
+ */
+ fputs_careful(final, stdout, '*', false, 0);
+
+ if (len < 0 || (size_t)len >= sizeof(final))
+ putchar('\n');
+
+ recsdone++;
+ if (ctl->maxrecs && ctl->maxrecs <= recsdone)
+ return 1;
+
+ return 0;
+}
+
+#ifndef FUZZ_TARGET
+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);
+}
+#endif
+
+static int is_phantom(const struct last_control *ctl, struct utmpx *ut)
+{
+ struct passwd *pw;
+ char path[sizeof(ut->ut_line) + 16];
+ char user[sizeof(ut->ut_user) + 1];
+ int ret = 0;
+
+ if (ut->ut_tv.tv_sec < ctl->boot_time.tv_sec)
+ return 1;
+
+ mem2strcpy(user, ut->ut_user, sizeof(ut->ut_user), sizeof(user));
+ pw = getpwnam(user);
+ if (!pw)
+ return 1;
+ snprintf(path, sizeof(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;
+ char utline[sizeof(ut->ut_line) + 1];
+
+ mem2strcpy(utline, ut->ut_line, sizeof(ut->ut_line), sizeof(utline));
+
+ snprintf(path, sizeof(path), "/dev/%s", utline);
+ 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 */
+
+#ifndef FUZZ_TARGET
+ time(&lastdown);
+#else
+ lastdown = 1596001948;
+#endif
+ /*
+ * Fill in 'lastdate'
+ */
+ lastdate = currentdate = lastrch = lastdown;
+
+#ifndef FUZZ_TARGET
+ /*
+ * Install signal handlers
+ */
+ signal(SIGINT, int_handler);
+ signal(SIGQUIT, quit_handler);
+#endif
+
+ /*
+ * 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] &&
+ strncmp(ut.ut_user, "LOGIN", 5) != 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 (strncmp(ut.ut_user, "date", 4) == 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) {
+ snprintf(ut.ut_line, sizeof(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);
+ }
+}
+
+#ifdef FUZZ_TARGET
+# include "all-io.h"
+
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+ struct last_control ctl = {
+ .showhost = TRUE,
+ .name_len = LAST_LOGIN_LEN,
+ .time_fmt = LAST_TIMEFTM_SHORT,
+ .domain_len = LAST_DOMAIN_LEN,
+ .boot_time = {
+ .tv_sec = 1595978419,
+ .tv_usec = 816074
+ }
+ };
+ char name[] = "/tmp/test-last-fuzz.XXXXXX";
+ int fd;
+
+ fd = mkstemp_cloexec(name);
+ if (fd < 0)
+ err(EXIT_FAILURE, "mkstemp() failed");
+ if (write_all(fd, data, size) != 0)
+ err(EXIT_FAILURE, "write() failed");
+
+ process_wtmp_file(&ctl, name);
+
+ close(fd);
+ unlink(name);
+
+ return 0;
+}
+#else
+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);
+ close_stdout_atexit();
+ /*
+ * 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':
+ print_version(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_member(struct utmpx, ut_user))
+ ctl.name_len = sizeof_member(struct utmpx, ut_user);
+ if (ctl.domain_len < sizeof_member(struct utmpx, ut_host))
+ ctl.domain_len = sizeof_member(struct utmpx, 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;
+}
+#endif
diff --git a/login-utils/lastb.1 b/login-utils/lastb.1
new file mode 100644
index 0000000..0578311
--- /dev/null
+++ b/login-utils/lastb.1
@@ -0,0 +1 @@
+.so last.1 \ No newline at end of file
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..d4fae64
--- /dev/null
+++ b/login-utils/libuser.h
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ *
+ */
+#ifndef UTIL_LINUX_LOGIN_LIBUSER_H
+#define UTIL_LINUX_LOGIN_LIBUSER_H
+
+#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);
+
+#endif /* UTIL_LINUX_LOGIN_LIBUSER_H */
diff --git a/login-utils/login.1 b/login-utils/login.1
new file mode 100644
index 0000000..65331d3
--- /dev/null
+++ b/login-utils/login.1
@@ -0,0 +1,223 @@
+'\" t
+.\" Title: login
+.\" Author: [see the "AUTHOR(S)" section]
+.\" Generator: Asciidoctor 2.0.20
+.\" Date: 2023-12-01
+.\" Manual: User Commands
+.\" Source: util-linux 2.39.3
+.\" Language: English
+.\"
+.TH "LOGIN" "1" "2023-12-01" "util\-linux 2.39.3" "User Commands"
+.ie \n(.g .ds Aq \(aq
+.el .ds Aq '
+.ss \n[.ss] 0
+.nh
+.ad l
+.de URL
+\fI\\$2\fP <\\$1>\\$3
+..
+.als MTO URL
+.if \n[.g] \{\
+. mso www.tmac
+. am URL
+. ad l
+. .
+. am MTO
+. ad l
+. .
+. LINKSTYLE blue R < >
+.\}
+.SH "NAME"
+login \- begin session on the system
+.SH "SYNOPSIS"
+.sp
+\fBlogin\fP [\fB\-p\fP] [\fB\-h\fP \fIhost\fP] [\fB\-H\fP] [\fB\-f\fP \fIusername\fP|\fIusername\fP]
+.SH "DESCRIPTION"
+.sp
+\fBlogin\fP is used when signing onto a system. If no argument is given, \fBlogin\fP prompts for the username.
+.sp
+The user is then prompted for a password, where appropriate. Echoing is disabled to prevent revealing the password. Only a number of password failures are permitted before \fBlogin\fP exits and the communications link is severed. See \fBLOGIN_RETRIES\fP in the \fBCONFIG FILE ITEMS\fP section.
+.sp
+If password aging has been enabled for the account, the user may be prompted for a new password before proceeding. In such case old password must be provided and the new password entered before continuing. Please refer to \fBpasswd\fP(1) for more information.
+.sp
+The user and group ID will be set according to their values in the \fI/etc/passwd\fP 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 environment variable values for \fB$HOME\fP, \fB$USER\fP, \fB$SHELL\fP, \fB$PATH\fP, \fB$LOGNAME\fP, and \fB$MAIL\fP are set according to the appropriate fields in the password entry. \fB$PATH\fP defaults to \fI/usr/local/bin:/bin:/usr/bin\fP for normal users, and to \fI/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\fP for root, if not otherwise configured.
+.sp
+The environment variable \fB$TERM\fP will be preserved, if it exists, else it will be initialized to the terminal type on your tty. Other environment variables are preserved if the \fB\-p\fP option is given.
+.sp
+The environment variables defined by PAM are always preserved.
+.sp
+Then the user\(cqs shell is started. If no shell is specified for the user in \fI/etc/passwd\fP, then \fI/bin/sh\fP is used. If there is no home directory specified in \fI/etc/passwd\fP, then \fI/\fP is used, followed by \fI.hushlogin\fP check as described below.
+.sp
+If the file \fI.hushlogin\fP 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 \fI/var/log/lastlog\fP exists, the last login time is printed, and the current login is recorded.
+.SH "OPTIONS"
+.sp
+\fB\-p\fP
+.RS 4
+Used by \fBgetty\fP(8) to tell \fBlogin\fP to preserve the environment.
+.RE
+.sp
+\fB\-f\fP
+.RS 4
+Used to skip a login authentication. This option is usually used by the \fBgetty\fP(8) autologin feature.
+.RE
+.sp
+\fB\-h\fP
+.RS 4
+Used by other servers (such as \fBtelnetd\fP(8) to pass the name of the remote host to \fBlogin\fP so that it can be placed in utmp and wtmp. Only the superuser is allowed use this option.
+.sp
+Note that the \fB\-h\fP option has an impact on the \fBPAM service\fP \fBname\fP. The standard service name is \fIlogin\fP, but with the \fB\-h\fP option, the name is \fIremote\fP. It is necessary to create proper PAM config files (for example, \fI/etc/pam.d/login\fP and \fI/etc/pam.d/remote\fP).
+.RE
+.sp
+\fB\-H\fP
+.RS 4
+Used by other servers (for example, \fBtelnetd\fP(8)) to tell \fBlogin\fP that printing the hostname should be suppressed in the login: prompt. See also \fBLOGIN_PLAIN_PROMPT\fP below.
+.RE
+.sp
+\fB\-h\fP, \fB\-\-help\fP
+.RS 4
+Display help text and exit.
+.RE
+.sp
+\fB\-V\fP, \fB\-\-version\fP
+.RS 4
+Print version and exit.
+.RE
+.SH "CONFIG FILE ITEMS"
+.sp
+\fBlogin\fP reads the \fI/etc/login.defs\fP configuration file (see \fBlogin.defs\fP(5)). Note that the configuration file could be distributed with another package (usually shadow\-utils). The following configuration items are relevant for \fBlogin\fP:
+.sp
+\fBMOTD_FILE\fP (string)
+.RS 4
+Specifies a ":" delimited list of "message of the day" files and directories to be displayed upon login. If the specified path is a directory then displays all files with .motd file extension in version\-sort order from the directory.
+.sp
+The default value is \fI/usr/share/misc/motd:/run/motd:/etc/motd\fP. If the \fBMOTD_FILE\fP 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 the \fBpam_motd\fP(8) PAM module.
+.sp
+The directories in the \fBMOTD_FILE\fP are supported since version 2.36.
+.sp
+Note that \fBlogin\fP does not implement any filenames overriding behavior like pam_motd (see also \fBMOTD_FIRSTONLY\fP), but all content from all files is displayed. It is recommended to keep extra logic in content generators and use \fI/run/motd.d\fP rather than rely on overriding behavior hardcoded in system tools.
+.RE
+.sp
+\fBMOTD_FIRSTONLY\fP (boolean)
+.RS 4
+Forces \fBlogin\fP to stop display content specified by \fBMOTD_FILE\fP after the first accessible item in the list. Note that a directory is one item in this case. This option allows \fBlogin\fP semantics to be configured to be more compatible with pam_motd. The default value is \fIno\fP.
+.RE
+.sp
+\fBLOGIN_PLAIN_PROMPT\fP (boolean)
+.RS 4
+Tell \fBlogin\fP that printing the hostname should be suppressed in the login: prompt. This is an alternative to the \fB\-H\fP command line option. The default value is \fIno\fP.
+.RE
+.sp
+\fBLOGIN_TIMEOUT\fP (number)
+.RS 4
+Maximum time in seconds for login. The default value is \fI60\fP.
+.RE
+.sp
+\fBLOGIN_RETRIES\fP (number)
+.RS 4
+Maximum number of login retries in case of a bad password. The default value is \fI3\fP.
+.RE
+.sp
+\fBLOGIN_KEEP_USERNAME\fP (boolean)
+.RS 4
+Tell \fBlogin\fP to only re\-prompt for the password if authentication failed, but the username is valid. The default value is \fIno\fP.
+.RE
+.sp
+\fBFAIL_DELAY\fP (number)
+.RS 4
+Delay in seconds before being allowed another three tries after a login failure. The default value is \fI5\fP.
+.RE
+.sp
+\fBTTYPERM\fP (string)
+.RS 4
+The terminal permissions. The default value is \fI0600\fP or \fI0620\fP if tty group is used.
+.RE
+.sp
+\fBTTYGROUP\fP (string)
+.RS 4
+The login tty will be owned by the \fBTTYGROUP\fP. The default value is \fItty\fP. If the \fBTTYGROUP\fP does not exist, then the ownership of the terminal is set to the user\(cqs primary group.
+.sp
+The \fBTTYGROUP\fP can be either the name of a group or a numeric group identifier.
+.RE
+.sp
+\fBHUSHLOGIN_FILE\fP (string)
+.RS 4
+If defined, this file can inhibit all the usual chatter during the login sequence. If a full pathname (for example, \fI/etc/hushlogins\fP) is specified, then hushed mode will be enabled if the user\(cqs 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.
+.sp
+If a full pathname is not specified, then hushed mode will be enabled if the file exists in the user\(cqs home directory.
+.sp
+The default is to check \fI/etc/hushlogins\fP and if it does not exist then \fI~/.hushlogin\fP.
+.sp
+If the \fBHUSHLOGIN_FILE\fP item is empty, then all the checks are disabled.
+.RE
+.sp
+\fBDEFAULT_HOME\fP (boolean)
+.RS 4
+Indicate if login is allowed if we cannot change directory to the home directory. If set to \fIyes\fP, the user will login in the root (/) directory if it is not possible to change directory to their home. The default value is \fIyes\fP.
+.RE
+.sp
+\fBLASTLOG_UID_MAX\fP (unsigned number)
+.RS 4
+Highest user ID number for which the \fIlastlog\fP entries should be updated. As higher user IDs are usually tracked by remote user identity and authentication services there is no need to create a huge sparse \fIlastlog\fP file for them. No LASTLOG_UID_MAX option present in the configuration means that there is no user ID limit for writing \fIlastlog\fP entries. The default value is \fIULONG_MAX\fP.
+.RE
+.sp
+\fBLOG_UNKFAIL_ENAB\fP (boolean)
+.RS 4
+Enable display of unknown usernames when login failures are recorded. The default value is \fIno\fP.
+.sp
+Note that logging unknown usernames may be a security issue if a user enters their password instead of their login name.
+.RE
+.sp
+\fBENV_PATH\fP (string)
+.RS 4
+If set, it will be used to define the \fBPATH\fP environment variable when a regular user logs in. The default value is \fI/usr/local/bin:/bin:/usr/bin\fP.
+.RE
+.sp
+\fBENV_ROOTPATH\fP (string), \fBENV_SUPATH\fP (string)
+.RS 4
+If set, it will be used to define the PATH environment variable when the superuser logs in. \fBENV_ROOTPATH\fP takes precedence. The default value is \fI/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\fP.
+.RE
+.SH "FILES"
+.sp
+\fI/var/run/utmp\fP,
+\fI/var/log/wtmp\fP,
+\fI/var/log/lastlog\fP,
+\fI/var/spool/mail/*\fP,
+\fI/etc/motd\fP,
+\fI/etc/passwd\fP,
+\fI/etc/nologin\fP,
+\fI/etc/pam.d/login\fP,
+\fI/etc/pam.d/remote\fP,
+\fI/etc/hushlogins\fP,
+\fI$HOME/.hushlogin\fP
+.SH "BUGS"
+.sp
+The undocumented BSD \fB\-r\fP option is not supported. This may be required by some \fBrlogind\fP(8) programs.
+.sp
+A recursive login, as used to be possible in the good old days, no longer works; for most purposes \fBsu\fP(1) is a satisfactory substitute. Indeed, for security reasons, \fBlogin\fP does a \fBvhangup\fP(2) system call to remove any possible listening processes on the tty. This is to avoid password sniffing. If one uses the command \fBlogin\fP, then the surrounding shell gets killed by \fBvhangup\fP(2) because it\(cqs no longer the true owner of the tty. This can be avoided by using \fBexec login\fP in a top\-level shell or xterm.
+.SH "AUTHORS"
+.sp
+Derived from BSD login 5.40 (5/9/89) by \c
+.MTO "glad\(atdaimi.dk" "Michael Glad" ""
+for HP\-UX. Ported to Linux 0.12:
+.MTO "poe\(atdaimi.aau.dk" "Peter Orbaek" "."
+Rewritten to a PAM\-only version by
+.MTO "kzak\(atredhat.com" "Karel Zak" ""
+.SH "SEE ALSO"
+.sp
+\fBmail\fP(1),
+\fBpasswd\fP(1),
+\fBpasswd\fP(5),
+\fButmp\fP(5),
+\fBenviron\fP(7),
+\fBgetty\fP(8),
+\fBinit\fP(8),
+\fBlastlog\fP(8),
+\fBshutdown\fP(8)
+.SH "REPORTING BUGS"
+.sp
+For bug reports, use the issue tracker at \c
+.URL "https://github.com/util\-linux/util\-linux/issues" "" "."
+.SH "AVAILABILITY"
+.sp
+The \fBlogin\fP command is part of the util\-linux package which can be downloaded from \c
+.URL "https://www.kernel.org/pub/linux/utils/util\-linux/" "Linux Kernel Archive" "." \ No newline at end of file
diff --git a/login-utils/login.1.adoc b/login-utils/login.1.adoc
new file mode 100644
index 0000000..a3404f3
--- /dev/null
+++ b/login-utils/login.1.adoc
@@ -0,0 +1,179 @@
+//po4a: entry man manual
+////
+Copyright 1993 Rickard E. Faith (faith@cs.unc.edu)
+May be distributed under the GNU General Public License
+////
+= login(1)
+:doctype: manpage
+:man manual: User Commands
+:man source: util-linux {release-version}
+:page-layout: base
+:command: login
+
+== NAME
+
+login - begin session on the system
+
+== SYNOPSIS
+
+*login* [*-p*] [*-h* _host_] [*-H*] [*-f* _username_|_username_]
+
+== DESCRIPTION
+
+*login* is used when signing onto a system. If no argument is given, *login* prompts for the username.
+
+The user is then prompted for a password, where appropriate. Echoing is disabled to prevent revealing the password. Only a number of password failures are permitted before *login* exits and the communications link is severed. See *LOGIN_RETRIES* in the *CONFIG FILE ITEMS* section.
+
+If password aging has been enabled for the account, the user may be prompted for a new password before proceeding. In such case old password must be provided and the new password entered before continuing. Please refer to *passwd*(1) for more information.
+
+The user and group ID will be set according to their values in the _/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 environment variable values for *$HOME*, *$USER*, *$SHELL*, *$PATH*, *$LOGNAME*, and *$MAIL* are set according to the appropriate fields in the password entry. *$PATH* defaults to _/usr/local/bin:/bin:/usr/bin_ for normal users, and to _/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin_ for root, if not otherwise configured.
+
+The environment variable *$TERM* will be preserved, if it exists, else it will be initialized to the terminal type on your tty. Other environment variables are preserved if the *-p* option is given.
+
+The environment variables defined by PAM are always preserved.
+
+Then the user's shell is started. If no shell is specified for the user in _/etc/passwd_, then _/bin/sh_ is used. If there is no home directory specified in _/etc/passwd_, then _/_ is used, followed by _.hushlogin_ check as described below.
+
+If the file _.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 _/var/log/lastlog_ exists, the last login time is printed, and the current login is recorded.
+
+== OPTIONS
+
+*-p*::
+Used by *getty*(8) to tell *login* to preserve the environment.
+
+*-f*::
+Used to skip a login authentication. This option is usually used by the *getty*(8) autologin feature.
+
+*-h*::
+Used by other servers (such as *telnetd*(8) to pass the name of the remote host to *login* so that it can be placed in utmp and wtmp. Only the superuser is allowed use this option.
++
+Note that the *-h* option has an impact on the *PAM service* *name*. The standard service name is _login_, but with the *-h* option, the name is _remote_. It is necessary to create proper PAM config files (for example, _/etc/pam.d/login_ and _/etc/pam.d/remote_).
+
+*-H*::
+Used by other servers (for example, *telnetd*(8)) to tell *login* that printing the hostname should be suppressed in the login: prompt. See also *LOGIN_PLAIN_PROMPT* below.
+
+include::man-common/help-version.adoc[]
+
+== CONFIG FILE ITEMS
+
+*login* reads the _/etc/login.defs_ configuration file (see *login.defs*(5)). Note that the configuration file could be distributed with another package (usually shadow-utils). The following configuration items are relevant for *login*:
+
+*MOTD_FILE* (string)::
+Specifies a ":" delimited list of "message of the day" files and directories to be displayed upon login. If the specified path is a directory then displays all files with .motd file extension in version-sort order from the directory.
++
+The default value is _/usr/share/misc/motd:/run/motd:/etc/motd_. If the *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 the *pam_motd*(8) PAM module.
++
+The directories in the *MOTD_FILE* are supported since version 2.36.
++
+Note that *login* does not implement any filenames overriding behavior like pam_motd (see also *MOTD_FIRSTONLY*), but all content from all files is displayed. It is recommended to keep extra logic in content generators and use _/run/motd.d_ rather than rely on overriding behavior hardcoded in system tools.
+
+*MOTD_FIRSTONLY* (boolean)::
+
+Forces *login* to stop display content specified by *MOTD_FILE* after the first accessible item in the list. Note that a directory is one item in this case. This option allows *login* semantics to be configured to be more compatible with pam_motd. The default value is _no_.
+
+*LOGIN_PLAIN_PROMPT* (boolean)::
+
+Tell *login* that printing the hostname should be suppressed in the login: prompt. This is an alternative to the *-H* command line option. The default value is _no_.
+
+*LOGIN_TIMEOUT* (number)::
+
+Maximum time in seconds for login. The default value is _60_.
+
+*LOGIN_RETRIES* (number)::
+
+Maximum number of login retries in case of a bad password. The default value is _3_.
+
+*LOGIN_KEEP_USERNAME* (boolean)::
+
+Tell *login* to only re-prompt for the password if authentication failed, but the username is valid. The default value is _no_.
+
+*FAIL_DELAY* (number)::
+
+Delay in seconds before being allowed another three tries after a login failure. The default value is _5_.
+
+*TTYPERM* (string)::
+
+The terminal permissions. The default value is _0600_ or _0620_ if tty group is used.
+
+*TTYGROUP* (string)::
+
+The login tty will be owned by the *TTYGROUP*. The default value is _tty_. If the *TTYGROUP* does not exist, then the ownership of the terminal is set to the user's primary group.
++
+The *TTYGROUP* can be either the name of a group or a numeric group identifier.
+
+*HUSHLOGIN_FILE* (string)::
+
+If defined, this file can inhibit all the usual chatter during the login sequence. If a full pathname (for example, _/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.
++
+If a full pathname is not specified, then hushed mode will be enabled if the file exists in the user's home directory.
++
+The default is to check _/etc/hushlogins_ and if it does not exist then _~/.hushlogin_.
++
+If the *HUSHLOGIN_FILE* item is empty, then all the checks are disabled.
+
+*DEFAULT_HOME* (boolean)::
+
+Indicate if login is allowed if we cannot change directory to the home directory. If set to _yes_, the user will login in the root (/) directory if it is not possible to change directory to their home. The default value is _yes_.
+
+*LASTLOG_UID_MAX* (unsigned number)::
+
+Highest user ID number for which the _lastlog_ entries should be updated. As higher user IDs are usually tracked by remote user identity and authentication services there is no need to create a huge sparse _lastlog_ file for them. No LASTLOG_UID_MAX option present in the configuration means that there is no user ID limit for writing _lastlog_ entries. The default value is _ULONG_MAX_.
+
+*LOG_UNKFAIL_ENAB* (boolean)::
+
+Enable display of unknown usernames when login failures are recorded. The default value is _no_.
++
+Note that logging unknown usernames may be a security issue if a user enters their password instead of their login name.
+
+*ENV_PATH* (string)::
+
+If set, it will be used to define the *PATH* environment variable when a regular user logs in. The default value is _/usr/local/bin:/bin:/usr/bin_.
+
+*ENV_ROOTPATH* (string)::
+*ENV_SUPATH* (string)::
+
+If set, it will be used to define the PATH environment variable when the superuser logs in. *ENV_ROOTPATH* takes precedence. The default value is _/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin_.
+
+== FILES
+
+_/var/run/utmp_,
+_/var/log/wtmp_,
+_/var/log/lastlog_,
+_/var/spool/mail/*_,
+_/etc/motd_,
+_/etc/passwd_,
+_/etc/nologin_,
+_/etc/pam.d/login_,
+_/etc/pam.d/remote_,
+_/etc/hushlogins_,
+_$HOME/.hushlogin_
+
+== BUGS
+
+The undocumented BSD *-r* option is not supported. This may be required by some *rlogind*(8) programs.
+
+A recursive login, as used to be possible in the good old days, no longer works; for most purposes *su*(1) is a satisfactory substitute. Indeed, for security reasons, *login* does a *vhangup*(2) system call to remove any possible listening processes on the tty. This is to avoid password sniffing. If one uses the command *login*, then the surrounding shell gets killed by *vhangup*(2) because it's no longer the true owner of the tty. This can be avoided by using *exec login* in a top-level shell or xterm.
+
+== AUTHORS
+
+Derived from BSD login 5.40 (5/9/89) by mailto:glad@daimi.dk[Michael Glad] for HP-UX. Ported to Linux 0.12: mailto:poe@daimi.aau.dk[Peter Orbaek]. Rewritten to a PAM-only version by mailto:kzak@redhat.com[Karel Zak]
+
+== SEE ALSO
+
+*mail*(1),
+*passwd*(1),
+*passwd*(5),
+*utmp*(5),
+*environ*(7),
+*getty*(8),
+*init*(8),
+*lastlog*(8),
+*shutdown*(8)
+
+include::man-common/bugreports.adoc[]
+
+include::man-common/footer.adoc[]
+
+ifdef::translation[]
+include::man-common/translation.adoc[]
+endif::[]
diff --git a/login-utils/login.c b/login-utils/login.c
new file mode 100644
index 0000000..1812b90
--- /dev/null
+++ b/login-utils/login.c
@@ -0,0 +1,1547 @@
+/*
+ * login(1)
+ *
+ * This program is derived from 4.3 BSD software and is subject to the
+ * copyright notice below.
+ *
+ * Copyright (C) 2011 Karel Zak <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 MERCHANTABILITY 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
+
+#ifdef HAVE_LIBAUDIT
+# include <libaudit.h>
+#endif
+
+#include "c.h"
+#include "pathnames.h"
+#include "strutils.h"
+#include "nls.h"
+#include "env.h"
+#include "xalloc.h"
+#include "all-io.h"
+#include "fileutils.h"
+#include "timeutils.h"
+#include "ttyutils.h"
+#include "pwdutils.h"
+
+#include "logindefs.h"
+
+#define LOGIN_MAX_TRIES 3
+#define LOGIN_EXIT_TIMEOUT 5
+#define LOGIN_TIMEOUT 60
+
+static char **argv0;
+static size_t argv_lth;
+
+#define VCS_PATH_MAX 64
+
+#if defined(HAVE_SCANDIRAT) && defined(HAVE_OPENAT)
+# include <dirent.h>
+# define MOTDDIR_SUPPORT
+# define MOTDDIR_EXT ".motd"
+# define MOTDDIR_EXTSIZ (sizeof(MOTDDIR_EXT) - 1)
+#endif
+
+/*
+ * Login control struct
+ */
+struct login_context {
+ const char *tty_path; /* ttyname() return value */
+ const char *tty_name; /* tty_path without /dev prefix */
+ const char *tty_number; /* end of the tty_path */
+ mode_t tty_mode; /* chmod() mode */
+
+ const char *username; /* points to PAM, pwd or cmd_username */
+ char *cmd_username; /* username specified on command line */
+
+ struct passwd *pwd; /* user info */
+ char *pwdbuf; /* pwd strings */
+
+ pam_handle_t *pamh; /* PAM handler */
+ struct pam_conv conv; /* PAM conversation */
+
+#ifdef LOGIN_CHOWN_VCS
+ char vcsn[VCS_PATH_MAX]; /* virtual console name */
+ char vcsan[VCS_PATH_MAX];
+#endif
+
+ char *thishost; /* this machine */
+ char *thisdomain; /* this machine's domain */
+ char *hostname; /* remote machine */
+ char hostaddress[16]; /* remote address */
+
+ pid_t pid;
+
+ unsigned int quiet:1, /* hush file exists */
+ remote:1, /* login -h */
+ nohost:1, /* login -H */
+ noauth:1, /* login -f */
+ keep_env:1; /* login -p */
+};
+
+static pid_t child_pid = 0;
+static volatile sig_atomic_t got_sig = 0;
+static char *timeout_msg;
+
+#ifdef LOGIN_CHOWN_VCS
+/* true if the filedescriptor fd is a console tty, very Linux specific */
+static int is_consoletty(int fd)
+{
+ struct stat stb;
+
+ if ((fstat(fd, &stb) >= 0)
+ && (major(stb.st_rdev) == TTY_MAJOR)
+ && (minor(stb.st_rdev) < 64)) {
+ return 1;
+ }
+ return 0;
+}
+#endif
+
+/*
+ * Robert Ambrose writes:
+ * A couple of my users have a problem with login processes hanging around
+ * soaking up pts's. What they seem to hung up on is trying to write out the
+ * message 'Login timed out after %d seconds' when the connection has already
+ * been dropped.
+ * What I did was add a second timeout while trying to write the message, so
+ * the process just exits if the second timeout expires.
+ */
+static void __attribute__((__noreturn__))
+ timedout2(int sig __attribute__((__unused__)))
+{
+ struct termios ti;
+
+ /* reset echo */
+ tcgetattr(0, &ti);
+ ti.c_lflag |= ECHO;
+ tcsetattr(0, TCSANOW, &ti);
+ _exit(EXIT_SUCCESS); /* %% */
+}
+
+static void timedout(int sig __attribute__((__unused__)))
+{
+ signal(SIGALRM, timedout2);
+ alarm(10);
+ if (timeout_msg)
+ ignore_result( write(STDERR_FILENO, timeout_msg, strlen(timeout_msg)) );
+ signal(SIGALRM, SIG_IGN);
+ alarm(0);
+ timedout2(0);
+}
+
+/*
+ * This handler can be used to inform a shell about signals to login. If you have
+ * (root) permissions, you can kill all login children by one signal to the
+ * login process.
+ *
+ * Also, a parent who is session leader is able (before setsid() in the child)
+ * to inform the child when the controlling tty goes away (e.g. modem hangup).
+ */
+static void sig_handler(int signal)
+{
+ if (child_pid > 0) {
+ kill(-child_pid, signal);
+ if (signal == SIGTERM)
+ kill(-child_pid, SIGHUP); /* because the shell often ignores SIGTERM */
+ } else
+ got_sig = 1;
+}
+
+/*
+ * Let us delay all exit() calls when the user is not authenticated
+ * or the session not fully initialized (loginpam_session()).
+ */
+static void __attribute__((__noreturn__)) sleepexit(int eval)
+{
+ sleep((unsigned int)getlogindefs_num("FAIL_DELAY", LOGIN_EXIT_TIMEOUT));
+ exit(eval);
+}
+
+static void process_title_init(int argc, char **argv)
+{
+ int i;
+ char **envp = environ;
+
+ /*
+ * Move the environment so we can reuse the memory.
+ * (Code borrowed from sendmail.)
+ * WARNING: ugly assumptions on memory layout here;
+ * if this ever causes problems, #undef DO_PS_FIDDLING
+ */
+ for (i = 0; envp[i] != NULL; i++)
+ continue;
+
+ environ = xmalloc(sizeof(char *) * (i + 1));
+
+ for (i = 0; envp[i] != NULL; i++)
+ environ[i] = xstrdup(envp[i]);
+ environ[i] = NULL;
+
+ if (i > 0)
+ argv_lth = envp[i - 1] + strlen(envp[i - 1]) - argv[0];
+ else
+ argv_lth = argv[argc - 1] + strlen(argv[argc - 1]) - argv[0];
+ if (argv_lth > 1)
+ argv0 = argv;
+}
+
+static void process_title_update(const char *username)
+{
+ size_t i;
+ const char prefix[] = "login -- ";
+ char buf[sizeof(prefix) + LOGIN_NAME_MAX];
+
+ if (!argv0)
+ return;
+
+ if (sizeof(buf) < (sizeof(prefix) + strlen(username) + 1))
+ return;
+
+ snprintf(buf, sizeof(buf), "%s%s", prefix, username);
+
+ i = strlen(buf);
+ if (i > argv_lth - 2) {
+ i = argv_lth - 2;
+ buf[i] = '\0';
+ }
+ memset(argv0[0], '\0', argv_lth); /* clear the memory area */
+ strcpy(argv0[0], buf);
+
+ argv0[1] = NULL;
+}
+
+static const char *get_thishost(struct login_context *cxt, const char **domain)
+{
+ if (!cxt->thishost) {
+ cxt->thishost = xgethostname();
+ if (!cxt->thishost) {
+ if (domain)
+ *domain = NULL;
+ return NULL;
+ }
+ cxt->thisdomain = strchr(cxt->thishost, '.');
+ if (cxt->thisdomain)
+ *cxt->thisdomain++ = '\0';
+ }
+
+ if (domain)
+ *domain = cxt->thisdomain;
+ return cxt->thishost;
+}
+
+#ifdef MOTDDIR_SUPPORT
+static int motddir_filter(const struct dirent *d)
+{
+ size_t namesz;
+
+#ifdef _DIRENT_HAVE_D_TYPE
+ if (d->d_type != DT_UNKNOWN && d->d_type != DT_REG &&
+ d->d_type != DT_LNK)
+ return 0;
+#endif
+ if (*d->d_name == '.')
+ return 0;
+
+ namesz = strlen(d->d_name);
+ if (!namesz || namesz < MOTDDIR_EXTSIZ + 1 ||
+ strcmp(d->d_name + (namesz - MOTDDIR_EXTSIZ), MOTDDIR_EXT) != 0)
+ return 0;
+
+ return 1; /* accept */
+}
+
+static int motddir(const char *dirname)
+{
+ int dd, nfiles, i, done = 0;
+ struct dirent **namelist = NULL;
+
+ dd = open(dirname, O_RDONLY | O_CLOEXEC | O_DIRECTORY);
+ if (dd < 0)
+ return 0;
+
+ nfiles = scandirat(dd, ".", &namelist, motddir_filter, versionsort);
+ if (nfiles <= 0)
+ goto done;
+
+ for (i = 0; i < nfiles; i++) {
+ struct dirent *d = namelist[i];
+ int fd;
+
+ fd = openat(dd, d->d_name, O_RDONLY | O_CLOEXEC);
+ if (fd >= 0) {
+ ul_copy_file(fd, fileno(stdout));
+ close(fd);
+ done++;
+ }
+ }
+
+ for (i = 0; i < nfiles; i++)
+ free(namelist[i]);
+ free(namelist);
+done:
+ close(dd);
+ return done;
+}
+#endif /* MOTDDIR_SUPPORT */
+
+/*
+ * Output the /etc/motd file.
+ *
+ * It determines the name of a login announcement file/dir and outputs it to the
+ * user's terminal at login time. The MOTD_FILE configuration option is a
+ * colon-delimited list of filenames or directories. An empty option disables
+ * message-of-the-day printing completely.
+ */
+static void motd(void)
+{
+ const char *mb;
+ char *file, *list;
+ int firstonly, done = 0;
+
+ firstonly = getlogindefs_bool("MOTD_FIRSTONLY", 0);
+
+ mb = getlogindefs_str("MOTD_FILE", _PATH_MOTDFILE);
+ if (!mb || !*mb)
+ return;
+
+ list = xstrdup(mb);
+
+ for (file = strtok(list, ":"); file; file = strtok(NULL, ":")) {
+ struct stat st;
+
+ if (stat(file, &st) < 0)
+ continue;
+#ifdef MOTDDIR_SUPPORT
+ if (S_ISDIR(st.st_mode))
+ done += motddir(file);
+#endif
+ if (S_ISREG(st.st_mode) && st.st_size > 0) {
+ int fd = open(file, O_RDONLY, 0);
+ if (fd >= 0) {
+ ul_copy_file(fd, fileno(stdout));
+ close(fd);
+ }
+ done++;
+ }
+ if (firstonly && done)
+ break;
+ }
+ free(list);
+}
+
+/*
+ * Display message of the day and you have mail notifications
+ */
+static void display_login_messages(void)
+{
+ motd();
+
+#ifdef LOGIN_STAT_MAIL
+ /*
+ * This turns out to be a bad idea: when the mail spool
+ * is NFS mounted, and the NFS connection hangs, the
+ * login hangs, even root cannot login.
+ * Checking for mail should be done from the shell.
+ */
+ {
+ struct stat st;
+ char *mail;
+
+ mail = getenv("MAIL");
+ if (mail && stat(mail, &st) == 0 && st.st_size != 0) {
+ if (st.st_mtime > st.st_atime)
+ printf(_("You have new mail.\n"));
+ else
+ printf(_("You have mail.\n"));
+ }
+ }
+#endif
+}
+
+/*
+ * Nice and simple code provided by Linus Torvalds 16-Feb-93.
+ * Non-blocking stuff by Maciej W. Rozycki, macro@ds2.pg.gda.pl, 1999.
+ *
+ * He writes: "Login performs open() on a tty in a blocking mode.
+ * In some cases it may make login wait in open() for carrier infinitely,
+ * for example if the line is a simplistic case of a three-wire serial
+ * connection. I believe login should open the line in non-blocking mode,
+ * leaving the decision to make a connection to getty (where it actually
+ * belongs)."
+ */
+static void open_tty(const char *tty)
+{
+ int i, fd, flags;
+
+ fd = open(tty, O_RDWR | O_NONBLOCK);
+ if (fd == -1) {
+ syslog(LOG_ERR, _("FATAL: can't reopen tty: %m"));
+ sleepexit(EXIT_FAILURE);
+ }
+
+ if (!isatty(fd)) {
+ close(fd);
+ syslog(LOG_ERR, _("FATAL: %s is not a terminal"), tty);
+ sleepexit(EXIT_FAILURE);
+ }
+
+ flags = fcntl(fd, F_GETFL);
+ flags &= ~O_NONBLOCK;
+ fcntl(fd, F_SETFL, flags);
+
+ for (i = 0; i < fd; i++)
+ close(i);
+ for (i = 0; i < 3; i++)
+ if (fd != i)
+ dup2(fd, i);
+ if (fd >= 3)
+ close(fd);
+}
+
+static inline void chown_err(const char *what, uid_t uid, gid_t gid)
+{
+ syslog(LOG_ERR, _("chown (%s, %u, %u) failed: %m"), what, uid, gid);
+}
+
+static inline void chmod_err(const char *what, mode_t mode)
+{
+ syslog(LOG_ERR, _("chmod (%s, %u) failed: %m"), what, mode);
+}
+
+static void chown_tty(struct login_context *cxt)
+{
+ const char *grname;
+ uid_t uid = cxt->pwd->pw_uid;
+ gid_t gid = cxt->pwd->pw_gid;
+
+ grname = getlogindefs_str("TTYGROUP", TTYGRPNAME);
+ if (grname && *grname) {
+ struct group *gr = getgrnam(grname);
+ if (gr) /* group by name */
+ gid = gr->gr_gid;
+ else /* group by ID */
+ gid = (gid_t) getlogindefs_num("TTYGROUP", gid);
+ }
+ if (fchown(0, uid, gid)) /* tty */
+ chown_err(cxt->tty_name, uid, gid);
+ if (fchmod(0, cxt->tty_mode))
+ chmod_err(cxt->tty_name, cxt->tty_mode);
+
+#ifdef LOGIN_CHOWN_VCS
+ if (is_consoletty(0)) {
+ if (chown(cxt->vcsn, uid, gid)) /* vcs */
+ chown_err(cxt->vcsn, uid, gid);
+ if (chmod(cxt->vcsn, cxt->tty_mode))
+ chmod_err(cxt->vcsn, cxt->tty_mode);
+
+ if (chown(cxt->vcsan, uid, gid)) /* vcsa */
+ chown_err(cxt->vcsan, uid, gid);
+ if (chmod(cxt->vcsan, cxt->tty_mode))
+ chmod_err(cxt->vcsan, cxt->tty_mode);
+ }
+#endif
+}
+
+/*
+ * Reads the current terminal path and initializes cxt->tty_* variables.
+ */
+static void init_tty(struct login_context *cxt)
+{
+ struct stat st;
+ struct termios tt, ttt;
+ struct winsize ws;
+
+ cxt->tty_mode = (mode_t) getlogindefs_num("TTYPERM", TTY_MODE);
+
+ get_terminal_name(&cxt->tty_path, &cxt->tty_name, &cxt->tty_number);
+
+ /*
+ * In case login is suid it was possible to use a hardlink as stdin
+ * and exploit races for a local root exploit. (Wojciech Purczynski).
+ *
+ * More precisely, the problem is ttyn := ttyname(0); ...; chown(ttyn);
+ * here ttyname() might return "/tmp/x", a hardlink to a pseudotty.
+ * All of this is a problem only when login is suid, which it isn't.
+ */
+ if (!cxt->tty_path || !*cxt->tty_path ||
+ lstat(cxt->tty_path, &st) != 0 || !S_ISCHR(st.st_mode) ||
+ (st.st_nlink > 1 && strncmp(cxt->tty_path, "/dev/", 5) != 0) ||
+ access(cxt->tty_path, R_OK | W_OK) != 0) {
+
+ syslog(LOG_ERR, _("FATAL: bad tty"));
+ sleepexit(EXIT_FAILURE);
+ }
+
+#ifdef LOGIN_CHOWN_VCS
+ if (cxt->tty_number) {
+ /* find names of Virtual Console devices, for later mode change */
+ snprintf(cxt->vcsn, sizeof(cxt->vcsn), "/dev/vcs%s", cxt->tty_number);
+ snprintf(cxt->vcsan, sizeof(cxt->vcsan), "/dev/vcsa%s", cxt->tty_number);
+ }
+#endif
+
+ /* The TTY size might be reset to 0x0 by the kernel when we close the stdin/stdout/stderr file
+ * descriptors so let's save the size now so we can reapply it later */
+ memset(&ws, 0, sizeof(struct winsize));
+ if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) < 0)
+ syslog(LOG_WARNING, _("TIOCGWINSZ ioctl failed: %m"));
+
+ tcgetattr(0, &tt);
+ ttt = tt;
+ ttt.c_cflag &= ~HUPCL;
+
+ if ((fchown(0, 0, 0) || fchmod(0, cxt->tty_mode)) && errno != EROFS) {
+
+ syslog(LOG_ERR, _("FATAL: %s: change permissions failed: %m"),
+ cxt->tty_path);
+ sleepexit(EXIT_FAILURE);
+ }
+
+ /* Kill processes left on this tty */
+ tcsetattr(0, TCSANOW, &ttt);
+
+ /*
+ * Let's close file descriptors before vhangup
+ * https://lkml.org/lkml/2012/6/5/145
+ */
+ close(STDIN_FILENO);
+ close(STDOUT_FILENO);
+ close(STDERR_FILENO);
+
+ signal(SIGHUP, SIG_IGN); /* so vhangup() won't kill us */
+ vhangup();
+ signal(SIGHUP, SIG_DFL);
+
+ /* open stdin,stdout,stderr to the tty */
+ open_tty(cxt->tty_path);
+
+ /* restore tty modes */
+ tcsetattr(0, TCSAFLUSH, &tt);
+
+ /* Restore tty size */
+ if ((ws.ws_row > 0 || ws.ws_col > 0)
+ && ioctl(STDIN_FILENO, TIOCSWINSZ, &ws) < 0)
+ syslog(LOG_WARNING, _("TIOCSWINSZ ioctl failed: %m"));
+}
+
+/*
+ * Logs failed login attempts in _PATH_BTMP, if it exists.
+ * Must be called only with username the name of an actual user.
+ * The most common login failure is to give password instead of username.
+ */
+static void log_btmp(struct login_context *cxt)
+{
+ struct utmpx ut;
+ struct timeval tv;
+
+ memset(&ut, 0, sizeof(ut));
+
+ str2memcpy(ut.ut_user,
+ cxt->username ? cxt->username : "(unknown)",
+ sizeof(ut.ut_user));
+
+ if (cxt->tty_number)
+ str2memcpy(ut.ut_id, cxt->tty_number, sizeof(ut.ut_id));
+ if (cxt->tty_name)
+ str2memcpy(ut.ut_line, cxt->tty_name, sizeof(ut.ut_line));
+
+ gettimeofday(&tv, NULL);
+ ut.ut_tv.tv_sec = tv.tv_sec;
+ ut.ut_tv.tv_usec = tv.tv_usec;
+
+ ut.ut_type = LOGIN_PROCESS; /* XXX doesn't matter */
+ ut.ut_pid = cxt->pid;
+
+ if (cxt->hostname) {
+ str2memcpy(ut.ut_host, cxt->hostname, sizeof(ut.ut_host));
+ if (*cxt->hostaddress)
+ memcpy(&ut.ut_addr_v6, cxt->hostaddress,
+ sizeof(ut.ut_addr_v6));
+ }
+
+ updwtmpx(_PATH_BTMP, &ut);
+}
+
+#ifdef HAVE_LIBAUDIT
+static void log_audit(struct login_context *cxt, int status)
+{
+ int audit_fd;
+ struct passwd *pwd = cxt->pwd;
+
+ audit_fd = audit_open();
+ if (audit_fd == -1)
+ return;
+ if (!pwd && cxt->username)
+ pwd = getpwnam(cxt->username);
+
+ audit_log_acct_message(audit_fd,
+ AUDIT_USER_LOGIN,
+ NULL,
+ "login",
+ cxt->username ? cxt->username : "(unknown)",
+ pwd ? pwd->pw_uid : (unsigned int)-1,
+ cxt->hostname,
+ NULL,
+ cxt->tty_name,
+ status);
+
+ close(audit_fd);
+}
+#else /* !HAVE_LIBAUDIT */
+# define log_audit(cxt, status)
+#endif /* HAVE_LIBAUDIT */
+
+static void log_lastlog(struct login_context *cxt)
+{
+ struct sigaction sa, oldsa_xfsz;
+ struct lastlog ll;
+ off_t offset;
+ time_t t;
+ int fd;
+
+ if (!cxt->pwd)
+ return;
+
+ if (cxt->pwd->pw_uid > (uid_t) getlogindefs_num("LASTLOG_UID_MAX", ULONG_MAX))
+ return;
+
+ /* lastlog is huge on systems with large UIDs, ignore SIGXFSZ */
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_handler = SIG_IGN;
+ sigaction(SIGXFSZ, &sa, &oldsa_xfsz);
+
+ fd = open(_PATH_LASTLOG, O_RDWR, 0);
+ if (fd < 0)
+ goto done;
+ offset = cxt->pwd->pw_uid * sizeof(ll);
+
+ /*
+ * Print last log message.
+ */
+ if (!cxt->quiet) {
+ if ((pread(fd, (void *)&ll, sizeof(ll), offset) == sizeof(ll)) &&
+ ll.ll_time != 0) {
+ char time_string[CTIME_BUFSIZ];
+ char buf[sizeof(ll.ll_host) + 1];
+
+ time_t ll_time = (time_t)ll.ll_time;
+
+ ctime_r(&ll_time, time_string);
+ printf(_("Last login: %.*s "), 24 - 5, time_string);
+
+ if (*ll.ll_host != '\0') {
+ mem2strcpy(buf, ll.ll_host, sizeof(ll.ll_host), sizeof(buf));
+ printf(_("from %s\n"), buf);
+ } else {
+ mem2strcpy(buf, ll.ll_line, sizeof(ll.ll_line), sizeof(buf));
+ printf(_("on %s\n"), buf);
+ }
+ }
+ }
+
+ memset((char *)&ll, 0, sizeof(ll));
+
+ time(&t);
+ ll.ll_time = t; /* ll_time is always 32bit */
+
+ if (cxt->tty_name)
+ str2memcpy(ll.ll_line, cxt->tty_name, sizeof(ll.ll_line));
+ if (cxt->hostname)
+ str2memcpy(ll.ll_host, cxt->hostname, sizeof(ll.ll_host));
+
+ if (pwrite(fd, (void *)&ll, sizeof(ll), offset) != sizeof(ll))
+ warn(_("write lastlog failed"));
+done:
+ if (fd >= 0)
+ close(fd);
+
+ sigaction(SIGXFSZ, &oldsa_xfsz, NULL); /* restore original setting */
+}
+
+/*
+ * Update wtmp and utmp logs.
+ */
+static void log_utmp(struct login_context *cxt)
+{
+ struct utmpx ut = { 0 };
+ struct utmpx *utp = NULL;
+ struct timeval tv = { 0 };
+
+ utmpxname(_PATH_UTMP);
+ setutxent();
+
+ /* Find pid in utmp.
+ *
+ * login sometimes overwrites the runlevel entry in /var/run/utmp,
+ * confusing sysvinit. I added a test for the entry type, and the
+ * problem was gone. (In a runlevel entry, st_pid is not really a pid
+ * but some number calculated from the previous and current runlevel.)
+ * -- Michael Riepe <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 inline int is_pam_failure(int rc)
+{
+ return rc != PAM_SUCCESS;
+}
+
+static pam_handle_t *init_loginpam(struct login_context *cxt)
+{
+ pam_handle_t *pamh = NULL;
+ int rc;
+
+ /*
+ * username is initialized to NULL and if specified on the command line
+ * it is set. Therefore, we are safe not setting it to anything.
+ */
+ rc = pam_start(cxt->remote ? "remote" : "login",
+ cxt->username, &cxt->conv, &pamh);
+ if (rc != PAM_SUCCESS) {
+ warnx(_("PAM failure, aborting: %s"), pam_strerror(pamh, rc));
+ syslog(LOG_ERR, _("Couldn't initialize PAM: %s"),
+ pam_strerror(pamh, rc));
+ sleepexit(EXIT_FAILURE);
+ }
+
+ /* hostname & tty are either set to NULL or their correct values,
+ * depending on how much we know. */
+ rc = pam_set_item(pamh, PAM_RHOST, cxt->hostname);
+ if (is_pam_failure(rc))
+ loginpam_err(pamh, rc);
+
+ if (cxt->tty_path) {
+ rc = pam_set_item(pamh, PAM_TTY, cxt->tty_path);
+ if (is_pam_failure(rc))
+ loginpam_err(pamh, rc);
+ }
+
+ /*
+ * Andrew.Taylor@cal.montage.ca: Provide a user prompt to PAM so that
+ * the "login: " prompt gets localized. Unfortunately, PAM doesn't have
+ * an interface to specify the "Password: " string (yet).
+ */
+ rc = pam_set_item(pamh, PAM_USER_PROMPT, loginpam_get_prompt(cxt));
+ if (is_pam_failure(rc))
+ loginpam_err(pamh, rc);
+
+ /* We don't need the original username. We have to follow PAM. */
+ cxt->username = NULL;
+ cxt->pamh = pamh;
+
+ return pamh;
+}
+
+static void loginpam_auth(struct login_context *cxt)
+{
+ int rc, show_unknown, keep_username;
+ unsigned int retries, failcount = 0;
+ const char *hostname = cxt->hostname ? cxt->hostname :
+ cxt->tty_name ? cxt->tty_name : "<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);
+ keep_username = getlogindefs_bool("LOGIN_KEEP_USERNAME", 0);
+
+ /*
+ * There may be better ways to deal with some of these conditions, but
+ * at least this way I don't think we'll be giving away information...
+ *
+ * Perhaps someday we can trust that all PAM modules will pay attention
+ * to failure count and get rid of LOGIN_MAX_TRIES?
+ */
+ rc = pam_authenticate(pamh, 0);
+
+ while ((++failcount < retries) &&
+ ((rc == PAM_AUTH_ERR) ||
+ (rc == PAM_USER_UNKNOWN) ||
+ (rc == PAM_CRED_INSUFFICIENT) ||
+ (rc == PAM_AUTHINFO_UNAVAIL))) {
+
+ if (rc == PAM_USER_UNKNOWN && !show_unknown)
+ /*
+ * Logging unknown usernames may be a security issue if
+ * a user enters their password instead of their login name.
+ */
+ cxt->username = NULL;
+ else
+ loginpam_get_username(pamh, &cxt->username);
+
+ syslog(LOG_NOTICE,
+ _("FAILED LOGIN %u FROM %s FOR %s, %s"),
+ failcount, hostname,
+ cxt->username ? cxt->username : "(unknown)",
+ pam_strerror(pamh, rc));
+
+ log_btmp(cxt);
+ log_audit(cxt, 0);
+
+ if (!keep_username || rc == PAM_USER_UNKNOWN) {
+ pam_set_item(pamh, PAM_USER, NULL);
+ fprintf(stderr, _("Login incorrect\n\n"));
+ } else
+ fprintf(stderr, _("Password incorrect\n\n"));
+
+ rc = pam_authenticate(pamh, 0);
+ }
+
+ if (is_pam_failure(rc)) {
+
+ if (rc == PAM_USER_UNKNOWN && !show_unknown)
+ cxt->username = NULL;
+ else
+ loginpam_get_username(pamh, &cxt->username);
+
+ if (rc == PAM_MAXTRIES)
+ syslog(LOG_NOTICE,
+ _("TOO MANY LOGIN TRIES (%u) FROM %s FOR %s, %s"),
+ failcount, hostname,
+ cxt->username ? cxt->username : "(unknown)",
+ pam_strerror(pamh, rc));
+ else
+ syslog(LOG_NOTICE,
+ _("FAILED LOGIN SESSION FROM %s FOR %s, %s"),
+ hostname,
+ cxt->username ? cxt->username : "(unknown)",
+ pam_strerror(pamh, rc));
+
+ log_btmp(cxt);
+ log_audit(cxt, 0);
+
+ fprintf(stderr, _("\nLogin incorrect\n"));
+ pam_end(pamh, rc);
+ sleepexit(EXIT_SUCCESS);
+ }
+}
+
+static void loginpam_acct(struct login_context *cxt)
+{
+ int rc;
+ pam_handle_t *pamh = cxt->pamh;
+
+ rc = pam_acct_mgmt(pamh, 0);
+
+ if (rc == PAM_NEW_AUTHTOK_REQD)
+ rc = pam_chauthtok(pamh, PAM_CHANGE_EXPIRED_AUTHTOK);
+
+ if (is_pam_failure(rc))
+ loginpam_err(pamh, rc);
+
+ /*
+ * First get the username that we are actually using, though.
+ */
+ rc = loginpam_get_username(pamh, &cxt->username);
+ if (is_pam_failure(rc))
+ loginpam_err(pamh, rc);
+
+ if (!cxt->username || !*cxt->username) {
+ warnx(_("\nSession setup problem, abort."));
+ syslog(LOG_ERR, _("NULL user name. Abort."));
+ pam_end(pamh, PAM_SYSTEM_ERR);
+ sleepexit(EXIT_FAILURE);
+ }
+}
+
+/*
+ * Note that the position of the pam_setcred() call is discussable:
+ *
+ * - the PAM docs recommend pam_setcred() before pam_open_session()
+ * - but the original RFC http://www.opengroup.org/rfc/mirror-rfc/rfc86.0.txt
+ * uses pam_setcred() after pam_open_session()
+ *
+ * The old login versions (before year 2011) followed the RFC. This is probably
+ * not optimal, because there could be a dependence between some session modules
+ * and the user's credentials.
+ *
+ * The best is probably to follow openssh and call pam_setcred() before and
+ * after pam_open_session(). -- kzak@redhat.com (18-Nov-2011)
+ *
+ */
+static void loginpam_session(struct login_context *cxt)
+{
+ int rc;
+ pam_handle_t *pamh = cxt->pamh;
+
+ rc = pam_setcred(pamh, PAM_ESTABLISH_CRED);
+ if (is_pam_failure(rc))
+ loginpam_err(pamh, rc);
+
+ rc = pam_open_session(pamh, cxt->quiet ? PAM_SILENT : 0);
+ if (is_pam_failure(rc)) {
+ pam_setcred(cxt->pamh, PAM_DELETE_CRED);
+ loginpam_err(pamh, rc);
+ }
+
+ rc = pam_setcred(pamh, PAM_REINITIALIZE_CRED);
+ if (is_pam_failure(rc)) {
+ pam_close_session(pamh, 0);
+ loginpam_err(pamh, rc);
+ }
+}
+
+/*
+ * Detach the controlling terminal, fork, restore syslog stuff, and create
+ * a new session.
+ */
+static void fork_session(struct login_context *cxt)
+{
+ struct sigaction sa, oldsa_hup, oldsa_term;
+
+ signal(SIGALRM, SIG_DFL);
+ signal(SIGQUIT, SIG_DFL);
+ signal(SIGTSTP, SIG_IGN);
+
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_handler = SIG_IGN;
+ sigaction(SIGINT, &sa, NULL);
+
+ sigaction(SIGHUP, &sa, &oldsa_hup); /* ignore when TIOCNOTTY */
+
+ /*
+ * Detach the controlling tty.
+ * We don't need the tty in a parent who only waits for a child.
+ * The child calls setsid() that detaches from the tty as well.
+ */
+ ioctl(0, TIOCNOTTY, NULL);
+
+ /*
+ * We have to beware of SIGTERM, because leaving a PAM session
+ * without pam_close_session() is a pretty bad thing.
+ */
+ sa.sa_handler = sig_handler;
+ sigaction(SIGHUP, &sa, NULL);
+ sigaction(SIGTERM, &sa, &oldsa_term);
+
+ closelog();
+
+ /*
+ * We must fork before setuid(), because we need to call
+ * pam_close_session() as root.
+ */
+ child_pid = fork();
+ if (child_pid < 0) {
+ warn(_("fork failed"));
+
+ pam_setcred(cxt->pamh, PAM_DELETE_CRED);
+ pam_end(cxt->pamh, pam_close_session(cxt->pamh, 0));
+ sleepexit(EXIT_FAILURE);
+ }
+
+ if (child_pid) {
+ /*
+ * parent - wait for child to finish, then clean up session
+ */
+ close(STDIN_FILENO);
+ close(STDOUT_FILENO);
+ close(STDERR_FILENO);
+ free_getlogindefs_data();
+
+ sa.sa_handler = SIG_IGN;
+ sigaction(SIGQUIT, &sa, NULL);
+ sigaction(SIGINT, &sa, NULL);
+
+ /* wait as long as any child is there */
+ while (wait(NULL) == -1 && errno == EINTR) ;
+ openlog("login", LOG_ODELAY, LOG_AUTHPRIV);
+
+ pam_setcred(cxt->pamh, PAM_DELETE_CRED);
+ pam_end(cxt->pamh, pam_close_session(cxt->pamh, 0));
+ exit(EXIT_SUCCESS);
+ }
+
+ /*
+ * child
+ */
+ sigaction(SIGHUP, &oldsa_hup, NULL); /* restore old state */
+ sigaction(SIGTERM, &oldsa_term, NULL);
+ if (got_sig)
+ exit(EXIT_FAILURE);
+
+ /*
+ * Problem: if the user's shell is a shell like ash that doesn't do
+ * setsid() or setpgrp(), then a ctrl-\, sending SIGQUIT to every
+ * process in the pgrp, will kill us.
+ */
+
+ /* start new session */
+ setsid();
+
+ /* make sure we have a controlling tty */
+ open_tty(cxt->tty_path);
+ openlog("login", LOG_ODELAY, LOG_AUTHPRIV); /* reopen */
+
+ /*
+ * TIOCSCTTY: steal tty from other process group.
+ */
+ if (ioctl(0, TIOCSCTTY, 1))
+ syslog(LOG_ERR, _("TIOCSCTTY failed: %m"));
+ signal(SIGINT, SIG_DFL);
+}
+
+/*
+ * Initialize $TERM, $HOME, ...
+ */
+static void init_environ(struct login_context *cxt)
+{
+ struct passwd *pwd = cxt->pwd;
+ char *termenv, **env;
+ char tmp[PATH_MAX];
+ int len, i;
+
+ termenv = getenv("TERM");
+ if (termenv)
+ termenv = xstrdup(termenv);
+
+ /* destroy environment unless user has requested preservation (-p) */
+ if (!cxt->keep_env)
+ environ = xcalloc(1, sizeof(char *));
+
+ xsetenv("HOME", pwd->pw_dir, 0); /* legal to override */
+ xsetenv("USER", pwd->pw_name, 1);
+ xsetenv("SHELL", pwd->pw_shell, 1);
+ xsetenv("TERM", termenv ? termenv : "dumb", 1);
+ free(termenv);
+
+ if (pwd->pw_uid) {
+ if (logindefs_setenv("PATH", "ENV_PATH", _PATH_DEFPATH) != 0)
+ err(EXIT_FAILURE, _("failed to set the %s environment variable"), "PATH");
+
+ } else if (logindefs_setenv("PATH", "ENV_ROOTPATH", NULL) != 0 &&
+ logindefs_setenv("PATH", "ENV_SUPATH", _PATH_DEFPATH_ROOT) != 0) {
+ err(EXIT_FAILURE, _("failed to set the %s environment variable"), "PATH");
+ }
+
+ /* mailx will give a funny error msg if you forget this one */
+ len = snprintf(tmp, sizeof(tmp), "%s/%s", _PATH_MAILDIR, pwd->pw_name);
+ if (len > 0 && (size_t)len < sizeof(tmp))
+ xsetenv("MAIL", tmp, 0);
+
+ /* LOGNAME is not documented in login(1) but HP-UX 6.5 does it. We'll
+ * not allow modifying it.
+ */
+ xsetenv("LOGNAME", pwd->pw_name, 1);
+
+ env = pam_getenvlist(cxt->pamh);
+ for (i = 0; env && env[i]; i++)
+ putenv(env[i]);
+}
+
+/*
+ * This is called for the -h option, initializes cxt->{hostname,hostaddress}.
+ */
+static void init_remote_info(struct login_context *cxt, char *remotehost)
+{
+ const char *domain;
+ char *p;
+ struct addrinfo hints, *info = NULL;
+
+ cxt->remote = 1;
+
+ get_thishost(cxt, &domain);
+
+ if (domain && (p = strchr(remotehost, '.')) &&
+ strcasecmp(p + 1, domain) == 0)
+ *p = '\0';
+
+ cxt->hostname = xstrdup(remotehost);
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_flags = AI_ADDRCONFIG;
+ cxt->hostaddress[0] = 0;
+
+ if (getaddrinfo(cxt->hostname, NULL, &hints, &info) == 0 && info) {
+ if (info->ai_family == AF_INET) {
+ struct sockaddr_in *sa =
+ (struct sockaddr_in *)info->ai_addr;
+
+ memcpy(cxt->hostaddress, &(sa->sin_addr), sizeof(sa->sin_addr));
+
+ } else if (info->ai_family == AF_INET6) {
+ struct sockaddr_in6 *sa =
+ (struct sockaddr_in6 *)info->ai_addr;
+#ifdef IN6_IS_ADDR_V4MAPPED
+ if (IN6_IS_ADDR_V4MAPPED(&sa->sin6_addr)) {
+ const uint8_t *bytes = sa->sin6_addr.s6_addr;
+ struct in_addr addr = { *(const in_addr_t *)(bytes + 12) };
+
+ memcpy(cxt->hostaddress, &addr, sizeof(struct in_addr));
+ } else
+#endif
+ memcpy(cxt->hostaddress, &(sa->sin6_addr), sizeof(sa->sin6_addr));
+ }
+ freeaddrinfo(info);
+ }
+}
+
+static void __attribute__((__noreturn__)) usage(void)
+{
+ fputs(USAGE_HEADER, stdout);
+ printf(_(" %s [-p] [-h <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 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);
+}
+
+static void initialize(int argc, char **argv, struct login_context *cxt)
+{
+ int c;
+ unsigned int timeout;
+ struct sigaction act;
+
+ /* the only two longopts to satisfy UL standards */
+ enum { HELP_OPTION = CHAR_MAX + 1 };
+ const struct option longopts[] = {
+ {"help", no_argument, NULL, HELP_OPTION},
+ {"version", no_argument, NULL, 'V'},
+ {NULL, 0, NULL, 0}
+ };
+
+ /*
+ * This bounds the time given to login. Not a define, so it can
+ * be patched on machines where it's too small.
+ */
+ timeout = (unsigned int)getlogindefs_num("LOGIN_TIMEOUT", LOGIN_TIMEOUT);
+
+ /* TRANSLATORS: The standard value for %u is 60. */
+ xasprintf(&timeout_msg, _("%s: timed out after %u seconds"),
+ program_invocation_short_name, timeout);
+
+ signal(SIGALRM, timedout);
+ sigaction(SIGALRM, NULL, &act);
+ act.sa_flags &= ~SA_RESTART;
+ sigaction(SIGALRM, &act, NULL);
+ alarm(timeout);
+ signal(SIGQUIT, SIG_IGN);
+ signal(SIGINT, SIG_IGN);
+
+ setpriority(PRIO_PROCESS, 0, 0);
+ process_title_init(argc, argv);
+
+ while ((c = getopt_long(argc, argv, "fHh:pV", longopts, NULL)) != -1)
+ switch (c) {
+ case 'f':
+ cxt->noauth = 1;
+ break;
+
+ case 'H':
+ cxt->nohost = 1;
+ break;
+
+ case 'h':
+ if (getuid()) {
+ fprintf(stderr,
+ _("login: -h is for superuser only\n"));
+ exit(EXIT_FAILURE);
+ }
+ init_remote_info(cxt, optarg);
+ break;
+
+ case 'p':
+ cxt->keep_env = 1;
+ break;
+
+ case 'V':
+ print_version(EXIT_SUCCESS);
+ case HELP_OPTION:
+ usage();
+ default:
+ errtryhelp(EXIT_FAILURE);
+ }
+ argc -= optind;
+ argv += optind;
+
+ if (*argv) {
+ char *p = *argv;
+
+ /* username from command line */
+ cxt->cmd_username = xstrdup(p);
+ /* used temporary, it'll be replaced by username from PAM or/and cxt->pwd */
+ cxt->username = cxt->cmd_username;
+
+ /* Wipe the name - some people mistype their password here. */
+ /* (Of course we are too late, but perhaps this helps a little...) */
+#ifdef HAVE_EXPLICIT_BZERO
+ explicit_bzero(p, strlen(p));
+#else
+ while (*p)
+ *p++ = ' ';
+#endif
+ }
+#ifdef HAVE_CLOSE_RANGE
+ if (close_range(STDERR_FILENO + 1, ~0U, 0) < 0)
+#endif
+ ul_close_all_fds(STDERR_FILENO + 1, ~0U);
+}
+
+int main(int argc, char **argv)
+{
+ char *child_argv[10];
+ int child_argc = 0;
+ struct passwd *pwd;
+ struct login_context cxt = {
+ .tty_mode = TTY_MODE, /* tty chmod() */
+ .pid = getpid(), /* PID */
+#ifdef HAVE_SECURITY_PAM_MISC_H
+ .conv = { misc_conv, NULL } /* Linux-PAM conversation function */
+#elif defined(HAVE_SECURITY_OPENPAM_H)
+ .conv = { openpam_ttyconv, NULL } /* OpenPAM conversation function */
+#endif
+ };
+
+ setlocale(LC_ALL, "");
+ bindtextdomain(PACKAGE, LOCALEDIR);
+ textdomain(PACKAGE);
+
+ initialize(argc, argv, &cxt);
+
+ setpgrp(); /* set pgid to pid this means that setsid() will fail */
+ init_tty(&cxt);
+
+ openlog("login", LOG_ODELAY, LOG_AUTHPRIV);
+
+ init_loginpam(&cxt);
+
+ /* login -f, then the user has already been authenticated */
+ cxt.noauth = cxt.noauth && getuid() == 0 ? 1 : 0;
+
+ if (!cxt.noauth)
+ loginpam_auth(&cxt);
+
+ /*
+ * Authentication may be skipped (for example, during krlogin, rlogin,
+ * etc...), but it doesn't mean that we can skip other account checks.
+ * The account could be disabled or the password has expired (although
+ * the kerberos ticket is valid). -- kzak@redhat.com (22-Feb-2006)
+ */
+ loginpam_acct(&cxt);
+
+ cxt.pwd = xgetpwnam(cxt.username, &cxt.pwdbuf);
+ if (!cxt.pwd) {
+ warnx(_("\nSession setup problem, abort."));
+ syslog(LOG_ERR, _("Invalid user name \"%s\". Abort."),
+ cxt.username);
+ pam_end(cxt.pamh, PAM_SYSTEM_ERR);
+ sleepexit(EXIT_FAILURE);
+ }
+
+ pwd = cxt.pwd;
+ cxt.username = pwd->pw_name;
+
+ /*
+ * Initialize the supplementary group list. This should be done before
+ * pam_setcred, because PAM modules might add groups during that call.
+ *
+ * For root we don't call initgroups, instead we call setgroups with
+ * group 0. This avoids the need to step through the whole group file,
+ * which can cause problems if NIS, NIS+, LDAP or something similar
+ * is used and the machine has network problems.
+ */
+ {
+ int retcode;
+
+ retcode = pwd->pw_uid ? initgroups(cxt.username, pwd->pw_gid) : /* user */
+ setgroups(0, NULL); /* root */
+ if (retcode < 0) {
+ syslog(LOG_ERR, _("groups initialization failed: %m"));
+ warnx(_("\nSession setup problem, abort."));
+ pam_end(cxt.pamh, PAM_SYSTEM_ERR);
+ sleepexit(EXIT_FAILURE);
+ }
+ }
+
+ cxt.quiet = get_hushlogin_status(pwd, 1) == 1 ? 1 : 0;
+
+ /*
+ * Open PAM session (after successful authentication and account check).
+ */
+ loginpam_session(&cxt);
+
+ /* committed to login -- turn off timeout */
+ alarm((unsigned int)0);
+ free(timeout_msg);
+ timeout_msg = NULL;
+
+ endpwent();
+
+ log_utmp(&cxt);
+ log_audit(&cxt, 1);
+ log_lastlog(&cxt);
+
+ chown_tty(&cxt);
+
+ if (setgid(pwd->pw_gid) < 0 && pwd->pw_gid) {
+ syslog(LOG_ALERT, _("setgid() failed"));
+ exit(EXIT_FAILURE);
+ }
+
+ if (pwd->pw_shell == NULL || *pwd->pw_shell == '\0')
+ pwd->pw_shell = _PATH_BSHELL;
+
+ init_environ(&cxt); /* init $HOME, $TERM ... */
+
+ process_title_update(cxt.username);
+
+ log_syslog(&cxt);
+
+ if (!cxt.quiet)
+ display_login_messages();
+
+ /*
+ * Detach the controlling terminal, fork, and create a new session
+ * and reinitialize syslog stuff.
+ */
+ fork_session(&cxt);
+
+ /* discard permissions last so we can't get killed and drop core */
+ if (setuid(pwd->pw_uid) < 0 && pwd->pw_uid) {
+ syslog(LOG_ALERT, _("setuid() failed"));
+ exit(EXIT_FAILURE);
+ }
+
+ /* wait until here to change directory! */
+ if (chdir(pwd->pw_dir) < 0) {
+ warn(_("%s: change directory failed"), pwd->pw_dir);
+
+ if (!getlogindefs_bool("DEFAULT_HOME", 1))
+ exit(0);
+ if (chdir("/"))
+ exit(EXIT_FAILURE);
+ pwd->pw_dir = "/";
+ printf(_("Logging in with home = \"/\".\n"));
+ }
+
+ /* if the shell field has a space: treat it like a shell script */
+ if (strchr(pwd->pw_shell, ' ')) {
+ char *buff;
+
+ xasprintf(&buff, "exec %s", pwd->pw_shell);
+ child_argv[child_argc++] = "/bin/sh";
+ child_argv[child_argc++] = "-sh";
+ child_argv[child_argc++] = "-c";
+ child_argv[child_argc++] = buff;
+ } else {
+ char tbuf[PATH_MAX + 2], *p;
+
+ tbuf[0] = '-';
+ xstrncpy(tbuf + 1, ((p = strrchr(pwd->pw_shell, '/')) ?
+ p + 1 : pwd->pw_shell), sizeof(tbuf) - 1);
+
+ child_argv[child_argc++] = pwd->pw_shell;
+ child_argv[child_argc++] = xstrdup(tbuf);
+ }
+
+ child_argv[child_argc++] = NULL;
+
+ /* http://www.linux-pam.org/Linux-PAM-html/adg-interface-by-app-expected.html#adg-pam_end */
+ (void) pam_end(cxt.pamh, PAM_SUCCESS|PAM_DATA_SILENT);
+
+ execvp(child_argv[0], child_argv + 1);
+
+ if (!strcmp(child_argv[0], "/bin/sh"))
+ warn(_("couldn't exec shell script"));
+ else
+ warn(_("no shell"));
+
+ exit(EXIT_SUCCESS);
+}
diff --git a/login-utils/lslogins.1 b/login-utils/lslogins.1
new file mode 100644
index 0000000..e4f366f
--- /dev/null
+++ b/login-utils/lslogins.1
@@ -0,0 +1,234 @@
+'\" t
+.\" Title: lslogins
+.\" Author: [see the "AUTHOR(S)" section]
+.\" Generator: Asciidoctor 2.0.20
+.\" Date: 2023-11-30
+.\" Manual: User Commands
+.\" Source: util-linux 2.39.3
+.\" Language: English
+.\"
+.TH "LSLOGINS" "1" "2023-11-30" "util\-linux 2.39.3" "User Commands"
+.ie \n(.g .ds Aq \(aq
+.el .ds Aq '
+.ss \n[.ss] 0
+.nh
+.ad l
+.de URL
+\fI\\$2\fP <\\$1>\\$3
+..
+.als MTO URL
+.if \n[.g] \{\
+. mso www.tmac
+. am URL
+. ad l
+. .
+. am MTO
+. ad l
+. .
+. LINKSTYLE blue R < >
+.\}
+.SH "NAME"
+lslogins \- display information about known users in the system
+.SH "SYNOPSIS"
+.sp
+\fBlslogins\fP [options] [\fB\-s\fP|\fB\-u\fP[=\fIUID\fP]] [\fB\-g\fP \fIgroups\fP] [\fB\-l\fP \fIlogins\fP] [\fIusername\fP]
+.SH "DESCRIPTION"
+.sp
+Examine the wtmp and btmp logs, \fI/etc/shadow\fP (if necessary) and \fI/passwd\fP and output the desired data.
+.sp
+The optional argument \fIusername\fP forces \fBlslogins\fP to print all available details about the specified user only. In this case the output format is different than in case of \fB\-l\fP or \fB\-g\fP and unknown is \fIusername\fP reported as an error.
+.sp
+The default action is to list info about all the users in the system.
+.SH "OPTIONS"
+.sp
+Mandatory arguments to long options are mandatory for short options too.
+.sp
+\fB\-a\fP, \fB\-\-acc\-expiration\fP
+.RS 4
+Display data about the date of last password change and the account expiration date (see \fBshadow\fP(5) for more info). (Requires root privileges.)
+.RE
+.sp
+\fB\-\-btmp\-file\fP \fIpath\fP
+.RS 4
+Alternate path for btmp.
+.RE
+.sp
+\fB\-c\fP, \fB\-\-colon\-separate\fP
+.RS 4
+Separate info about each user with a colon instead of a newline.
+.RE
+.sp
+\fB\-e\fP, \fB\-\-export\fP
+.RS 4
+Output data in the format of NAME=VALUE. See also option \fB\-\-shell\fP.
+.RE
+.sp
+\fB\-f\fP, \fB\-\-failed\fP
+.RS 4
+Display data about the users\*(Aq last failed login attempts.
+.RE
+.sp
+\fB\-G\fP, \fB\-\-supp\-groups\fP
+.RS 4
+Show information about supplementary groups.
+.RE
+.sp
+\fB\-g\fP, \fB\-\-groups\fP=\fIgroups\fP
+.RS 4
+Only show data of users belonging to \fIgroups\fP. More than one group may be specified; the list has to be comma\-separated. Unknown group names are ignored.
+.sp
+Note that the relation between user and group may be invisible for the primary group if the user is not explicitly specified as group member (e.g., in \fI/etc/group\fP). If the command \fBlslogins\fP scans for groups then it uses the groups database only, and the user database with primary GID is not used at all.
+.RE
+.sp
+\fB\-L\fP, \fB\-\-last\fP
+.RS 4
+Display data containing information about the users\*(Aq last login sessions.
+.RE
+.sp
+\fB\-l\fP, \fB\-\-logins\fP=\fIlogins\fP
+.RS 4
+Only show data of users with a login specified in \fIlogins\fP (user names or user IDs). More than one login may be specified; the list has to be comma\-separated. Unknown login names are ignored.
+.RE
+.sp
+\fB\-n\fP, \fB\-\-newline\fP
+.RS 4
+Display each piece of information on a separate line.
+.RE
+.sp
+\fB\-\-noheadings\fP
+.RS 4
+Do not print a header line.
+.RE
+.sp
+\fB\-\-notruncate\fP
+.RS 4
+Don\(cqt truncate output.
+.RE
+.sp
+\fB\-o\fP, \fB\-\-output\fP \fIlist\fP
+.RS 4
+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.
+.RE
+.sp
+\fB\-\-output\-all\fP
+.RS 4
+Output all available columns. \fB\-\-help\fP to get a list of all supported columns.
+.RE
+.sp
+\fB\-p\fP, \fB\-\-pwd\fP
+.RS 4
+Display information related to login by password (see also \fB\-afL\fP).
+.RE
+.sp
+\fB\-r\fP, \fB\-\-raw\fP
+.RS 4
+Raw output (no columnation).
+.RE
+.sp
+\fB\-s\fP, \fB\-\-system\-accs\fP
+.RS 4
+Show system accounts. These are by default all accounts with a UID between 101 and 999 (inclusive), with the exception of either nobody or nfsnobody (UID 65534). This hardcoded default may be overwritten by parameters \fBSYS_UID_MIN\fP and \fBSYS_UID_MAX\fP in the file \fI/etc/login.defs\fP.
+.RE
+.sp
+\fB\-\-time\-format\fP \fItype\fP
+.RS 4
+Display dates in short, full or iso format. The default is short, this time format is designed to be space efficient and human readable.
+.RE
+.sp
+\fB\-u\fP, \fB\-\-user\-accs\fP
+.RS 4
+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 \fI/etc/login.defs\fP.
+.RE
+.sp
+\fB\-h\fP, \fB\-\-help\fP
+.RS 4
+Display help text and exit.
+.RE
+.sp
+\fB\-V\fP, \fB\-\-version\fP
+.RS 4
+Print version and exit.
+.RE
+.sp
+\fB\-\-wtmp\-file\fP \fIpath\fP
+.RS 4
+Alternate path for wtmp.
+.RE
+.sp
+\fB\-\-lastlog\fP \fIpath\fP
+.RS 4
+Alternate path for \fBlastlog\fP(8).
+.RE
+.sp
+\fB\-y\fP, \fB\-\-shell\fP
+.RS 4
+The column name will be modified to contain only characters allowed for shell variable identifiers. This is usable, for example, with \fB\-\-export\fP. Note that this feature has been automatically enabled for \fB\-\-export\fP in version 2.37, but due to compatibility issues, now it\(cqs necessary to request this behavior by \fB\-\-shell\fP.
+.RE
+.sp
+\fB\-Z\fP, \fB\-\-context\fP
+.RS 4
+Display the users\*(Aq security context.
+.RE
+.sp
+\fB\-z\fP, \fB\-\-print0\fP
+.RS 4
+Delimit user entries with a nul character, instead of a newline.
+.RE
+.SH "EXIT STATUS"
+.sp
+0
+.RS 4
+if OK,
+.RE
+.sp
+1
+.RS 4
+if incorrect arguments specified,
+.RE
+.sp
+2
+.RS 4
+if a serious error occurs (e.g., a corrupt log).
+.RE
+.SH "NOTES"
+.sp
+The default UID thresholds are read from \fI/etc/login.defs\fP.
+.SS "Password status"
+.sp
+Multiple fields describe password status.
+.sp
+\fB"Password is locked"\fP
+.RS 4
+The password is prefixed by \*(Aq!!\*(Aq, and the user cannot login although the password is set or empty. This is common for new accounts without a set password.
+.RE
+.sp
+\fB"Password not required (empty)"\fP
+.RS 4
+The password is not set (hash is missing); this is common for locked system accounts. Not requiring a password does not mean the user can log\-in without a password. It depends on the password "lock" status.
+.RE
+.sp
+\fB"Login by password disabled"\fP
+.RS 4
+\*(Aqyes\*(Aq means that there is no valid password. The password hash is missing, or the hash method is unknown or contains invalid chars.
+.RE
+.SH "HISTORY"
+.sp
+The \fBlslogins\fP utility is inspired by the \fBlogins\fP utility, which first appeared in FreeBSD 4.10.
+.SH "AUTHORS"
+.sp
+.MTO "ooprala\(atredhat.com" "Ondrej Oprala" ","
+.MTO "kzak\(atredhat.com" "Karel Zak" ""
+.SH "SEE ALSO"
+.sp
+\fBgroup\fP(5),
+\fBpasswd\fP(5),
+\fBshadow\fP(5),
+\fButmp\fP(5)
+.SH "REPORTING BUGS"
+.sp
+For bug reports, use the issue tracker at \c
+.URL "https://github.com/util\-linux/util\-linux/issues" "" "."
+.SH "AVAILABILITY"
+.sp
+The \fBlslogins\fP command is part of the util\-linux package which can be downloaded from \c
+.URL "https://www.kernel.org/pub/linux/utils/util\-linux/" "Linux Kernel Archive" "." \ No newline at end of file
diff --git a/login-utils/lslogins.1.adoc b/login-utils/lslogins.1.adoc
new file mode 100644
index 0000000..6371c2d
--- /dev/null
+++ b/login-utils/lslogins.1.adoc
@@ -0,0 +1,157 @@
+//po4a: entry man manual
+// Copyright 2014 Ondrej Oprala (ondrej.oprala@gmail.com)
+// May be distributed under the GNU General Public License
+= lslogins(1)
+:doctype: manpage
+:man manual: User Commands
+:man source: util-linux {release-version}
+:page-layout: base
+:command: lslogins
+
+== NAME
+
+lslogins - display information about known users in the system
+
+== SYNOPSIS
+
+*lslogins* [options] [*-s*|*-u*[=_UID_]] [*-g* _groups_] [*-l* _logins_] [_username_]
+
+== DESCRIPTION
+
+Examine the wtmp and btmp logs, _/etc/shadow_ (if necessary) and _/passwd_ and output the desired data.
+
+The optional argument _username_ forces *lslogins* to print all available details about the specified user only. In this case the output format is different than in case of *-l* or *-g* and unknown is _username_ reported as an error.
+
+The default action is to list info about all the users in the system.
+
+== OPTIONS
+
+Mandatory arguments to long options are mandatory for short options too.
+
+*-a*, *--acc-expiration*::
+Display data about the date of last password change and the account expiration date (see *shadow*(5) for more info). (Requires root privileges.)
+
+*--btmp-file* _path_::
+Alternate path for btmp.
+
+*-c*, *--colon-separate*::
+Separate info about each user with a colon instead of a newline.
+
+*-e*, *--export*::
+Output data in the format of NAME=VALUE. See also option *--shell*.
+
+*-f*, *--failed*::
+Display data about the users' last failed login attempts.
+
+*-G*, *--supp-groups*::
+Show information about supplementary groups.
+
+*-g*, **--groups**=_groups_::
+Only show data of users belonging to _groups_. More than one group may be specified; the list has to be comma-separated. Unknown group names are ignored.
++
+Note that the relation between user and group may be invisible for the primary group if the user is not explicitly specified as group member (e.g., in _/etc/group_). If the command *lslogins* scans for groups then it uses the groups database only, and the user database with primary GID is not used at all.
+
+*-L*, *--last*::
+Display data containing information about the users' last login sessions.
+
+*-l*, **--logins**=_logins_::
+Only show data of users with a login specified in _logins_ (user names or user IDs). More than one login may be specified; the list has to be comma-separated. Unknown login names are ignored.
+
+*-n*, *--newline*::
+Display each piece of information on a separate line.
+
+*--noheadings*::
+Do not print a header line.
+
+*--notruncate*::
+Don't truncate output.
+
+*-o*, *--output* _list_::
+Specify which output columns to print. The default list of columns may be extended if _list_ is specified in the format _+list_.
+
+*--output-all*::
+Output all available columns. *--help* to get a list of all supported columns.
+
+*-p*, *--pwd*::
+Display information related to login by password (see also *-afL*).
+
+*-r*, *--raw*::
+Raw output (no columnation).
+
+*-s*, *--system-accs*::
+Show system accounts. These are by default all accounts with a UID between 101 and 999 (inclusive), with the exception of either nobody or nfsnobody (UID 65534). This hardcoded default may be overwritten by parameters *SYS_UID_MIN* and *SYS_UID_MAX* in the file _/etc/login.defs_.
+
+*--time-format* _type_::
+Display dates in short, full or iso format. The default is short, this time format is designed to be space efficient and human readable.
+
+*-u*, *--user-accs*::
+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_.
+
+include::man-common/help-version.adoc[]
+
+*--wtmp-file* _path_::
+Alternate path for wtmp.
+
+*--lastlog* _path_::
+Alternate path for *lastlog*(8).
+
+*-y*, *--shell*::
+The column name will be modified to contain only characters allowed for shell variable identifiers. This is usable, for example, with *--export*. Note that this feature has been automatically enabled for *--export* in version 2.37, but due to compatibility issues, now it's necessary to request this behavior by *--shell*.
+
+*-Z*, *--context*::
+Display the users' security context.
+
+*-z*, *--print0*::
+Delimit user entries with a nul character, instead of a newline.
+
+== EXIT STATUS
+
+0::
+if OK,
+
+1::
+if incorrect arguments specified,
+
+2::
+if a serious error occurs (e.g., a corrupt log).
+
+== NOTES
+
+The default UID thresholds are read from _/etc/login.defs_.
+
+=== Password status
+
+Multiple fields describe password status.
+
+*"Password is locked"*::
+The password is prefixed by '!!', and the user cannot login although the password is set or empty. This is common for new accounts without a set password.
+
+*"Password not required (empty)"*::
+The password is not set (hash is missing); this is common for locked system accounts. Not requiring a password does not mean the user can log-in without a password. It depends on the password "lock" status.
+
+*"Login by password disabled"*::
+'yes' means that there is no valid password. The password hash is missing, or the hash method is unknown or contains invalid chars.
+
+== HISTORY
+
+The *lslogins* utility is inspired by the *logins* utility, which first appeared in FreeBSD 4.10.
+
+== AUTHORS
+
+mailto:ooprala@redhat.com[Ondrej Oprala],
+mailto:kzak@redhat.com[Karel Zak]
+
+== SEE ALSO
+
+*group*(5),
+*passwd*(5),
+*shadow*(5),
+*utmp*(5)
+
+include::man-common/bugreports.adoc[]
+
+include::man-common/footer.adoc[]
+
+ifdef::translation[]
+include::man-common/translation.adoc[]
+endif::[]
diff --git a/login-utils/lslogins.c b/login-utils/lslogins.c
new file mode 100644
index 0000000..effaba4
--- /dev/null
+++ b/login-utils/lslogins.c
@@ -0,0 +1,1734 @@
+/*
+ * 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 <lastlog.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 "fileutils.h"
+#include "logindefs.h"
+#include "procfs.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 101
+#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
+};
+
+enum {
+ LASTLOG_TIME,
+ LASTLOG_LINE,
+ LASTLOG_HOST
+};
+
+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
+ char *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 defined"), N_("Password not required (empty)"), 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;
+
+ int lastlogin_fd;
+
+ 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,
+ shellvar : 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 parse_utmpx(const char *path, size_t *nrecords, struct utmpx **records)
+{
+ size_t i, imax = 1;
+ struct utmpx *ary = NULL;
+ struct stat st;
+
+ *nrecords = 0;
+ *records = NULL;
+
+ if (utmpxname(path) < 0)
+ return -errno;
+
+ /* optimize allocation according to file size, the realloc() below is
+ * just fallback only */
+ if (stat(path, &st) == 0 && (size_t) st.st_size >= sizeof(struct utmpx)) {
+ imax = st.st_size / sizeof(struct utmpx);
+ ary = xmalloc(imax * sizeof(struct utmpx));
+ }
+
+ for (i = 0; ; i++) {
+ struct utmpx *u;
+ errno = 0;
+ u = getutxent();
+ if (!u) {
+ if (errno)
+ goto fail;
+ break;
+ }
+ if (i == imax)
+ ary = xrealloc(ary, (imax *= 2) * sizeof(struct utmpx));
+ ary[i] = *u;
+ }
+
+ *nrecords = i;
+ *records = ary;
+ endutxent();
+ return 0;
+fail:
+ endutxent();
+ free(ary);
+ if (errno) {
+ if (errno != EACCES)
+ err(EXIT_FAILURE, "%s", path);
+ return -errno;
+ }
+ return -EINVAL;
+}
+
+static void get_lastlog(struct lslogins_control *ctl, uid_t uid, void *dst, int what)
+{
+ struct lastlog ll;
+
+ if (ctl->lastlogin_fd < 0 ||
+ pread(ctl->lastlogin_fd, (void *)&ll, sizeof(ll), uid * sizeof(ll)) != sizeof(ll))
+ return;
+
+ switch (what) {
+ case LASTLOG_TIME: {
+ time_t *t = (time_t *)dst;
+ *t = ll.ll_time;
+ break;
+ }
+ case LASTLOG_LINE:
+ mem2strcpy(dst, ll.ll_line, sizeof(ll.ll_line), sizeof(ll.ll_line) + 1);
+ break;
+ case LASTLOG_HOST:
+ mem2strcpy(dst, ll.ll_host, sizeof(ll.ll_host), sizeof(ll.ll_host) + 1);
+ break;
+ default:
+ abort();
+ }
+}
+
+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;
+}
+
+#ifdef __linux__
+static int get_nprocs(const uid_t uid)
+{
+ DIR *dir;
+ struct dirent *d;
+ int nprocs = 0;
+
+ dir = opendir(_PATH_PROC);
+ if (!dir)
+ return 0;
+
+ while ((d = xreaddir(dir))) {
+ if (procfs_dirent_match_uid(dir, d, uid))
+ ++nprocs;
+ }
+
+ closedir(dir);
+ return nprocs;
+}
+#endif
+
+static const char *get_pwd_method(const char *str, const char **next)
+{
+ const char *p = str;
+ const char *res = NULL;
+
+ if (!p || *p++ != '$')
+ return NULL;
+
+ switch (*p) {
+ case '1':
+ res = "MD5";
+ break;
+ case '2':
+ switch(*(p+1)) {
+ case 'a':
+ case 'y':
+ p++;
+ res = "Blowfish";
+ break;
+ case 'b':
+ p++;
+ res = "bcrypt";
+ break;
+ }
+ break;
+ case '3':
+ res = "NT";
+ break;
+ case '5':
+ res = "SHA-256";
+ break;
+ case '6':
+ res = "SHA-512";
+ break;
+ case '7':
+ res = "scrypt";
+ break;
+ case 'y':
+ res = "yescrypt";
+ break;
+ case 'g':
+ if (*(p + 1) == 'y') {
+ p++;
+ res = "gost-yescrypt";
+ }
+ break;
+ case '_':
+ res = "bsdicrypt";
+ break;
+ default:
+ res = "unknown";
+ break;
+ }
+ p++;
+
+ if (*p != '$')
+ return NULL;
+ if (next)
+ *next = ++p;
+ return res;
+}
+
+#define is_invalid_pwd_char(x) (isspace((unsigned char) (x)) || \
+ (x) == ':' || (x) == ';' || (x) == '*' || \
+ (x) == '!' || (x) == '\\')
+#define is_valid_pwd_char(x) (isascii((unsigned char) (x)) && !is_invalid_pwd_char(x))
+
+/*
+ * This function do not accept empty passwords or locked accouns.
+ */
+static int valid_pwd(const char *str)
+{
+ const char *p = str;
+
+ if (!str || !*str)
+ return 0;
+
+ /* $id$ */
+ if (get_pwd_method(str, &p) == NULL)
+ return 0;
+
+ if (!p || !*p)
+ return 0;
+ /* salt$ */
+ for (; *p; p++) {
+ if (*p == '$') {
+ p++;
+ break;
+ }
+ if (!is_valid_pwd_char(*p))
+ return 0;
+ }
+
+ if (!*p)
+ return 0;
+ /* encrypted */
+ for (; *p; p++) {
+ if (!is_valid_pwd_char(*p)) {
+ 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 (!user->nsgroups &&
+ get_sgroups(&user->sgroups, &user->nsgroups, pwd) < 0)
+ 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);
+ } else {
+ time = 0;
+ get_lastlog(ctl, pwd->pw_uid, &time, LASTLOG_TIME);
+ if (time)
+ user->last_login = make_time(ctl->time_mode, time);
+ }
+ break;
+ case COL_LAST_TTY:
+ user->last_tty = xcalloc(1, sizeof(user_wtmp->ut_line) + 1);
+ if (user_wtmp) {
+ mem2strcpy(user->last_tty, user_wtmp->ut_line,
+ sizeof(user_wtmp->ut_line),
+ sizeof(user_wtmp->ut_line) + 1);;
+ } else
+ get_lastlog(ctl, user->uid, user->last_tty, LASTLOG_LINE);
+ break;
+ case COL_LAST_HOSTNAME:
+ user->last_hostname = xcalloc(1, sizeof(user_wtmp->ut_host) + 1);
+ if (user_wtmp) {
+ mem2strcpy(user->last_hostname, user_wtmp->ut_host,
+ sizeof(user_wtmp->ut_host),
+ sizeof(user_wtmp->ut_host) + 1);;
+ } else
+ get_lastlog(ctl, user->uid, user->last_hostname, LASTLOG_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 = xmalloc(sizeof(user_btmp->ut_line) + 1);
+ mem2strcpy(user->failed_tty, user_btmp->ut_line,
+ sizeof(user_btmp->ut_line),
+ sizeof(user_btmp->ut_line) + 1);;
+ }
+ 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) {
+ const char *p = shadow->sp_pwdp;
+
+ while (p && (*p == '!' || *p == '*'))
+ p++;
+
+ if (!p || !*p)
+ user->pwd_empty = STATUS_TRUE;
+ } else
+ user->pwd_empty = STATUS_UNKNOWN;
+ break;
+ case COL_PWDDENY:
+ if (shadow) {
+ const char *p = shadow->sp_pwdp;
+
+ while (p && (*p == '!' || *p == '*'))
+ p++;
+
+ if (p && *p && p != shadow->sp_pwdp && !valid_pwd(p))
+ user->pwd_deny = STATUS_TRUE;
+ } else
+ user->pwd_deny = STATUS_UNKNOWN;
+ break;
+ case COL_PWDLOCK:
+ if (shadow) {
+ const char *p = shadow->sp_pwdp;
+ int i = 0;
+
+ /* 'passwd --lock' uses two exclamation marks,
+ * shadow(5) describes the lock as "field which
+ * starts with an exclamation mark". Let's
+ * support more '!' ...
+ */
+ while (p && *p == '!')
+ p++, i++;
+
+ if (i != 0 && (!*p || valid_pwd(p)))
+ user->pwd_lock = STATUS_TRUE;
+ } else
+ user->pwd_lock = STATUS_UNKNOWN;
+ break;
+ case COL_PWDMETHOD:
+ if (shadow) {
+ const char *p = shadow->sp_pwdp;
+
+ while (p && (*p == '!' || *p == '*'))
+ p++;
+ user->pwd_method = get_pwd_method(p, 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 || getcon(&user->context) != 0)
+ user->context = NULL;
+#endif
+ break;
+ case COL_NPROCS:
+#ifdef __linux__
+
+ xasprintf(&user->nprocs, "%d", get_nprocs(pwd->pw_uid));
+#endif
+ 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 = NULL;
+
+ if (!s || !*s)
+ return -1;
+
+ errno = 0;
+ *ul = strtoul(s, &end, 0);
+ if (errno == 0 && end && !*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;
+
+ if (!ctl)
+ return;
+
+ 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);
+ if (ctl->shellvar)
+ scols_table_enable_shellvar(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:
+#ifdef __linux__
+
+ rc = scols_line_set_data(ln, n, user->nprocs);
+#endif
+ 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;
+ }
+}
+#ifdef HAVE_LIBSYSTEMD
+static char *get_journal_data(sd_journal *j, const char *name)
+{
+ const char *data = NULL, *p;
+ size_t len = 0;
+
+ if (sd_journal_get_data(j, name, (const void **) &data, &len) < 0
+ || !data || !len)
+ return NULL;
+
+ /* Get rid of journal entry field identifiers */
+ p = strnchr(data, len, '=');
+ if (!p || !*(p + 1))
+ return NULL;
+ p++;
+
+ return xstrndup(p, len - (p - data));
+}
+
+static void print_journal_tail(const char *journal_path, uid_t uid, size_t len, int time_mode)
+{
+ sd_journal *j;
+ char *match;
+
+ 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 {
+ char *id, *pid, *msg, *ts;
+ uint64_t x;
+ time_t t;
+
+ sd_journal_get_realtime_usec(j, &x);
+ t = x / 1000000;
+ ts = make_time(time_mode, t);
+
+ id = get_journal_data(j, "SYSLOG_IDENTIFIER");
+ pid = get_journal_data(j, "_PID");
+ msg = get_journal_data(j, "MESSAGE");
+
+ if (ts && id && pid && msg)
+ fprintf(stdout, "%s %s[%s]: %s\n", ts, id, pid, msg);
+
+ free(ts);
+ free(id);
+ free(pid);
+ free(msg);
+ } while (sd_journal_next(j));
+
+ 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->nprocs);
+ 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(_(" -y, --shell use column names to be usable as shell variable identifiers\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(_(" --lastlog <path> set an alternate path for lastlog\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_lastlog = _PATH_LASTLOG, *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_LASTLOG,
+ 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' },
+ { "shell", no_argument, 0, 'y' },
+ { "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 },
+ { "lastlog-file", required_argument, 0, OPT_LASTLOG },
+#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);
+ close_stdout_atexit();
+
+ 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:prsuVyzZ",
+ 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 'y':
+ ctl->shellvar = 1;
+ break;
+ case 'z':
+ outmode = OUT_NUL;
+ break;
+ case OPT_LASTLOG:
+ path_lastlog = optarg;
+ 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':
+ print_version(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 columns */
+#ifdef __linux__
+ add_column(columns, ncolumns++, COL_NPROCS);
+#endif
+ 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_utmpx(path_wtmp, &ctl->wtmp_size, &ctl->wtmp);
+ ctl->lastlogin_fd = open(path_lastlog, O_RDONLY, 0);
+ }
+ if (require_btmp())
+ parse_utmpx(path_btmp, &ctl->btmp_size, &ctl->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);
+
+ if (ctl->lastlogin_fd >= 0)
+ close(ctl->lastlogin_fd);
+ free_ctl(ctl);
+
+ return EXIT_SUCCESS;
+}
diff --git a/login-utils/meson.build b/login-utils/meson.build
new file mode 100644
index 0000000..14d7b39
--- /dev/null
+++ b/login-utils/meson.build
@@ -0,0 +1,63 @@
+chfn_chsh_sources = files(
+ 'ch-common.c',
+ 'ch-common.h',
+)
+chfn_chsh_deps = [lib_readline]
+
+if chfn_chsh_password
+ chfn_chsh_sources += files(
+ 'auth.c',
+ 'auth.h',
+ )
+ chfn_chsh_deps += [
+ lib_pam,
+ lib_pam_misc,
+ ]
+endif
+
+if lib_user.found()
+ chfn_chsh_sources += files(
+ 'libuser.c',
+ 'libuser.h',
+ )
+ chfn_chsh_deps += lib_user
+else
+ chfn_chsh_sources += files(
+ 'islocal.c',
+ 'islocal.h',
+ 'setpwnam.c',
+ 'setpwnam.h',
+ )
+endif
+
+if lib_selinux.found()
+ chfn_chsh_sources += selinux_utils_c
+ chfn_chsh_deps += [lib_selinux]
+endif
+
+chfn_sources = files(
+ 'chfn.c',
+)
+
+test_islocal_sources = files(
+ 'islocal.c',
+)
+
+test_consoles_sources = files(
+ 'sulogin-consoles.c',
+)
+
+last_sources = files(
+ 'last.c',
+) + \
+ monotonic_c
+
+login_sources = files(
+ 'login.c',
+)
+
+sulogin_sources = files(
+ 'sulogin.c',
+ 'sulogin-consoles.c',
+ 'sulogin-consoles.h',
+)
diff --git a/login-utils/newgrp.1 b/login-utils/newgrp.1
new file mode 100644
index 0000000..bc3bcd4
--- /dev/null
+++ b/login-utils/newgrp.1
@@ -0,0 +1,70 @@
+'\" t
+.\" Title: newgrp
+.\" Author: [see the "AUTHOR(S)" section]
+.\" Generator: Asciidoctor 2.0.20
+.\" Date: 2023-10-23
+.\" Manual: User Commands
+.\" Source: util-linux 2.39.3
+.\" Language: English
+.\"
+.TH "NEWGRP" "1" "2023-10-23" "util\-linux 2.39.3" "User Commands"
+.ie \n(.g .ds Aq \(aq
+.el .ds Aq '
+.ss \n[.ss] 0
+.nh
+.ad l
+.de URL
+\fI\\$2\fP <\\$1>\\$3
+..
+.als MTO URL
+.if \n[.g] \{\
+. mso www.tmac
+. am URL
+. ad l
+. .
+. am MTO
+. ad l
+. .
+. LINKSTYLE blue R < >
+.\}
+.SH "NAME"
+newgrp \- log in to a new group
+.SH "SYNOPSIS"
+.sp
+\fBnewgrp\fP [\fIgroup\fP]
+.SH "DESCRIPTION"
+.sp
+\fBnewgrp\fP changes the group identification of its caller, analogously to \fBlogin\fP(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.
+.sp
+If no group is specified, the GID is changed to the login GID.
+.SH "OPTIONS"
+.sp
+\fB\-h\fP, \fB\-\-help\fP
+.RS 4
+Display help text and exit.
+.RE
+.sp
+\fB\-V\fP, \fB\-\-version\fP
+.RS 4
+Print version and exit.
+.RE
+.SH "FILES"
+.sp
+\fI/etc/group\fP,
+\fI/etc/passwd\fP
+.SH "AUTHORS"
+.sp
+Originally by Michael Haardt. Currently maintained by \c
+.MTO "poe\(atdaimi.aau.dk" "Peter Orbaek" "."
+.SH "SEE ALSO"
+.sp
+\fBlogin\fP(1),
+\fBgroup\fP(5)
+.SH "REPORTING BUGS"
+.sp
+For bug reports, use the issue tracker at \c
+.URL "https://github.com/util\-linux/util\-linux/issues" "" "."
+.SH "AVAILABILITY"
+.sp
+The \fBnewgrp\fP command is part of the util\-linux package which can be downloaded from \c
+.URL "https://www.kernel.org/pub/linux/utils/util\-linux/" "Linux Kernel Archive" "." \ No newline at end of file
diff --git a/login-utils/newgrp.1.adoc b/login-utils/newgrp.1.adoc
new file mode 100644
index 0000000..0e73eaf
--- /dev/null
+++ b/login-utils/newgrp.1.adoc
@@ -0,0 +1,49 @@
+//po4a: entry man manual
+// Original author unknown. This man page is in the public domain.
+// Modified Sat Oct 9 17:46:48 1993 by faith@cs.unc.edu
+= newgrp(1)
+:doctype: manpage
+:man manual: User Commands
+:man source: util-linux {release-version}
+:page-layout: base
+:command: newgrp
+
+== NAME
+
+newgrp - log in to a new group
+
+== SYNOPSIS
+
+*newgrp* [_group_]
+
+== DESCRIPTION
+
+*newgrp* changes the group identification of its caller, analogously to *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.
+
+If no group is specified, the GID is changed to the login GID.
+
+== OPTIONS
+
+include::man-common/help-version.adoc[]
+
+== FILES
+
+_/etc/group_,
+_/etc/passwd_
+
+== AUTHORS
+
+Originally by Michael Haardt. Currently maintained by mailto:poe@daimi.aau.dk[Peter Orbaek].
+
+== SEE ALSO
+
+*login*(1),
+*group*(5)
+
+include::man-common/bugreports.adoc[]
+
+include::man-common/footer.adoc[]
+
+ifdef::translation[]
+include::man-common/translation.adoc[]
+endif::[]
diff --git a/login-utils/newgrp.c b/login-utils/newgrp.c
new file mode 100644
index 0000000..2acbc91
--- /dev/null
+++ b/login-utils/newgrp.c
@@ -0,0 +1,241 @@
+/* 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, *spwd;
+
+ 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 */
+ spwd = get_gshadow_pwd(ge->gr_name);
+ pwd = spwd ? spwd : 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;
+ }
+
+ free(spwd);
+
+ /* 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);
+ close_stdout_atexit();
+
+ while ((ch = getopt_long(argc, argv, "Vh", longopts, NULL)) != -1)
+ switch (ch) {
+ case 'V':
+ print_version(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 *)NULL);
+ errexec(shell);
+}
diff --git a/login-utils/nologin.8 b/login-utils/nologin.8
new file mode 100644
index 0000000..f2ecf5d
--- /dev/null
+++ b/login-utils/nologin.8
@@ -0,0 +1,100 @@
+'\" t
+.\" Title: nologin
+.\" Author: [see the "AUTHOR(S)" section]
+.\" Generator: Asciidoctor 2.0.20
+.\" Date: 2023-10-23
+.\" Manual: System Administration
+.\" Source: util-linux 2.39.3
+.\" Language: English
+.\"
+.TH "NOLOGIN" "8" "2023-10-23" "util\-linux 2.39.3" "System Administration"
+.ie \n(.g .ds Aq \(aq
+.el .ds Aq '
+.ss \n[.ss] 0
+.nh
+.ad l
+.de URL
+\fI\\$2\fP <\\$1>\\$3
+..
+.als MTO URL
+.if \n[.g] \{\
+. mso www.tmac
+. am URL
+. ad l
+. .
+. am MTO
+. ad l
+. .
+. LINKSTYLE blue R < >
+.\}
+.SH "NAME"
+nologin \- politely refuse a login
+.SH "SYNOPSIS"
+.sp
+\fBnologin\fP [\fB\-V\fP] [\fB\-h\fP]
+.SH "DESCRIPTION"
+.sp
+\fBnologin\fP 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.
+.sp
+If the file \fI/etc/nologin.txt\fP exists, \fBnologin\fP displays its contents to the user instead of the default message.
+.sp
+The exit status returned by \fBnologin\fP is always 1.
+.SH "OPTIONS"
+.sp
+\fB\-c\fP, \fB\-\-command\fP \fIcommand\fP
+.sp
+\fB\-\-init\-file\fP
+.sp
+\fB\-i\fP \fB\-\-interactive\fP
+.sp
+\fB\-\-init\-file\fP \fIfile\fP
+.sp
+\fB\-i\fP, \fB\-\-interactive\fP
+.sp
+\fB\-l\fP, \fB\-\-login\fP
+.sp
+\fB\-\-noprofile\fP
+.sp
+\fB\-\-norc\fP
+.sp
+\fB\-\-posix\fP
+.sp
+\fB\-\-rcfile\fP \fIfile\fP
+.sp
+\fB\-r\fP, \fB\-\-restricted\fP
+.sp
+These shell command\-line options are ignored to avoid \fBnologin\fP error.
+.sp
+\fB\-h\fP, \fB\-\-help\fP
+.RS 4
+Display help text and exit.
+.RE
+.sp
+\fB\-V\fP, \fB\-\-version\fP
+.RS 4
+Print version and exit.
+.RE
+.SH "NOTES"
+.sp
+\fBnologin\fP is a per\-account way to disable login (usually used for system accounts like http or ftp). \fBnologin\fP uses \fI/etc/nologin.txt\fP as an optional source for a non\-default message, the login access is always refused independently of the file.
+.sp
+\fBpam_nologin\fP(8) PAM module usually prevents all non\-root users from logging into the system. \fBpam_nologin\fP(8) functionality is controlled by \fI/var/run/nologin\fP or the \fI/etc/nologin\fP file.
+.SH "HISTORY"
+.sp
+The \fBnologin\fP command appeared in 4.4BSD.
+.SH "AUTHORS"
+.sp
+.MTO "kzak\(atredhat.com" "Karel Zak" ""
+.SH "SEE ALSO"
+.sp
+\fBlogin\fP(1),
+\fBpasswd\fP(5),
+\fBpam_nologin\fP(8)
+.SH "REPORTING BUGS"
+.sp
+For bug reports, use the issue tracker at \c
+.URL "https://github.com/util\-linux/util\-linux/issues" "" "."
+.SH "AVAILABILITY"
+.sp
+The \fBnologin\fP command is part of the util\-linux package which can be downloaded from \c
+.URL "https://www.kernel.org/pub/linux/utils/util\-linux/" "Linux Kernel Archive" "." \ No newline at end of file
diff --git a/login-utils/nologin.8.adoc b/login-utils/nologin.8.adoc
new file mode 100644
index 0000000..6be978b
--- /dev/null
+++ b/login-utils/nologin.8.adoc
@@ -0,0 +1,79 @@
+//po4a: entry man manual
+= nologin(8)
+:doctype: manpage
+:man manual: System Administration
+:man source: util-linux {release-version}
+:page-layout: base
+:command: nologin
+
+== NAME
+
+nologin - politely refuse a login
+
+== SYNOPSIS
+
+*nologin* [*-V*] [*-h*]
+
+== DESCRIPTION
+
+*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.
+
+If the file _/etc/nologin.txt_ exists, *nologin* displays its contents to the user instead of the default message.
+
+The exit status returned by *nologin* is always 1.
+
+== OPTIONS
+
+*-c*, *--command* _command_
+
+*--init-file*
+
+*-i* *--interactive*
+
+*--init-file* _file_
+
+*-i*, *--interactive*
+
+*-l*, *--login*
+
+*--noprofile*
+
+*--norc*
+
+*--posix*
+
+*--rcfile* _file_
+
+*-r*, *--restricted*
+
+These shell command-line options are ignored to avoid *nologin* error.
+
+include::man-common/help-version.adoc[]
+
+== NOTES
+
+*nologin* is a per-account way to disable login (usually used for system accounts like http or ftp). *nologin* uses _/etc/nologin.txt_ as an optional source for a non-default message, the login access is always refused independently of the file.
+
+*pam_nologin*(8) PAM module usually prevents all non-root users from logging into the system. *pam_nologin*(8) functionality is controlled by _/var/run/nologin_ or the _/etc/nologin_ file.
+
+== HISTORY
+
+The *nologin* command appeared in 4.4BSD.
+
+== AUTHORS
+
+mailto:kzak@redhat.com[Karel Zak]
+
+== SEE ALSO
+
+*login*(1),
+*passwd*(5),
+*pam_nologin*(8)
+
+include::man-common/bugreports.adoc[]
+
+include::man-common/footer.adoc[]
+
+ifdef::translation[]
+include::man-common/translation.adoc[]
+endif::[]
diff --git a/login-utils/nologin.c b/login-utils/nologin.c
new file mode 100644
index 0000000..ecbd0d2
--- /dev/null
+++ b/login-utils/nologin.c
@@ -0,0 +1,111 @@
+/*
+ * 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"
+#include "fileutils.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);
+ fputs(_(" -c, --command <command> does nothing (for compatibility with su -c)\n"), out);
+ printf(USAGE_HELP_OPTIONS(26));
+
+ printf(USAGE_MAN_TAIL("nologin(8)"));
+ exit(EXIT_FAILURE);
+}
+
+int main(int argc, char *argv[])
+{
+ int c, fd = -1;
+ struct stat st;
+ enum {
+ OPT_INIT_FILE = CHAR_MAX + 1,
+ OPT_NOPROFILE,
+ OPT_NORC,
+ OPT_POSIX,
+ OPT_RCFILE
+ };
+ static const struct option longopts[] = {
+ { "command", required_argument, NULL, 'c' },
+ { "init-file", required_argument, NULL, OPT_INIT_FILE },
+ { "interactive", no_argument, NULL, 'i' },
+ { "login", no_argument, NULL, 'l' },
+ { "noprofile", no_argument, NULL, OPT_NOPROFILE },
+ { "norc", no_argument, NULL, OPT_NORC },
+ { "posix", no_argument, NULL, OPT_POSIX },
+ { "rcfile", required_argument, NULL, OPT_RCFILE },
+ { "restricted", no_argument, NULL, 'r' },
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { NULL, 0, NULL, 0 }
+ };
+
+ setlocale(LC_ALL, "");
+ bindtextdomain(PACKAGE, LOCALEDIR);
+ textdomain(PACKAGE);
+
+ while ((c = getopt_long(argc, argv, "c:ilrhV", longopts, NULL)) != -1) {
+ switch (c) {
+ case 'c':
+ case OPT_INIT_FILE:
+ case 'i':
+ case 'l':
+ case OPT_NOPROFILE:
+ case OPT_NORC:
+ case OPT_POSIX:
+ case OPT_RCFILE:
+ case 'r':
+ /* Ignore well known shell command-line options */
+ break;
+ case 'h':
+ usage();
+ case 'V':
+ print_version(EXIT_FAILURE); /* yes 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 {
+ ul_copy_file(fd, STDOUT_FILENO);
+ 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..3130a03
--- /dev/null
+++ b/login-utils/runuser.1
@@ -0,0 +1,281 @@
+'\" t
+.\" Title: runuser
+.\" Author: [see the "AUTHOR(S)" section]
+.\" Generator: Asciidoctor 2.0.20
+.\" Date: 2023-10-23
+.\" Manual: User Commands
+.\" Source: util-linux 2.39.3
+.\" Language: English
+.\"
+.TH "RUNUSER" "1" "2023-10-23" "util\-linux 2.39.3" "User Commands"
+.ie \n(.g .ds Aq \(aq
+.el .ds Aq '
+.ss \n[.ss] 0
+.nh
+.ad l
+.de URL
+\fI\\$2\fP <\\$1>\\$3
+..
+.als MTO URL
+.if \n[.g] \{\
+. mso www.tmac
+. am URL
+. ad l
+. .
+. am MTO
+. ad l
+. .
+. LINKSTYLE blue R < >
+.\}
+.SH "NAME"
+runuser \- run a command with substitute user and group ID
+.SH "SYNOPSIS"
+.sp
+\fBrunuser\fP [options] \fB\-u\fP \fIuser\fP [[\-\-] \fIcommand\fP [\fIargument\fP...]]
+.sp
+\fBrunuser\fP [options] [\fB\-\fP] [\fIuser\fP [\fIargument\fP...]]
+.SH "DESCRIPTION"
+.sp
+\fBrunuser\fP can be used to run commands with a substitute user and group ID. If the option \fB\-u\fP is not given, \fBrunuser\fP falls back to \fBsu\fP\-compatible semantics and a shell is executed. The difference between the commands \fBrunuser\fP and \fBsu\fP is that \fBrunuser\fP 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 \fBrunuser\fP does not have to be installed with set\-user\-ID permissions.
+.sp
+If the PAM session is not required, then the recommended solution is to use the \fBsetpriv\fP(1) command.
+.sp
+When called without arguments, \fBrunuser\fP defaults to running an interactive shell as \fIroot\fP.
+.sp
+For backward compatibility, \fBrunuser\fP defaults to not changing the current directory and to setting only the environment variables \fBHOME\fP and \fBSHELL\fP (plus \fBUSER\fP and \fBLOGNAME\fP if the target \fIuser\fP is not root). This version of \fBrunuser\fP uses PAM for session management.
+.sp
+Note that \fBrunuser\fP in all cases use PAM (pam_getenvlist()) to do the final environment modification. Command\-line options such as \fB\-\-login\fP and \fB\-\-preserve\-environment\fP affect the environment before it is modified by PAM.
+.sp
+Since version 2.38 \fBrunuser\fP resets process resource limits RLIMIT_NICE, RLIMIT_RTPRIO, RLIMIT_FSIZE, RLIMIT_AS and RLIMIT_NOFILE.
+.SH "OPTIONS"
+.sp
+\fB\-c\fP, \fB\-\-command\fP=\fIcommand\fP
+.RS 4
+Pass \fIcommand\fP to the shell with the \fB\-c\fP option.
+.RE
+.sp
+\fB\-f\fP, \fB\-\-fast\fP
+.RS 4
+Pass \fB\-f\fP to the shell, which may or may not be useful, depending on the shell.
+.RE
+.sp
+\fB\-g\fP, \fB\-\-group\fP=\fIgroup\fP
+.RS 4
+The primary group to be used. This option is allowed for the root user only.
+.RE
+.sp
+\fB\-G\fP, \fB\-\-supp\-group\fP=\fIgroup\fP
+.RS 4
+Specify a supplementary 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\fP is not specified.
+.RE
+.sp
+\fB\-\fP, \fB\-l\fP, \fB\-\-login\fP
+.RS 4
+Start the shell as a login shell with an environment similar to a real login:
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+. sp -1
+. IP \(bu 2.3
+.\}
+clears all the environment variables except for \fBTERM\fP and variables specified by \fB\-\-whitelist\-environment\fP
+.RE
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+. sp -1
+. IP \(bu 2.3
+.\}
+initializes the environment variables \fBHOME\fP, \fBSHELL\fP, \fBUSER\fP, \fBLOGNAME\fP, and \fBPATH\fP
+.RE
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+. sp -1
+. IP \(bu 2.3
+.\}
+changes to the target user\(cqs home directory
+.RE
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+. sp -1
+. IP \(bu 2.3
+.\}
+sets argv[0] of the shell to \*(Aq\fB\-\fP\*(Aq in order to make the shell a login shell
+.RE
+.RE
+.sp
+\fB\-P\fP, \fB\-\-pty\fP
+.RS 4
+Create a pseudo\-terminal for the session. The independent terminal provides better security as the user does not share a terminal with the original session. This can be used to avoid TIOCSTI ioctl terminal injection and other security attacks against terminal file descriptors. The entire session can also be moved to the background (e.g., \fBrunuser \-\-pty\fP \fB\-u\fP \fIusername\fP \fB\-\-\fP \fIcommand\fP \fB&\fP). If the pseudo\-terminal is enabled, then \fBrunuser\fP works as a proxy between the sessions (sync stdin and stdout).
+.sp
+This feature is mostly designed for interactive sessions. If the standard input is not a terminal, but for example a pipe (e.g., \fBecho "date" | runuser \-\-pty \-u\fP \fIuser\fP), then the \fBECHO\fP flag for the pseudo\-terminal is disabled to avoid messy output.
+.RE
+.sp
+\fB\-m\fP, \fB\-p\fP, \fB\-\-preserve\-environment\fP
+.RS 4
+Preserve the entire environment, i.e., do not set \fBHOME\fP, \fBSHELL\fP, \fBUSER\fP or \fBLOGNAME\fP. The option is ignored if the option \fB\-\-login\fP is specified.
+.RE
+.sp
+\fB\-s\fP, \fB\-\-shell\fP=\fIshell\fP
+.RS 4
+Run the specified \fIshell\fP instead of the default. The shell to run is selected according to the following rules, in order:
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+. sp -1
+. IP \(bu 2.3
+.\}
+the shell specified with \fB\-\-shell\fP
+.RE
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+. sp -1
+. IP \(bu 2.3
+.\}
+the shell specified in the environment variable \fBSHELL\fP if the \fB\-\-preserve\-environment\fP option is used
+.RE
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+. sp -1
+. IP \(bu 2.3
+.\}
+the shell listed in the passwd entry of the target user
+.RE
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+. sp -1
+. IP \(bu 2.3
+.\}
+/bin/sh
+.sp
+If the target user has a restricted shell (i.e., not listed in \fI/etc/shells\fP), then the \fB\-\-shell\fP option and the \fBSHELL\fP environment variables are ignored unless the calling user is root.
+.RE
+.RE
+.sp
+\fB\-\-session\-command=\fP\fIcommand\fP
+.RS 4
+Same as \fB\-c\fP, but do not create a new session. (Discouraged.)
+.RE
+.sp
+\fB\-w\fP, \fB\-\-whitelist\-environment\fP=\fIlist\fP
+.RS 4
+Don\(cqt reset the environment variables specified in the comma\-separated \fIlist\fP when clearing the environment for \fB\-\-login\fP. The whitelist is ignored for the environment variables \fBHOME\fP, \fBSHELL\fP, \fBUSER\fP, \fBLOGNAME\fP, and \fBPATH\fP.
+.RE
+.sp
+\fB\-h\fP, \fB\-\-help\fP
+.RS 4
+Display help text and exit.
+.RE
+.sp
+\fB\-V\fP, \fB\-\-version\fP
+.RS 4
+Print version and exit.
+.RE
+.SH "CONFIG FILES"
+.sp
+\fBrunuser\fP reads the \fI/etc/default/runuser\fP and \fI/etc/login.defs\fP configuration files. The following configuration items are relevant for \fBrunuser\fP:
+.sp
+\fBENV_PATH\fP (string)
+.RS 4
+Defines the PATH environment variable for a regular user. The default value is \fI/usr/local/bin:/bin:/usr/bin\fP.
+.RE
+.sp
+\fBENV_ROOTPATH\fP (string), \fBENV_SUPATH\fP (string)
+.RS 4
+Defines the \fBPATH\fP environment variable for root. \fBENV_SUPATH\fP takes precedence. The default value is \fI/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\fP.
+.RE
+.sp
+\fBALWAYS_SET_PATH\fP (boolean)
+.RS 4
+If set to \fIyes\fP and \fB\-\-login\fP and \fB\-\-preserve\-environment\fP were not specified \fBrunuser\fP initializes \fBPATH\fP.
+.RE
+.sp
+The environment variable \fBPATH\fP may be different on systems where \fI/bin\fP and \fI/sbin\fP are merged into \fI/usr\fP; this variable is also affected by the \fB\-\-login\fP command\-line option and the PAM system setting (e.g., \fBpam_env\fP(8)).
+.SH "EXIT STATUS"
+.sp
+\fBrunuser\fP normally returns the exit status of the command it executed. If the command was killed by a signal, \fBrunuser\fP returns the number of the signal plus 128.
+.sp
+Exit status generated by \fBrunuser\fP itself:
+.sp
+1
+.RS 4
+Generic error before executing the requested command
+.RE
+.sp
+126
+.RS 4
+The requested command could not be executed
+.RE
+.sp
+127
+.RS 4
+The requested command was not found
+.RE
+.SH "FILES"
+.sp
+\fI/etc/pam.d/runuser\fP
+.RS 4
+default PAM configuration file
+.RE
+.sp
+\fI/etc/pam.d/runuser\-l\fP
+.RS 4
+PAM configuration file if \fB\-\-login\fP is specified
+.RE
+.sp
+\fI/etc/default/runuser\fP
+.RS 4
+runuser specific logindef config file
+.RE
+.sp
+\fI/etc/login.defs\fP
+.RS 4
+global logindef config file
+.RE
+.SH "HISTORY"
+.sp
+This \fBrunuser\fP command was derived from coreutils\*(Aq \fBsu\fP, which was based on an implementation by David MacKenzie, and the Fedora \fBrunuser\fP command by Dan Walsh.
+.SH "SEE ALSO"
+.sp
+\fBsetpriv\fP(1),
+\fBsu\fP(1),
+\fBlogin.defs\fP(5),
+\fBshells\fP(5),
+\fBpam\fP(8)
+.SH "REPORTING BUGS"
+.sp
+For bug reports, use the issue tracker at \c
+.URL "https://github.com/util\-linux/util\-linux/issues" "" "."
+.SH "AVAILABILITY"
+.sp
+The \fBrunuser\fP command is part of the util\-linux package which can be downloaded from \c
+.URL "https://www.kernel.org/pub/linux/utils/util\-linux/" "Linux Kernel Archive" "." \ No newline at end of file
diff --git a/login-utils/runuser.1.adoc b/login-utils/runuser.1.adoc
new file mode 100644
index 0000000..3872d8d
--- /dev/null
+++ b/login-utils/runuser.1.adoc
@@ -0,0 +1,143 @@
+//po4a: entry man manual
+= runuser(1)
+:doctype: manpage
+:man manual: User Commands
+:man source: util-linux {release-version}
+:page-layout: base
+:command: runuser
+
+== NAME
+
+runuser - run a command with substitute user and group ID
+
+== SYNOPSIS
+
+*runuser* [options] *-u* _user_ [[--] _command_ [_argument_...]]
+
+*runuser* [options] [*-*] [_user_ [_argument_...]]
+
+== DESCRIPTION
+
+*runuser* can be used to run commands with a substitute user and group ID. If the option *-u* is not given, *runuser* falls back to *su*-compatible semantics and a shell is executed. The difference between the commands *runuser* and *su* is that *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 *runuser* does not have to be installed with set-user-ID permissions.
+
+If the PAM session is not required, then the recommended solution is to use the *setpriv*(1) command.
+
+When called without arguments, *runuser* defaults to running an interactive shell as _root_.
+
+For backward compatibility, *runuser* defaults to not changing the current directory and to setting only the environment variables *HOME* and *SHELL* (plus *USER* and *LOGNAME* if the target _user_ is not root). This version of *runuser* uses PAM for session management.
+
+Note that *runuser* in all cases use PAM (pam_getenvlist()) to do the final environment modification. Command-line options such as *--login* and *--preserve-environment* affect the environment before it is modified by PAM.
+
+Since version 2.38 *runuser* resets process resource limits RLIMIT_NICE, RLIMIT_RTPRIO, RLIMIT_FSIZE, RLIMIT_AS and RLIMIT_NOFILE.
+
+== OPTIONS
+
+*-c*, *--command*=_command_::
+Pass _command_ to the shell with the *-c* option.
+
+*-f*, *--fast*::
+Pass *-f* to the shell, which may or may not be useful, depending on the shell.
+
+*-g*, *--group*=_group_::
+The primary group to be used. This option is allowed for the root user only.
+
+*-G*, *--supp-group*=_group_::
+Specify a supplementary 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 *--group* is not specified.
+
+*-*, *-l*, *--login*::
+Start the shell as a login shell with an environment similar to a real login:
++
+* clears all the environment variables except for *TERM* and variables specified by *--whitelist-environment*
+* initializes the environment variables *HOME*, *SHELL*, *USER*, *LOGNAME*, and *PATH*
+* changes to the target user's home directory
+* sets argv[0] of the shell to '*-*' in order to make the shell a login shell
+
+*-P*, *--pty*::
+Create a pseudo-terminal for the session. The independent terminal provides better security as the user does not share a terminal with the original session. This can be used to avoid TIOCSTI ioctl terminal injection and other security attacks against terminal file descriptors. The entire session can also be moved to the background (e.g., *runuser --pty* *-u* _username_ *--* _command_ *&*). If the pseudo-terminal is enabled, then *runuser* works as a proxy between the sessions (sync stdin and stdout).
++
+This feature is mostly designed for interactive sessions. If the standard input is not a terminal, but for example a pipe (e.g., *echo "date" | runuser --pty -u* _user_), then the *ECHO* flag for the pseudo-terminal is disabled to avoid messy output.
+
+*-m*, *-p*, *--preserve-environment*::
+Preserve the entire environment, i.e., do not set *HOME*, *SHELL*, *USER* or *LOGNAME*. The option is ignored if the option *--login* is specified.
+
+*-s*, *--shell*=_shell_::
+Run the specified _shell_ instead of the default. The shell to run is selected according to the following rules, in order:
+
+* the shell specified with *--shell*
+* the shell specified in the environment variable *SHELL* if the *--preserve-environment* option is used
+* the shell listed in the passwd entry of the target user
+* /bin/sh
++
+If the target user has a restricted shell (i.e., not listed in _/etc/shells_), then the *--shell* option and the *SHELL* environment variables are ignored unless the calling user is root.
+
+**--session-command=**__command__::
+Same as *-c*, but do not create a new session. (Discouraged.)
+
+*-w*, *--whitelist-environment*=_list_::
+Don't reset the environment variables specified in the comma-separated _list_ when clearing the environment for *--login*. The whitelist is ignored for the environment variables *HOME*, *SHELL*, *USER*, *LOGNAME*, and *PATH*.
+
+include::man-common/help-version.adoc[]
+
+== CONFIG FILES
+
+*runuser* reads the _/etc/default/runuser_ and _/etc/login.defs_ configuration files. The following configuration items are relevant for *runuser*:
+
+*ENV_PATH* (string)::
+Defines the PATH environment variable for a regular user. The default value is _/usr/local/bin:/bin:/usr/bin_.
+
+*ENV_ROOTPATH* (string)::
+*ENV_SUPATH* (string)::
+Defines the *PATH* environment variable for root. *ENV_SUPATH* takes precedence. The default value is _/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin_.
+
+*ALWAYS_SET_PATH* (boolean)::
+If set to _yes_ and *--login* and *--preserve-environment* were not specified *runuser* initializes *PATH*.
+
+The environment variable *PATH* may be different on systems where _/bin_ and _/sbin_ are merged into _/usr_; this variable is also affected by the *--login* command-line option and the PAM system setting (e.g., *pam_env*(8)).
+
+== EXIT STATUS
+
+*runuser* normally returns the exit status of the command it executed. If the command was killed by a signal, *runuser* returns the number of the signal plus 128.
+
+Exit status generated by *runuser* itself:
+
+1::
+Generic error before executing the requested command
+126::
+The requested command could not be executed
+127::
+The requested command was not found
+
+
+== FILES
+
+_/etc/pam.d/runuser_::
+default PAM configuration file
+
+_/etc/pam.d/runuser-l_::
+PAM configuration file if *--login* is specified
+
+_/etc/default/runuser_::
+runuser specific logindef config file
+
+_/etc/login.defs_::
+global logindef config file
+
+== HISTORY
+
+This *runuser* command was derived from coreutils' *su*, which was based on an implementation by David MacKenzie, and the Fedora *runuser* command by Dan Walsh.
+
+== SEE ALSO
+
+*setpriv*(1),
+*su*(1),
+*login.defs*(5),
+*shells*(5),
+*pam*(8)
+
+include::man-common/bugreports.adoc[]
+
+include::man-common/footer.adoc[]
+
+ifdef::translation[]
+include::man-common/translation.adoc[]
+endif::[]
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/setpwnam.c b/login-utils/setpwnam.c
new file mode 100644
index 0000000..3e3c1ab
--- /dev/null
+++ b/login-utils/setpwnam.c
@@ -0,0 +1,216 @@
+/*
+ * 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, rc;
+ uint8_t found = 0;
+ size_t namelen;
+ size_t contlen;
+ size_t buflen = 256;
+ 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 */
+ /* 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 = 1;
+ 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..b674920
--- /dev/null
+++ b/login-utils/su-common.c
@@ -0,0 +1,1293 @@
+/*
+ * 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>
+#include <sys/time.h>
+
+#ifdef HAVE_SYS_RESOURCE_H
+# include <sys/resource.h>
+#endif
+
+#ifdef HAVE_PTY
+# include <pty.h>
+# include <poll.h>
+# include <sys/signalfd.h>
+# include "pty-session.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 "shells.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"
+
+#ifdef HAVE_LIBECONF
+#define _PATH_LOGINDEFS_SU "default/su"
+#define _PATH_LOGINDEFS_RUNUSER "default/runuser"
+#else
+#define _PATH_LOGINDEFS_SU "/etc/default/su"
+#define _PATH_LOGINDEFS_RUNUSER "/etc/default/runuser"
+#endif
+
+#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_path; /* tty device path */
+ 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 ul_pty *pty; /* pseudo terminal handler (for --pty) */
+#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 */
+ force_pty :1, /* create pseudo-terminal */
+ restricted :1; /* false for root user */
+};
+
+
+static sig_atomic_t volatile caught_signal = 0;
+
+/* 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(&su->tty_path, &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->force_pty && ul_pty_is_running(su->pty)) {
+ 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;
+#ifdef USE_PTY
+ /* inform pty suff that we have no child anymore */
+ if (su->force_pty)
+ ul_pty_set_child(su->pty, (pid_t) -1);
+#endif
+ } 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 wait_for_child_cb(
+ void *data,
+ pid_t child __attribute__((__unused__)))
+{
+ wait_for_child((struct su_context *) data);
+}
+
+static void chownmod_pty(struct su_context *su)
+{
+ gid_t gid = su->pwd->pw_gid;
+ mode_t mode = (mode_t) getlogindefs_num("TTYPERM", TTY_MODE);
+ const char *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 (ul_pty_chownmod_slave(su->pty,
+ su->pwd->pw_uid,
+ gid, mode))
+ warn(_("change owner or mode for pseudo-terminal failed"));
+}
+#endif
+
+/* 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, LOG_PID, 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);
+#else
+ return PAM_CONV_ERR;
+#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_path) {
+ rc = pam_set_item(su->pamh, PAM_TTY, su->tty_path);
+ 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 : _("authentication failed"));
+ }
+}
+
+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)
+{
+ struct sigaction action;
+ int status;
+
+ DBG(MISC, ul_debug("forking..."));
+#ifdef USE_PTY
+ if (su->force_pty) {
+ struct ul_pty_callbacks *cb;
+
+ /* set callbacks */
+ ul_pty_set_callback_data(su->pty, (void *) su);
+
+ cb = ul_pty_get_callbacks(su->pty);
+ cb->child_wait = wait_for_child_cb;
+ cb->child_sigstop = wait_for_child_cb;
+
+ ul_pty_slave_echo(su->pty, 1);
+
+ /* create pty */
+ if (ul_pty_setup(su->pty))
+ err(EXIT_FAILURE, _("failed to create pseudo-terminal"));
+ if (ul_pty_signals_setup(su->pty))
+ err(EXIT_FAILURE, _("failed to initialize signals handler"));
+ }
+#endif
+ fflush(stdout); /* ??? */
+
+ /* set default handler for SIGCHLD */
+ sigemptyset(&action.sa_mask);
+ action.sa_flags = 0;
+ action.sa_handler = SIG_DFL;
+ if (sigaction(SIGCHLD, &action, NULL)) {
+ supam_cleanup(su, PAM_ABORT);
+#ifdef USE_PTY
+ if (su->force_pty)
+ ul_pty_cleanup(su->pty);
+#endif
+ err(EXIT_FAILURE, _("cannot set child signal handler"));
+ }
+
+ switch ((int) (su->child = fork())) {
+ case -1: /* error */
+ supam_cleanup(su, PAM_ABORT);
+#ifdef USE_PTY
+ if (su->force_pty)
+ ul_pty_cleanup(su->pty);
+#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->force_pty) {
+ ul_pty_set_child(su->pty, su->child);
+
+ ul_pty_proxy_master(su->pty);
+
+ /* ul_pty_proxy_master() keeps classic signal handler are out of game */
+ caught_signal = ul_pty_get_delivered_signal(su->pty);
+
+ ul_pty_cleanup(su->pty);
+ } 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);
+ }
+
+ 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_SUPATH", NULL)) != 0)
+ rc = logindefs_setenv("PATH", "ENV_ROOTPATH", _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 re-alloc(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 establish 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)
+{
+ if (is_known_shell(shell)) {
+ return false;
+ }
+#ifdef USE_VENDORDIR
+ DBG(MISC, ul_debug("%s is restricted shell (not in e.g. vendor shells file, /etc/shells, ...)", shell));
+#else
+ DBG(MISC, ul_debug("%s is restricted shell (not in /etc/shells)", shell));
+#endif
+ 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"));
+#ifndef HAVE_LIBECONF
+ logindefs_load_file(_PATH_LOGINDEFS);
+#endif
+ logindefs_load_file(su->runuser ? _PATH_LOGINDEFS_RUNUSER : _PATH_LOGINDEFS_SU);
+}
+
+/*
+ * 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;
+}
+
+/* Don't rely on PAM and reset the most important limits. */
+static void sanitize_prlimits(void)
+{
+#ifdef HAVE_SYS_RESOURCE_H
+ struct rlimit lm = { .rlim_cur = 0, .rlim_max = 0 };
+
+ /* reset to zero */
+#ifdef RLIMIT_NICE
+ setrlimit(RLIMIT_NICE, &lm);
+#endif
+#ifdef RLIMIT_RTPRIO
+ setrlimit(RLIMIT_RTPRIO, &lm);
+#endif
+
+ /* reset to unlimited */
+ lm.rlim_cur = RLIM_INFINITY;
+ lm.rlim_max = RLIM_INFINITY;
+ setrlimit(RLIMIT_FSIZE, &lm);
+ setrlimit(RLIMIT_AS, &lm);
+
+ /* reset soft limit only */
+ getrlimit(RLIMIT_NOFILE, &lm);
+ if (lm.rlim_cur != FD_SETSIZE) {
+ lm.rlim_cur = FD_SETSIZE;
+ setrlimit(RLIMIT_NOFILE, &lm);
+ }
+#endif
+}
+
+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
+ }, *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);
+ close_stdout_atexit();
+
+ 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->force_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':
+ print_version(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 or the user entry does not "
+ "contain all the required fields"), 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
+ && strcmp(shell, su->pwd->pw_shell) != 0
+ && 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 */
+
+ sanitize_prlimits();
+
+ supam_open_session(su);
+
+#ifdef USE_PTY
+ if (su->force_pty) {
+ ON_DBG(PTY, ul_pty_init_debug(0xffff));
+
+ su->pty = ul_new_pty(su->isterm);
+ if (!su->pty)
+ err(EXIT_FAILURE, _("failed to allocate pty handler"));
+ }
+#endif
+ create_watching_parent(su);
+ /* Now we're in the child. */
+
+#ifdef USE_PTY
+ if (su->force_pty)
+ chownmod_pty(su);
+#endif
+ change_identity(su->pwd);
+ if (!su->same_session) {
+ /* note that on --pty we call setsid() in ul_pty_init_slave() */
+ DBG(MISC, ul_debug("call setsid()"));
+ setsid();
+ }
+#ifdef USE_PTY
+ if (su->force_pty)
+ ul_pty_init_slave(su->pty);
+#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);
+
+ /* http://www.linux-pam.org/Linux-PAM-html/adg-interface-by-app-expected.html#adg-pam_end */
+ (void) pam_end(su->pamh, PAM_SUCCESS|PAM_DATA_SILENT);
+
+ 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..a768834
--- /dev/null
+++ b/login-utils/su.1
@@ -0,0 +1,299 @@
+'\" t
+.\" Title: su
+.\" Author: [see the "AUTHOR(S)" section]
+.\" Generator: Asciidoctor 2.0.20
+.\" Date: 2023-10-23
+.\" Manual: User Commands
+.\" Source: util-linux 2.39.3
+.\" Language: English
+.\"
+.TH "SU" "1" "2023-10-23" "util\-linux 2.39.3" "User Commands"
+.ie \n(.g .ds Aq \(aq
+.el .ds Aq '
+.ss \n[.ss] 0
+.nh
+.ad l
+.de URL
+\fI\\$2\fP <\\$1>\\$3
+..
+.als MTO URL
+.if \n[.g] \{\
+. mso www.tmac
+. am URL
+. ad l
+. .
+. am MTO
+. ad l
+. .
+. LINKSTYLE blue R < >
+.\}
+.SH "NAME"
+su \- run a command with substitute user and group ID
+.SH "SYNOPSIS"
+.sp
+\fBsu\fP [options] [\fB\-\fP] [\fIuser\fP [\fIargument\fP...]]
+.SH "DESCRIPTION"
+.sp
+\fBsu\fP allows commands to be run with a substitute user and group ID.
+.sp
+When called with no \fIuser\fP specified, \fBsu\fP defaults to running an interactive shell as \fIroot\fP. When \fIuser\fP is specified, additional \fIargument\fPs can be supplied, in which case they are passed to the shell.
+.sp
+For backward compatibility, \fBsu\fP defaults to not change the current directory and to only set the environment variables \fBHOME\fP and \fBSHELL\fP (plus \fBUSER\fP and \fBLOGNAME\fP if the target \fIuser\fP is not root). It is recommended to always use the \fB\-\-login\fP option (instead of its shortcut \fB\-\fP) to avoid side effects caused by mixing environments.
+.sp
+This version of \fBsu\fP uses PAM for authentication, account and session management. Some configuration options found in other \fBsu\fP implementations, such as support for a wheel group, have to be configured via PAM.
+.sp
+\fBsu\fP 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 \fBrunuser\fP(1) that does not require authentication and provides separate PAM configuration. If the PAM session is not required at all then the recommended solution is to use command \fBsetpriv\fP(1).
+.sp
+Note that \fBsu\fP in all cases uses PAM (\fBpam_getenvlist\fP(3)) to do the final environment modification. Command\-line options such as \fB\-\-login\fP and \fB\-\-preserve\-environment\fP affect the environment before it is modified by PAM.
+.sp
+Since version 2.38 \fBsu\fP resets process resource limits RLIMIT_NICE, RLIMIT_RTPRIO, RLIMIT_FSIZE, RLIMIT_AS and RLIMIT_NOFILE.
+.SH "OPTIONS"
+.sp
+\fB\-c\fP, \fB\-\-command\fP=\fIcommand\fP
+.RS 4
+Pass \fIcommand\fP to the shell with the \fB\-c\fP option.
+.RE
+.sp
+\fB\-f\fP, \fB\-\-fast\fP
+.RS 4
+Pass \fB\-f\fP to the shell, which may or may not be useful, depending on the shell.
+.RE
+.sp
+\fB\-g\fP, \fB\-\-group\fP=\fIgroup\fP
+.RS 4
+Specify the primary group. This option is available to the root user only.
+.RE
+.sp
+\fB\-G\fP, \fB\-\-supp\-group\fP=\fIgroup\fP
+.RS 4
+Specify a supplementary 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\fP is not specified.
+.RE
+.sp
+\fB\-\fP, \fB\-l\fP, \fB\-\-login\fP
+.RS 4
+Start the shell as a login shell with an environment similar to a real login:
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+. sp -1
+. IP \(bu 2.3
+.\}
+clears all the environment variables except \fBTERM\fP and variables specified by \fB\-\-whitelist\-environment\fP
+.RE
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+. sp -1
+. IP \(bu 2.3
+.\}
+initializes the environment variables \fBHOME\fP, \fBSHELL\fP, \fBUSER\fP, \fBLOGNAME\fP, and \fBPATH\fP
+.RE
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+. sp -1
+. IP \(bu 2.3
+.\}
+changes to the target user\(cqs home directory
+.RE
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+. sp -1
+. IP \(bu 2.3
+.\}
+sets argv[0] of the shell to \*(Aq\fB\-\fP\*(Aq in order to make the shell a login shell
+.RE
+.RE
+.sp
+\fB\-m\fP, \fB\-p\fP, \fB\-\-preserve\-environment\fP
+.RS 4
+Preserve the entire environment, i.e., do not set \fBHOME\fP, \fBSHELL\fP, \fBUSER\fP or \fBLOGNAME\fP. This option is ignored if the option \fB\-\-login\fP is specified.
+.RE
+.sp
+\fB\-P\fP, \fB\-\-pty\fP
+.RS 4
+Create a pseudo\-terminal for the session. The independent terminal provides better security as the user does not share a terminal with the original session. This can be used to avoid \fBTIOCSTI\fP ioctl terminal injection and other security attacks against terminal file descriptors. The entire session can also be moved to the background (e.g., \fBsu \-\-pty\fP \fB\-\fP \fIusername\fP \fB\-c\fP \fIapplication\fP \fB&\fP). If the pseudo\-terminal is enabled, then \fBsu\fP works as a proxy between the sessions (sync stdin and stdout).
+.sp
+This feature is mostly designed for interactive sessions. If the standard input is not a terminal, but for example a pipe (e.g., \fBecho "date" | su \-\-pty\fP), then the \fBECHO\fP flag for the pseudo\-terminal is disabled to avoid messy output.
+.RE
+.sp
+\fB\-s\fP, \fB\-\-shell\fP=\fIshell\fP
+.RS 4
+Run the specified \fIshell\fP instead of the default. The shell to run is selected according to the following rules, in order:
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+. sp -1
+. IP \(bu 2.3
+.\}
+the shell specified with \fB\-\-shell\fP
+.RE
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+. sp -1
+. IP \(bu 2.3
+.\}
+the shell specified in the environment variable \fBSHELL\fP, if the \fB\-\-preserve\-environment\fP option is used
+.RE
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+. sp -1
+. IP \(bu 2.3
+.\}
+the shell listed in the passwd entry of the target user
+.RE
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+. sp -1
+. IP \(bu 2.3
+.\}
+/bin/sh
+.RE
+.RE
+.sp
+If the target user has a restricted shell (i.e., not listed in \fI/etc/shells\fP), the \fB\-\-shell\fP option and the \fBSHELL\fP environment variables are ignored unless the calling user is root.
+.sp
+\fB\-\-session\-command=\fP\fIcommand\fP
+.RS 4
+Same as \fB\-c\fP, but do not create a new session. (Discouraged.)
+.RE
+.sp
+\fB\-w\fP, \fB\-\-whitelist\-environment\fP=\fIlist\fP
+.RS 4
+Don\(cqt reset the environment variables specified in the comma\-separated \fIlist\fP when clearing the environment for \fB\-\-login\fP. The whitelist is ignored for the environment variables \fBHOME\fP, \fBSHELL\fP, \fBUSER\fP, \fBLOGNAME\fP, and \fBPATH\fP.
+.RE
+.sp
+\fB\-h\fP, \fB\-\-help\fP
+.RS 4
+Display help text and exit.
+.RE
+.sp
+\fB\-V\fP, \fB\-\-version\fP
+.RS 4
+Print version and exit.
+.RE
+.SH "SIGNALS"
+.sp
+Upon receiving either \fBSIGINT\fP, \fBSIGQUIT\fP or \fBSIGTERM\fP, \fBsu\fP terminates its child and afterwards terminates itself with the received signal. The child is terminated by \fBSIGTERM\fP, after unsuccessful attempt and 2 seconds of delay the child is killed by \fBSIGKILL\fP.
+.SH "CONFIG FILES"
+.sp
+\fBsu\fP reads the \fI/etc/default/su\fP and \fI/etc/login.defs\fP configuration files. The following configuration items are relevant for \fBsu\fP:
+.sp
+\fBFAIL_DELAY\fP (number)
+.RS 4
+Delay in seconds in case of an authentication failure. The number must be a non\-negative integer.
+.RE
+.sp
+\fBENV_PATH\fP (string)
+.RS 4
+Defines the \fBPATH\fP environment variable for a regular user. The default value is \fI/usr/local/bin:/bin:/usr/bin\fP.
+.RE
+.sp
+\fBENV_ROOTPATH\fP (string), \fBENV_SUPATH\fP (string)
+.RS 4
+Defines the \fBPATH\fP environment variable for root. \fBENV_SUPATH\fP takes precedence. The default value is \fI/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\fP.
+.RE
+.sp
+\fBALWAYS_SET_PATH\fP (boolean)
+.RS 4
+If set to \fIyes\fP and \fB\-\-login\fP and \fB\-\-preserve\-environment\fP were not specified \fBsu\fP initializes \fBPATH\fP.
+.sp
+The environment variable \fBPATH\fP may be different on systems where \fI/bin\fP and \fI/sbin\fP are merged into \fI/usr\fP; this variable is also affected by the \fB\-\-login\fP command\-line option and the PAM system setting (e.g., \fBpam_env\fP(8)).
+.RE
+.SH "EXIT STATUS"
+.sp
+\fBsu\fP normally returns the exit status of the command it executed. If the command was killed by a signal, \fBsu\fP returns the number of the signal plus 128.
+.sp
+Exit status generated by \fBsu\fP itself:
+.sp
+1
+.RS 4
+Generic error before executing the requested command
+.RE
+.sp
+126
+.RS 4
+The requested command could not be executed
+.RE
+.sp
+127
+.RS 4
+The requested command was not found
+.RE
+.SH "FILES"
+.sp
+\fI/etc/pam.d/su\fP
+.RS 4
+default PAM configuration file
+.RE
+.sp
+\fI/etc/pam.d/su\-l\fP
+.RS 4
+PAM configuration file if \fB\-\-login\fP is specified
+.RE
+.sp
+\fI/etc/default/su\fP
+.RS 4
+command specific logindef config file
+.RE
+.sp
+\fI/etc/login.defs\fP
+.RS 4
+global logindef config file
+.RE
+.SH "NOTES"
+.sp
+For security reasons, \fBsu\fP always logs failed log\-in attempts to the \fIbtmp\fP file, but it does not write to the \fIlastlog\fP file at all. This solution can be used to control \fBsu\fP behavior by PAM configuration. If you want to use the \fBpam_lastlog\fP(8) module to print warning message about failed log\-in attempts then \fBpam_lastlog\fP(8) has to be configured to update the \fIlastlog\fP file as well. For example by:
+.RS 3
+.ll -.6i
+.sp
+session required pam_lastlog.so nowtmp
+.br
+.RE
+.ll
+.SH "HISTORY"
+.sp
+This \fBsu\fP command was derived from coreutils\*(Aq \fBsu\fP, which was based on an implementation by David MacKenzie. The util\-linux version has been refactored by Karel Zak.
+.SH "SEE ALSO"
+.sp
+\fBsetpriv\fP(1),
+\fBlogin.defs\fP(5),
+\fBshells\fP(5),
+\fBpam\fP(8),
+\fBrunuser\fP(1)
+.SH "REPORTING BUGS"
+.sp
+For bug reports, use the issue tracker at \c
+.URL "https://github.com/util\-linux/util\-linux/issues" "" "."
+.SH "AVAILABILITY"
+.sp
+The \fBsu\fP command is part of the util\-linux package which can be downloaded from \c
+.URL "https://www.kernel.org/pub/linux/utils/util\-linux/" "Linux Kernel Archive" "." \ No newline at end of file
diff --git a/login-utils/su.1.adoc b/login-utils/su.1.adoc
new file mode 100644
index 0000000..36a892f
--- /dev/null
+++ b/login-utils/su.1.adoc
@@ -0,0 +1,159 @@
+//po4a: entry man manual
+= su(1)
+:doctype: manpage
+:man manual: User Commands
+:man source: util-linux {release-version}
+:page-layout: base
+:command: su
+:colon: :
+
+== NAME
+
+su - run a command with substitute user and group ID
+
+== SYNOPSIS
+
+*su* [options] [*-*] [_user_ [_argument_...]]
+
+== DESCRIPTION
+
+*su* allows commands to be run with a substitute user and group ID.
+
+When called with no _user_ specified, *su* defaults to running an interactive shell as _root_. When _user_ is specified, additional __argument__s can be supplied, in which case they are passed to the shell.
+
+For backward compatibility, *su* defaults to not change the current directory and to only set the environment variables *HOME* and *SHELL* (plus *USER* and *LOGNAME* if the target _user_ is not root). It is recommended to always use the *--login* option (instead of its shortcut *-*) to avoid side effects caused by mixing environments.
+
+This version of *su* uses PAM for authentication, account and session management. Some configuration options found in other *su* implementations, such as support for a wheel group, have to be configured via PAM.
+
+*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 *runuser*(1) that does not require authentication and provides separate PAM configuration. If the PAM session is not required at all then the recommended solution is to use command *setpriv*(1).
+
+Note that *su* in all cases uses PAM (*pam_getenvlist*(3)) to do the final environment modification. Command-line options such as *--login* and *--preserve-environment* affect the environment before it is modified by PAM.
+
+Since version 2.38 *su* resets process resource limits RLIMIT_NICE, RLIMIT_RTPRIO, RLIMIT_FSIZE, RLIMIT_AS and RLIMIT_NOFILE.
+
+== OPTIONS
+
+*-c*, **--command**=__command__::
+Pass _command_ to the shell with the *-c* option.
+
+*-f*, *--fast*::
+Pass *-f* to the shell, which may or may not be useful, depending on the shell.
+
+*-g*, **--group**=__group__::
+Specify the primary group. This option is available to the root user only.
+
+*-G*, **--supp-group**=__group__::
+Specify a supplementary 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 *--group* is not specified.
+
+*-*, *-l*, *--login*::
+Start the shell as a login shell with an environment similar to a real login:
+
+* clears all the environment variables except *TERM* and variables specified by *--whitelist-environment*
+* initializes the environment variables *HOME*, *SHELL*, *USER*, *LOGNAME*, and *PATH*
+* changes to the target user's home directory
+* sets argv[0] of the shell to '*-*' in order to make the shell a login shell
+
+*-m*, *-p*, *--preserve-environment*::
+Preserve the entire environment, i.e., do not set *HOME*, *SHELL*, *USER* or *LOGNAME*. This option is ignored if the option *--login* is specified.
+
+*-P*, *--pty*::
+Create a pseudo-terminal for the session. The independent terminal provides better security as the user does not share a terminal with the original session. This can be used to avoid *TIOCSTI* ioctl terminal injection and other security attacks against terminal file descriptors. The entire session can also be moved to the background (e.g., *su --pty* **-** __username__ *-c* _application_ *&*). If the pseudo-terminal is enabled, then *su* works as a proxy between the sessions (sync stdin and stdout).
++
+This feature is mostly designed for interactive sessions. If the standard input is not a terminal, but for example a pipe (e.g., *echo "date" | su --pty*), then the *ECHO* flag for the pseudo-terminal is disabled to avoid messy output.
+
+*-s*, **--shell**=__shell__::
+Run the specified _shell_ instead of the default. The shell to run is selected according to the following rules, in order:
+
+* the shell specified with *--shell*
+* the shell specified in the environment variable *SHELL*, if the *--preserve-environment* option is used
+* the shell listed in the passwd entry of the target user
+* /bin/sh
+
+If the target user has a restricted shell (i.e., not listed in _/etc/shells_), the *--shell* option and the *SHELL* environment variables are ignored unless the calling user is root.
+
+**--session-command=**__command__::
+Same as *-c*, but do not create a new session. (Discouraged.)
+
+*-w*, **--whitelist-environment**=__list__::
+Don't reset the environment variables specified in the comma-separated _list_ when clearing the environment for *--login*. The whitelist is ignored for the environment variables *HOME*, *SHELL*, *USER*, *LOGNAME*, and *PATH*.
+
+include::man-common/help-version.adoc[]
+
+== SIGNALS
+
+Upon receiving either *SIGINT*, *SIGQUIT* or *SIGTERM*, *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*.
+
+== CONFIG FILES
+
+//TRANSLATORS: Keep {colon} untranslated
+*su* reads the _/etc/default/su_ and _/etc/login.defs_ configuration files. The following configuration items are relevant for *su*{colon}
+
+*FAIL_DELAY* (number)::
+Delay in seconds in case of an authentication failure. The number must be a non-negative integer.
+
+*ENV_PATH* (string)::
+Defines the *PATH* environment variable for a regular user. The default value is _/usr/local/bin:/bin:/usr/bin_.
+
+*ENV_ROOTPATH* (string)::
+*ENV_SUPATH* (string)::
+Defines the *PATH* environment variable for root. *ENV_SUPATH* takes precedence. The default value is _/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin_.
+
+*ALWAYS_SET_PATH* (boolean)::
+If set to _yes_ and *--login* and *--preserve-environment* were not specified *su* initializes *PATH*.
++
+The environment variable *PATH* may be different on systems where _/bin_ and _/sbin_ are merged into _/usr_; this variable is also affected by the *--login* command-line option and the PAM system setting (e.g., *pam_env*(8)).
+
+== EXIT STATUS
+
+*su* normally returns the exit status of the command it executed. If the command was killed by a signal, *su* returns the number of the signal plus 128.
+
+Exit status generated by *su* itself:
+
+1::
+Generic error before executing the requested command
+126::
+The requested command could not be executed
+127::
+The requested command was not found
+
+== FILES
+
+_/etc/pam.d/su_::
+default PAM configuration file
+
+_/etc/pam.d/su-l_::
+PAM configuration file if *--login* is specified
+
+_/etc/default/su_::
+command specific logindef config file
+
+_/etc/login.defs_::
+global logindef config file
+
+== NOTES
+
+For security reasons, *su* always logs failed log-in attempts to the _btmp_ file, but it does not write to the _lastlog_ file at all. This solution can be used to control *su* behavior by PAM configuration. If you want to use the *pam_lastlog*(8) module to print warning message about failed log-in attempts then *pam_lastlog*(8) has to be configured to update the _lastlog_ file as well. For example by:
+
+____
+session required pam_lastlog.so nowtmp
+____
+
+== HISTORY
+
+This *su* command was derived from coreutils' *su*, which was based on an implementation by David MacKenzie. The util-linux version has been refactored by Karel Zak.
+
+== SEE ALSO
+
+*setpriv*(1),
+*login.defs*(5),
+*shells*(5),
+*pam*(8),
+*runuser*(1)
+
+include::man-common/bugreports.adoc[]
+
+include::man-common/footer.adoc[]
+
+ifdef::translation[]
+include::man-common/translation.adoc[]
+endif::[]
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..9ae5255
--- /dev/null
+++ b/login-utils/sulogin-consoles.c
@@ -0,0 +1,829 @@
+/*
+ * 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>
+
+#if defined(USE_SULOGIN_EMERGENCY_MOUNT)
+# include <sys/mount.h>
+# ifndef MS_RELATIME
+# define MS_RELATIME (1<<21)
+# endif
+# 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("appending %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..12032c9
--- /dev/null
+++ b/login-utils/sulogin-consoles.h
@@ -0,0 +1,55 @@
+/*
+ * 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
+#define CON_EIO 0x0004
+ 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..2f639b5
--- /dev/null
+++ b/login-utils/sulogin.8
@@ -0,0 +1,89 @@
+'\" t
+.\" Title: sulogin
+.\" Author: [see the "AUTHOR(S)" section]
+.\" Generator: Asciidoctor 2.0.20
+.\" Date: 2023-10-23
+.\" Manual: System Administration
+.\" Source: util-linux 2.39.3
+.\" Language: English
+.\"
+.TH "SULOGIN" "8" "2023-10-23" "util\-linux 2.39.3" "System Administration"
+.ie \n(.g .ds Aq \(aq
+.el .ds Aq '
+.ss \n[.ss] 0
+.nh
+.ad l
+.de URL
+\fI\\$2\fP <\\$1>\\$3
+..
+.als MTO URL
+.if \n[.g] \{\
+. mso www.tmac
+. am URL
+. ad l
+. .
+. am MTO
+. ad l
+. .
+. LINKSTYLE blue R < >
+.\}
+.SH "NAME"
+sulogin \- single\-user login
+.SH "SYNOPSIS"
+.sp
+\fBsulogin\fP [options] [\fItty\fP]
+.SH "DESCRIPTION"
+.sp
+\fBsulogin\fP is invoked by \fBinit\fP when the system goes into single\-user mode.
+.sp
+The user is prompted:
+.sp
+Give root password for system maintenance (or type Control\-D for normal startup):
+.sp
+If the root account is locked and \fB\-\-force\fP is specified, no password is required.
+.sp
+\fBsulogin\fP will be connected to the current terminal, or to the optional \fItty\fP device that can be specified on the command line (typically \fI/dev/console\fP).
+.sp
+When the user exits from the single\-user shell, or presses control\-D at the prompt, the system will continue to boot.
+.SH "OPTIONS"
+.sp
+\fB\-e\fP, \fB\-\-force\fP
+.RS 4
+If the default method of obtaining the root password from the system via \fBgetpwnam\fP(3) fails, then examine \fI/etc/passwd\fP and \fI/etc/shadow\fP to get the password. If these files are damaged or nonexistent, or when root account is locked by \*(Aq!\*(Aq or \*(Aq*\*(Aq at the begin of the password then \fBsulogin\fP will \fBstart a root shell without asking for a password\fP.
+.sp
+Only use the \fB\-e\fP option if you are sure the console is physically protected against unauthorized access.
+.RE
+.sp
+\fB\-p\fP, \fB\-\-login\-shell\fP
+.RS 4
+Specifying this option causes \fBsulogin\fP to start the shell process as a login shell.
+.RE
+.sp
+\fB\-t\fP, \fB\-\-timeout\fP \fIseconds\fP
+.RS 4
+Specify the maximum amount of time to wait for user input. By default, \fBsulogin\fP will wait forever.
+.RE
+.sp
+\fB\-h\fP, \fB\-\-help\fP
+.RS 4
+Display help text and exit.
+.RE
+.sp
+\fB\-V\fP, \fB\-\-version\fP
+.RS 4
+Print version and exit.
+.RE
+.SH "ENVIRONMENT"
+.sp
+\fBsulogin\fP looks for the environment variable \fBSUSHELL\fP or \fBsushell\fP to determine what shell to start. If the environment variable is not set, it will try to execute root\(cqs shell from \fI/etc/passwd\fP. If that fails, it will fall back to \fI/bin/sh\fP.
+.SH "AUTHORS"
+.sp
+\fBsulogin\fP was written by Miquel van Smoorenburg for sysvinit and later ported to util\-linux by Dave Reisner and Karel Zak.
+.SH "REPORTING BUGS"
+.sp
+For bug reports, use the issue tracker at \c
+.URL "https://github.com/util\-linux/util\-linux/issues" "" "."
+.SH "AVAILABILITY"
+.sp
+The \fBsulogin\fP command is part of the util\-linux package which can be downloaded from \c
+.URL "https://www.kernel.org/pub/linux/utils/util\-linux/" "Linux Kernel Archive" "." \ No newline at end of file
diff --git a/login-utils/sulogin.8.adoc b/login-utils/sulogin.8.adoc
new file mode 100644
index 0000000..3165d61
--- /dev/null
+++ b/login-utils/sulogin.8.adoc
@@ -0,0 +1,80 @@
+//po4a: entry man manual
+////
+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
+////
+= sulogin(8)
+:doctype: manpage
+:man manual: System Administration
+:man source: util-linux {release-version}
+:page-layout: base
+:command: sulogin
+:asterisk: *
+
+== NAME
+
+sulogin - single-user login
+
+== SYNOPSIS
+
+*sulogin* [options] [_tty_]
+
+== DESCRIPTION
+
+*sulogin* is invoked by *init* when the system goes into single-user mode.
+
+The user is prompted:
+
+Give root password for system maintenance (or type Control-D for normal startup):
+
+If the root account is locked and *--force* is specified, no password is required.
+
+*sulogin* will be connected to the current terminal, or to the optional _tty_ device that can be specified on the command line (typically _/dev/console_).
+
+When the user exits from the single-user shell, or presses control-D at the prompt, the system will continue to boot.
+
+== OPTIONS
+
+*-e*, *--force*::
+If the default method of obtaining the root password from the system via *getpwnam*(3) fails, then examine _/etc/passwd_ and _/etc/shadow_ to get the password. If these files are damaged or nonexistent, or when root account is locked by '!' or '{asterisk}' at the begin of the password then *sulogin* will *start a root shell without asking for a password*.
+//TRANSLATORS: Keep {asterisk} untranslated.
++
+Only use the *-e* option if you are sure the console is physically protected against unauthorized access.
+
+*-p*, *--login-shell*::
+Specifying this option causes *sulogin* to start the shell process as a login shell.
+
+*-t*, *--timeout* _seconds_::
+Specify the maximum amount of time to wait for user input. By default, *sulogin* will wait forever.
+
+include::man-common/help-version.adoc[]
+
+== ENVIRONMENT
+
+*sulogin* looks for the environment variable *SUSHELL* or *sushell* to determine what shell to start. If the environment variable is not set, it will try to execute root's shell from _/etc/passwd_. If that fails, it will fall back to _/bin/sh_.
+
+== AUTHORS
+
+*sulogin* was written by Miquel van Smoorenburg for sysvinit and later ported to util-linux by Dave Reisner and Karel Zak.
+
+include::man-common/bugreports.adoc[]
+
+include::man-common/footer.adoc[]
+
+ifdef::translation[]
+include::man-common/translation.adoc[]
+endif::[]
diff --git a/login-utils/sulogin.c b/login-utils/sulogin.c
new file mode 100644
index 0000000..45a558d
--- /dev/null
+++ b/login-utils/sulogin.c
@@ -0,0 +1,1199 @@
+/*
+ * 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>
+# include <linux/serial.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;
+
+#define SULOGIN_PASSWORD_BUFSIZ 128
+
+#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;
+#if defined(TIOCGSERIAL)
+ struct serial_struct serinfo = { .flags = 0 };
+#endif
+#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);
+ errno = 0;
+#endif
+
+#ifdef TIOCGSERIAL
+ if (ioctl(fd, TIOCGSERIAL, &serinfo) >= 0)
+ con->flags |= CON_SERIAL;
+ errno = 0;
+#endif
+
+#ifdef KDGKBMODE
+ if (!(con->flags & CON_SERIAL)
+ && ioctl(fd, KDGKBMODE, &mode) < 0)
+ con->flags |= CON_SERIAL;
+ errno = 0;
+#endif
+ if (tcgetattr(fd, tio) < 0) {
+ int saveno = errno;
+#if defined(KDGKBMODE) || defined(TIOCGSERIAL)
+ if (con->flags & CON_SERIAL) { /* Try to recover this */
+
+# if defined(TIOCGSERIAL)
+ serinfo.flags |= ASYNC_SKIP_TEST; /* Skip test of UART */
+
+ if (ioctl(fd, TIOCSSERIAL, &serinfo) < 0)
+ goto tcgeterr;
+ if (ioctl(fd, TIOCSERCONFIG) < 0) /* Try to autoconfigure */
+ goto tcgeterr;
+ if (ioctl(fd, TIOCGSERIAL, &serinfo) < 0)
+ goto tcgeterr; /* Ouch */
+# endif
+ if (tcgetattr(fd, tio) < 0) /* Retry to get tty attributes */
+ saveno = errno;
+ }
+# if defined(TIOCGSERIAL)
+ tcgeterr:
+# endif
+ if (saveno)
+#endif
+ {
+ FILE *fcerr = fdopen(fd, "w");
+ if (fcerr) {
+ fprintf(fcerr, _("tcgetattr failed"));
+ fclose(fcerr);
+ }
+ warn(_("tcgetattr failed"));
+
+ con->flags &= ~CON_SERIAL;
+ if (saveno != EIO)
+ con->flags |= CON_NOTTY;
+ else
+ con->flags |= CON_EIO;
+
+ errno = 0;
+ return;
+ }
+ }
+
+ /* Handle lines other than virtual consoles here */
+#if defined(KDGKBMODE) || defined(TIOCGSERIAL)
+ if (con->flags & CON_SERIAL)
+#endif
+ {
+ speed_t ispeed, ospeed;
+ struct winsize ws;
+ errno = 0;
+
+ /* 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 update = 0;
+
+ if (ws.ws_row == 0) {
+ ws.ws_row = 24;
+ update++;
+ }
+ if (ws.ws_col == 0) {
+ ws.ws_col = 80;
+ update++;
+ }
+ if (update)
+ 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_EIO)
+ return;
+ if ((con->flags & CON_SERIAL) == 0) {
+ xsetenv("TERM", "linux", 0);
+ return;
+ }
+ if (con->flags & CON_NOTTY) {
+ xsetenv("TERM", "dumb", 0);
+ return;
+ }
+
+#if defined (__s390__) || defined (__s390x__)
+ xsetenv("TERM", "dumb", 0);
+#else
+ xsetenv("TERM", "vt102", 0);
+#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 = 1;
+}
+
+static void chld_handler(int sig __attribute__((unused)))
+{
+ sigchild = 1;
+}
+
+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());
+ pid_t ttypgrp;
+
+ if (con->flags & CON_NOTTY)
+ goto notty;
+ if (con->flags & CON_EIO)
+ return;
+
+ ttypgrp = tcgetpgrp(fd);
+
+ /*
+ * 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);
+ }
+notty:
+ 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 char *getpasswd(struct console *con)
+{
+ struct sigaction sa;
+ struct termios tty;
+ static char pass[SULOGIN_PASSWORD_BUFSIZ], *ptr;
+ struct chardata *cp;
+ char *ret = NULL;
+ unsigned char tc;
+ char c, ascval;
+ int eightbit;
+ const int fd = con->fd;
+
+ if (con->flags & CON_EIO)
+ goto out;
+
+ cp = &con->cp;
+ tty = con->tio;
+ tc = 0;
+ ret = pass;
+
+ tty.c_iflag &= ~(IUCLC|IXON|IXOFF|IXANY);
+ tty.c_lflag &= ~(ECHO|ECHOE|ECHOK|ECHONL|TOSTOP|ISIG);
+
+ if ((con->flags & CON_NOTTY) == 0)
+ 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') {
+ errno = 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 EIO:
+ con->flags |= CON_EIO;
+ /* fallthrough */
+ default:
+ warn(_("cannot read %s"), con->tty);
+ break;
+ case 0:
+ 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:
+#ifdef HAVE_EXPLICIT_BZERO
+ if (ret == NULL)
+ explicit_bzero(pass, sizeof(pass));
+#endif
+ 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) {
+ char *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, (char *)NULL);
+ warn(_("failed to execute %s"), su_shell);
+
+ xsetenv("SHELL", "/bin/sh", 1);
+ execl("/bin/sh", profile ? "-sh" : "sh", (char *)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)"));
+
+ exit(EXIT_SUCCESS);
+}
+
+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 an own session.
+ */
+ if ((pid = getpid()) == 1) {
+ setsid();
+ ignore_result( ioctl(STDIN_FILENO, TIOCSCTTY, (char *) 1) );
+ }
+
+ setlocale(LC_ALL, "");
+ bindtextdomain(PACKAGE, LOCALEDIR);
+ textdomain(PACKAGE);
+ close_stdout_atexit();
+
+ /*
+ * 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':
+ {
+ static const char *features[] = {
+#ifdef USE_SULOGIN_EMERGENCY_MOUNT
+ "emergency-mount",
+#endif
+ NULL
+ };
+ print_version_with_features(EXIT_SUCCESS, features);
+ }
+ case 'h':
+ usage();
+ 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;
+ if (con->flags & CON_EIO)
+ goto next;
+
+ switch ((con->pid = fork())) {
+ case 0:
+ mask_signal(SIGCHLD, SIG_DFL, NULL);
+ dup2(con->fd, STDERR_FILENO);
+ nofork:
+ setup(con);
+ while (1) {
+ const char *passwd = pwd->pw_passwd;
+ 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) {
+#ifdef HAVE_EXPLICIT_BZERO
+ explicit_bzero(answer, SULOGIN_PASSWORD_BUFSIZ);
+#endif
+ 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++;
+ }
+#ifdef HAVE_EXPLICIT_BZERO
+ explicit_bzero(answer, SULOGIN_PASSWORD_BUFSIZ);
+#endif
+ 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;
+ }
+ next:
+ 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..0d054a1
--- /dev/null
+++ b/login-utils/utmpdump.1
@@ -0,0 +1,100 @@
+'\" t
+.\" Title: utmpdump
+.\" Author: [see the "AUTHOR(S)" section]
+.\" Generator: Asciidoctor 2.0.20
+.\" Date: 2023-10-23
+.\" Manual: User Commands
+.\" Source: util-linux 2.39.3
+.\" Language: English
+.\"
+.TH "UTMPDUMP" "1" "2023-10-23" "util\-linux 2.39.3" "User Commands"
+.ie \n(.g .ds Aq \(aq
+.el .ds Aq '
+.ss \n[.ss] 0
+.nh
+.ad l
+.de URL
+\fI\\$2\fP <\\$1>\\$3
+..
+.als MTO URL
+.if \n[.g] \{\
+. mso www.tmac
+. am URL
+. ad l
+. .
+. am MTO
+. ad l
+. .
+. LINKSTYLE blue R < >
+.\}
+.SH "NAME"
+utmpdump \- dump UTMP and WTMP files in raw format
+.SH "SYNOPSIS"
+.sp
+\fButmpdump\fP [options] \fIfilename\fP
+.SH "DESCRIPTION"
+.sp
+\fButmpdump\fP is a simple program to dump UTMP and WTMP files in raw format, so they can be examined. \fButmpdump\fP reads from stdin unless a \fIfilename\fP is passed.
+.SH "OPTIONS"
+.sp
+\fB\-f\fP, \fB\-\-follow\fP
+.RS 4
+Output appended data as the file grows.
+.RE
+.sp
+\fB\-o\fP, \fB\-\-output\fP \fIfile\fP
+.RS 4
+Write command output to \fIfile\fP instead of standard output.
+.RE
+.sp
+\fB\-r\fP, \fB\-\-reverse\fP
+.RS 4
+Undump, write back edited login information into the utmp or wtmp files.
+.RE
+.sp
+\fB\-h\fP, \fB\-\-help\fP
+.RS 4
+Display help text and exit.
+.RE
+.sp
+\fB\-V\fP, \fB\-\-version\fP
+.RS 4
+Print version and exit.
+.RE
+.SH "NOTES"
+.sp
+\fButmpdump\fP 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:
+.RS 3
+.ll -.6i
+.sp
+\fButmpdump \-r < ascii_file > wtmp\fP
+.br
+.RE
+.ll
+.sp
+But be warned, \fButmpdump\fP was written for debugging purposes only.
+.SS "File formats"
+.sp
+Only the binary version of the \fButmp\fP(5) is standardised. Textual dumps may become incompatible in future.
+.sp
+The version 2.28 was the last one that printed text output using \fBctime\fP(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"
+.sp
+You may \fBnot\fP use the \fB\-r\fP option, as the format for the utmp/wtmp files strongly depends on the input format. This tool was \fBnot\fP written for normal use, but for debugging only.
+.SH "AUTHORS"
+.sp
+Michael Krapp
+.SH "SEE ALSO"
+.sp
+\fBlast\fP(1),
+\fBw\fP(1),
+\fBwho\fP(1),
+\fButmp\fP(5)
+.SH "REPORTING BUGS"
+.sp
+For bug reports, use the issue tracker at \c
+.URL "https://github.com/util\-linux/util\-linux/issues" "" "."
+.SH "AVAILABILITY"
+.sp
+The \fButmpdump\fP command is part of the util\-linux package which can be downloaded from \c
+.URL "https://www.kernel.org/pub/linux/utils/util\-linux/" "Linux Kernel Archive" "." \ No newline at end of file
diff --git a/login-utils/utmpdump.1.adoc b/login-utils/utmpdump.1.adoc
new file mode 100644
index 0000000..a7672d3
--- /dev/null
+++ b/login-utils/utmpdump.1.adoc
@@ -0,0 +1,88 @@
+//po4a: entry man manual
+////
+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
+////
+= utmpdump(1)
+:doctype: manpage
+:man manual: User Commands
+:man source: util-linux {release-version}
+:page-layout: base
+:command: utmpdump
+
+== NAME
+
+utmpdump - dump UTMP and WTMP files in raw format
+
+== SYNOPSIS
+
+*utmpdump* [options] _filename_
+
+== DESCRIPTION
+
+*utmpdump* is a simple program to dump UTMP and WTMP files in raw format, so they can be examined. *utmpdump* reads from stdin unless a _filename_ is passed.
+
+== OPTIONS
+
+*-f*, *--follow*::
+Output appended data as the file grows.
+
+*-o*, *--output* _file_::
+Write command output to _file_ instead of standard output.
+
+*-r*, *--reverse*::
+Undump, write back edited login information into the utmp or wtmp files.
+
+include::man-common/help-version.adoc[]
+
+== NOTES
+
+*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:
+
+____
+*utmpdump -r < ascii_file > wtmp*
+____
+
+But be warned, *utmpdump* was written for debugging purposes only.
+
+=== File formats
+
+Only the binary version of the *utmp*(5) is standardised. Textual dumps may become incompatible in future.
+
+The version 2.28 was the last one that printed text output using *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.
+
+== BUGS
+
+You may *not* use the *-r* option, as the format for the utmp/wtmp files strongly depends on the input format. This tool was *not* written for normal use, but for debugging only.
+
+== AUTHORS
+
+Michael Krapp
+
+== SEE ALSO
+
+*last*(1),
+*w*(1),
+*who*(1),
+*utmp*(5)
+
+include::man-common/bugreports.adoc[]
+
+include::man-common/footer.adoc[]
+
+ifdef::translation[]
+include::man-common/translation.adoc[]
+endif::[]
diff --git a/login-utils/utmpdump.c b/login-utils/utmpdump.c
new file mode 100644
index 0000000..a0ff2b1
--- /dev/null
+++ b/login-utils/utmpdump.c
@@ -0,0 +1,414 @@
+/*
+ * 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 && *++s) {
+ suseconds_t us;
+ char *end = NULL;
+
+ errno = 0;
+ us = strtol(s, &end, 10);
+ if (errno == 0 && end && end > s)
+ return us;
+ }
+ 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);
+
+ if (size < 0)
+ err(EXIT_FAILURE, _("%s: cannot get file position"), filename);
+
+ 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 */
+#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));
+
+ if (sscanf(line, "[%hd] [%d] [%4c] ",
+ &ut.ut_type, &ut.ut_pid, ut.ut_id) != 3) {
+ warnx(_("parse error: %s"), line);
+ continue;
+ }
+
+ 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);
+ close_stdout_atexit();
+
+ 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();
+ case 'V':
+ print_version(EXIT_SUCCESS);
+ default:
+ errtryhelp(EXIT_FAILURE);
+ }
+ }
+
+ if (!out)
+ out = stdout;
+
+ if (follow && (out != stdout || !isatty(STDOUT_FILENO))) {
+ setvbuf(out, NULL, _IOLBF, 0);
+ }
+
+ 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..c6e670a
--- /dev/null
+++ b/login-utils/vigr.8
@@ -0,0 +1 @@
+.so vipw.8 \ No newline at end of file
diff --git a/login-utils/vipw.8 b/login-utils/vipw.8
new file mode 100644
index 0000000..ba8c3b0
--- /dev/null
+++ b/login-utils/vipw.8
@@ -0,0 +1,71 @@
+'\" t
+.\" Title: vipw
+.\" Author: [see the "AUTHOR(S)" section]
+.\" Generator: Asciidoctor 2.0.20
+.\" Date: 2023-10-23
+.\" Manual: System Administration
+.\" Source: util-linux 2.39.3
+.\" Language: English
+.\"
+.TH "VIPW" "8" "2023-10-23" "util\-linux 2.39.3" "System Administration"
+.ie \n(.g .ds Aq \(aq
+.el .ds Aq '
+.ss \n[.ss] 0
+.nh
+.ad l
+.de URL
+\fI\\$2\fP <\\$1>\\$3
+..
+.als MTO URL
+.if \n[.g] \{\
+. mso www.tmac
+. am URL
+. ad l
+. .
+. am MTO
+. ad l
+. .
+. LINKSTYLE blue R < >
+.\}
+.SH "NAME"
+vipw, vigr \- edit the password or group file
+.SH "SYNOPSIS"
+.sp
+\fBvipw\fP [options]
+.sp
+\fBvigr\fP [options]
+.SH "DESCRIPTION"
+.sp
+\fBvipw\fP 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, \fBvipw\fP will ask you to try again later. The default editor for \fBvipw\fP and \fBvigr\fP is \fBvi\fP(1). \fBvigr\fP edits the group file in the same manner as \fBvipw\fP does the passwd file.
+.SH "OPTIONS"
+.sp
+\fB\-h\fP, \fB\-\-help\fP
+.RS 4
+Display help text and exit.
+.RE
+.sp
+\fB\-V\fP, \fB\-\-version\fP
+.RS 4
+Print version and exit.
+.RE
+.SH "ENVIRONMENT"
+.sp
+If the following environment variable exists, it will be utilized by \fBvipw\fP and \fBvigr\fP:
+.sp
+\fBEDITOR\fP
+.RS 4
+The editor specified by the string \fBEDITOR\fP will be invoked instead of the default editor \fBvi\fP(1).
+.RE
+.SH "HISTORY"
+.sp
+The \fBvipw\fP command appeared in 4.0BSD. The \fBvigr\fP command appeared in Util\-Linux 2.6.
+.SH "SEE ALSO"
+.sp
+\fBvi\fP(1),
+\fBpasswd\fP(1),
+\fBflock\fP(2),
+\fBpasswd\fP(5)
+.SH "AVAILABILITY"
+.sp
+The \fBvigr\fP and \fBvipw\fP commands are part of the util\-linux package and are available from \c
+.URL "https://www.kernel.org/pub/linux/utils/util\-linux/" "Linux Kernel Archive" "" \ No newline at end of file
diff --git a/login-utils/vipw.8.adoc b/login-utils/vipw.8.adoc
new file mode 100644
index 0000000..58acb9c
--- /dev/null
+++ b/login-utils/vipw.8.adoc
@@ -0,0 +1,81 @@
+//po4a: entry man manual
+////
+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
+////
+= vipw(8)
+:doctype: manpage
+:man manual: System Administration
+:man source: util-linux {release-version}
+:page-layout: base
+:command: vipw
+
+== NAME
+
+vipw, vigr - edit the password or group file
+
+== SYNOPSIS
+
+*vipw* [options]
+
+*vigr* [options]
+
+== DESCRIPTION
+
+*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, *vipw* will ask you to try again later. The default editor for *vipw* and *vigr* is *vi*(1). *vigr* edits the group file in the same manner as *vipw* does the passwd file.
+
+== OPTIONS
+
+include::man-common/help-version.adoc[]
+
+== ENVIRONMENT
+
+If the following environment variable exists, it will be utilized by *vipw* and *vigr*:
+
+*EDITOR*::
+The editor specified by the string *EDITOR* will be invoked instead of the default editor *vi*(1).
+
+== HISTORY
+
+The *vipw* command appeared in 4.0BSD. The *vigr* command appeared in Util-Linux 2.6.
+
+== SEE ALSO
+
+*vi*(1),
+*passwd*(1),
+*flock*(2),
+*passwd*(5)
+
+== AVAILABILITY
+
+The *vigr* and *vipw* commands are part of the util-linux package and are available from https://www.kernel.org/pub/linux/utils/util-linux/[Linux Kernel Archive]
diff --git a/login-utils/vipw.c b/login-utils/vipw.c
new file mode 100644
index 0000000..5049706
--- /dev/null
+++ b/login-utils/vipw.c
@@ -0,0 +1,369 @@
+/*
+ * 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 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);
+
+ /* Set SIGCHLD to default for waitpid. */
+ (void)signal(SIGCHLD, SIG_DFL);
+
+ /* Create with exact permissions. */
+ (void)umask(0);
+}
+
+static FILE * pw_tmpfile(int lockfd)
+{
+ FILE *fd;
+ char *tmpname = NULL;
+ int res;
+
+ if ((fd = xfmkstemp(&tmpname, "/etc", ".vipw")) == NULL) {
+ ulckpwdf();
+ err(EXIT_FAILURE, _("can't open temporary file"));
+ }
+
+ tmp_file = tmpname;
+ res = ul_copy_file(lockfd, fileno(fd));
+ if (res == UL_COPY_READ_ERROR)
+ pw_error(orig_file, 1, 1);
+ else if (res == UL_COPY_WRITE_ERROR)
+ pw_error(tmp_file, 1, 1);
+ return fd;
+}
+
+static void pw_write(void)
+{
+ char tmp[FILENAMELEN + 4];
+
+ snprintf(tmp, sizeof(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) {
+ char *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, (char *)NULL);
+ errexec(editor);
+ }
+ for (;;) {
+ pid = waitpid(pid, &pstat, WUNTRACED);
+ if (pid != -1 && WIFSTOPPED(pstat)) {
+ /* the editor suspended, so suspend us as well */
+ kill(getpid(), SIGSTOP);
+ kill(pid, SIGCONT);
+ } else {
+ break;
+ }
+ }
+ if (pid == -1)
+ pw_error(editor, 1, 1);
+ else if (!WIFEXITED(pstat) || WEXITSTATUS(pstat) != 0) {
+ warnx("%s: unsuccessful execution", editor);
+ pw_error(editor, 0, 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);
+ close_stdout_atexit();
+
+ 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':
+ print_version(EXIT_SUCCESS);
+ case 'h':
+ usage();
+ default:
+ errtryhelp(EXIT_FAILURE);
+ }
+ }
+
+ edit_file(0);
+
+ if (program == VIGR)
+ xstrncpy(orig_file, SGROUP_FILE, sizeof(orig_file));
+ else
+ xstrncpy(orig_file, SHADOW_FILE, sizeof(orig_file));
+
+ 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);
+
+ fflush(stdout);
+ if (fgets(response, sizeof(response), stdin) &&
+ rpmatch(response) == RPMATCH_YES)
+ edit_file(1);
+ }
+ exit(EXIT_SUCCESS);
+}