b7dd4d
From d897789b4dc7d115c915842eabf33ed3de20110a Mon Sep 17 00:00:00 2001
b7dd4d
From: Lennart Poettering <lennart@poettering.net>
b7dd4d
Date: Tue, 7 Aug 2018 13:49:34 +0200
b7dd4d
Subject: [PATCH] logind: optionally watch utmp for login data
b7dd4d
b7dd4d
This allows us to determine the TTY an ssh session is for, which is
b7dd4d
useful to to proper idle detection for ssh sessions.
b7dd4d
b7dd4d
Fixes: #9622
b7dd4d
(cherry picked from commit 3d0ef5c7e00155bc74f6f71c34cad518a4ff56ba)
b7dd4d
b7dd4d
Related: #2122288
b7dd4d
---
b7dd4d
 src/login/logind-core.c    | 143 +++++++++++++++++++++++++++++++++++++
b7dd4d
 src/login/logind-dbus.c    |   5 ++
b7dd4d
 src/login/logind-session.c |  24 +++++++
b7dd4d
 src/login/logind-session.h |  14 +++-
b7dd4d
 src/login/logind.c         |  10 +++
b7dd4d
 src/login/logind.h         |   8 +++
b7dd4d
 6 files changed, 203 insertions(+), 1 deletion(-)
b7dd4d
b7dd4d
diff --git a/src/login/logind-core.c b/src/login/logind-core.c
b7dd4d
index 0ed812a2c8..7e33f8e6aa 100644
b7dd4d
--- a/src/login/logind-core.c
b7dd4d
+++ b/src/login/logind-core.c
b7dd4d
@@ -5,6 +5,9 @@
b7dd4d
 #include <sys/ioctl.h>
b7dd4d
 #include <sys/types.h>
b7dd4d
 #include <linux/vt.h>
b7dd4d
+#if ENABLE_UTMP
b7dd4d
+#include <utmpx.h>
b7dd4d
+#endif
b7dd4d
 
b7dd4d
 #include "alloc-util.h"
b7dd4d
 #include "bus-error.h"
b7dd4d
@@ -14,6 +17,7 @@
b7dd4d
 #include "fd-util.h"
b7dd4d
 #include "logind.h"
b7dd4d
 #include "parse-util.h"
b7dd4d
+#include "path-util.h"
b7dd4d
 #include "process-util.h"
b7dd4d
 #include "strv.h"
b7dd4d
 #include "terminal-util.h"
b7dd4d
@@ -692,3 +696,142 @@ bool manager_all_buttons_ignored(Manager *m) {
b7dd4d
 
b7dd4d
         return true;
b7dd4d
 }
