diff --git a/TODO b/TODO index 8e4c1206035..e79aeda90e0 100644 --- a/TODO +++ b/TODO @@ -25,6 +25,12 @@ Features: * set memory.oom.group in cgroupsv2 for all leaf cgroups +* drop umask() calls and suchlike from our generators, pid1 should set things up correctly anyway + +* paranoia: whenever we process passwords, call mlock() on the memory + first. i.e. look for all places we use string_erase()/string_free_erase() and + augment them with mlock() + * whenever oom_kill memory.event event is triggered print a nice log message * Move RestrictAddressFamily= to the new cgroup create socket @@ -34,6 +40,14 @@ Features: * chown() tty a service is attached to after the service goes down +* replace systemd-reboot.service's ExecStart= with a single SuccessAction= + line, so that we don't need to fork() for executing the reboot + service. Similar for other services like this, such as systemd-exit.service + and so on. Of course, for this to work service units with no ExecYYZ= set but + SuccessAction= set need to be acceptable. + +* optionally: turn on cgroup delegation for per-session scope units + * optionally, if a per-partition GPT flag is set for the root/home/… partitions format the partition on next boot and unset the flag, in order to implement factory reset. also, add a second flag that simply indicates whether such a @@ -41,20 +55,6 @@ Features: show state of these flags, and optionally trigger such a factory reset on next boot by setting the flag. -* logind: maybe watch utmp asynchronously using inotify, and populate our own - tracked session metadata from the fields available therein. Why bother? Right - now, all "ssh" sessions will be tracked without their TTY by logind (which is - not just unfriendly to users as this means "loginctl session-status" shows - less information than "who" in many cases, but also breaks the IdleAction - logic, as we never can detect such sessions as idle, as we have no TTY to - watch). ssh sets the PAM_TTY field on its PAM sessions to "ssh" rather than - the actual pty, because the PAM session is opened early on for new - connections, but the PTY only registered much later (if at all). ssh writes - the utmp record only after a TTY is actually registered, hence we could use - this data then, and use it if it is available. Using utmp for this is ugly of - course, and watching things asynchronously even more so, but it should be - good enough for the idle detection logic at least. - * maybe extend .path units to expose fanotify() per-mount change events * Add a "systemctl list-units --by-slice" mode or so, which rearranges the @@ -472,8 +472,6 @@ Features: * maybe add support for specifier expansion in user.conf, specifically DefaultEnvironment= -* introduce systemd-timesync-wait.service or so to sync on an NTP fix? - * consider showing the unit names during boot up in the status output, not just the unit descriptions * maybe allow timer units with an empty Units= setting, so that they @@ -615,7 +613,6 @@ Features: - document chaining of signal handler for SIGCHLD and child handlers - define more intervals where we will shift wakeup intervals around in, 1h, 6h, 24h, ... - generate a failure of a default event loop is executed out-of-thread - - maybe add support for inotify events (which we can do safely now, with O_PATH) * investigate endianness issues of UUID vs. GUID @@ -674,11 +671,9 @@ Features: * logind: - logind: optionally, ignore idle-hint logic for autosuspend, block suspend as long as a session is around - - When we update the kernel all kind of hibernation should be prohibited until shutdown/reboot - logind: wakelock/opportunistic suspend support - Add pretty name for seats in logind - logind: allow showing logout dialog from system? - - session scopes/user unit: add RequiresMountsFor for the home directory of the user - add Suspend() bus calls which take timestamps to fix double suspend issues when somebody hits suspend and closes laptop quickly. - if pam_systemd is invoked by su from a process that is outside of a any session we should probably just become a NOP, since that's @@ -851,8 +846,6 @@ Features: "machinectl start" with a new --ephemeral switch - "machinectl status" should also show internal logs of the container in question - - "machinectl list-images" should show os-release data, as well as - machine-info data (including deployment level) - "machinectl history" - "machinectl diff" - "machinectl commit" that takes a writable snapshot of a tree, invokes a @@ -1048,8 +1041,6 @@ External: * kernel: add device_type = "fb", "fbcon" to class "graphics" -* drop accountsservice's StandardOutput=syslog and Type=dbus fields - * /usr/bin/service should actually show the new command line * fedora: suggest auto-restart on failure, but not on success and not on coredump. also, ask people to think about changing the start limit logic. Also point people to RestartPreventExitStatus=, SuccessExitStatus= diff --git a/man/logind.conf.xml b/man/logind.conf.xml index d3551a1bc25..a4078589578 100644 --- a/man/logind.conf.xml +++ b/man/logind.conf.xml @@ -184,6 +184,17 @@ 5. + + UserStopDelaySec= + + Specifies how long to keep the user record and per-user service + user@.service around for a user after they logged out fully. If set to zero, the per-user + service is terminated immediately when the last session of the user has ended. If this option is configured to + non-zero rapid logout/login cycles are sped up, as the user's service manager is not constantly restarted. If + set to infinity the per-user service for a user is never terminated again after first login, + and continues to run until system shutdown. Defaults to 10s. + + HandlePowerKey= HandleSuspendKey= diff --git a/man/rules/meson.build b/man/rules/meson.build index 303b5846549..3602bbaa1a8 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -180,6 +180,7 @@ manpages = [ 'sd_bus_error_get_errno', 'sd_bus_error_has_name', 'sd_bus_error_is_set', + 'sd_bus_error_move', 'sd_bus_error_set', 'sd_bus_error_set_const', 'sd_bus_error_set_errno', diff --git a/man/sd_bus_error.xml b/man/sd_bus_error.xml index 807ca86302d..36cdf4c338d 100644 --- a/man/sd_bus_error.xml +++ b/man/sd_bus_error.xml @@ -31,6 +31,7 @@ sd_bus_error_set_errnofv sd_bus_error_get_errno sd_bus_error_copy + sd_bus_error_move sd_bus_error_is_set sd_bus_error_has_name @@ -114,6 +115,12 @@ const sd_bus_error *e + + int sd_bus_error_move + sd_bus_error *dst + sd_bus_error *e + + int sd_bus_error_is_set const sd_bus_error *e @@ -148,7 +155,7 @@ should have both fields initialized to NULL. Set an error structure to SD_BUS_ERROR_NULL in order to reset both fields to NULL. When no longer necessary, resources - held by the sd_bus_errorstructure should + held by the sd_bus_error structure should be destroyed with sd_bus_error_free(). sd_bus_error_set() sets an error @@ -245,6 +252,14 @@ Otherwise, they will be copied. Returns a converted errno-like, negative error code. + sd_bus_error_move() is similar to sd_bus_error_copy(), but will + move any error information from e into dst, resetting the + former. This function cannot fail, as no new memory is allocated. Note that if e is not set + (or NULL) dst is initializated to + SD_BUS_ERROR_NULL. Moreover, if dst is NULL no + operation is executed on it and and resources held by e are freed and reset. Returns a + converted errno-like, negative error code. + sd_bus_error_is_set() will return a non-zero value if e is non-NULL and an error has been set, @@ -287,9 +302,8 @@ NULL, and a positive errno value mapped from e->name otherwise. - sd_bus_error_copy() returns 0 or a - positive integer on success, and a negative error value converted - from the error name otherwise. + sd_bus_error_copy() and sd_bus_error_move() return 0 or a positive + integer on success, and a negative error value converted from the error name otherwise. sd_bus_error_is_set() returns a non-zero value when e and the diff --git a/man/systemd-socket-proxyd.xml b/man/systemd-socket-proxyd.xml index d48dbc02b60..1869465a3b8 100644 --- a/man/systemd-socket-proxyd.xml +++ b/man/systemd-socket-proxyd.xml @@ -52,7 +52,7 @@ socat1. The main differences for systemd-socket-proxyd are support for socket activation with - Accept=false and an event-driven + Accept=no and an event-driven design that scales better with the number of connections. diff --git a/man/systemd.netdev.xml b/man/systemd.netdev.xml index 380d0088ce1..971d81aa42b 100644 --- a/man/systemd.netdev.xml +++ b/man/systemd.netdev.xml @@ -1470,10 +1470,10 @@ Name=ipip-tun Kind=ipip [Tunnel] -Independent=true +Independent=yes Local=10.65.208.212 Remote=10.65.208.211 -FooOverUDP=true +FooOverUDP=yes FOUDestinationPort=5555 @@ -1484,8 +1484,8 @@ Name=tap-test Kind=tap [Tap] -MultiQueue=true -PacketInfo=true +MultiQueue=yes +PacketInfo=yes /etc/systemd/network/25-sit.netdev diff --git a/man/systemd.network.xml b/man/systemd.network.xml index 500467b67e5..def6ad23154 100644 --- a/man/systemd.network.xml +++ b/man/systemd.network.xml @@ -666,7 +666,7 @@ An IPv6 address, for which Neighbour Advertisement messages will be proxied. This option may be specified more than once. systemd-networkd will add the entries to the kernel's IPv6 neighbor proxy table. - This option implies but has no effect if + This option implies but has no effect if has been set to false. Defaults to unset. diff --git a/man/systemd.offline-updates.xml b/man/systemd.offline-updates.xml index 113d74a2209..13fdfc28def 100644 --- a/man/systemd.offline-updates.xml +++ b/man/systemd.offline-updates.xml @@ -134,7 +134,7 @@ - The update service should declare DefaultDependencies=false, + The update service should declare DefaultDependencies=no, Requires=sysinit.target, After=sysinit.target, After=system-update-pre.target and explicitly pull in any other services it requires. diff --git a/man/systemd.resource-control.xml b/man/systemd.resource-control.xml index 6d5778aa72f..08ae2d049ad 100644 --- a/man/systemd.resource-control.xml +++ b/man/systemd.resource-control.xml @@ -465,7 +465,7 @@ control group attribute, see cgroup-v2.txt. - Implies IOAccounting=true. + Implies IOAccounting=yes. These settings are supported only if the unified control group hierarchy is used. @@ -760,7 +760,7 @@ the startup phase. Using StartupCPUShares= allows prioritizing specific services at boot-up differently than during normal runtime. - Implies CPUAccounting=true. + Implies CPUAccounting=yes. These settings are deprecated. Use CPUWeight= and StartupCPUWeight= instead. @@ -781,7 +781,7 @@ attribute, see memory.txt. - Implies MemoryAccounting=true. + Implies MemoryAccounting=yes. This setting is deprecated. Use MemoryMax= instead. @@ -822,7 +822,7 @@ boot-up differently than during runtime. Implies - BlockIOAccounting=true. + BlockIOAccounting=yes. These settings are deprecated. Use IOWeight= and StartupIOWeight= instead. @@ -844,7 +844,7 @@ url="https://www.kernel.org/doc/Documentation/cgroup-v1/blkio-controller.txt">blkio-controller.txt. Implies - BlockIOAccounting=true. + BlockIOAccounting=yes. This setting is deprecated. Use IODeviceWeight= instead. @@ -869,7 +869,7 @@ Implies - BlockIOAccounting=true. + BlockIOAccounting=yes. These settings are deprecated. Use IOReadBandwidthMax= and IOWriteBandwidthMax= instead. diff --git a/man/systemd.socket.xml b/man/systemd.socket.xml index 4671c71bc85..72807be7b69 100644 --- a/man/systemd.socket.xml +++ b/man/systemd.socket.xml @@ -68,8 +68,8 @@ or it must be a template unit named the same way. Example: a socket file foo.socket needs a matching service foo.service if - is set. If - is set, a service template file + is set. If + is set, a service template file foo@.service must exist from which services are instantiated for each incoming connection. @@ -395,17 +395,17 @@ incoming traffic. Defaults to . For performance reasons, it is recommended to write new daemons only in a way that is suitable for - . A daemon listening on an + . A daemon listening on an AF_UNIX socket may, but does not need to, call close2 on the received socket before exiting. However, it must not unlink the socket from a file system. It should not invoke shutdown2 - on sockets it got with Accept=false, but it + on sockets it got with Accept=no, but it may do so for sockets it got with - Accept=true set. Setting - Accept=true is mostly useful to allow + Accept=yes set. Setting + Accept=yes is mostly useful to allow daemons designed for usage with inetd8 to work unmodified with systemd socket @@ -429,11 +429,11 @@ MaxConnections= The maximum number of connections to simultaneously run services instances for, when - is set. If more concurrent + is set. If more concurrent connections are coming in, they will be refused until at least one existing connection is terminated. This setting has no effect on sockets configured with - or datagram sockets. Defaults to + or datagram sockets. Defaults to 64. diff --git a/man/systemd.xml b/man/systemd.xml index e8f022f5597..77aade81587 100644 --- a/man/systemd.xml +++ b/man/systemd.xml @@ -1083,7 +1083,7 @@ quiet Turn off status output at boot, much like - systemd.show_status=false would. Note that + systemd.show_status=no would. Note that this option is also read by the kernel itself and disables kernel log output. Passing this option hence turns off the usual output from both the system manager and the kernel. diff --git a/meson.build b/meson.build index 39c427cd26e..30834c86e38 100644 --- a/meson.build +++ b/meson.build @@ -1762,15 +1762,15 @@ if conf.get('ENABLE_LOGIND') == 1 args : pam_systemd.full_path()) endif endif -endif -executable('systemd-user-runtime-dir', - user_runtime_dir_sources, - include_directories : includes, - link_with : [libshared, liblogind_core], - install_rpath : rootlibexecdir, - install : true, - install_dir : rootlibexecdir) + executable('systemd-user-runtime-dir', + user_runtime_dir_sources, + include_directories : includes, + link_with : [libshared], + install_rpath : rootlibexecdir, + install : true, + install_dir : rootlibexecdir) +endif if conf.get('HAVE_PAM') == 1 executable('systemd-user-sessions', diff --git a/src/core/transaction.c b/src/core/transaction.c index 045930838b1..486c6a4a05b 100644 --- a/src/core/transaction.c +++ b/src/core/transaction.c @@ -526,7 +526,9 @@ static int transaction_is_destructive(Transaction *tr, JobMode mode, sd_bus_erro if (j->unit->job && (mode == JOB_FAIL || j->unit->job->irreversible) && job_type_is_conflicting(j->unit->job->type, j->type)) return sd_bus_error_setf(e, BUS_ERROR_TRANSACTION_IS_DESTRUCTIVE, - "Transaction is destructive."); + "Transaction for %s/%s is destructive (%s has '%s' job queued, but '%s' is included in transaction).", + tr->anchor_job->unit->id, job_type_to_string(tr->anchor_job->type), + j->unit->id, job_type_to_string(j->unit->job->type), job_type_to_string(j->type)); } return 0; diff --git a/src/core/unit.c b/src/core/unit.c index 1b9bbf70577..d90456e03c3 100644 --- a/src/core/unit.c +++ b/src/core/unit.c @@ -2000,7 +2000,7 @@ bool unit_is_unneeded(Unit *u) { * restart, then don't clean this one up. */ HASHMAP_FOREACH_KEY(v, other, u->dependencies[deps[j]], i) { - if (u->job) + if (other->job) return false; if (!UNIT_IS_INACTIVE_OR_FAILED(unit_active_state(other))) diff --git a/src/libsystemd/libsystemd.sym b/src/libsystemd/libsystemd.sym index ba682b879af..8d0bebe2adf 100644 --- a/src/libsystemd/libsystemd.sym +++ b/src/libsystemd/libsystemd.sym @@ -577,6 +577,8 @@ global: sd_bus_set_method_call_timeout; sd_bus_get_method_call_timeout; + sd_bus_error_move; + sd_device_ref; sd_device_unref; diff --git a/src/libsystemd/sd-bus/bus-error.c b/src/libsystemd/sd-bus/bus-error.c index 0f79ddc4278..e73f7057e1e 100644 --- a/src/libsystemd/sd-bus/bus-error.c +++ b/src/libsystemd/sd-bus/bus-error.c @@ -308,6 +308,28 @@ finish: return -bus_error_name_to_errno(e->name); } +_public_ int sd_bus_error_move(sd_bus_error *dest, sd_bus_error *e) { + int r; + + if (!sd_bus_error_is_set(e)) { + + if (dest) + *dest = SD_BUS_ERROR_NULL; + + return 0; + } + + r = -bus_error_name_to_errno(e->name); + + if (dest) { + *dest = *e; + *e = SD_BUS_ERROR_NULL; + } else + sd_bus_error_free(e); + + return r; +} + _public_ int sd_bus_error_set_const(sd_bus_error *e, const char *name, const char *message) { if (!name) return 0; diff --git a/src/login/logind-core.c b/src/login/logind-core.c index 87506088a1c..6354dfe6e64 100644 --- a/src/login/logind-core.c +++ b/src/login/logind-core.c @@ -5,6 +5,9 @@ #include #include #include +#if ENABLE_UTMP +#include +#endif #include "sd-device.h" @@ -17,6 +20,7 @@ #include "fd-util.h" #include "logind.h" #include "parse-util.h" +#include "path-util.h" #include "process-util.h" #include "strv.h" #include "terminal-util.h" @@ -29,6 +33,8 @@ void manager_reset_config(Manager *m) { m->reserve_vt = 6; m->remove_ipc = true; m->inhibit_delay_max = 5 * USEC_PER_SEC; + m->user_stop_delay = 10 * USEC_PER_SEC; + m->handle_power_key = HANDLE_POWEROFF; m->handle_suspend_key = HANDLE_SUSPEND; m->handle_hibernate_key = HANDLE_HIBERNATE; @@ -90,15 +96,16 @@ int manager_add_device(Manager *m, const char *sysfs, bool master, Device **_dev int manager_add_seat(Manager *m, const char *id, Seat **_seat) { Seat *s; + int r; assert(m); assert(id); s = hashmap_get(m->seats, id); if (!s) { - s = seat_new(m, id); - if (!s) - return -ENOMEM; + r = seat_new(&s, m, id); + if (r < 0) + return r; } if (_seat) @@ -109,15 +116,16 @@ int manager_add_seat(Manager *m, const char *id, Seat **_seat) { int manager_add_session(Manager *m, const char *id, Session **_session) { Session *s; + int r; assert(m); assert(id); s = hashmap_get(m->sessions, id); if (!s) { - s = session_new(m, id); - if (!s) - return -ENOMEM; + r = session_new(&s, m, id); + if (r < 0) + return r; } if (_session) @@ -126,7 +134,14 @@ int manager_add_session(Manager *m, const char *id, Session **_session) { return 0; } -int manager_add_user(Manager *m, uid_t uid, gid_t gid, const char *name, User **_user) { +int manager_add_user( + Manager *m, + uid_t uid, + gid_t gid, + const char *name, + const char *home, + User **_user) { + User *u; int r; @@ -135,7 +150,7 @@ int manager_add_user(Manager *m, uid_t uid, gid_t gid, const char *name, User ** u = hashmap_get(m->users, UID_TO_PTR(uid)); if (!u) { - r = user_new(&u, m, uid, gid, name); + r = user_new(&u, m, uid, gid, name, home); if (r < 0) return r; } @@ -146,7 +161,12 @@ int manager_add_user(Manager *m, uid_t uid, gid_t gid, const char *name, User ** return 0; } -int manager_add_user_by_name(Manager *m, const char *name, User **_user) { +int manager_add_user_by_name( + Manager *m, + const char *name, + User **_user) { + + const char *home = NULL; uid_t uid; gid_t gid; int r; @@ -154,11 +174,11 @@ int manager_add_user_by_name(Manager *m, const char *name, User **_user) { assert(m); assert(name); - r = get_user_creds(&name, &uid, &gid, NULL, NULL, 0); + r = get_user_creds(&name, &uid, &gid, &home, NULL, 0); if (r < 0) return r; - return manager_add_user(m, uid, gid, name, _user); + return manager_add_user(m, uid, gid, name, home, _user); } int manager_add_user_by_uid(Manager *m, uid_t uid, User **_user) { @@ -171,7 +191,7 @@ int manager_add_user_by_uid(Manager *m, uid_t uid, User **_user) { if (!p) return errno > 0 ? -errno : -ENOENT; - return manager_add_user(m, uid, p->pw_gid, p->pw_name, _user); + return manager_add_user(m, uid, p->pw_gid, p->pw_name, p->pw_dir, _user); } int manager_add_inhibitor(Manager *m, const char* id, Inhibitor **_inhibitor) { @@ -335,13 +355,16 @@ int manager_get_session_by_pid(Manager *m, pid_t pid, Session **ret) { if (!pid_is_valid(pid)) return -EINVAL; - r = cg_pid_get_unit(pid, &unit); - if (r < 0) - goto not_found; + s = hashmap_get(m->sessions_by_leader, PID_TO_PTR(pid)); + if (!s) { + r = cg_pid_get_unit(pid, &unit); + if (r < 0) + goto not_found; - s = hashmap_get(m->session_units, unit); - if (!s) - goto not_found; + s = hashmap_get(m->session_units, unit); + if (!s) + goto not_found; + } if (ret) *ret = s; @@ -677,3 +700,142 @@ bool manager_all_buttons_ignored(Manager *m) { return true; } + +int manager_read_utmp(Manager *m) { +#if ENABLE_UTMP + int r; + + assert(m); + + if (utmpxname(_PATH_UTMPX) < 0) + return log_error_errno(errno, "Failed to set utmp path to " _PATH_UTMPX ": %m"); + + setutxent(); + + for (;;) { + _cleanup_free_ char *t = NULL; + struct utmpx *u; + const char *c; + Session *s; + + errno = 0; + u = getutxent(); + if (!u) { + if (errno != 0) + log_warning_errno(errno, "Failed to read " _PATH_UTMPX ", ignoring: %m"); + r = 0; + break; + } + + if (u->ut_type != USER_PROCESS) + continue; + + if (!pid_is_valid(u->ut_pid)) + continue; + + t = strndup(u->ut_line, sizeof(u->ut_line)); + if (!t) { + r = log_oom(); + break; + } + + c = path_startswith(t, "/dev/"); + if (c) { + r = free_and_strdup(&t, c); + if (r < 0) { + log_oom(); + break; + } + } + + if (isempty(t)) + continue; + + s = hashmap_get(m->sessions_by_leader, PID_TO_PTR(u->ut_pid)); + if (!s) + continue; + + if (s->tty_validity == TTY_FROM_UTMP && !streq_ptr(s->tty, t)) { + /* This may happen on multiplexed SSH connection (i.e. 'SSH connection sharing'). In + * this case PAM and utmp sessions don't match. In such a case let's invalidate the TTY + * information and never acquire it again. */ + + s->tty = mfree(s->tty); + s->tty_validity = TTY_UTMP_INCONSISTENT; + log_debug("Session '%s' has inconsistent TTY information, dropping TTY information.", s->id); + continue; + } + + /* Never override what we figured out once */ + if (s->tty || s->tty_validity >= 0) + continue; + + s->tty = TAKE_PTR(t); + s->tty_validity = TTY_FROM_UTMP; + log_debug("Acquired TTY information '%s' from utmp for session '%s'.", s->tty, s->id); + } + + endutxent(); + return r; +#else + return 0 +#endif +} + +#if ENABLE_UTMP +static int manager_dispatch_utmp(sd_event_source *s, const struct inotify_event *event, void *userdata) { + Manager *m = userdata; + + assert(m); + + /* If there's indication the file itself might have been removed or became otherwise unavailable, then let's + * reestablish the watch on whatever there's now. */ + if ((event->mask & (IN_ATTRIB|IN_DELETE_SELF|IN_MOVE_SELF|IN_Q_OVERFLOW|IN_UNMOUNT)) != 0) + manager_connect_utmp(m); + + (void) manager_read_utmp(m); + return 0; +} +#endif + +void manager_connect_utmp(Manager *m) { +#if ENABLE_UTMP + sd_event_source *s = NULL; + int r; + + assert(m); + + /* Watch utmp for changes via inotify. We do this to deal with tools such as ssh, which will register the PAM + * session early, and acquire a TTY only much later for the connection. Thus during PAM the TTY won't be known + * yet. ssh will register itself with utmp when it finally acquired the TTY. Hence, let's make use of this, and + * watch utmp for the TTY asynchronously. We use the PAM session's leader PID as key, to find the right entry. + * + * Yes, relying on utmp is pretty ugly, but it's good enough for informational purposes, as well as idle + * detection (which, for tty sessions, relies on the TTY used) */ + + r = sd_event_add_inotify(m->event, &s, _PATH_UTMPX, IN_MODIFY|IN_MOVE_SELF|IN_DELETE_SELF|IN_ATTRIB, manager_dispatch_utmp, m); + if (r < 0) + log_full_errno(r == -ENOENT ? LOG_DEBUG: LOG_WARNING, r, "Failed to create inotify watch on " _PATH_UTMPX ", ignoring: %m"); + else { + r = sd_event_source_set_priority(s, SD_EVENT_PRIORITY_IDLE); + if (r < 0) + log_warning_errno(r, "Failed to adjust utmp event source priority, ignoring: %m"); + + (void) sd_event_source_set_description(s, "utmp"); + } + + sd_event_source_unref(m->utmp_event_source); + m->utmp_event_source = s; +#endif +} + +void manager_reconnect_utmp(Manager *m) { +#if ENABLE_UTMP + assert(m); + + if (m->utmp_event_source) + return; + + manager_connect_utmp(m); +#endif +} diff --git a/src/login/logind-dbus.c b/src/login/logind-dbus.c index ed6da4445ba..032504e63a9 100644 --- a/src/login/logind-dbus.c +++ b/src/login/logind-dbus.c @@ -773,6 +773,9 @@ static int method_create_session(sd_bus_message *message, void *userdata, sd_bus } while (hashmap_get(m->sessions, id)); } + /* If we are not watching utmp aleady, try again */ + manager_reconnect_utmp(m); + r = manager_add_user_by_uid(m, uid, &user); if (r < 0) goto fail; @@ -782,9 +785,8 @@ static int method_create_session(sd_bus_message *message, void *userdata, sd_bus goto fail; session_set_user(session, user); + session_set_leader(session, leader); - session->leader = leader; - session->audit_id = audit_id; session->type = t; session->class = c; session->remote = remote; @@ -796,6 +798,8 @@ static int method_create_session(sd_bus_message *message, void *userdata, sd_bus r = -ENOMEM; goto fail; } + + session->tty_validity = TTY_FROM_PAM; } if (!isempty(display)) { @@ -846,9 +850,9 @@ static int method_create_session(sd_bus_message *message, void *userdata, sd_bus r = sd_bus_message_enter_container(message, 'a', "(sv)"); if (r < 0) - return r; + goto fail; - r = session_start(session, message); + r = session_start(session, message, error); if (r < 0) goto fail; @@ -2648,6 +2652,7 @@ const sd_bus_vtable manager_vtable[] = { SD_BUS_PROPERTY("BlockInhibited", "s", property_get_inhibited, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), SD_BUS_PROPERTY("DelayInhibited", "s", property_get_inhibited, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), SD_BUS_PROPERTY("InhibitDelayMaxUSec", "t", NULL, offsetof(Manager, inhibit_delay_max), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("UserStopDelayUSec", "t", NULL, offsetof(Manager, user_stop_delay), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("HandlePowerKey", "s", property_get_handle_action, offsetof(Manager, handle_power_key), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("HandleSuspendKey", "s", property_get_handle_action, offsetof(Manager, handle_suspend_key), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("HandleHibernateKey", "s", property_get_handle_action, offsetof(Manager, handle_hibernate_key), SD_BUS_VTABLE_PROPERTY_CONST), @@ -2728,24 +2733,20 @@ const sd_bus_vtable manager_vtable[] = { }; static int session_jobs_reply(Session *s, const char *unit, const char *result) { - int r = 0; - assert(s); assert(unit); if (!s->started) - return r; + return 0; - if (streq(result, "done")) - r = session_send_create_reply(s, NULL); - else { + if (result && !streq(result, "done")) { _cleanup_(sd_bus_error_free) sd_bus_error e = SD_BUS_ERROR_NULL; - sd_bus_error_setf(&e, BUS_ERROR_JOB_FAILED, "Start job for unit %s failed with '%s'", unit, result); - r = session_send_create_reply(s, &e); + sd_bus_error_setf(&e, BUS_ERROR_JOB_FAILED, "Start job for unit '%s' failed with '%s'", unit, result); + return session_send_create_reply(s, &e); } - return r; + return session_send_create_reply(s, NULL); } int match_job_removed(sd_bus_message *message, void *userdata, sd_bus_error *error) { @@ -2778,30 +2779,29 @@ int match_job_removed(sd_bus_message *message, void *userdata, sd_bus_error *err } session = hashmap_get(m->session_units, unit); - if (session && streq_ptr(path, session->scope_job)) { - session->scope_job = mfree(session->scope_job); - session_jobs_reply(session, unit, result); + if (session) { + if (streq_ptr(path, session->scope_job)) { + session->scope_job = mfree(session->scope_job); + (void) session_jobs_reply(session, unit, result); + + session_save(session); + user_save(session->user); + } - session_save(session); - user_save(session->user); session_add_to_gc_queue(session); } user = hashmap_get(m->user_units, unit); - if (user && - (streq_ptr(path, user->service_job) || - streq_ptr(path, user->slice_job))) { - - if (streq_ptr(path, user->service_job)) + if (user) { + if (streq_ptr(path, user->service_job)) { user->service_job = mfree(user->service_job); - if (streq_ptr(path, user->slice_job)) - user->slice_job = mfree(user->slice_job); + LIST_FOREACH(sessions_by_user, session, user->sessions) + (void) session_jobs_reply(session, unit, NULL /* don't propagate user service failures to the client */); - LIST_FOREACH(sessions_by_user, session, user->sessions) - session_jobs_reply(session, unit, result); + user_save(user); + } - user_save(user); user_add_to_gc_queue(user); } @@ -2933,13 +2933,15 @@ int manager_start_scope( pid_t pid, const char *slice, const char *description, - const char *after, - const char *after2, + char **wants, + char **after, + const char *requires_mounts_for, sd_bus_message *more_properties, sd_bus_error *error, char **job) { _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL; + char **i; int r; assert(manager); @@ -2977,14 +2979,20 @@ int manager_start_scope( return r; } - if (!isempty(after)) { - r = sd_bus_message_append(m, "(sv)", "After", "as", 1, after); + STRV_FOREACH(i, wants) { + r = sd_bus_message_append(m, "(sv)", "Wants", "as", 1, *i); if (r < 0) return r; } - if (!isempty(after2)) { - r = sd_bus_message_append(m, "(sv)", "After", "as", 1, after2); + STRV_FOREACH(i, after) { + r = sd_bus_message_append(m, "(sv)", "After", "as", 1, *i); + if (r < 0) + return r; + } + + if (!empty_or_root(requires_mounts_for)) { + r = sd_bus_message_append(m, "(sv)", "RequiresMountsFor", "as", 1, requires_mounts_for); if (r < 0) return r; } @@ -3081,7 +3089,8 @@ int manager_stop_unit(Manager *manager, const char *unit, sd_bus_error *error, c return strdup_job(reply, job); } -int manager_abandon_scope(Manager *manager, const char *scope, sd_bus_error *error) { +int manager_abandon_scope(Manager *manager, const char *scope, sd_bus_error *ret_error) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; _cleanup_free_ char *path = NULL; int r; @@ -3098,17 +3107,16 @@ int manager_abandon_scope(Manager *manager, const char *scope, sd_bus_error *err path, "org.freedesktop.systemd1.Scope", "Abandon", - error, + &error, NULL, NULL); if (r < 0) { - if (sd_bus_error_has_name(error, BUS_ERROR_NO_SUCH_UNIT) || - sd_bus_error_has_name(error, BUS_ERROR_LOAD_FAILED) || - sd_bus_error_has_name(error, BUS_ERROR_SCOPE_NOT_RUNNING)) { - sd_bus_error_free(error); + if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_UNIT) || + sd_bus_error_has_name(&error, BUS_ERROR_LOAD_FAILED) || + sd_bus_error_has_name(&error, BUS_ERROR_SCOPE_NOT_RUNNING)) return 0; - } + sd_bus_error_move(ret_error, &error); return r; } @@ -3130,7 +3138,7 @@ int manager_kill_unit(Manager *manager, const char *unit, KillWho who, int signo "ssi", unit, who == KILL_LEADER ? "main" : "all", signo); } -int manager_unit_is_active(Manager *manager, const char *unit) { +int manager_unit_is_active(Manager *manager, const char *unit, sd_bus_error *ret_error) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; _cleanup_free_ char *path = NULL; @@ -3166,17 +3174,18 @@ int manager_unit_is_active(Manager *manager, const char *unit) { sd_bus_error_has_name(&error, BUS_ERROR_LOAD_FAILED)) return false; + sd_bus_error_move(ret_error, &error); return r; } r = sd_bus_message_read(reply, "s", &state); if (r < 0) - return -EINVAL; + return r; - return !streq(state, "inactive") && !streq(state, "failed"); + return !STR_IN_SET(state, "inactive", "failed"); } -int manager_job_is_active(Manager *manager, const char *path) { +int manager_job_is_active(Manager *manager, const char *path, sd_bus_error *ret_error) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; int r; @@ -3201,6 +3210,7 @@ int manager_job_is_active(Manager *manager, const char *path) { if (sd_bus_error_has_name(&error, SD_BUS_ERROR_UNKNOWN_OBJECT)) return false; + sd_bus_error_move(ret_error, &error); return r; } diff --git a/src/login/logind-gperf.gperf b/src/login/logind-gperf.gperf index c85339dcd35..8829ce7d85a 100644 --- a/src/login/logind-gperf.gperf +++ b/src/login/logind-gperf.gperf @@ -23,6 +23,7 @@ Login.KillUserProcesses, config_parse_bool, 0, offse Login.KillOnlyUsers, config_parse_strv, 0, offsetof(Manager, kill_only_users) Login.KillExcludeUsers, config_parse_strv, 0, offsetof(Manager, kill_exclude_users) Login.InhibitDelayMaxSec, config_parse_sec, 0, offsetof(Manager, inhibit_delay_max) +Login.UserStopDelaySec, config_parse_sec, 0, offsetof(Manager, user_stop_delay) Login.HandlePowerKey, config_parse_handle_action, 0, offsetof(Manager, handle_power_key) Login.HandleSuspendKey, config_parse_handle_action, 0, offsetof(Manager, handle_suspend_key) Login.HandleHibernateKey, config_parse_handle_action, 0, offsetof(Manager, handle_hibernate_key) diff --git a/src/login/logind-seat.c b/src/login/logind-seat.c index f24fe96841e..cc337992308 100644 --- a/src/login/logind-seat.c +++ b/src/login/logind-seat.c @@ -21,33 +21,42 @@ #include "terminal-util.h" #include "util.h" -Seat *seat_new(Manager *m, const char *id) { - Seat *s; +int seat_new(Seat** ret, Manager *m, const char *id) { + _cleanup_(seat_freep) Seat *s = NULL; + int r; + assert(ret); assert(m); assert(id); - s = new0(Seat, 1); + if (!seat_name_is_valid(id)) + return -EINVAL; + + s = new(Seat, 1); if (!s) - return NULL; + return -ENOMEM; + + *s = (Seat) { + .manager = m, + }; s->state_file = strappend("/run/systemd/seats/", id); if (!s->state_file) - return mfree(s); + return -ENOMEM; s->id = basename(s->state_file); - s->manager = m; - if (hashmap_put(m->seats, s->id, s) < 0) { - free(s->state_file); - return mfree(s); - } + r = hashmap_put(m->seats, s->id, s); + if (r < 0) + return r; - return s; + *ret = TAKE_PTR(s); + return 0; } -void seat_free(Seat *s) { - assert(s); +Seat* seat_free(Seat *s) { + if (!s) + return NULL; if (s->in_gc_queue) LIST_REMOVE(gc_queue, s->manager->seat_gc_queue, s); @@ -64,7 +73,8 @@ void seat_free(Seat *s) { free(s->positions); free(s->state_file); - free(s); + + return mfree(s); } int seat_save(Seat *s) { @@ -165,7 +175,7 @@ static int vt_allocate(unsigned int vtnr) { xsprintf(p, "/dev/tty%u", vtnr); fd = open_terminal(p, O_RDWR|O_NOCTTY|O_CLOEXEC); if (fd < 0) - return -errno; + return fd; return 0; } @@ -189,10 +199,8 @@ int seat_preallocate_vts(Seat *s) { int q; q = vt_allocate(i); - if (q < 0) { - log_error_errno(q, "Failed to preallocate VT %u: %m", i); - r = q; - } + if (q < 0) + r = log_error_errno(q, "Failed to preallocate VT %u: %m", i); } return r; @@ -209,9 +217,9 @@ int seat_apply_acls(Seat *s, Session *old_active) { !!s->active, s->active ? s->active->user->uid : 0); if (r < 0) - log_error_errno(r, "Failed to apply ACLs: %m"); + return log_error_errno(r, "Failed to apply ACLs: %m"); - return r; + return 0; } int seat_set_active(Seat *s, Session *session) { @@ -231,7 +239,7 @@ int seat_set_active(Seat *s, Session *session) { session_send_changed(old_active, "Active", NULL); } - seat_apply_acls(s, old_active); + (void) seat_apply_acls(s, old_active); if (session && session->started) { session_send_changed(session, "Active", NULL); @@ -411,7 +419,7 @@ int seat_start(Seat *s) { } int seat_stop(Seat *s, bool force) { - int r = 0; + int r; assert(s); @@ -421,9 +429,9 @@ int seat_stop(Seat *s, bool force) { "SEAT_ID=%s", s->id, LOG_MESSAGE("Removed seat %s.", s->id)); - seat_stop_sessions(s, force); + r = seat_stop_sessions(s, force); - unlink(s->state_file); + (void) unlink(s->state_file); seat_add_to_gc_queue(s); if (s->started) diff --git a/src/login/logind-seat.h b/src/login/logind-seat.h index 70878bbe526..51cd468e26f 100644 --- a/src/login/logind-seat.h +++ b/src/login/logind-seat.h @@ -27,8 +27,10 @@ struct Seat { LIST_FIELDS(Seat, gc_queue); }; -Seat *seat_new(Manager *m, const char *id); -void seat_free(Seat *s); +int seat_new(Seat **ret, Manager *m, const char *id); +Seat* seat_free(Seat *s); + +DEFINE_TRIVIAL_CLEANUP_FUNC(Seat *, seat_free); int seat_save(Seat *s); int seat_load(Seat *s); diff --git a/src/login/logind-session-dbus.c b/src/login/logind-session-dbus.c index 25c4981dc00..5b09a07ffac 100644 --- a/src/login/logind-session-dbus.c +++ b/src/login/logind-session-dbus.c @@ -689,6 +689,15 @@ int session_send_lock_all(Manager *m, bool lock) { return r; } +static bool session_ready(Session *s) { + assert(s); + + /* Returns true when the session is ready, i.e. all jobs we enqueued for it are done (regardless if successful or not) */ + + return !s->scope_job && + !s->user->service_job; +} + int session_send_create_reply(Session *s, sd_bus_error *error) { _cleanup_(sd_bus_message_unrefp) sd_bus_message *c = NULL; _cleanup_close_ int fifo_fd = -1; @@ -696,19 +705,16 @@ int session_send_create_reply(Session *s, sd_bus_error *error) { assert(s); - /* This is called after the session scope and the user service - * were successfully created, and finishes where + /* This is called after the session scope and the user service were successfully created, and finishes where * bus_manager_create_session() left off. */ if (!s->create_message) return 0; - if (!sd_bus_error_is_set(error) && (s->scope_job || s->user->service_job)) + if (!sd_bus_error_is_set(error) && !session_ready(s)) return 0; - c = s->create_message; - s->create_message = NULL; - + c = TAKE_PTR(s->create_message); if (error) return sd_bus_reply_method_error(c, error); @@ -716,8 +722,7 @@ int session_send_create_reply(Session *s, sd_bus_error *error) { if (fifo_fd < 0) return fifo_fd; - /* Update the session state file before we notify the client - * about the result. */ + /* Update the session state file before we notify the client about the result. */ session_save(s); p = session_bus_path(s); diff --git a/src/login/logind-session.c b/src/login/logind-session.c index 226cc49b010..a1c0a08fda8 100644 --- a/src/login/logind-session.c +++ b/src/login/logind-session.c @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -24,57 +25,63 @@ #include "mkdir.h" #include "parse-util.h" #include "path-util.h" +#include "process-util.h" #include "string-table.h" +#include "strv.h" #include "terminal-util.h" #include "user-util.h" #include "util.h" -#include "process-util.h" #define RELEASE_USEC (20*USEC_PER_SEC) static void session_remove_fifo(Session *s); -Session* session_new(Manager *m, const char *id) { - Session *s; +int session_new(Session **ret, Manager *m, const char *id) { + _cleanup_(session_freep) Session *s = NULL; + int r; + assert(ret); assert(m); assert(id); - assert(session_id_valid(id)); - s = new0(Session, 1); + if (!session_id_valid(id)) + return -EINVAL; + + s = new(Session, 1); if (!s) - return NULL; + return -ENOMEM; + + *s = (Session) { + .manager = m, + .fifo_fd = -1, + .vtfd = -1, + .audit_id = AUDIT_SESSION_INVALID, + .tty_validity = _TTY_VALIDITY_INVALID, + }; s->state_file = strappend("/run/systemd/sessions/", id); if (!s->state_file) - return mfree(s); - - s->devices = hashmap_new(&devt_hash_ops); - if (!s->devices) { - free(s->state_file); - return mfree(s); - } + return -ENOMEM; s->id = basename(s->state_file); - if (hashmap_put(m->sessions, s->id, s) < 0) { - hashmap_free(s->devices); - free(s->state_file); - return mfree(s); - } + s->devices = hashmap_new(&devt_hash_ops); + if (!s->devices) + return -ENOMEM; - s->manager = m; - s->fifo_fd = -1; - s->vtfd = -1; - s->audit_id = AUDIT_SESSION_INVALID; + r = hashmap_put(m->sessions, s->id, s); + if (r < 0) + return r; - return s; + *ret = TAKE_PTR(s); + return 0; } -void session_free(Session *s) { +Session* session_free(Session *s) { SessionDevice *sd; - assert(s); + if (!s) + return NULL; if (s->in_gc_queue) LIST_REMOVE(gc_queue, s->manager->session_gc_queue, s); @@ -95,6 +102,8 @@ void session_free(Session *s) { if (s->user->display == s) s->user->display = NULL; + + user_update_last_session_timer(s->user); } if (s->seat) { @@ -112,6 +121,9 @@ void session_free(Session *s) { free(s->scope); } + if (pid_is_valid(s->leader)) + (void) hashmap_remove_value(s->manager->sessions_by_leader, PID_TO_PTR(s->leader), s); + free(s->scope_job); sd_bus_message_unref(s->create_message); @@ -126,7 +138,8 @@ void session_free(Session *s) { hashmap_remove(s->manager->sessions, s->id); free(s->state_file); - free(s); + + return mfree(s); } void session_set_user(Session *s, User *u) { @@ -135,6 +148,32 @@ void session_set_user(Session *s, User *u) { s->user = u; LIST_PREPEND(sessions_by_user, u->sessions, s); + + user_update_last_session_timer(u); +} + +int session_set_leader(Session *s, pid_t pid) { + int r; + + assert(s); + + if (!pid_is_valid(pid)) + return -EINVAL; + + if (s->leader == pid) + return 0; + + r = hashmap_put(s->manager->sessions_by_leader, PID_TO_PTR(pid), s); + if (r < 0) + return r; + + if (pid_is_valid(s->leader)) + (void) hashmap_remove_value(s->manager->sessions_by_leader, PID_TO_PTR(s->leader), s); + + s->leader = pid; + (void) audit_session_from_pid(pid, &s->audit_id); + + return 1; } static void session_save_devices(Session *s, FILE *f) { @@ -170,20 +209,21 @@ int session_save(Session *s) { if (r < 0) goto fail; - assert(s->user); - - fchmod(fileno(f), 0644); + (void) __fsetlocking(f, FSETLOCKING_BYCALLER); + (void) fchmod(fileno(f), 0644); fprintf(f, "# This is private data. Do not parse.\n" "UID="UID_FMT"\n" "USER=%s\n" "ACTIVE=%i\n" + "IS_DISPLAY=%i\n" "STATE=%s\n" "REMOTE=%i\n", s->user->uid, s->user->name, session_is_active(s), + s->user->display == s, session_state_to_string(session_get_state(s)), s->remote); @@ -207,6 +247,9 @@ int session_save(Session *s) { if (s->tty) fprintf(f, "TTY=%s\n", s->tty); + if (s->tty_validity >= 0) + fprintf(f, "TTY_VALIDITY=%s\n", tty_validity_to_string(s->tty_validity)); + if (s->display) fprintf(f, "DISPLAY=%s\n", s->display); @@ -343,6 +386,7 @@ static int session_load_devices(Session *s, const char *devices) { int session_load(Session *s) { _cleanup_free_ char *remote = NULL, *seat = NULL, + *tty_validity = NULL, *vtnr = NULL, *state = NULL, *position = NULL, @@ -354,7 +398,8 @@ int session_load(Session *s) { *monotonic = NULL, *controller = NULL, *active = NULL, - *devices = NULL; + *devices = NULL, + *is_display = NULL; int k, r; @@ -367,6 +412,7 @@ int session_load(Session *s) { "FIFO", &s->fifo_path, "SEAT", &seat, "TTY", &s->tty, + "TTY_VALIDITY", &tty_validity, "DISPLAY", &s->display, "REMOTE_HOST", &s->remote_host, "REMOTE_USER", &s->remote_user, @@ -384,6 +430,7 @@ int session_load(Session *s) { "CONTROLLER", &controller, "ACTIVE", &active, "DEVICES", &devices, + "IS_DISPLAY", &is_display, NULL); if (r < 0) @@ -442,9 +489,27 @@ int session_load(Session *s) { seat_claim_position(s->seat, s, npos); } + if (tty_validity) { + TTYValidity v; + + v = tty_validity_from_string(tty_validity); + if (v < 0) + log_debug("Failed to parse TTY validity: %s", tty_validity); + else + s->tty_validity = v; + } + if (leader) { - if (parse_pid(leader, &s->leader) >= 0) - (void) audit_session_from_pid(s->leader, &s->audit_id); + pid_t pid; + + r = parse_pid(leader, &pid); + if (r < 0) + log_debug_errno(r, "Failed to parse leader PID of session: %s", leader); + else { + r = session_set_leader(s, pid); + if (r < 0) + log_warning_errno(r, "Failed to set session leader PID, ignoring: %m"); + } } if (type) { @@ -491,6 +556,18 @@ int session_load(Session *s) { s->was_active = k; } + if (is_display) { + /* Note that when enumerating users are loaded before sessions, hence the display session to use is + * something we have to store along with the session and not the user, as in that case we couldn't + * apply it at the time we load the user. */ + + k = parse_boolean(is_display); + if (k < 0) + log_warning_errno(k, "Failed to parse IS_DISPLAY session property: %m"); + else if (k > 0) + s->user->display = s; + } + if (controller) { if (bus_name_has_owner(s->manager->bus, controller, NULL) > 0) { session_set_controller(s, controller, false, false); @@ -516,7 +593,7 @@ int session_activate(Session *s) { /* on seats with VTs, we let VTs manage session-switching */ if (seat_has_vts(s->seat)) { - if (!s->vtnr) + if (s->vtnr == 0) return -EOPNOTSUPP; return chvt(s->vtnr); @@ -539,18 +616,18 @@ int session_activate(Session *s) { return 0; } -static int session_start_scope(Session *s, sd_bus_message *properties) { +static int session_start_scope(Session *s, sd_bus_message *properties, sd_bus_error *error) { int r; assert(s); assert(s->user); if (!s->scope) { - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; _cleanup_free_ char *scope = NULL; - char *job = NULL; const char *description; + s->scope_job = mfree(s->scope_job); + scope = strjoin("session-", s->id, ".scope"); if (!scope) return log_oom(); @@ -563,17 +640,16 @@ static int session_start_scope(Session *s, sd_bus_message *properties) { s->leader, s->user->slice, description, - "systemd-logind.service", - "systemd-user-sessions.service", + STRV_MAKE(s->user->runtime_dir_service, s->user->service), /* These two have StopWhenUnneeded= set, hence add a dep towards them */ + STRV_MAKE("systemd-logind.service", "systemd-user-sessions.service", s->user->runtime_dir_service, s->user->service), /* And order us after some more */ + s->user->home, properties, - &error, - &job); + error, + &s->scope_job); if (r < 0) - return log_error_errno(r, "Failed to start session scope %s: %s", scope, bus_error_message(&error, r)); - + return log_error_errno(r, "Failed to start session scope %s: %s", scope, bus_error_message(error, r)); s->scope = TAKE_PTR(scope); - free_and_replace(s->scope_job, job); } if (s->scope) @@ -582,7 +658,7 @@ static int session_start_scope(Session *s, sd_bus_message *properties) { return 0; } -int session_start(Session *s, sd_bus_message *properties) { +int session_start(Session *s, sd_bus_message *properties, sd_bus_error *error) { int r; assert(s); @@ -590,6 +666,9 @@ int session_start(Session *s, sd_bus_message *properties) { if (!s->user) return -ESTALE; + if (s->stopping) + return -EINVAL; + if (s->started) return 0; @@ -597,8 +676,7 @@ int session_start(Session *s, sd_bus_message *properties) { if (r < 0) return r; - /* Create cgroup */ - r = session_start_scope(s, properties); + r = session_start_scope(s, properties, error); if (r < 0) return r; @@ -649,21 +727,24 @@ static int session_stop_scope(Session *s, bool force) { * that is left in the scope is "left-over". Informing systemd about this has the benefit that it will log * when killing any processes left after this point. */ r = manager_abandon_scope(s->manager, s->scope, &error); - if (r < 0) + if (r < 0) { log_warning_errno(r, "Failed to abandon session scope, ignoring: %s", bus_error_message(&error, r)); + sd_bus_error_free(&error); + } + + s->scope_job = mfree(s->scope_job); /* Optionally, let's kill everything that's left now. */ if (force || manager_shall_kill(s->manager, s->user->name)) { - char *job = NULL; - r = manager_stop_unit(s->manager, s->scope, &error, &job); - if (r < 0) - return log_error_errno(r, "Failed to stop session scope: %s", bus_error_message(&error, r)); + r = manager_stop_unit(s->manager, s->scope, &error, &s->scope_job); + if (r < 0) { + if (force) + return log_error_errno(r, "Failed to stop session scope: %s", bus_error_message(&error, r)); - free(s->scope_job); - s->scope_job = job; + log_warning_errno(r, "Failed to stop session scope, ignoring: %s", bus_error_message(&error, r)); + } } else { - s->scope_job = mfree(s->scope_job); /* With no killing, this session is allowed to persist in "closing" state indefinitely. * Therefore session stop and session removal may be two distinct events. @@ -683,8 +764,17 @@ int session_stop(Session *s, bool force) { assert(s); + /* This is called whenever we begin with tearing down a session record. It's called in four cases: explicit API + * request via the bus (either directly for the session object or for the seat or user object this session + * belongs to; 'force' is true), or due to automatic GC (i.e. scope vanished; 'force' is false), or because the + * session FIFO saw an EOF ('force' is false), or because the release timer hit ('force' is false). */ + if (!s->user) return -ESTALE; + if (!s->started) + return 0; + if (s->stopping) + return 0; s->timer_event_source = sd_event_source_unref(s->timer_event_source); @@ -776,7 +866,7 @@ int session_release(Session *s) { return sd_event_add_time(s->manager->event, &s->timer_event_source, CLOCK_MONOTONIC, - now(CLOCK_MONOTONIC) + RELEASE_USEC, 0, + usec_add(now(CLOCK_MONOTONIC), RELEASE_USEC), 0, release_timeout_callback, s); } @@ -855,7 +945,7 @@ int session_get_idle_hint(Session *s, dual_timestamp *t) { /* For sessions with a leader but no explicitly configured * tty, let's check the controlling tty of the leader */ - if (s->leader > 0) { + if (pid_is_valid(s->leader)) { r = get_process_ctty_atime(s->leader, &atime); if (r >= 0) goto found_atime; @@ -939,7 +1029,8 @@ int session_create_fifo(Session *s) { if (r < 0) return r; - if (asprintf(&s->fifo_path, "/run/systemd/sessions/%s.ref", s->id) < 0) + s->fifo_path = strjoin("/run/systemd/sessions/", s->id, ".ref"); + if (!s->fifo_path) return -ENOMEM; if (mkfifo(s->fifo_path, 0600) < 0 && errno != EEXIST) @@ -951,7 +1042,6 @@ int session_create_fifo(Session *s) { s->fifo_fd = open(s->fifo_path, O_RDONLY|O_CLOEXEC|O_NONBLOCK); if (s->fifo_fd < 0) return -errno; - } if (!s->fifo_event_source) { @@ -981,12 +1071,14 @@ static void session_remove_fifo(Session *s) { s->fifo_fd = safe_close(s->fifo_fd); if (s->fifo_path) { - unlink(s->fifo_path); + (void) unlink(s->fifo_path); s->fifo_path = mfree(s->fifo_path); } } bool session_may_gc(Session *s, bool drop_not_started) { + int r; + assert(s); if (drop_not_started && !s->started) @@ -1000,11 +1092,25 @@ bool session_may_gc(Session *s, bool drop_not_started) { return false; } - if (s->scope_job && manager_job_is_active(s->manager, s->scope_job)) - return false; + if (s->scope_job) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - if (s->scope && manager_unit_is_active(s->manager, s->scope)) - return false; + r = manager_job_is_active(s->manager, s->scope_job, &error); + if (r < 0) + log_debug_errno(r, "Failed to determine whether job '%s' is pending, ignoring: %s", s->scope_job, bus_error_message(&error, r)); + if (r != 0) + return false; + } + + if (s->scope) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + + r = manager_unit_is_active(s->manager, s->scope, &error); + if (r < 0) + log_debug_errno(r, "Failed to determine whether unit '%s' is active, ignoring: %s", s->scope, bus_error_message(&error, r)); + if (r != 0) + return false; + } return true; } @@ -1309,3 +1415,11 @@ static const char* const kill_who_table[_KILL_WHO_MAX] = { }; DEFINE_STRING_TABLE_LOOKUP(kill_who, KillWho); + +static const char* const tty_validity_table[_TTY_VALIDITY_MAX] = { + [TTY_FROM_PAM] = "from-pam", + [TTY_FROM_UTMP] = "from-utmp", + [TTY_UTMP_INCONSISTENT] = "utmp-inconsistent", +}; + +DEFINE_STRING_TABLE_LOOKUP(tty_validity, TTYValidity); diff --git a/src/login/logind-session.h b/src/login/logind-session.h index 29ca399dafa..4f9c967dd93 100644 --- a/src/login/logind-session.h +++ b/src/login/logind-session.h @@ -46,6 +46,14 @@ enum KillWho { _KILL_WHO_INVALID = -1 }; +typedef enum TTYValidity { + TTY_FROM_PAM, + TTY_FROM_UTMP, + TTY_UTMP_INCONSISTENT, /* may happen on ssh sessions with multiplexed TTYs */ + _TTY_VALIDITY_MAX, + _TTY_VALIDITY_INVALID = -1, +} TTYValidity; + struct Session { Manager *manager; @@ -60,8 +68,9 @@ struct Session { dual_timestamp timestamp; - char *tty; char *display; + char *tty; + TTYValidity tty_validity; bool remote; char *remote_user; @@ -97,6 +106,7 @@ struct Session { sd_bus_message *create_message; + /* Set up when a client requested to release the session via the bus */ sd_event_source *timer_event_source; char *controller; @@ -109,9 +119,13 @@ struct Session { LIST_FIELDS(Session, gc_queue); }; -Session *session_new(Manager *m, const char *id); -void session_free(Session *s); +int session_new(Session **ret, Manager *m, const char *id); +Session* session_free(Session *s); + +DEFINE_TRIVIAL_CLEANUP_FUNC(Session *, session_free); + void session_set_user(Session *s, User *u); +int session_set_leader(Session *s, pid_t pid); bool session_may_gc(Session *s, bool drop_not_started); void session_add_to_gc_queue(Session *s); int session_activate(Session *s); @@ -121,7 +135,7 @@ void session_set_idle_hint(Session *s, bool b); int session_get_locked_hint(Session *s); void session_set_locked_hint(Session *s, bool b); int session_create_fifo(Session *s); -int session_start(Session *s, sd_bus_message *properties); +int session_start(Session *s, sd_bus_message *properties, sd_bus_error *error); int session_stop(Session *s, bool force); int session_finalize(Session *s); int session_release(Session *s); @@ -155,6 +169,9 @@ SessionClass session_class_from_string(const char *s) _pure_; const char *kill_who_to_string(KillWho k) _const_; KillWho kill_who_from_string(const char *s) _pure_; +const char* tty_validity_to_string(TTYValidity t) _const_; +TTYValidity tty_validity_from_string(const char *s) _pure_; + int session_prepare_vt(Session *s); void session_restore_vt(Session *s); void session_leave_vt(Session *s); diff --git a/src/login/logind-user-dbus.c b/src/login/logind-user-dbus.c index c662a26b9fa..9620fb0cfc2 100644 --- a/src/login/logind-user-dbus.c +++ b/src/login/logind-user-dbus.c @@ -109,7 +109,7 @@ static int property_get_idle_since_hint( assert(reply); assert(u); - user_get_idle_hint(u, &t); + (void) user_get_idle_hint(u, &t); k = streq(property, "IdleSinceHint") ? t.realtime : t.monotonic; return sd_bus_message_append(reply, "t", k); diff --git a/src/login/logind-user.c b/src/login/logind-user.c index f2664c323ea..16a83ae5d54 100644 --- a/src/login/logind-user.c +++ b/src/login/logind-user.c @@ -26,38 +26,52 @@ #include "special.h" #include "stdio-util.h" #include "string-table.h" +#include "strv.h" #include "unit-name.h" #include "user-util.h" #include "util.h" -int user_new(User **out, Manager *m, uid_t uid, gid_t gid, const char *name) { +int user_new(User **ret, + Manager *m, + uid_t uid, + gid_t gid, + const char *name, + const char *home) { + _cleanup_(user_freep) User *u = NULL; char lu[DECIMAL_STR_MAX(uid_t) + 1]; int r; - assert(out); + assert(ret); assert(m); assert(name); - u = new0(User, 1); + u = new(User, 1); if (!u) return -ENOMEM; - u->manager = m; - u->uid = uid; - u->gid = gid; - xsprintf(lu, UID_FMT, uid); + *u = (User) { + .manager = m, + .uid = uid, + .gid = gid, + .last_session_timestamp = USEC_INFINITY, + }; u->name = strdup(name); if (!u->name) return -ENOMEM; + u->home = strdup(home); + if (!u->home) + return -ENOMEM; + if (asprintf(&u->state_file, "/run/systemd/users/"UID_FMT, uid) < 0) return -ENOMEM; if (asprintf(&u->runtime_path, "/run/user/"UID_FMT, uid) < 0) return -ENOMEM; + xsprintf(lu, UID_FMT, uid); r = slice_build_subslice(SPECIAL_USER_SLICE, lu, &u->slice); if (r < 0) return r; @@ -66,6 +80,10 @@ int user_new(User **out, Manager *m, uid_t uid, gid_t gid, const char *name) { if (r < 0) return r; + r = unit_name_build("user-runtime-dir", lu, ".service", &u->runtime_dir_service); + if (r < 0) + return r; + r = hashmap_put(m->users, UID_TO_PTR(uid), u); if (r < 0) return r; @@ -78,8 +96,11 @@ int user_new(User **out, Manager *m, uid_t uid, gid_t gid, const char *name) { if (r < 0) return r; - *out = TAKE_PTR(u); + r = hashmap_put(m->user_units, u->runtime_dir_service, u); + if (r < 0) + return r; + *ret = TAKE_PTR(u); return 0; } @@ -96,19 +117,25 @@ User *user_free(User *u) { if (u->service) hashmap_remove_value(u->manager->user_units, u->service, u); + if (u->runtime_dir_service) + hashmap_remove_value(u->manager->user_units, u->runtime_dir_service, u); + if (u->slice) hashmap_remove_value(u->manager->user_units, u->slice, u); hashmap_remove_value(u->manager->users, UID_TO_PTR(u->uid), u); - u->slice_job = mfree(u->slice_job); + (void) sd_event_source_unref(u->timer_event_source); + u->service_job = mfree(u->service_job); u->service = mfree(u->service); + u->runtime_dir_service = mfree(u->runtime_dir_service); u->slice = mfree(u->slice); u->runtime_path = mfree(u->runtime_path); u->state_file = mfree(u->state_file); u->name = mfree(u->name); + u->home = mfree(u->home); return mfree(u); } @@ -135,9 +162,11 @@ static int user_save_internal(User *u) { fprintf(f, "# This is private data. Do not parse.\n" "NAME=%s\n" - "STATE=%s\n", + "STATE=%s\n" /* friendly user-facing state */ + "STOPPING=%s\n", /* low-level state */ u->name, - user_state_to_string(user_get_state(u))); + user_state_to_string(user_get_state(u)), + yes_no(u->stopping)); /* LEGACY: no-one reads RUNTIME= anymore, drop it at some point */ if (u->runtime_path) @@ -146,9 +175,6 @@ static int user_save_internal(User *u) { if (u->service_job) fprintf(f, "SERVICE_JOB=%s\n", u->service_job); - if (u->slice_job) - fprintf(f, "SLICE_JOB=%s\n", u->slice_job); - if (u->display) fprintf(f, "DISPLAY=%s\n", u->display->id); @@ -159,6 +185,10 @@ static int user_save_internal(User *u) { u->timestamp.realtime, u->timestamp.monotonic); + if (u->last_session_timestamp != USEC_INFINITY) + fprintf(f, "LAST_SESSION_TIMESTAMP=" USEC_FMT "\n", + u->last_session_timestamp); + if (u->sessions) { Session *i; bool first; @@ -272,103 +302,83 @@ int user_save(User *u) { if (!u->started) return 0; - return user_save_internal (u); + return user_save_internal(u); } int user_load(User *u) { - _cleanup_free_ char *display = NULL, *realtime = NULL, *monotonic = NULL; - Session *s = NULL; + _cleanup_free_ char *realtime = NULL, *monotonic = NULL, *stopping = NULL, *last_session_timestamp = NULL; int r; assert(u); r = parse_env_file(NULL, u->state_file, NEWLINE, - "SERVICE_JOB", &u->service_job, - "SLICE_JOB", &u->slice_job, - "DISPLAY", &display, - "REALTIME", &realtime, - "MONOTONIC", &monotonic, + "SERVICE_JOB", &u->service_job, + "STOPPING", &stopping, + "REALTIME", &realtime, + "MONOTONIC", &monotonic, + "LAST_SESSION_TIMESTAMP", &last_session_timestamp, NULL); - if (r < 0) { - if (r == -ENOENT) - return 0; - + if (r == -ENOENT) + return 0; + if (r < 0) return log_error_errno(r, "Failed to read %s: %m", u->state_file); + + if (stopping) { + r = parse_boolean(stopping); + if (r < 0) + log_debug_errno(r, "Failed to parse 'STOPPING' boolean: %s", stopping); + else + u->stopping = r; } - if (display) - s = hashmap_get(u->manager->sessions, display); - - if (s && s->display && display_is_local(s->display)) - u->display = s; - if (realtime) - timestamp_deserialize(realtime, &u->timestamp.realtime); + (void) timestamp_deserialize(realtime, &u->timestamp.realtime); if (monotonic) - timestamp_deserialize(monotonic, &u->timestamp.monotonic); - - return r; -} - -static int user_start_service(User *u) { - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - char *job; - int r; - - assert(u); - - u->service_job = mfree(u->service_job); - - r = manager_start_unit( - u->manager, - u->service, - &error, - &job); - if (r < 0) - /* we don't fail due to this, let's try to continue */ - log_error_errno(r, "Failed to start user service, ignoring: %s", bus_error_message(&error, r)); - else - u->service_job = job; + (void) timestamp_deserialize(monotonic, &u->timestamp.monotonic); + if (last_session_timestamp) + (void) timestamp_deserialize(last_session_timestamp, &u->last_session_timestamp); return 0; } -int user_start(User *u) { +static void user_start_service(User *u) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; int r; assert(u); + /* Start the service containing the "systemd --user" instance (user@.service). Note that we don't explicitly + * start the per-user slice or the systemd-runtime-dir@.service instance, as those are pulled in both by + * user@.service and the session scopes as dependencies. */ + + u->service_job = mfree(u->service_job); + + r = manager_start_unit(u->manager, u->service, &error, &u->service_job); + if (r < 0) + log_warning_errno(r, "Failed to start user service '%s', ignoring: %s", u->service, bus_error_message(&error, r)); +} + +int user_start(User *u) { + assert(u); + if (u->started && !u->stopping) return 0; - /* - * If u->stopping is set, the user is marked for removal and the slice - * and service stop-jobs are queued. We have to clear that flag before - * queing the start-jobs again. If they succeed, the user object can be - * re-used just fine (pid1 takes care of job-ordering and proper - * restart), but if they fail, we want to force another user_stop() so - * possibly pending units are stopped. - * Note that we don't clear u->started, as we have no clue what state - * the user is in on failure here. Hence, we pretend the user is - * running so it will be properly taken down by GC. However, we clearly - * return an error from user_start() in that case, so no further - * reference to the user is taken. - */ + /* If u->stopping is set, the user is marked for removal and service stop-jobs are queued. We have to clear + * that flag before queing the start-jobs again. If they succeed, the user object can be re-used just fine + * (pid1 takes care of job-ordering and proper restart), but if they fail, we want to force another user_stop() + * so possibly pending units are stopped. */ u->stopping = false; if (!u->started) log_debug("Starting services for new user %s.", u->name); - /* Save the user data so far, because pam_systemd will read the - * XDG_RUNTIME_DIR out of it while starting up systemd --user. - * We need to do user_save_internal() because we have not - * "officially" started yet. */ + /* Save the user data so far, because pam_systemd will read the XDG_RUNTIME_DIR out of it while starting up + * systemd --user. We need to do user_save_internal() because we have not "officially" started yet. */ user_save_internal(u); - /* Spawn user systemd */ - r = user_start_service(u); - if (r < 0) - return r; + /* Start user@UID.service */ + user_start_service(u); if (!u->started) { if (!dual_timestamp_is_set(&u->timestamp)) @@ -383,60 +393,50 @@ int user_start(User *u) { return 0; } -static int user_stop_slice(User *u) { +static void user_stop_service(User *u) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - char *job; int r; assert(u); + assert(u->service); - r = manager_stop_unit(u->manager, u->slice, &error, &job); + /* The reverse of user_start_service(). Note that we only stop user@UID.service here, and let StopWhenUnneeded= + * deal with the slice and the user-runtime-dir@.service instance. */ + + u->service_job = mfree(u->service_job); + + r = manager_stop_unit(u->manager, u->service, &error, &u->service_job); if (r < 0) - return log_error_errno(r, "Failed to stop user slice: %s", bus_error_message(&error, r)); - - return free_and_replace(u->slice_job, job); -} - -static int user_stop_service(User *u) { - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - char *job; - int r; - - assert(u); - - r = manager_stop_unit(u->manager, u->service, &error, &job); - if (r < 0) - return log_error_errno(r, "Failed to stop user service: %s", bus_error_message(&error, r)); - - return free_and_replace(u->service_job, job); + log_warning_errno(r, "Failed to stop user service '%s', ignoring: %s", u->service, bus_error_message(&error, r)); } int user_stop(User *u, bool force) { Session *s; - int r = 0, k; + int r = 0; assert(u); - /* Stop jobs have already been queued */ - if (u->stopping) { + /* This is called whenever we begin with tearing down a user record. It's called in two cases: explicit API + * request to do so via the bus (in which case 'force' is true) and automatically due to GC, if there's no + * session left pinning it (in which case 'force' is false). Note that this just initiates tearing down of the + * user, the User object will remain in memory until user_finalize() is called, see below. */ + + if (!u->started) + return 0; + + if (u->stopping) { /* Stop jobs have already been queued */ user_save(u); - return r; + return 0; } LIST_FOREACH(sessions_by_user, s, u->sessions) { + int k; + k = session_stop(s, force); if (k < 0) r = k; } - /* Kill systemd */ - k = user_stop_service(u); - if (k < 0) - r = k; - - /* Kill cgroup */ - k = user_stop_slice(u); - if (k < 0) - r = k; + user_stop_service(u); u->stopping = true; @@ -451,6 +451,9 @@ int user_finalize(User *u) { assert(u); + /* Called when the user is really ready to be freed, i.e. when all unit stop jobs and suchlike for it are + * done. This is called as a result of an earlier user_done() when all jobs are completed. */ + if (u->started) log_debug("User %s logged out.", u->name); @@ -472,7 +475,7 @@ int user_finalize(User *u) { r = k; } - unlink(u->state_file); + (void) unlink(u->state_file); user_add_to_gc_queue(u); if (u->started) { @@ -528,11 +531,40 @@ int user_check_linger_file(User *u) { return -ENOMEM; p = strjoina("/var/lib/systemd/linger/", cc); + if (access(p, F_OK) < 0) { + if (errno != ENOENT) + return -errno; - return access(p, F_OK) >= 0; + return false; + } + + return true; +} + +static bool user_unit_active(User *u) { + const char *i; + int r; + + assert(u->service); + assert(u->runtime_dir_service); + assert(u->slice); + + FOREACH_STRING(i, u->service, u->runtime_dir_service, u->slice) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + + r = manager_unit_is_active(u->manager, i, &error); + if (r < 0) + log_debug_errno(r, "Failed to determine whether unit '%s' is active, ignoring: %s", u->service, bus_error_message(&error, r)); + if (r != 0) + return true; + } + + return false; } bool user_may_gc(User *u, bool drop_not_started) { + int r; + assert(u); if (drop_not_started && !u->started) @@ -541,14 +573,35 @@ bool user_may_gc(User *u, bool drop_not_started) { if (u->sessions) return false; - if (user_check_linger_file(u) > 0) + if (u->last_session_timestamp != USEC_INFINITY) { + /* All sessions have been closed. Let's see if we shall leave the user record around for a bit */ + + if (u->manager->user_stop_delay == USEC_INFINITY) + return false; /* Leave it around forever! */ + if (u->manager->user_stop_delay > 0 && + now(CLOCK_MONOTONIC) < usec_add(u->last_session_timestamp, u->manager->user_stop_delay)) + return false; /* Leave it around for a bit longer. */ + } + + /* Is this a user that shall stay around forever ("linger")? Before we say "no" to GC'ing for lingering users, let's check + * if any of the three units that we maintain for this user is still around. If none of them is, + * there's no need to keep this user around even if lingering is enabled. */ + if (user_check_linger_file(u) > 0 && user_unit_active(u)) return false; - if (u->slice_job && manager_job_is_active(u->manager, u->slice_job)) - return false; + /* Check if our job is still pending */ + if (u->service_job) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - if (u->service_job && manager_job_is_active(u->manager, u->service_job)) - return false; + r = manager_job_is_active(u->manager, u->service_job, &error); + if (r < 0) + log_debug_errno(r, "Failed to determine whether job '%s' is pending, ignoring: %s", u->service_job, bus_error_message(&error, r)); + if (r != 0) + return false; + } + + /* Note that we don't care if the three units we manage for each user object are up or not, as we are managing + * their state rather than tracking it. */ return true; } @@ -571,7 +624,7 @@ UserState user_get_state(User *u) { if (u->stopping) return USER_CLOSING; - if (!u->started || u->slice_job || u->service_job) + if (!u->started || u->service_job) return USER_OPENING; if (u->sessions) { @@ -590,7 +643,7 @@ UserState user_get_state(User *u) { return all_closing ? USER_CLOSING : USER_ONLINE; } - if (user_check_linger_file(u) > 0) + if (user_check_linger_file(u) > 0 && user_unit_active(u)) return USER_LINGERING; return USER_CLOSING; @@ -603,11 +656,10 @@ int user_kill(User *u, int signo) { } static bool elect_display_filter(Session *s) { - /* Return true if the session is a candidate for the user’s ‘primary - * session’ or ‘display’. */ + /* Return true if the session is a candidate for the user’s ‘primary session’ or ‘display’. */ assert(s); - return (s->class == SESSION_USER && !s->stopping); + return s->class == SESSION_USER && s->started && !s->stopping; } static int elect_display_compare(Session *s1, Session *s2) { @@ -653,9 +705,8 @@ void user_elect_display(User *u) { assert(u); - /* This elects a primary session for each user, which we call - * the "display". We try to keep the assignment stable, but we - * "upgrade" to better choices. */ + /* This elects a primary session for each user, which we call the "display". We try to keep the assignment + * stable, but we "upgrade" to better choices. */ log_debug("Electing new display for user %s", u->name); LIST_FOREACH(sessions_by_user, s, u->sessions) { @@ -671,6 +722,59 @@ void user_elect_display(User *u) { } } +static int user_stop_timeout_callback(sd_event_source *es, uint64_t usec, void *userdata) { + User *u = userdata; + + assert(u); + user_add_to_gc_queue(u); + + return 0; +} + +void user_update_last_session_timer(User *u) { + int r; + + assert(u); + + if (u->sessions) { + /* There are sessions, turn off the timer */ + u->last_session_timestamp = USEC_INFINITY; + u->timer_event_source = sd_event_source_unref(u->timer_event_source); + return; + } + + if (u->last_session_timestamp != USEC_INFINITY) + return; /* Timer already started */ + + u->last_session_timestamp = now(CLOCK_MONOTONIC); + + assert(!u->timer_event_source); + + if (u->manager->user_stop_delay == 0 || u->manager->user_stop_delay == USEC_INFINITY) + return; + + if (sd_event_get_state(u->manager->event) == SD_EVENT_FINISHED) { + log_debug("Not allocating user stop timeout, since we are already exiting."); + return; + } + + r = sd_event_add_time(u->manager->event, + &u->timer_event_source, + CLOCK_MONOTONIC, + usec_add(u->last_session_timestamp, u->manager->user_stop_delay), 0, + user_stop_timeout_callback, u); + if (r < 0) + log_warning_errno(r, "Failed to enqueue user stop event source, ignoring: %m"); + + if (DEBUG_LOGGING) { + char s[FORMAT_TIMESPAN_MAX]; + + log_debug("Last session of user '%s' logged out, terminating user context in %s.", + u->name, + format_timespan(s, sizeof(s), u->manager->user_stop_delay, USEC_PER_MSEC)); + } +} + static const char* const user_state_table[_USER_STATE_MAX] = { [USER_OFFLINE] = "offline", [USER_OPENING] = "opening", diff --git a/src/login/logind-user.h b/src/login/logind-user.h index eba23252841..c41973e27dc 100644 --- a/src/login/logind-user.h +++ b/src/login/logind-user.h @@ -23,27 +23,34 @@ struct User { uid_t uid; gid_t gid; char *name; + char *home; char *state_file; char *runtime_path; - char *slice; - char *service; + + char *slice; /* user-UID.slice */ + char *service; /* user@UID.service */ + char *runtime_dir_service; /* user-runtime-dir@UID.service */ char *service_job; - char *slice_job; Session *display; - dual_timestamp timestamp; + dual_timestamp timestamp; /* When this User object was 'started' the first time */ + usec_t last_session_timestamp; /* When the number of sessions of this user went from 1 to 0 the last time */ + + /* Set up when the last session of the user logs out */ + sd_event_source *timer_event_source; bool in_gc_queue:1; - bool started:1; - bool stopping:1; + + bool started:1; /* Whenever the user being started, has been started or is being stopped again. */ + bool stopping:1; /* Whenever the user is being stopped or has been stopped. */ LIST_HEAD(Session, sessions); LIST_FIELDS(User, gc_queue); }; -int user_new(User **out, Manager *m, uid_t uid, gid_t gid, const char *name); +int user_new(User **out, Manager *m, uid_t uid, gid_t gid, const char *name, const char *home); User *user_free(User *u); DEFINE_TRIVIAL_CLEANUP_FUNC(User *, user_free); @@ -60,6 +67,7 @@ int user_load(User *u); int user_kill(User *u, int signo); int user_check_linger_file(User *u); void user_elect_display(User *u); +void user_update_last_session_timer(User *u); extern const sd_bus_vtable user_vtable[]; int user_node_enumerator(sd_bus *bus, const char *path, void *userdata, char ***nodes, sd_bus_error *error); diff --git a/src/login/logind.c b/src/login/logind.c index e90c8575dcd..44899064c59 100644 --- a/src/login/logind.c +++ b/src/login/logind.c @@ -35,18 +35,21 @@ static int manager_new(Manager **ret) { assert(ret); - m = new0(Manager, 1); + m = new(Manager, 1); if (!m) return -ENOMEM; - m->console_active_fd = -1; - m->reserve_vt_fd = -1; + *m = (Manager) { + .console_active_fd = -1, + .reserve_vt_fd = -1, + }; m->idle_action_not_before_usec = now(CLOCK_MONOTONIC); m->devices = hashmap_new(&string_hash_ops); m->seats = hashmap_new(&string_hash_ops); m->sessions = hashmap_new(&string_hash_ops); + m->sessions_by_leader = hashmap_new(NULL); m->users = hashmap_new(NULL); m->inhibitors = hashmap_new(&string_hash_ops); m->buttons = hashmap_new(&string_hash_ops); @@ -54,7 +57,7 @@ static int manager_new(Manager **ret) { m->user_units = hashmap_new(&string_hash_ops); m->session_units = hashmap_new(&string_hash_ops); - if (!m->devices || !m->seats || !m->sessions || !m->users || !m->inhibitors || !m->buttons || !m->user_units || !m->session_units) + if (!m->devices || !m->seats || !m->sessions || !m->sessions_by_leader || !m->users || !m->inhibitors || !m->buttons || !m->user_units || !m->session_units) return -ENOMEM; r = sd_event_default(&m->event); @@ -109,6 +112,7 @@ static Manager* manager_unref(Manager *m) { hashmap_free(m->devices); hashmap_free(m->seats); hashmap_free(m->sessions); + hashmap_free(m->sessions_by_leader); hashmap_free(m->users); hashmap_free(m->inhibitors); hashmap_free(m->buttons); @@ -129,6 +133,10 @@ static Manager* manager_unref(Manager *m) { sd_event_source_unref(m->udev_button_event_source); sd_event_source_unref(m->lid_switch_ignore_event_source); +#if ENABLE_UTMP + sd_event_source_unref(m->utmp_event_source); +#endif + safe_close(m->console_active_fd); udev_monitor_unref(m->udev_seat_monitor); @@ -787,28 +795,28 @@ static int manager_connect_console(Manager *m) { assert(m); assert(m->console_active_fd < 0); - /* On certain architectures (S390 and Xen, and containers), - /dev/tty0 does not exist, so don't fail if we can't open - it. */ + /* On certain systems (such as S390, Xen, and containers) /dev/tty0 does not exist (as there is no VC), so + * don't fail if we can't open it. */ + if (access("/dev/tty0", F_OK) < 0) return 0; m->console_active_fd = open("/sys/class/tty/tty0/active", O_RDONLY|O_NOCTTY|O_CLOEXEC); if (m->console_active_fd < 0) { - /* On some systems the device node /dev/tty0 may exist - * even though /sys/class/tty/tty0 does not. */ - if (errno == ENOENT) + /* On some systems /dev/tty0 may exist even though /sys/class/tty/tty0 does not. These are broken, but + * common. Let's complain but continue anyway. */ + if (errno == ENOENT) { + log_warning_errno(errno, "System has /dev/tty0 but not /sys/class/tty/tty0/active which is broken, ignoring: %m"); return 0; + } return log_error_errno(errno, "Failed to open /sys/class/tty/tty0/active: %m"); } r = sd_event_add_io(m->event, &m->console_active_event_source, m->console_active_fd, 0, manager_dispatch_console, m); - if (r < 0) { - log_error("Failed to watch foreground console"); - return r; - } + if (r < 0) + return log_error_errno(r, "Failed to watch foreground console: %m"); /* * SIGRTMIN is used as global VT-release signal, SIGRTMIN + 1 is used @@ -827,7 +835,7 @@ static int manager_connect_console(Manager *m) { r = sd_event_add_signal(m->event, NULL, SIGRTMIN, manager_vt_switch, m); if (r < 0) - return r; + return log_error_errno(r, "Failed to subscribe to signal: %m"); return 0; } @@ -951,13 +959,13 @@ static void manager_gc(Manager *m, bool drop_not_started) { /* First, if we are not closing yet, initiate stopping */ if (session_may_gc(session, drop_not_started) && session_get_state(session) != SESSION_CLOSING) - session_stop(session, false); + (void) session_stop(session, false); /* Normally, this should make the session referenced * again, if it doesn't then let's get rid of it * immediately */ if (session_may_gc(session, drop_not_started)) { - session_finalize(session); + (void) session_finalize(session); session_free(session); } } @@ -968,11 +976,11 @@ static void manager_gc(Manager *m, bool drop_not_started) { /* First step: queue stop jobs */ if (user_may_gc(user, drop_not_started)) - user_stop(user, false); + (void) user_stop(user, false); /* Second step: finalize user */ if (user_may_gc(user, drop_not_started)) { - user_finalize(user); + (void) user_finalize(user); user_free(user); } } @@ -1067,6 +1075,9 @@ static int manager_startup(Manager *m) { if (r < 0) return log_error_errno(r, "Failed to register SIGHUP handler: %m"); + /* Connect to utmp */ + manager_connect_utmp(m); + /* Connect to console */ r = manager_connect_console(m); if (r < 0) @@ -1122,15 +1133,18 @@ static int manager_startup(Manager *m) { /* Reserve the special reserved VT */ manager_reserve_vt(m); + /* Read in utmp if it exists */ + manager_read_utmp(m); + /* And start everything */ HASHMAP_FOREACH(seat, m->seats, i) - seat_start(seat); + (void) seat_start(seat); HASHMAP_FOREACH(user, m->users, i) - user_start(user); + (void) user_start(user); HASHMAP_FOREACH(session, m->sessions, i) - session_start(session, NULL); + (void) session_start(session, NULL, NULL); HASHMAP_FOREACH(inhibitor, m->inhibitors, i) inhibitor_start(inhibitor); diff --git a/src/login/logind.h b/src/login/logind.h index e25bc1e13fb..b1a7b932eb9 100644 --- a/src/login/logind.h +++ b/src/login/logind.h @@ -27,6 +27,7 @@ struct Manager { Hashmap *devices; Hashmap *seats; Hashmap *sessions; + Hashmap *sessions_by_leader; Hashmap *users; Hashmap *inhibitors; Hashmap *buttons; @@ -43,6 +44,10 @@ struct Manager { sd_event_source *udev_vcsa_event_source; sd_event_source *udev_button_event_source; +#if ENABLE_UTMP + sd_event_source *utmp_event_source; +#endif + int console_active_fd; unsigned n_autovts; @@ -62,6 +67,7 @@ struct Manager { Hashmap *user_units; usec_t inhibit_delay_max; + usec_t user_stop_delay; /* If an action is currently being executed or is delayed, * this is != 0 and encodes what is being done */ @@ -128,7 +134,7 @@ int manager_add_device(Manager *m, const char *sysfs, bool master, Device **_dev int manager_add_button(Manager *m, const char *name, Button **_button); int manager_add_seat(Manager *m, const char *id, Seat **_seat); int manager_add_session(Manager *m, const char *id, Session **_session); -int manager_add_user(Manager *m, uid_t uid, gid_t gid, const char *name, User **_user); +int manager_add_user(Manager *m, uid_t uid, gid_t gid, const char *name, const char *home, User **_user); int manager_add_user_by_name(Manager *m, const char *name, User **_user); int manager_add_user_by_uid(Manager *m, uid_t uid, User **_user); int manager_add_inhibitor(Manager *m, const char* id, Inhibitor **_inhibitor); @@ -149,6 +155,10 @@ bool manager_is_docked_or_external_displays(Manager *m); bool manager_is_on_external_power(void); bool manager_all_buttons_ignored(Manager *m); +int manager_read_utmp(Manager *m); +void manager_connect_utmp(Manager *m); +void manager_reconnect_utmp(Manager *m); + extern const sd_bus_vtable manager_vtable[]; int match_job_removed(sd_bus_message *message, void *userdata, sd_bus_error *error); @@ -161,13 +171,13 @@ int bus_manager_shutdown_or_sleep_now_or_later(Manager *m, const char *unit_name int manager_send_changed(Manager *manager, const char *property, ...) _sentinel_; -int manager_start_scope(Manager *manager, const char *scope, pid_t pid, const char *slice, const char *description, const char *after, const char *after2, sd_bus_message *more_properties, sd_bus_error *error, char **job); +int manager_start_scope(Manager *manager, const char *scope, pid_t pid, const char *slice, const char *description, char **wants, char **after, const char *requires_mounts_for, sd_bus_message *more_properties, sd_bus_error *error, char **job); int manager_start_unit(Manager *manager, const char *unit, sd_bus_error *error, char **job); int manager_stop_unit(Manager *manager, const char *unit, sd_bus_error *error, char **job); int manager_abandon_scope(Manager *manager, const char *scope, sd_bus_error *error); int manager_kill_unit(Manager *manager, const char *unit, KillWho who, int signo, sd_bus_error *error); -int manager_unit_is_active(Manager *manager, const char *unit); -int manager_job_is_active(Manager *manager, const char *path); +int manager_unit_is_active(Manager *manager, const char *unit, sd_bus_error *error); +int manager_job_is_active(Manager *manager, const char *path, sd_bus_error *error); /* gperf lookup function */ const struct ConfigPerfItem* logind_gperf_lookup(const char *key, GPERF_LEN_TYPE length); diff --git a/src/login/meson.build b/src/login/meson.build index 0e1ed18f7a8..1cc75fd1cfe 100644 --- a/src/login/meson.build +++ b/src/login/meson.build @@ -58,7 +58,6 @@ loginctl_sources = files(''' user_runtime_dir_sources = files(''' user-runtime-dir.c - logind.h '''.split()) if conf.get('ENABLE_LOGIND') == 1 diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index 90ccbd7a830..99c253df4a8 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -308,6 +308,36 @@ static int update_environment(pam_handle_t *handle, const char *key, const char return r; } +static bool validate_runtime_directory(pam_handle_t *handle, const char *path, uid_t uid) { + struct stat st; + + assert(path); + + /* Just some extra paranoia: let's not set $XDG_RUNTIME_DIR if the directory we'd set it to isn't actually set + * up properly for us. */ + + if (lstat(path, &st) < 0) { + pam_syslog(handle, LOG_ERR, "Failed to stat() runtime directory '%s': %s", path, strerror(errno)); + goto fail; + } + + if (!S_ISDIR(st.st_mode)) { + pam_syslog(handle, LOG_ERR, "Runtime directory '%s' is not actually a directory.", path); + goto fail; + } + + if (st.st_uid != uid) { + pam_syslog(handle, LOG_ERR, "Runtime directory '%s' is not owned by UID " UID_FMT ", as it should.", path, uid); + goto fail; + } + + return true; + +fail: + pam_syslog(handle, LOG_WARNING, "Not setting $XDG_RUNTIME_DIR, as the directory is not in order."); + return false; +} + _public_ PAM_EXTERN int pam_sm_open_session( pam_handle_t *handle, int flags, @@ -367,10 +397,12 @@ _public_ PAM_EXTERN int pam_sm_open_session( if (asprintf(&rt, "/run/user/"UID_FMT, pw->pw_uid) < 0) return PAM_BUF_ERR; - r = pam_misc_setenv(handle, "XDG_RUNTIME_DIR", rt, 0); - if (r != PAM_SUCCESS) { - pam_syslog(handle, LOG_ERR, "Failed to set runtime dir."); - return r; + if (validate_runtime_directory(handle, rt, pw->pw_uid)) { + r = pam_misc_setenv(handle, "XDG_RUNTIME_DIR", rt, 0); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to set runtime dir."); + return r; + } } return PAM_SUCCESS; @@ -574,9 +606,11 @@ _public_ PAM_EXTERN int pam_sm_open_session( * in privileged apps clobbering the runtime directory * unnecessarily. */ - r = update_environment(handle, "XDG_RUNTIME_DIR", runtime_path); - if (r != PAM_SUCCESS) - return r; + if (validate_runtime_directory(handle, runtime_path, pw->pw_uid)) { + r = update_environment(handle, "XDG_RUNTIME_DIR", runtime_path); + if (r != PAM_SUCCESS) + return r; + } } /* Most likely we got the session/type/class from environment variables, but might have gotten the data diff --git a/src/login/user-runtime-dir.c b/src/login/user-runtime-dir.c index 69f95a25ab7..a149e96b268 100644 --- a/src/login/user-runtime-dir.c +++ b/src/login/user-runtime-dir.c @@ -3,9 +3,11 @@ #include #include +#include "sd-bus.h" + +#include "bus-error.h" #include "fs-util.h" #include "label.h" -#include "logind.h" #include "mkdir.h" #include "mount-util.h" #include "path-util.h" @@ -17,21 +19,28 @@ #include "strv.h" #include "user-util.h" -static int gather_configuration(size_t *runtime_dir_size) { - Manager m = {}; +static int acquire_runtime_dir_size(uint64_t *ret) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL; int r; - manager_reset_config(&m); - - r = manager_parse_config_file(&m); + r = sd_bus_default_system(&bus); if (r < 0) - log_warning_errno(r, "Failed to parse logind.conf: %m"); + return log_error_errno(r, "Failed to connect to system bus: %m"); + + r = sd_bus_get_property_trivial(bus, "org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", "RuntimeDirectorySize", &error, 't', ret); + if (r < 0) + return log_error_errno(r, "Failed to acquire runtime directory size: %s", bus_error_message(&error, r)); - *runtime_dir_size = m.runtime_dir_size; return 0; } -static int user_mkdir_runtime_path(const char *runtime_path, uid_t uid, gid_t gid, size_t runtime_dir_size) { +static int user_mkdir_runtime_path( + const char *runtime_path, + uid_t uid, + gid_t gid, + uint64_t runtime_dir_size) { + int r; assert(runtime_path); @@ -49,10 +58,10 @@ static int user_mkdir_runtime_path(const char *runtime_path, uid_t uid, gid_t gi char options[sizeof("mode=0700,uid=,gid=,size=,smackfsroot=*") + DECIMAL_STR_MAX(uid_t) + DECIMAL_STR_MAX(gid_t) - + DECIMAL_STR_MAX(size_t)]; + + DECIMAL_STR_MAX(uint64_t)]; xsprintf(options, - "mode=0700,uid=" UID_FMT ",gid=" GID_FMT ",size=%zu%s", + "mode=0700,uid=" UID_FMT ",gid=" GID_FMT ",size=%" PRIu64 "%s", uid, gid, runtime_dir_size, mac_smack_use() ? ",smackfsroot=*" : ""); @@ -113,7 +122,7 @@ static int user_remove_runtime_path(const char *runtime_path) { static int do_mount(const char *user) { char runtime_path[sizeof("/run/user") + DECIMAL_STR_MAX(uid_t)]; - size_t runtime_dir_size; + uint64_t runtime_dir_size; uid_t uid; gid_t gid; int r; @@ -126,9 +135,11 @@ static int do_mount(const char *user) { : "Failed to look up user \"%s\": %m", user); - xsprintf(runtime_path, "/run/user/" UID_FMT, uid); + r = acquire_runtime_dir_size(&runtime_dir_size); + if (r < 0) + return r; - assert_se(gather_configuration(&runtime_dir_size) == 0); + xsprintf(runtime_path, "/run/user/" UID_FMT, uid); log_debug("Will mount %s owned by "UID_FMT":"GID_FMT, runtime_path, uid, gid); return user_mkdir_runtime_path(runtime_path, uid, gid, runtime_dir_size); diff --git a/src/systemd/sd-bus.h b/src/systemd/sd-bus.h index 5bc69659166..ce35756861d 100644 --- a/src/systemd/sd-bus.h +++ b/src/systemd/sd-bus.h @@ -422,6 +422,7 @@ int sd_bus_error_set_errnof(sd_bus_error *e, int error, const char *format, ...) int sd_bus_error_set_errnofv(sd_bus_error *e, int error, const char *format, va_list ap) _sd_printf_(3,0); int sd_bus_error_get_errno(const sd_bus_error *e); int sd_bus_error_copy(sd_bus_error *dest, const sd_bus_error *e); +int sd_bus_error_move(sd_bus_error *dest, sd_bus_error *e); int sd_bus_error_is_set(const sd_bus_error *e); int sd_bus_error_has_name(const sd_bus_error *e, const char *name); diff --git a/units/user-.slice.d/10-defaults.conf b/units/user-.slice.d/10-defaults.conf index f1d118562c7..c81a00e0502 100644 --- a/units/user-.slice.d/10-defaults.conf +++ b/units/user-.slice.d/10-defaults.conf @@ -11,6 +11,7 @@ Description=User Slice of UID %j Documentation=man:user@.service(5) After=systemd-user-sessions.service +StopWhenUnneeded=yes [Slice] TasksMax=33% diff --git a/units/user-runtime-dir@.service.in b/units/user-runtime-dir@.service.in index 8c5407b881f..c168b89f985 100644 --- a/units/user-runtime-dir@.service.in +++ b/units/user-runtime-dir@.service.in @@ -8,9 +8,9 @@ # (at your option) any later version. [Unit] -Description=/run/user/%i mount wrapper +Description=User Runtime Directory /run/user/%i Documentation=man:user@.service(5) -After=systemd-user-sessions.service +After=systemd-user-sessions.service dbus.service StopWhenUnneeded=yes IgnoreOnIsolate=yes @@ -18,5 +18,5 @@ IgnoreOnIsolate=yes ExecStart=@rootlibexecdir@/systemd-user-runtime-dir start %i ExecStop=@rootlibexecdir@/systemd-user-runtime-dir stop %i Type=oneshot -RemainAfterExit=true +RemainAfterExit=yes Slice=user-%i.slice