From 034a2a52ac0ec83e0229941d635d310b23eb04df Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 22 Jul 2011 21:01:15 +0200 Subject: [PATCH] sd-login: beef up login api, to add monitoring and enumerating --- TODO | 2 + src/install.c | 2 +- src/libsystemd-login.sym | 12 +- src/logind-user.c | 38 ++++ src/sd-login.c | 422 ++++++++++++++++++++++++++++++++++++--- src/sd-login.h | 69 ++++++- src/test-login.c | 86 +++++++- src/util.c | 84 ++++++++ src/util.h | 3 + tmpfiles.d/systemd.conf | 3 + 10 files changed, 684 insertions(+), 37 deletions(-) diff --git a/TODO b/TODO index e8af4737..ca8f1f16 100644 --- a/TODO +++ b/TODO @@ -20,6 +20,8 @@ F15 External: Features: +* move PAM code into its own binary + * logind: ensure ACLs are updated on login and logout * warn if the user stops a service but not its associated socket diff --git a/src/install.c b/src/install.c index e1c69444..9f415edc 100644 --- a/src/install.c +++ b/src/install.c @@ -1462,7 +1462,7 @@ int unit_file_disable( q = remove_marked_symlinks(remove_symlinks_to, config_path, changes, n_changes); if (r == 0) - r = 1; + r = q; finish: install_context_done(&c); diff --git a/src/libsystemd-login.sym b/src/libsystemd-login.sym index 708ef6ff..cd5f52c3 100644 --- a/src/libsystemd-login.sym +++ b/src/libsystemd-login.sym @@ -11,13 +11,23 @@ LIBSYSTEMD_LOGIN_31 { global: + sd_get_seats; + sd_get_sessions; + sd_get_uids; + sd_login_monitor_flush; + sd_login_monitor_get_fd; + sd_login_monitor_new; + sd_login_monitor_unref; + sd_pid_get_owner_uid; sd_pid_get_session; sd_seat_get_active; + sd_seat_get_sessions; sd_session_get_seat; sd_session_get_uid; sd_session_is_active; + sd_uid_get_seats; + sd_uid_get_sessions; sd_uid_get_state; - sd_uid_is_active_on_seat; sd_uid_is_on_seat; local: *; diff --git a/src/logind-user.c b/src/logind-user.c index 3a677ff7..3c245c09 100644 --- a/src/logind-user.c +++ b/src/logind-user.c @@ -134,6 +134,44 @@ int user_save(User *u) { "DISPLAY=%s\n", u->display->id); + if (u->sessions) { + Session *i; + + fputs("SESSIONS=", f); + LIST_FOREACH(sessions_by_user, i, u->sessions) { + fprintf(f, + "%s%c", + i->id, + i->sessions_by_seat_next ? ' ' : '\n'); + } + + fputs("SEATS=", f); + LIST_FOREACH(sessions_by_user, i, u->sessions) { + if (i->seat) + fprintf(f, + "%s%c", + i->seat->id, + i->sessions_by_seat_next ? ' ' : '\n'); + } + + fputs("ACTIVE_SESSIONS=", f); + LIST_FOREACH(sessions_by_user, i, u->sessions) + if (session_is_active(i)) + fprintf(f, + "%lu%c", + (unsigned long) i->user->uid, + i->sessions_by_seat_next ? ' ' : '\n'); + + fputs("ACTIVE_SEATS=", f); + LIST_FOREACH(sessions_by_user, i, u->sessions) { + if (session_is_active(i) && i->seat) + fprintf(f, + "%s%c", + i->seat->id, + i->sessions_by_seat_next ? ' ' : '\n'); + } + } + fflush(f); if (ferror(f) || rename(temp_path, u->state_file) < 0) { diff --git a/src/sd-login.c b/src/sd-login.c index 3ae850d8..6dfc2d08 100644 --- a/src/sd-login.c +++ b/src/sd-login.c @@ -22,15 +22,17 @@ #include #include #include +#include #include "util.h" #include "cgroup-util.h" #include "macro.h" #include "sd-login.h" +#include "strv.h" -_public_ int sd_pid_get_session(pid_t pid, char **session) { - int r; +static int pid_get_cgroup(pid_t pid, char **root, char **cgroup) { char *cg_process, *cg_init, *p; + int r; if (pid == 0) pid = getpid(); @@ -38,9 +40,6 @@ _public_ int sd_pid_get_session(pid_t pid, char **session) { if (pid <= 0) return -EINVAL; - if (!session) - return -EINVAL; - r = cg_get_by_pid(SYSTEMD_CGROUP_CONTROLLER, pid, &cg_process); if (r < 0) return r; @@ -63,26 +62,57 @@ _public_ int sd_pid_get_session(pid_t pid, char **session) { free(cg_init); - if (!startswith(p, "/user/")) { - free(cg_process); - return -ENOENT; + if (cgroup) { + char* c; + + c = strdup(p); + if (!c) { + free(cg_process); + return -ENOMEM; + } + + *cgroup = c; } - p += 6; - if (startswith(p, "shared/") || streq(p, "shared")) { + if (root) { + cg_process[p-cg_process] = 0; + *root = cg_process; + } else free(cg_process); + + return 0; +} + +_public_ int sd_pid_get_session(pid_t pid, char **session) { + int r; + char *cgroup, *p; + + if (!session) + return -EINVAL; + + r = pid_get_cgroup(pid, NULL, &cgroup); + if (r < 0) + return r; + + if (!startswith(cgroup, "/user/")) { + free(cgroup); return -ENOENT; } - p = strchr(p, '/'); + p = strchr(cgroup + 6, '/'); if (!p) { - free(cg_process); + free(cgroup); return -ENOENT; } p++; + if (startswith(p, "shared/") || streq(p, "shared")) { + free(cgroup); + return -ENOENT; + } + p = strndup(p, strcspn(p, "/")); - free(cg_process); + free(cgroup); if (!p) return -ENOMEM; @@ -91,6 +121,54 @@ _public_ int sd_pid_get_session(pid_t pid, char **session) { return 0; } +_public_ int sd_pid_get_owner_uid(pid_t pid, uid_t *uid) { + int r; + char *root, *cgroup, *p, *cc; + struct stat st; + + if (!uid) + return -EINVAL; + + r = pid_get_cgroup(pid, &root, &cgroup); + if (r < 0) + return r; + + if (!startswith(cgroup, "/user/")) { + free(cgroup); + free(root); + return -ENOENT; + } + + p = strchr(cgroup + 6, '/'); + if (!p) { + free(cgroup); + return -ENOENT; + } + + p++; + p += strcspn(p, "/"); + *p = 0; + + r = cg_get_path(SYSTEMD_CGROUP_CONTROLLER, root, cgroup, &cc); + free(root); + free(cgroup); + + if (r < 0) + return -ENOMEM; + + r = lstat(cc, &st); + free(cc); + + if (r < 0) + return -errno; + + if (!S_ISDIR(st.st_mode)) + return -ENOTDIR; + + *uid = st.st_uid; + return 0; +} + _public_ int sd_uid_get_state(uid_t uid, char**state) { char *p, *s = NULL; int r; @@ -122,19 +200,22 @@ _public_ int sd_uid_get_state(uid_t uid, char**state) { return 0; } -static int uid_is_on_seat_internal(uid_t uid, const char *seat, const char *variable) { +_public_ int sd_uid_is_on_seat(uid_t uid, int require_active, const char *seat) { char *p, *w, *t, *state, *s = NULL; size_t l; int r; + const char *variable; if (!seat) return -EINVAL; + variable = require_active ? "ACTIVE_UID" : "UIDS"; + p = strappend("/run/systemd/seats/", seat); if (!p) return -ENOMEM; - r = parse_env_file(p, NEWLINE, "UIDS", &s, NULL); + r = parse_env_file(p, NEWLINE, variable, &s, NULL); free(p); if (r < 0) { @@ -165,12 +246,54 @@ static int uid_is_on_seat_internal(uid_t uid, const char *seat, const char *vari return 0; } -_public_ int sd_uid_is_on_seat(uid_t uid, const char *seat) { - return uid_is_on_seat_internal(uid, seat, "UIDS"); +static int uid_get_array(uid_t uid, const char *variable, char ***array) { + char *p, *s = NULL; + char **a; + int r; + + if (!array) + return -EINVAL; + + if (asprintf(&p, "/run/systemd/users/%lu", (unsigned long) uid) < 0) + return -ENOMEM; + + r = parse_env_file(p, NEWLINE, + variable, &s, + NULL); + free(p); + + if (r < 0) { + free(s); + + if (r == -ENOENT) { + *array = NULL; + return 0; + } + + return r; + } + + if (!s) { + *array = NULL; + return 0; + } + + a = strv_split(s, " "); + free(s); + + if (!a) + return -ENOMEM; + + *array = a; + return 0; +} + +_public_ int sd_uid_get_sessions(uid_t uid, int require_active, char ***sessions) { + return uid_get_array(uid, require_active ? "ACTIVE_SESSIONS" : "SESSIONS", sessions); } -_public_ int sd_uid_is_active_on_seat(uid_t uid, const char *seat) { - return uid_is_on_seat_internal(uid, seat, "ACTIVE_UID"); +_public_ int sd_uid_get_seats(uid_t uid, int require_active, char ***seats) { + return uid_get_array(uid, require_active ? "ACTIVE_SEATS" : "SEATS", seats); } _public_ int sd_session_is_active(const char *session) { @@ -291,25 +414,21 @@ _public_ int sd_seat_get_active(const char *seat, char **session, uid_t *uid) { if (session && !s) { free(t); - return -EIO; + return -ENOENT; } if (uid && !t) { free(s); - return -EIO; + return -ENOENT; } if (uid && t) { - unsigned long ul; - - r = safe_atolu(t, &ul); + r = parse_uid(t, uid); if (r < 0) { free(t); free(s); return r; } - - *uid = (uid_t) ul; } free(t); @@ -321,3 +440,254 @@ _public_ int sd_seat_get_active(const char *seat, char **session, uid_t *uid) { return 0; } + +_public_ int sd_seat_get_sessions(const char *seat, char ***sessions, uid_t **uids, unsigned *n_uids) { + char *p, *s = NULL, *t = NULL, **a = NULL; + uid_t *b = NULL; + unsigned n = 0; + int r; + + if (!seat) + return -EINVAL; + + if (!sessions && !uids) + return -EINVAL; + + p = strappend("/run/systemd/seats/", seat); + if (!p) + return -ENOMEM; + + r = parse_env_file(p, NEWLINE, + "SESSIONS", &s, + "ACTIVE_SESSIONS", &t, + NULL); + free(p); + + if (r < 0) { + free(s); + free(t); + return r; + } + + if (sessions && s) { + a = strv_split(s, " "); + if (!a) { + free(s); + free(t); + return -ENOMEM; + } + } + + free(s); + + if (uids && t) { + char *w, *state; + size_t l; + unsigned i = 0; + + FOREACH_WORD(w, l, t, state) + n++; + + b = new(uid_t, n); + if (!b) { + strv_free(a); + return -ENOMEM; + } + + FOREACH_WORD(w, l, t, state) { + char *k; + + k = strndup(w, l); + if (!k) { + free(t); + free(b); + return -ENOMEM; + } + + r = parse_uid(k, b + i); + free(k); + if (r < 0) + continue; + + i++; + } + } + + free(t); + + if (sessions) + *sessions = a; + + if (uids) + *uids = b; + + if (n_uids) + *n_uids = n; + + return 0; +} + +_public_ int sd_get_seats(char ***seats) { + + if (!seats) + return -EINVAL; + + return get_files_in_directory("/run/systemd/seats/", seats); +} + +_public_ int sd_get_sessions(char ***sessions) { + + if (!sessions) + return -EINVAL; + + return get_files_in_directory("/run/systemd/sessions/", sessions); +} + +_public_ int sd_get_uids(uid_t **users) { + DIR *d; + int r = 0; + unsigned n = 0; + uid_t *l = NULL; + + if (!users) + return -EINVAL; + + d = opendir("/run/systemd/users/"); + for (;;) { + struct dirent buffer, *de; + int k; + uid_t uid; + + k = readdir_r(d, &buffer, &de); + if (k != 0) { + r = -k; + goto finish; + } + + if (!de) + break; + + dirent_ensure_type(d, de); + + if (!dirent_is_file(de)) + continue; + + k = parse_uid(de->d_name, &uid); + if (k < 0) + continue; + + if ((unsigned) r >= n) { + uid_t *t; + + n = MAX(16, 2*r); + t = realloc(l, sizeof(uid_t) * n); + if (!t) { + r = -ENOMEM; + goto finish; + } + + l = t; + } + + assert((unsigned) r < n); + l[r++] = uid; + } + +finish: + if (d) + closedir(d); + + if (r >= 0) + *users = l; + else + free(l); + + return r; +} + +static inline int MONITOR_TO_FD(sd_login_monitor *m) { + return (int) (unsigned long) m - 1; +} + +static inline sd_login_monitor* FD_TO_MONITOR(int fd) { + return (sd_login_monitor*) (unsigned long) (fd + 1); +} + +_public_ int sd_login_monitor_new(const char *category, sd_login_monitor **m) { + const char *path; + int fd, k; + bool good = false; + + if (!m) + return -EINVAL; + + fd = inotify_init1(IN_NONBLOCK|IN_CLOEXEC); + if (fd < 0) + return errno; + + if (!category || streq(category, "seat")) { + k = inotify_add_watch(fd, "/run/systemd/seats/", IN_MOVED_TO|IN_DELETE); + if (k < 0) { + close_nointr_nofail(fd); + return -errno; + } + + good = true; + } + + if (!category || streq(category, "session")) { + k = inotify_add_watch(fd, "/run/systemd/sessions/", IN_MOVED_TO|IN_DELETE); + if (k < 0) { + close_nointr_nofail(fd); + return -errno; + } + + good = true; + } + + if (!category || streq(category, "uid")) { + k = inotify_add_watch(fd, "/run/systemd/users/", IN_MOVED_TO|IN_DELETE); + if (k < 0) { + close_nointr_nofail(fd); + return -errno; + } + + good = true; + } + + if (!good) { + close_nointr(fd); + return -EINVAL; + } + + *m = FD_TO_MONITOR(fd); + return 0; +} + +_public_ sd_login_monitor* sd_login_monitor_unref(sd_login_monitor *m) { + int fd; + + if (!m) + return NULL; + + fd = MONITOR_TO_FD(m); + close_nointr(fd); + + return NULL; +} + +_public_ int sd_login_monitor_flush(sd_login_monitor *m) { + + if (!m) + return -EINVAL; + + return flush_fd(MONITOR_TO_FD(m)); +} + +_public_ int sd_login_monitor_get_fd(sd_login_monitor *m) { + + if (!m) + return -EINVAL; + + return MONITOR_TO_FD(m); +} diff --git a/src/sd-login.h b/src/sd-login.h index f23ac805..c6835e12 100644 --- a/src/sd-login.h +++ b/src/sd-login.h @@ -24,17 +24,48 @@ #include -/* Get session from PID */ +/* + * A few points: + * + * Instead of returning an empty string array or empty uid array, we + * may return NULL. + * + * Free the data we return with libc free(). + * + * We return error codes as negative errno, kernel-style. + * + * These functions access data in /proc, /sys/fs/cgroup and /run. All + * of these are virtual file systems, hence the accesses are + * relatively cheap. + */ + +/* Get session from PID. Note that 'shared' processes of a user are + * not attached to a session, but only attached to a user. This will + * return an error for system processes and 'shared' processes of a + * user. */ int sd_pid_get_session(pid_t pid, char **session); +/* Get UID of the owner of the session of the PID (or in case the + * process is a 'shared' user process the UID of that user is + * returned). This will not return the UID of the process, but rather + * the UID of the owner of the cgroup the process is in. This will + * return an error for system processes. */ +int sd_pid_get_owner_uid(pid_t pid, uid_t *uid); + /* Get state from uid. Possible states: offline, lingering, online, active */ int sd_uid_get_state(uid_t uid, char**state); -/* Return 1 if uid has session on seat */ -int sd_uid_is_on_seat(uid_t uid, const char *seat); +/* Return 1 if uid has session on seat. If require_active is true will + * look for active sessions only. */ +int sd_uid_is_on_seat(uid_t uid, int require_active, const char *seat); + +/* Return sessions of user. If require_active is true will look + * for active sessions only. */ +int sd_uid_get_sessions(uid_t uid, int require_active, char ***sessions); -/* Return 1 if uid has active session on seat */ -int sd_uid_is_active_on_seat(uid_t uid, const char *seat); +/* Return seats of user is on. If require_active is true will look for + * active seats only. */ +int sd_uid_get_seats(uid_t uid, int require_active, char ***seats); /* Return 1 if the session is a active */ int sd_session_is_active(const char *session); @@ -48,4 +79,32 @@ int sd_session_get_seat(const char *session, char **seat); /* Return active session and user of seat */ int sd_seat_get_active(const char *seat, char **session, uid_t *uid); +/* Return sessions and users on seat */ +int sd_seat_get_sessions(const char *seat, char ***sessions, uid_t **uid, unsigned *n_uids); + +/* Get all seats */ +int sd_get_seats(char ***seats); + +/* Get all sessions */ +int sd_get_sessions(char ***sessions); + +/* Get all logged in users */ +int sd_get_uids(uid_t **users); + +/* Monitor object */ +typedef struct sd_login_monitor sd_login_monitor; + +/* Create a new monitor. Category must be NULL, "seat", "session", + * "uid" to get monitor events for the specific category (or all). */ +int sd_login_monitor_new(const char *category, sd_login_monitor** ret); + +/* Destroys the passed monitor. Returns NULL. */ +sd_login_monitor* sd_login_monitor_unref(sd_login_monitor *m); + +/* Flushes the monitor */ +int sd_login_monitor_flush(sd_login_monitor *m); + +/* Get FD from monitor */ +int sd_login_monitor_get_fd(sd_login_monitor *m); + #endif diff --git a/src/test-login.c b/src/test-login.c index 97313fcb..21cd3a19 100644 --- a/src/test-login.c +++ b/src/test-login.c @@ -19,26 +19,54 @@ along with systemd; If not, see . ***/ +#include +#include + #include "sd-login.h" #include "util.h" +#include "strv.h" int main(int argc, char* argv[]) { - int r, k; - uid_t u, u2; + int r, k; + uid_t u, u2; char *seat; char *session; char *state; char *session2; + char *t; + char **seats, **sessions; + uid_t *uids; + unsigned n; + struct pollfd pollfd; + sd_login_monitor *m; assert_se(sd_pid_get_session(0, &session) == 0); printf("session = %s\n", session); + assert_se(sd_pid_get_owner_uid(0, &u2) == 0); + printf("user = %lu\n", (unsigned long) u2); + + r = sd_uid_get_sessions(u2, false, &sessions); + assert_se(r >= 0); + assert_se(t = strv_join(sessions, ", ")); + strv_free(sessions); + printf("sessions = %s\n", t); + free(t); + + r = sd_uid_get_seats(u2, false, &seats); + assert_se(r >= 0); + assert_se(t = strv_join(seats, ", ")); + strv_free(seats); + printf("seats = %s\n", t); + free(t); + r = sd_session_is_active(session); assert_se(r >= 0); printf("active = %s\n", yes_no(r)); assert_se(sd_session_get_uid(session, &u) >= 0); printf("uid = %lu\n", (unsigned long) u); + assert_se(u == u2); assert_se(sd_session_get_seat(session, &seat) >= 0); printf("seat = %s\n", seat); @@ -46,9 +74,9 @@ int main(int argc, char* argv[]) { assert_se(sd_uid_get_state(u, &state) >= 0); printf("state = %s\n", state); - assert_se(sd_uid_is_on_seat(u, seat) > 0); + assert_se(sd_uid_is_on_seat(u, 0, seat) > 0); - k = sd_uid_is_active_on_seat(u, seat); + k = sd_uid_is_on_seat(u, 1, seat); assert_se(k >= 0); assert_se(!!r == !!r); @@ -56,10 +84,60 @@ int main(int argc, char* argv[]) { printf("session2 = %s\n", session2); printf("uid2 = %lu\n", (unsigned long) u2); + assert_se(sd_seat_get_sessions(seat, &sessions, &uids, &n) >= 0); + assert_se(t = strv_join(sessions, ", ")); + strv_free(sessions); + printf("sessions = %s\n", t); + free(t); + printf("uids ="); + for (k = 0; k < (int) n; k++) + printf(" %lu", (unsigned long) uids[k]); + printf("\n"); + free(uids); + free(session); free(state); free(session2); free(seat); + assert_se(sd_get_seats(&seats) >= 0); + assert_se(t = strv_join(seats, ", ")); + strv_free(seats); + printf("seats = %s\n", t); + free(t); + + assert_se(sd_get_sessions(&sessions) >= 0); + assert_se(t = strv_join(sessions, ", ")); + strv_free(sessions); + printf("sessions = %s\n", t); + free(t); + + r = sd_get_uids(&uids); + assert_se(r >= 0); + + printf("uids ="); + for (k = 0; k < r; k++) + printf(" %lu", (unsigned long) uids[k]); + printf("\n"); + + free(uids); + + r = sd_login_monitor_new("session", &m); + assert_se(r >= 0); + + zero(pollfd); + pollfd.fd = sd_login_monitor_get_fd(m); + pollfd.events = POLLIN; + + for (n = 0; n < 5; n++) { + r = poll(&pollfd, 1, -1); + assert_se(r >= 0); + + sd_login_monitor_flush(m); + printf("Wake!\n"); + } + + sd_login_monitor_unref(m); + return 0; } diff --git a/src/util.c b/src/util.c index 328a1ead..2d4f2291 100644 --- a/src/util.c +++ b/src/util.c @@ -317,6 +317,26 @@ int parse_pid(const char *s, pid_t* ret_pid) { return 0; } +int parse_uid(const char *s, uid_t* ret_uid) { + unsigned long ul = 0; + uid_t uid; + int r; + + assert(s); + assert(ret_uid); + + if ((r = safe_atolu(s, &ul)) < 0) + return r; + + uid = (uid_t) ul; + + if ((unsigned long) uid != ul) + return -ERANGE; + + *ret_uid = uid; + return 0; +} + int safe_atou(const char *s, unsigned *ret_u) { char *x = NULL; unsigned long l; @@ -5357,6 +5377,70 @@ int in_search_path(const char *path, char **search) { return r; } +int get_files_in_directory(const char *path, char ***list) { + DIR *d; + int r = 0; + unsigned n = 0; + char **l = NULL; + + assert(path); + assert(list); + + d = opendir(path); + for (;;) { + struct dirent buffer, *de; + int k; + + k = readdir_r(d, &buffer, &de); + if (k != 0) { + r = -k; + goto finish; + } + + if (!de) + break; + + dirent_ensure_type(d, de); + + if (!dirent_is_file(de)) + continue; + + if ((unsigned) r >= n) { + char **t; + + n = MAX(16, 2*r); + t = realloc(l, sizeof(char*) * n); + if (!t) { + r = -ENOMEM; + goto finish; + } + + l = t; + } + + assert((unsigned) r < n); + + l[r] = strdup(de->d_name); + if (!l[r]) { + r = -ENOMEM; + goto finish; + } + + l[++r] = NULL; + } + +finish: + if (d) + closedir(d); + + if (r >= 0) + *list = l; + else + strv_free(l); + + return r; +} + static const char *const ioprio_class_table[] = { [IOPRIO_CLASS_NONE] = "none", [IOPRIO_CLASS_RT] = "realtime", diff --git a/src/util.h b/src/util.h index f39c01fb..9537d130 100644 --- a/src/util.h +++ b/src/util.h @@ -136,6 +136,8 @@ void close_many(const int fds[], unsigned n_fd); int parse_boolean(const char *v); int parse_usec(const char *t, usec_t *usec); int parse_pid(const char *s, pid_t* ret_pid); +int parse_uid(const char *s, uid_t* ret_uid); +#define parse_gid(s, ret_uid) parse_uid(s, ret_uid) int safe_atou(const char *s, unsigned *ret_u); int safe_atoi(const char *s, int *ret_i); @@ -453,6 +455,7 @@ int glob_exists(const char *path); int dirent_ensure_type(DIR *d, struct dirent *de); int in_search_path(const char *path, char **search); +int get_files_in_directory(const char *path, char ***list); #define NULSTR_FOREACH(i, l) \ for ((i) = (l); (i) && *(i); (i) = strchr((i), 0)+1) diff --git a/tmpfiles.d/systemd.conf b/tmpfiles.d/systemd.conf index 5e8ed999..7d4b356a 100644 --- a/tmpfiles.d/systemd.conf +++ b/tmpfiles.d/systemd.conf @@ -23,3 +23,6 @@ r /forcequotacheck r /fastboot d /run/systemd/ask-password 0755 root root - +d /run/systemd/seats 0755 root root - +d /run/systemd/sessions 0755 root root - +d /run/systemd/users 0755 root root - -- 2.39.5