b7dd4d
+
b7dd4d
+int manager_read_utmp(Manager *m) {
b7dd4d
+#if ENABLE_UTMP
b7dd4d
+        int r;
b7dd4d
+
b7dd4d
+        assert(m);
b7dd4d
+
b7dd4d
+        if (utmpxname(_PATH_UTMPX) < 0)
b7dd4d
+                return log_error_errno(errno, "Failed to set utmp path to " _PATH_UTMPX ": %m");
b7dd4d
+
b7dd4d
+        setutxent();
b7dd4d
+
b7dd4d
+        for (;;) {
b7dd4d
+                _cleanup_free_ char *t = NULL;
b7dd4d
+                struct utmpx *u;
b7dd4d
+                const char *c;
b7dd4d
+                Session *s;
b7dd4d
+
b7dd4d
+                errno = 0;
b7dd4d
+                u = getutxent();
b7dd4d
+                if (!u) {
b7dd4d
+                        if (errno != 0)
b7dd4d
+                                log_warning_errno(errno, "Failed to read " _PATH_UTMPX ", ignoring: %m");
b7dd4d
+                        r = 0;
b7dd4d
+                        break;
b7dd4d
+                }
b7dd4d
+
b7dd4d
+                if (u->ut_type != USER_PROCESS)
b7dd4d
+                        continue;
b7dd4d
+
b7dd4d
+                if (!pid_is_valid(u->ut_pid))
b7dd4d
+                        continue;
b7dd4d
+
b7dd4d
+                t = strndup(u->ut_line, sizeof(u->ut_line));
b7dd4d
+                if (!t) {
b7dd4d
+                        r = log_oom();
b7dd4d
+                        break;
b7dd4d
+                }
b7dd4d
+
b7dd4d
+                c = path_startswith(t, "/dev/");
b7dd4d
+                if (c) {
b7dd4d
+                        r = free_and_strdup(&t, c);
b7dd4d
+                        if (r < 0) {
b7dd4d
+                                log_oom();
b7dd4d
+                                break;
b7dd4d
+                        }
b7dd4d
+                }
b7dd4d
+
b7dd4d
+                if (isempty(t))
b7dd4d
+                        continue;
b7dd4d
+
b7dd4d
+                s = hashmap_get(m->sessions_by_leader, PID_TO_PTR(u->ut_pid));
b7dd4d
+                if (!s)
b7dd4d
+                        continue;
b7dd4d
+
b7dd4d
+                if (s->tty_validity == TTY_FROM_UTMP && !streq_ptr(s->tty, t)) {
b7dd4d
+                        /* This may happen on multiplexed SSH connection (i.e. 'SSH connection sharing'). In
b7dd4d
+                         * this case PAM and utmp sessions don't match. In such a case let's invalidate the TTY
b7dd4d
+                         * information and never acquire it again. */
b7dd4d
+
b7dd4d
+                        s->tty = mfree(s->tty);
b7dd4d
+                        s->tty_validity = TTY_UTMP_INCONSISTENT;
b7dd4d
+                        log_debug("Session '%s' has inconsistent TTY information, dropping TTY information.", s->id);
b7dd4d
+                        continue;
b7dd4d
+                }
b7dd4d
+
b7dd4d
+                /* Never override what we figured out once */
b7dd4d
+                if (s->tty || s->tty_validity >= 0)
b7dd4d
+                        continue;
b7dd4d
+
b7dd4d
+                s->tty = TAKE_PTR(t);
b7dd4d
+                s->tty_validity = TTY_FROM_UTMP;
b7dd4d
+                log_debug("Acquired TTY information '%s' from utmp for session '%s'.", s->tty, s->id);
b7dd4d
+        }
b7dd4d
+
b7dd4d
+        endutxent();
b7dd4d
+        return r;
b7dd4d
+#else
b7dd4d
+        return 0
b7dd4d
+#endif
b7dd4d
+}
b7dd4d
+
b7dd4d
+#if ENABLE_UTMP
b7dd4d
+static int manager_dispatch_utmp(sd_event_source *s, const struct inotify_event *event, void *userdata) {
b7dd4d
+        Manager *m = userdata;
b7dd4d
+
b7dd4d
+        assert(m);
b7dd4d
+
b7dd4d
+        /* If there's indication the file itself might have been removed or became otherwise unavailable, then let's
b7dd4d
+         * reestablish the watch on whatever there's now. */
b7dd4d
+        if ((event->mask & (IN_ATTRIB|IN_DELETE_SELF|IN_MOVE_SELF|IN_Q_OVERFLOW|IN_UNMOUNT)) != 0)
b7dd4d
+                manager_connect_utmp(m);
b7dd4d
+
b7dd4d
+        (void) manager_read_utmp(m);
b7dd4d
+        return 0;
b7dd4d
+}
b7dd4d
+#endif
b7dd4d
+
b7dd4d
+void manager_connect_utmp(Manager *m) {
b7dd4d
+#if ENABLE_UTMP
b7dd4d
+        sd_event_source *s = NULL;
b7dd4d
+        int r;
b7dd4d
+
b7dd4d
+        assert(m);
b7dd4d
+
b7dd4d
+        /* Watch utmp for changes via inotify. We do this to deal with tools such as ssh, which will register the PAM
b7dd4d
+         * session early, and acquire a TTY only much later for the connection. Thus during PAM the TTY won't be known
b7dd4d
+         * yet. ssh will register itself with utmp when it finally acquired the TTY. Hence, let's make use of this, and
b7dd4d
+         * watch utmp for the TTY asynchronously. We use the PAM session's leader PID as key, to find the right entry.
b7dd4d
+         *
b7dd4d
+         * Yes, relying on utmp is pretty ugly, but it's good enough for informational purposes, as well as idle
b7dd4d
+         * detection (which, for tty sessions, relies on the TTY used) */
b7dd4d
+
b7dd4d
+        r = sd_event_add_inotify(m->event, &s, _PATH_UTMPX, IN_MODIFY|IN_MOVE_SELF|IN_DELETE_SELF|IN_ATTRIB, manager_dispatch_utmp, m);
b7dd4d
+        if (r < 0)
b7dd4d
+                log_full_errno(r == -ENOENT ? LOG_DEBUG: LOG_WARNING, r, "Failed to create inotify watch on " _PATH_UTMPX ", ignoring: %m");
b7dd4d
+        else {
b7dd4d
+                r = sd_event_source_set_priority(s, SD_EVENT_PRIORITY_IDLE);
b7dd4d
+                if (r < 0)
b7dd4d
+                        log_warning_errno(r, "Failed to adjust utmp event source priority, ignoring: %m");
b7dd4d
+
b7dd4d
+                (void) sd_event_source_set_description(s, "utmp");
b7dd4d
+        }
b7dd4d
+
b7dd4d
+        sd_event_source_unref(m->utmp_event_source);
b7dd4d
+        m->utmp_event_source = s;
b7dd4d
+#endif
b7dd4d
+}
b7dd4d
+
b7dd4d
+void manager_reconnect_utmp(Manager *m) {
b7dd4d
+#if ENABLE_UTMP
b7dd4d
+        assert(m);
b7dd4d
+
b7dd4d
+        if (m->utmp_event_source)
b7dd4d
+                return;
b7dd4d
+
b7dd4d
+        manager_connect_utmp(m);
b7dd4d
+#endif
b7dd4d
+}
b7dd4d
diff --git a/src/login/logind-dbus.c b/src/login/logind-dbus.c
b7dd4d
index 1bb152bc20..0248042308 100644
b7dd4d
--- a/src/login/logind-dbus.c
b7dd4d
+++ b/src/login/logind-dbus.c
b7dd4d
@@ -772,6 +772,9 @@ static int method_create_session(sd_bus_message *message, void *userdata, sd_bus
b7dd4d
                 } while (hashmap_get(m->sessions, id));
