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