b7dd4d
         }
b7dd4d
 
b7dd4d
+        /* If we are not watching utmp aleady, try again */
b7dd4d
+        manager_reconnect_utmp(m);
b7dd4d
+
b7dd4d
         r = manager_add_user_by_uid(m, uid, &user);
b7dd4d
         if (r < 0)
b7dd4d
                 goto fail;
b7dd4d
@@ -795,6 +798,8 @@ static int method_create_session(sd_bus_message *message, void *userdata, sd_bus
b7dd4d
                         r = -ENOMEM;
b7dd4d
                         goto fail;
b7dd4d
                 }
b7dd4d
+
b7dd4d
+                session->tty_validity = TTY_FROM_PAM;
b7dd4d
         }
b7dd4d
 
b7dd4d
         if (!isempty(display)) {
b7dd4d
diff --git a/src/login/logind-session.c b/src/login/logind-session.c
b7dd4d
index 1143a834a4..d666f86d3f 100644
b7dd4d
--- a/src/login/logind-session.c
b7dd4d
+++ b/src/login/logind-session.c
b7dd4d
@@ -56,6 +56,7 @@ int session_new(Session **ret, Manager *m, const char *id) {
b7dd4d
                 .fifo_fd = -1,
b7dd4d
                 .vtfd = -1,
b7dd4d
                 .audit_id = AUDIT_SESSION_INVALID,
b7dd4d
+                .tty_validity = _TTY_VALIDITY_INVALID,
b7dd4d
         };
b7dd4d
 
b7dd4d
         s->state_file = strappend("/run/systemd/sessions/", id);
b7dd4d
@@ -219,6 +220,9 @@ int session_save(Session *s) {
b7dd4d
         if (s->tty)
b7dd4d
                 fprintf(f, "TTY=%s\n", s->tty);
b7dd4d
 
b7dd4d
+        if (s->tty_validity >= 0)
b7dd4d
+                fprintf(f, "TTY_VALIDITY=%s\n", tty_validity_to_string(s->tty_validity));
b7dd4d
+
b7dd4d
         if (s->display)
b7dd4d
                 fprintf(f, "DISPLAY=%s\n", s->display);
b7dd4d
 
b7dd4d
@@ -355,6 +359,7 @@ static int session_load_devices(Session *s, const char *devices) {
b7dd4d
 int session_load(Session *s) {
b7dd4d
         _cleanup_free_ char *remote = NULL,
b7dd4d
                 *seat = NULL,
b7dd4d
+                *tty_validity = NULL,
b7dd4d
                 *vtnr = NULL,
b7dd4d
                 *state = NULL,
b7dd4d
                 *position = NULL,
b7dd4d
@@ -380,6 +385,7 @@ int session_load(Session *s) {
b7dd4d
                            "FIFO",           &s->fifo_path,
b7dd4d
                            "SEAT",           &seat,
b7dd4d
                            "TTY",            &s->tty,
b7dd4d
+                           "TTY_VALIDITY",   &tty_validity,
b7dd4d
                            "DISPLAY",        &s->display,
b7dd4d
                            "REMOTE_HOST",    &s->remote_host,
b7dd4d
                            "REMOTE_USER",    &s->remote_user,
b7dd4d
@@ -456,6 +462,16 @@ int session_load(Session *s) {
b7dd4d
                 seat_claim_position(s->seat, s, npos);
b7dd4d
         }
b7dd4d
 
b7dd4d
+        if (tty_validity) {
b7dd4d
+                TTYValidity v;
b7dd4d
+
b7dd4d
+                v = tty_validity_from_string(tty_validity);
b7dd4d
+                if (v < 0)
b7dd4d
+                        log_debug("Failed to parse TTY validity: %s", tty_validity);
b7dd4d
+                else
b7dd4d
+                        s->tty_validity = v;
b7dd4d
+        }
b7dd4d
+
b7dd4d
         if (leader) {
b7dd4d
                 if (parse_pid(leader, &s->leader) >= 0)
b7dd4d
                         (void) audit_session_from_pid(s->leader, &s->audit_id);
b7dd4d
@@ -1368,3 +1384,11 @@ static const char* const kill_who_table[_KILL_WHO_MAX] = {
b7dd4d
 };
b7dd4d
 
b7dd4d
 DEFINE_STRING_TABLE_LOOKUP(kill_who, KillWho);
b7dd4d
+
b7dd4d
+static const char* const tty_validity_table[_TTY_VALIDITY_MAX] = {
b7dd4d
+        [TTY_FROM_PAM] = "from-pam",
b7dd4d
+        [TTY_FROM_UTMP] = "from-utmp",
b7dd4d
+        [TTY_UTMP_INCONSISTENT] = "utmp-inconsistent",
b7dd4d
+};
b7dd4d
+
b7dd4d
+DEFINE_STRING_TABLE_LOOKUP(tty_validity, TTYValidity);
b7dd4d
diff --git a/src/login/logind-session.h b/src/login/logind-session.h
b7dd4d
index 9bd0c96a03..7da845cea3 100644
b7dd4d
--- a/src/login/logind-session.h
b7dd4d
+++ b/src/login/logind-session.h
b7dd4d
@@ -46,6 +46,14 @@ enum KillWho {
b7dd4d
         _KILL_WHO_INVALID = -1
b7dd4d
 };
b7dd4d
 
b7dd4d
+typedef enum TTYValidity {
b7dd4d
+        TTY_FROM_PAM,
b7dd4d
+        TTY_FROM_UTMP,
b7dd4d
+        TTY_UTMP_INCONSISTENT, /* may happen on ssh sessions with multiplexed TTYs */
b7dd4d
+        _TTY_VALIDITY_MAX,
b7dd4d
+        _TTY_VALIDITY_INVALID = -1,
b7dd4d
+} TTYValidity;
b7dd4d
+
b7dd4d
 struct Session {
b7dd4d
         Manager *manager;
b7dd4d
 
b7dd4d
@@ -60,8 +68,9 @@ struct Session {
b7dd4d
 
b7dd4d
         dual_timestamp timestamp;
b7dd4d
 
b7dd4d
-        char *tty;
b7dd4d
         char *display;
b7dd4d
+        char *tty;
b7dd4d
+        TTYValidity tty_validity;
b7dd4d
 
b7dd4d
         bool remote;
b7dd4d
         char *remote_user;
b7dd4d
@@ -159,6 +168,9 @@ SessionClass session_class_from_string(const char *s) _pure_;
b7dd4d
 const char *kill_who_to_string(KillWho k) _const_;
b7dd4d
 KillWho kill_who_from_string(const char *s) _pure_;
b7dd4d
 
b7dd4d
+const char* tty_validity_to_string(TTYValidity t) _const_;
b7dd4d
+TTYValidity tty_validity_from_string(const char *s) _pure_;
b7dd4d
+
b7dd4d
 int session_prepare_vt(Session *s);
b7dd4d
 void session_restore_vt(Session *s);
b7dd4d
 void session_leave_vt(Session *s);
b7dd4d
diff --git a/src/login/logind.c b/src/login/logind.c
b7dd4d
index 6c208c8e89..25de9a6ab2 100644
b7dd4d
--- a/src/login/logind.c
b7dd4d
+++ b/src/login/logind.c
b7dd4d
@@ -132,6 +132,10 @@ static Manager* manager_unref(Manager *m) {
b7dd4d
         sd_event_source_unref(m->udev_button_event_source);
b7dd4d
         sd_event_source_unref(m->lid_switch_ignore_event_source);
b7dd4d
 
b7dd4d
+#if ENABLE_UTMP
b7dd4d
+        sd_event_source_unref(m->utmp_event_source);
b7dd4d
+#endif
b7dd4d
+
b7dd4d
         safe_close(m->console_active_fd);
b7dd4d
 
b7dd4d
         udev_monitor_unref(m->udev_seat_monitor);
b7dd4d
@@ -1095,6 +1099,9 @@ static int manager_startup(Manager *m) {
b7dd4d
         if (r < 0)
b7dd4d
                 return log_error_errno(r, "Failed to register SIGHUP handler: %m");
b7dd4d
 
b7dd4d
+        /* Connect to utmp */
b7dd4d
+        manager_connect_utmp(m);
b7dd4d
+
b7dd4d
         /* Connect to console */
b7dd4d
         r = manager_connect_console(m);
b7dd4d
         if (r < 0)
b7dd4d
@@ -1150,6 +1157,9 @@ static int manager_startup(Manager *m) {
b7dd4d
         /* Reserve the special reserved VT */
b7dd4d
         manager_reserve_vt(m);
b7dd4d
 
b7dd4d
+        /* Read in utmp if it exists */
b7dd4d
+        manager_read_utmp(m);
b7dd4d
+
b7dd4d
         /* And start everything */
b7dd4d
         HASHMAP_FOREACH(seat, m->seats, i)
b7dd4d
                 seat_start(seat);
b7dd4d
diff --git a/src/login/logind.h b/src/login/logind.h
b7dd4d
index d29b01c75b..bb127bf4a5 100644
b7dd4d
--- a/src/login/logind.h
b7dd4d
+++ b/src/login/logind.h
b7dd4d
@@ -43,6 +43,10 @@ struct Manager {
b7dd4d
         sd_event_source *udev_vcsa_event_source;
b7dd4d
         sd_event_source *udev_button_event_source;
b7dd4d
 
b7dd4d
+#if ENABLE_UTMP
b7dd4d
+        sd_event_source *utmp_event_source;
b7dd4d
+#endif
b7dd4d
+
b7dd4d
         int console_active_fd;
b7dd4d
 
b7dd4d
         unsigned n_autovts;
b7dd4d
@@ -150,6 +154,10 @@ bool manager_is_docked_or_external_displays(Manager *m);
b7dd4d
 bool manager_is_on_external_power(void);
b7dd4d
 bool manager_all_buttons_ignored(Manager *m);
b7dd4d
 
b7dd4d
+int manager_read_utmp(Manager *m);
b7dd4d
+void manager_connect_utmp(Manager *m);
b7dd4d
+void manager_reconnect_utmp(Manager *m);
b7dd4d
+
b7dd4d
 extern const sd_bus_vtable manager_vtable[];
b7dd4d
 
b7dd4d
 int match_job_removed(sd_bus_message *message, void *userdata, sd_bus_error *error);