diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 15:35:18 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 15:35:18 +0000 |
commit | b750101eb236130cf056c675997decbac904cc49 (patch) | |
tree | a5df1a06754bdd014cb975c051c83b01c9a97532 /src/login/logind-session.c | |
parent | Initial commit. (diff) | |
download | systemd-upstream.tar.xz systemd-upstream.zip |
Adding upstream version 252.22.upstream/252.22upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | src/login/logind-session.c | 1560 |
1 files changed, 1560 insertions, 0 deletions
diff --git a/src/login/logind-session.c b/src/login/logind-session.c new file mode 100644 index 0000000..6d8a88f --- /dev/null +++ b/src/login/logind-session.c @@ -0,0 +1,1560 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <errno.h> +#include <fcntl.h> +#include <linux/kd.h> +#include <linux/vt.h> +#include <signal.h> +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <unistd.h> + +#include "sd-messages.h" + +#include "alloc-util.h" +#include "audit-util.h" +#include "bus-error.h" +#include "bus-util.h" +#include "devnum-util.h" +#include "env-file.h" +#include "escape.h" +#include "fd-util.h" +#include "fileio.h" +#include "format-util.h" +#include "io-util.h" +#include "logind-dbus.h" +#include "logind-seat-dbus.h" +#include "logind-session-dbus.h" +#include "logind-session.h" +#include "logind-user-dbus.h" +#include "mkdir-label.h" +#include "parse-util.h" +#include "path-util.h" +#include "process-util.h" +#include "serialize.h" +#include "string-table.h" +#include "strv.h" +#include "terminal-util.h" +#include "tmpfile-util.h" +#include "uid-alloc-range.h" +#include "user-util.h" +#include "util.h" + +#define RELEASE_USEC (20*USEC_PER_SEC) + +static void session_remove_fifo(Session *s); +static void session_restore_vt(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); + + if (!session_id_valid(id)) + return -EINVAL; + + s = new(Session, 1); + if (!s) + return -ENOMEM; + + *s = (Session) { + .manager = m, + .fifo_fd = -1, + .vtfd = -1, + .audit_id = AUDIT_SESSION_INVALID, + .tty_validity = _TTY_VALIDITY_INVALID, + }; + + s->state_file = path_join("/run/systemd/sessions", id); + if (!s->state_file) + return -ENOMEM; + + s->id = basename(s->state_file); + + s->devices = hashmap_new(&devt_hash_ops); + if (!s->devices) + return -ENOMEM; + + r = hashmap_put(m->sessions, s->id, s); + if (r < 0) + return r; + + *ret = TAKE_PTR(s); + return 0; +} + +Session* session_free(Session *s) { + SessionDevice *sd; + + if (!s) + return NULL; + + if (s->in_gc_queue) + LIST_REMOVE(gc_queue, s->manager->session_gc_queue, s); + + s->timer_event_source = sd_event_source_unref(s->timer_event_source); + + session_drop_controller(s); + + while ((sd = hashmap_first(s->devices))) + session_device_free(sd); + + hashmap_free(s->devices); + + if (s->user) { + LIST_REMOVE(sessions_by_user, s->user->sessions, s); + + if (s->user->display == s) + s->user->display = NULL; + + user_update_last_session_timer(s->user); + } + + if (s->seat) { + if (s->seat->active == s) + s->seat->active = NULL; + if (s->seat->pending_switch == s) + s->seat->pending_switch = NULL; + + seat_evict_position(s->seat, s); + LIST_REMOVE(sessions_by_seat, s->seat->sessions, s); + } + + if (s->scope) { + hashmap_remove(s->manager->session_units, s->scope); + 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); + + free(s->tty); + free(s->display); + free(s->remote_host); + free(s->remote_user); + free(s->service); + free(s->desktop); + + hashmap_remove(s->manager->sessions, s->id); + + sd_event_source_unref(s->fifo_event_source); + safe_close(s->fifo_fd); + + /* Note that we remove neither the state file nor the fifo path here, since we want both to survive + * daemon restarts */ + free(s->state_file); + free(s->fifo_path); + + sd_event_source_unref(s->stop_on_idle_event_source); + + return mfree(s); +} + +void session_set_user(Session *s, User *u) { + assert(s); + assert(!s->user); + + 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) { + SessionDevice *sd; + + if (!hashmap_isempty(s->devices)) { + fprintf(f, "DEVICES="); + HASHMAP_FOREACH(sd, s->devices) + fprintf(f, "%u:%u ", major(sd->dev), minor(sd->dev)); + fprintf(f, "\n"); + } +} + +int session_save(Session *s) { + _cleanup_free_ char *temp_path = NULL; + _cleanup_fclose_ FILE *f = NULL; + int r; + + assert(s); + + if (!s->user) + return -ESTALE; + + if (!s->started) + return 0; + + r = mkdir_safe_label("/run/systemd/sessions", 0755, 0, 0, MKDIR_WARN_MODE); + if (r < 0) + goto fail; + + r = fopen_temporary(s->state_file, &f, &temp_path); + if (r < 0) + goto fail; + + (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->user_record->uid, + s->user->user_record->user_name, + session_is_active(s), + s->user->display == s, + session_state_to_string(session_get_state(s)), + s->remote); + + if (s->type >= 0) + fprintf(f, "TYPE=%s\n", session_type_to_string(s->type)); + + if (s->original_type >= 0) + fprintf(f, "ORIGINAL_TYPE=%s\n", session_type_to_string(s->original_type)); + + if (s->class >= 0) + fprintf(f, "CLASS=%s\n", session_class_to_string(s->class)); + + if (s->scope) + fprintf(f, "SCOPE=%s\n", s->scope); + if (s->scope_job) + fprintf(f, "SCOPE_JOB=%s\n", s->scope_job); + + if (s->fifo_path) + fprintf(f, "FIFO=%s\n", s->fifo_path); + + if (s->seat) + fprintf(f, "SEAT=%s\n", s->seat->id); + + 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); + + if (s->remote_host) { + _cleanup_free_ char *escaped = NULL; + + escaped = cescape(s->remote_host); + if (!escaped) { + r = -ENOMEM; + goto fail; + } + + fprintf(f, "REMOTE_HOST=%s\n", escaped); + } + + if (s->remote_user) { + _cleanup_free_ char *escaped = NULL; + + escaped = cescape(s->remote_user); + if (!escaped) { + r = -ENOMEM; + goto fail; + } + + fprintf(f, "REMOTE_USER=%s\n", escaped); + } + + if (s->service) { + _cleanup_free_ char *escaped = NULL; + + escaped = cescape(s->service); + if (!escaped) { + r = -ENOMEM; + goto fail; + } + + fprintf(f, "SERVICE=%s\n", escaped); + } + + if (s->desktop) { + _cleanup_free_ char *escaped = NULL; + + escaped = cescape(s->desktop); + if (!escaped) { + r = -ENOMEM; + goto fail; + } + + fprintf(f, "DESKTOP=%s\n", escaped); + } + + if (s->seat && seat_has_vts(s->seat)) + fprintf(f, "VTNR=%u\n", s->vtnr); + + if (!s->vtnr) + fprintf(f, "POSITION=%u\n", s->position); + + if (pid_is_valid(s->leader)) + fprintf(f, "LEADER="PID_FMT"\n", s->leader); + + if (audit_session_is_valid(s->audit_id)) + fprintf(f, "AUDIT=%"PRIu32"\n", s->audit_id); + + if (dual_timestamp_is_set(&s->timestamp)) + fprintf(f, + "REALTIME="USEC_FMT"\n" + "MONOTONIC="USEC_FMT"\n", + s->timestamp.realtime, + s->timestamp.monotonic); + + if (s->controller) { + fprintf(f, "CONTROLLER=%s\n", s->controller); + session_save_devices(s, f); + } + + r = fflush_and_check(f); + if (r < 0) + goto fail; + + if (rename(temp_path, s->state_file) < 0) { + r = -errno; + goto fail; + } + + return 0; + +fail: + (void) unlink(s->state_file); + + if (temp_path) + (void) unlink(temp_path); + + return log_error_errno(r, "Failed to save session data %s: %m", s->state_file); +} + +static int session_load_devices(Session *s, const char *devices) { + int r = 0; + + assert(s); + + for (const char *p = devices;;) { + _cleanup_free_ char *word = NULL; + SessionDevice *sd; + dev_t dev; + int k; + + k = extract_first_word(&p, &word, NULL, 0); + if (k == 0) + break; + if (k < 0) { + r = k; + break; + } + + k = parse_devnum(word, &dev); + if (k < 0) { + r = k; + continue; + } + + /* The file descriptors for loaded devices will be reattached later. */ + k = session_device_new(s, dev, false, &sd); + if (k < 0) + r = k; + } + + if (r < 0) + log_error_errno(r, "Loading session devices for session %s failed: %m", s->id); + + return r; +} + +int session_load(Session *s) { + _cleanup_free_ char *remote = NULL, + *seat = NULL, + *tty_validity = NULL, + *vtnr = NULL, + *state = NULL, + *position = NULL, + *leader = NULL, + *type = NULL, + *original_type = NULL, + *class = NULL, + *uid = NULL, + *realtime = NULL, + *monotonic = NULL, + *controller = NULL, + *active = NULL, + *devices = NULL, + *is_display = NULL; + + int k, r; + + assert(s); + + r = parse_env_file(NULL, s->state_file, + "REMOTE", &remote, + "SCOPE", &s->scope, + "SCOPE_JOB", &s->scope_job, + "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, + "SERVICE", &s->service, + "DESKTOP", &s->desktop, + "VTNR", &vtnr, + "STATE", &state, + "POSITION", &position, + "LEADER", &leader, + "TYPE", &type, + "ORIGINAL_TYPE", &original_type, + "CLASS", &class, + "UID", &uid, + "REALTIME", &realtime, + "MONOTONIC", &monotonic, + "CONTROLLER", &controller, + "ACTIVE", &active, + "DEVICES", &devices, + "IS_DISPLAY", &is_display); + if (r < 0) + return log_error_errno(r, "Failed to read %s: %m", s->state_file); + + if (!s->user) { + uid_t u; + User *user; + + if (!uid) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), + "UID not specified for session %s", + s->id); + + r = parse_uid(uid, &u); + if (r < 0) { + log_error("Failed to parse UID value %s for session %s.", uid, s->id); + return r; + } + + user = hashmap_get(s->manager->users, UID_TO_PTR(u)); + if (!user) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), + "User of session %s not known.", + s->id); + + session_set_user(s, user); + } + + if (remote) { + k = parse_boolean(remote); + if (k >= 0) + s->remote = k; + } + + if (vtnr) + safe_atou(vtnr, &s->vtnr); + + if (seat && !s->seat) { + Seat *o; + + o = hashmap_get(s->manager->seats, seat); + if (o) + r = seat_attach_session(o, s); + if (!o || r < 0) + log_error("Cannot attach session %s to seat %s", s->id, seat); + } + + if (!s->seat || !seat_has_vts(s->seat)) + s->vtnr = 0; + + if (position && s->seat) { + unsigned npos; + + safe_atou(position, &npos); + 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) { + 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) { + SessionType t; + + t = session_type_from_string(type); + if (t >= 0) + s->type = t; + } + + if (original_type) { + SessionType ot; + + ot = session_type_from_string(original_type); + if (ot >= 0) + s->original_type = ot; + } else + /* Pre-v246 compat: initialize original_type if not set in the state file */ + s->original_type = s->type; + + if (class) { + SessionClass c; + + c = session_class_from_string(class); + if (c >= 0) + s->class = c; + } + + if (streq_ptr(state, "closing")) + s->stopping = true; + + if (s->fifo_path) { + int fd; + + /* If we open an unopened pipe for reading we will not + get an EOF. to trigger an EOF we hence open it for + writing, but close it right away which then will + trigger the EOF. This will happen immediately if no + other process has the FIFO open for writing, i. e. + when the session died before logind (re)started. */ + + fd = session_create_fifo(s); + safe_close(fd); + } + + if (realtime) + (void) deserialize_usec(realtime, &s->timestamp.realtime); + if (monotonic) + (void) deserialize_usec(monotonic, &s->timestamp.monotonic); + + if (active) { + k = parse_boolean(active); + if (k >= 0) + 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); + session_load_devices(s, devices); + } else + session_restore_vt(s); + } + + return r; +} + +int session_activate(Session *s) { + unsigned num_pending; + + assert(s); + assert(s->user); + + if (!s->seat) + return -EOPNOTSUPP; + + if (s->seat->active == s) + return 0; + + /* on seats with VTs, we let VTs manage session-switching */ + if (seat_has_vts(s->seat)) { + if (s->vtnr == 0) + return -EOPNOTSUPP; + + return chvt(s->vtnr); + } + + /* On seats without VTs, we implement session-switching in logind. We + * try to pause all session-devices and wait until the session + * controller acknowledged them. Once all devices are asleep, we simply + * switch the active session and be done. + * We save the session we want to switch to in seat->pending_switch and + * seat_complete_switch() will perform the final switch. */ + + s->seat->pending_switch = s; + + /* if no devices are running, immediately perform the session switch */ + num_pending = session_device_try_pause_all(s); + if (!num_pending) + seat_complete_switch(s->seat); + + return 0; +} + +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_strv_free_ char **after = NULL; + _cleanup_free_ char *scope = NULL; + const char *description; + + s->scope_job = mfree(s->scope_job); + + scope = strjoin("session-", s->id, ".scope"); + if (!scope) + return log_oom(); + + description = strjoina("Session ", s->id, " of User ", s->user->user_record->user_name); + + /* We usually want to order session scopes after systemd-user-sessions.service since the + * latter unit is used as login session barrier for unprivileged users. However the barrier + * doesn't apply for root as sysadmin should always be able to log in (and without waiting + * for any timeout to expire) in case something goes wrong during the boot process. Since + * ordering after systemd-user-sessions.service and the user instance is optional we make use + * of STRV_IGNORE with strv_new() to skip these order constraints when needed. */ + after = strv_new("systemd-logind.service", + s->user->runtime_dir_service, + !uid_is_system(s->user->user_record->uid) ? "systemd-user-sessions.service" : STRV_IGNORE, + s->user->service); + if (!after) + return log_oom(); + + r = manager_start_scope( + s->manager, + scope, + s->leader, + s->user->slice, + description, + /* These two have StopWhenUnneeded= set, hence add a dep towards them */ + STRV_MAKE(s->user->runtime_dir_service, + s->user->service), + after, + user_record_home_directory(s->user->user_record), + properties, + 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)); + + s->scope = TAKE_PTR(scope); + } + + (void) hashmap_put(s->manager->session_units, s->scope, s); + + return 0; +} + +static int session_dispatch_stop_on_idle(sd_event_source *source, uint64_t t, void *userdata) { + Session *s = userdata; + dual_timestamp ts; + int r, idle; + + assert(s); + + if (s->stopping) + return 0; + + idle = session_get_idle_hint(s, &ts); + if (idle) { + log_info("Session \"%s\" of user \"%s\" is idle, stopping.", s->id, s->user->user_record->user_name); + + return session_stop(s, /* force */ true); + } + + r = sd_event_source_set_time( + source, + usec_add(dual_timestamp_is_set(&ts) ? ts.monotonic : now(CLOCK_MONOTONIC), + s->manager->stop_idle_session_usec)); + if (r < 0) + return log_error_errno(r, "Failed to configure stop on idle session event source: %m"); + + r = sd_event_source_set_enabled(source, SD_EVENT_ONESHOT); + if (r < 0) + return log_error_errno(r, "Failed to enable stop on idle session event source: %m"); + + return 1; +} + +static int session_setup_stop_on_idle_timer(Session *s) { + int r; + + assert(s); + + if (s->manager->stop_idle_session_usec == USEC_INFINITY) + return 0; + + r = sd_event_add_time_relative( + s->manager->event, + &s->stop_on_idle_event_source, + CLOCK_MONOTONIC, + s->manager->stop_idle_session_usec, + 0, + session_dispatch_stop_on_idle, s); + if (r < 0) + return log_error_errno(r, "Failed to add stop on idle session event source: %m"); + + return 0; +} + +int session_start(Session *s, sd_bus_message *properties, sd_bus_error *error) { + int r; + + assert(s); + + if (!s->user) + return -ESTALE; + + if (s->stopping) + return -EINVAL; + + if (s->started) + return 0; + + r = user_start(s->user); + if (r < 0) + return r; + + r = session_start_scope(s, properties, error); + if (r < 0) + return r; + + r = session_setup_stop_on_idle_timer(s); + if (r < 0) + return r; + + log_struct(s->class == SESSION_BACKGROUND ? LOG_DEBUG : LOG_INFO, + "MESSAGE_ID=" SD_MESSAGE_SESSION_START_STR, + "SESSION_ID=%s", s->id, + "USER_ID=%s", s->user->user_record->user_name, + "LEADER="PID_FMT, s->leader, + LOG_MESSAGE("New session %s of user %s.", s->id, s->user->user_record->user_name)); + + if (!dual_timestamp_is_set(&s->timestamp)) + dual_timestamp_get(&s->timestamp); + + if (s->seat) + seat_read_active_vt(s->seat); + + s->started = true; + + user_elect_display(s->user); + + /* Save data */ + session_save(s); + user_save(s->user); + if (s->seat) + seat_save(s->seat); + + /* Send signals */ + session_send_signal(s, true); + user_send_changed(s->user, "Display", NULL); + + if (s->seat && s->seat->active == s) + seat_send_changed(s->seat, "ActiveSession", NULL); + + return 0; +} + +static int session_stop_scope(Session *s, bool force) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(s); + + if (!s->scope) + return 0; + + /* Let's always abandon the scope first. This tells systemd that we are not interested anymore, and everything + * 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) { + 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 || + (s->user->user_record->kill_processes != 0 && + (s->user->user_record->kill_processes > 0 || + manager_shall_kill(s->manager, s->user->user_record->user_name)))) { + + r = manager_stop_unit(s->manager, s->scope, force ? "replace" : "fail", &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)); + + log_warning_errno(r, "Failed to stop session scope, ignoring: %s", bus_error_message(&error, r)); + } + } else { + + /* With no killing, this session is allowed to persist in "closing" state indefinitely. + * Therefore session stop and session removal may be two distinct events. + * Session stop is quite significant on its own, let's log it. */ + log_struct(s->class == SESSION_BACKGROUND ? LOG_DEBUG : LOG_INFO, + "SESSION_ID=%s", s->id, + "USER_ID=%s", s->user->user_record->user_name, + "LEADER="PID_FMT, s->leader, + LOG_MESSAGE("Session %s logged out. Waiting for processes to exit.", s->id)); + } + + return 0; +} + +int session_stop(Session *s, bool force) { + int r; + + 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); + + if (s->seat) + seat_evict_position(s->seat, s); + + /* We are going down, don't care about FIFOs anymore */ + session_remove_fifo(s); + + /* Kill cgroup */ + r = session_stop_scope(s, force); + + s->stopping = true; + + user_elect_display(s->user); + + session_save(s); + user_save(s->user); + + return r; +} + +int session_finalize(Session *s) { + SessionDevice *sd; + + assert(s); + + if (!s->user) + return -ESTALE; + + if (s->started) + log_struct(s->class == SESSION_BACKGROUND ? LOG_DEBUG : LOG_INFO, + "MESSAGE_ID=" SD_MESSAGE_SESSION_STOP_STR, + "SESSION_ID=%s", s->id, + "USER_ID=%s", s->user->user_record->user_name, + "LEADER="PID_FMT, s->leader, + LOG_MESSAGE("Removed session %s.", s->id)); + + s->timer_event_source = sd_event_source_unref(s->timer_event_source); + + if (s->seat) + seat_evict_position(s->seat, s); + + /* Kill session devices */ + while ((sd = hashmap_first(s->devices))) + session_device_free(sd); + + (void) unlink(s->state_file); + session_add_to_gc_queue(s); + user_add_to_gc_queue(s->user); + + if (s->started) { + session_send_signal(s, false); + s->started = false; + } + + if (s->seat) { + if (s->seat->active == s) + seat_set_active(s->seat, NULL); + + seat_save(s->seat); + } + + user_save(s->user); + user_send_changed(s->user, "Display", NULL); + + return 0; +} + +static int release_timeout_callback(sd_event_source *es, uint64_t usec, void *userdata) { + Session *s = ASSERT_PTR(userdata); + + assert(es); + + session_stop(s, /* force = */ false); + return 0; +} + +int session_release(Session *s) { + assert(s); + + if (!s->started || s->stopping) + return 0; + + if (s->timer_event_source) + return 0; + + return sd_event_add_time_relative( + s->manager->event, + &s->timer_event_source, + CLOCK_MONOTONIC, + RELEASE_USEC, 0, + release_timeout_callback, s); +} + +bool session_is_active(Session *s) { + assert(s); + + if (!s->seat) + return true; + + return s->seat->active == s; +} + +static int get_tty_atime(const char *tty, usec_t *atime) { + _cleanup_free_ char *p = NULL; + struct stat st; + + assert(tty); + assert(atime); + + if (!path_is_absolute(tty)) { + p = path_join("/dev", tty); + if (!p) + return -ENOMEM; + + tty = p; + } else if (!path_startswith(tty, "/dev/")) + return -ENOENT; + + if (lstat(tty, &st) < 0) + return -errno; + + *atime = timespec_load(&st.st_atim); + return 0; +} + +static int get_process_ctty_atime(pid_t pid, usec_t *atime) { + _cleanup_free_ char *p = NULL; + int r; + + assert(pid > 0); + assert(atime); + + r = get_ctty(pid, NULL, &p); + if (r < 0) + return r; + + return get_tty_atime(p, atime); +} + +int session_get_idle_hint(Session *s, dual_timestamp *t) { + usec_t atime = 0, dtime = 0; + int r; + + assert(s); + + /* Graphical sessions have an explicit idle hint */ + if (SESSION_TYPE_IS_GRAPHICAL(s->type)) { + if (t) + *t = s->idle_hint_timestamp; + + return s->idle_hint; + } + + /* For sessions with an explicitly configured tty, let's check its atime */ + if (s->tty) { + r = get_tty_atime(s->tty, &atime); + if (r >= 0) + goto found_atime; + } + + /* For sessions with a leader but no explicitly configured tty, let's check the controlling tty of + * the leader */ + if (pid_is_valid(s->leader)) { + r = get_process_ctty_atime(s->leader, &atime); + if (r >= 0) + goto found_atime; + } + + if (t) + *t = DUAL_TIMESTAMP_NULL; + + return false; + +found_atime: + if (t) + dual_timestamp_from_realtime(t, atime); + + if (s->manager->idle_action_usec > 0 && s->manager->stop_idle_session_usec != USEC_INFINITY) + dtime = MIN(s->manager->idle_action_usec, s->manager->stop_idle_session_usec); + else if (s->manager->idle_action_usec > 0) + dtime = s->manager->idle_action_usec; + else if (s->manager->stop_idle_session_usec != USEC_INFINITY) + dtime = s->manager->stop_idle_session_usec; + else + return false; + + return usec_add(atime, dtime) <= now(CLOCK_REALTIME); +} + +int session_set_idle_hint(Session *s, bool b) { + assert(s); + + if (!SESSION_TYPE_IS_GRAPHICAL(s->type)) + return -ENOTTY; + + if (s->idle_hint == b) + return 0; + + s->idle_hint = b; + dual_timestamp_get(&s->idle_hint_timestamp); + + session_send_changed(s, "IdleHint", "IdleSinceHint", "IdleSinceHintMonotonic", NULL); + + if (s->seat) + seat_send_changed(s->seat, "IdleHint", "IdleSinceHint", "IdleSinceHintMonotonic", NULL); + + user_send_changed(s->user, "IdleHint", "IdleSinceHint", "IdleSinceHintMonotonic", NULL); + manager_send_changed(s->manager, "IdleHint", "IdleSinceHint", "IdleSinceHintMonotonic", NULL); + + return 1; +} + +int session_get_locked_hint(Session *s) { + assert(s); + + return s->locked_hint; +} + +void session_set_locked_hint(Session *s, bool b) { + assert(s); + + if (s->locked_hint == b) + return; + + s->locked_hint = b; + + session_send_changed(s, "LockedHint", NULL); +} + +void session_set_type(Session *s, SessionType t) { + assert(s); + + if (s->type == t) + return; + + s->type = t; + session_save(s); + + session_send_changed(s, "Type", NULL); +} + +int session_set_display(Session *s, const char *display) { + int r; + + assert(s); + assert(display); + + r = free_and_strdup(&s->display, display); + if (r <= 0) /* 0 means the strings were equal */ + return r; + + session_save(s); + + session_send_changed(s, "Display", NULL); + + return 1; +} + +static int session_dispatch_fifo(sd_event_source *es, int fd, uint32_t revents, void *userdata) { + Session *s = ASSERT_PTR(userdata); + + assert(s->fifo_fd == fd); + + /* EOF on the FIFO means the session died abnormally. */ + + session_remove_fifo(s); + session_stop(s, /* force = */ false); + + return 1; +} + +int session_create_fifo(Session *s) { + int r; + + assert(s); + + /* Create FIFO */ + if (!s->fifo_path) { + r = mkdir_safe_label("/run/systemd/sessions", 0755, 0, 0, MKDIR_WARN_MODE); + if (r < 0) + return r; + + 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) + return -errno; + } + + /* Open reading side */ + if (s->fifo_fd < 0) { + 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) { + r = sd_event_add_io(s->manager->event, &s->fifo_event_source, s->fifo_fd, 0, session_dispatch_fifo, s); + if (r < 0) + return r; + + /* Let's make sure we noticed dead sessions before we process new bus requests (which might + * create new sessions). */ + r = sd_event_source_set_priority(s->fifo_event_source, SD_EVENT_PRIORITY_NORMAL-10); + if (r < 0) + return r; + } + + /* Open writing side */ + return RET_NERRNO(open(s->fifo_path, O_WRONLY|O_CLOEXEC|O_NONBLOCK)); +} + +static void session_remove_fifo(Session *s) { + assert(s); + + s->fifo_event_source = sd_event_source_unref(s->fifo_event_source); + s->fifo_fd = safe_close(s->fifo_fd); + + if (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) + return true; + + if (!s->user) + return true; + + if (s->fifo_fd >= 0) { + if (pipe_eof(s->fifo_fd) <= 0) + return false; + } + + if (s->scope_job) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + + 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; +} + +void session_add_to_gc_queue(Session *s) { + assert(s); + + if (s->in_gc_queue) + return; + + LIST_PREPEND(gc_queue, s->manager->session_gc_queue, s); + s->in_gc_queue = true; +} + +SessionState session_get_state(Session *s) { + assert(s); + + /* always check closing first */ + if (s->stopping || s->timer_event_source) + return SESSION_CLOSING; + + if (s->scope_job || s->fifo_fd < 0) + return SESSION_OPENING; + + if (session_is_active(s)) + return SESSION_ACTIVE; + + return SESSION_ONLINE; +} + +int session_kill(Session *s, KillWho who, int signo) { + assert(s); + + if (!s->scope) + return -ESRCH; + + return manager_kill_unit(s->manager, s->scope, who, signo, NULL); +} + +static int session_open_vt(Session *s) { + char path[sizeof("/dev/tty") + DECIMAL_STR_MAX(s->vtnr)]; + + if (s->vtnr < 1) + return -ENODEV; + + if (s->vtfd >= 0) + return s->vtfd; + + sprintf(path, "/dev/tty%u", s->vtnr); + s->vtfd = open_terminal(path, O_RDWR | O_CLOEXEC | O_NONBLOCK | O_NOCTTY); + if (s->vtfd < 0) + return log_error_errno(s->vtfd, "cannot open VT %s of session %s: %m", path, s->id); + + return s->vtfd; +} + +static int session_prepare_vt(Session *s) { + int vt, r; + struct vt_mode mode = {}; + + if (s->vtnr < 1) + return 0; + + vt = session_open_vt(s); + if (vt < 0) + return vt; + + r = fchown(vt, s->user->user_record->uid, -1); + if (r < 0) { + r = log_error_errno(errno, + "Cannot change owner of /dev/tty%u: %m", + s->vtnr); + goto error; + } + + r = ioctl(vt, KDSKBMODE, K_OFF); + if (r < 0) { + r = log_error_errno(errno, + "Cannot set K_OFF on /dev/tty%u: %m", + s->vtnr); + goto error; + } + + r = ioctl(vt, KDSETMODE, KD_GRAPHICS); + if (r < 0) { + r = log_error_errno(errno, + "Cannot set KD_GRAPHICS on /dev/tty%u: %m", + s->vtnr); + goto error; + } + + /* Oh, thanks to the VT layer, VT_AUTO does not work with KD_GRAPHICS. + * So we need a dummy handler here which just acknowledges *all* VT + * switch requests. */ + mode.mode = VT_PROCESS; + mode.relsig = SIGRTMIN; + mode.acqsig = SIGRTMIN + 1; + r = ioctl(vt, VT_SETMODE, &mode); + if (r < 0) { + r = log_error_errno(errno, + "Cannot set VT_PROCESS on /dev/tty%u: %m", + s->vtnr); + goto error; + } + + return 0; + +error: + session_restore_vt(s); + return r; +} + +static void session_restore_vt(Session *s) { + int r; + + r = vt_restore(s->vtfd); + if (r == -EIO) { + int vt, old_fd; + + /* It might happen if the controlling process exited before or while we were + * restoring the VT as it would leave the old file-descriptor in a hung-up + * state. In this case let's retry with a fresh handle to the virtual terminal. */ + + /* We do a little dance to avoid having the terminal be available + * for reuse before we've cleaned it up. */ + old_fd = TAKE_FD(s->vtfd); + + vt = session_open_vt(s); + safe_close(old_fd); + + if (vt >= 0) + r = vt_restore(vt); + } + + if (r < 0) + log_warning_errno(r, "Failed to restore VT, ignoring: %m"); + + s->vtfd = safe_close(s->vtfd); +} + +void session_leave_vt(Session *s) { + int r; + + assert(s); + + /* This is called whenever we get a VT-switch signal from the kernel. + * We acknowledge all of them unconditionally. Note that session are + * free to overwrite those handlers and we only register them for + * sessions with controllers. Legacy sessions are not affected. + * However, if we switch from a non-legacy to a legacy session, we must + * make sure to pause all device before acknowledging the switch. We + * process the real switch only after we are notified via sysfs, so the + * legacy session might have already started using the devices. If we + * don't pause the devices before the switch, we might confuse the + * session we switch to. */ + + if (s->vtfd < 0) + return; + + session_device_pause_all(s); + r = vt_release(s->vtfd, false); + if (r == -EIO) { + int vt, old_fd; + + /* It might happen if the controlling process exited before or while we were + * restoring the VT as it would leave the old file-descriptor in a hung-up + * state. In this case let's retry with a fresh handle to the virtual terminal. */ + + /* We do a little dance to avoid having the terminal be available + * for reuse before we've cleaned it up. */ + old_fd = TAKE_FD(s->vtfd); + + vt = session_open_vt(s); + safe_close(old_fd); + + if (vt >= 0) + r = vt_release(vt, false); + } + if (r < 0) + log_debug_errno(r, "Cannot release VT of session %s: %m", s->id); +} + +bool session_is_controller(Session *s, const char *sender) { + return streq_ptr(ASSERT_PTR(s)->controller, sender); +} + +static void session_release_controller(Session *s, bool notify) { + _unused_ _cleanup_free_ char *name = NULL; + SessionDevice *sd; + + if (!s->controller) + return; + + name = s->controller; + + /* By resetting the controller before releasing the devices, we won't send notification signals. + * This avoids sending useless notifications if the controller is released on disconnects. */ + if (!notify) + s->controller = NULL; + + while ((sd = hashmap_first(s->devices))) + session_device_free(sd); + + s->controller = NULL; + s->track = sd_bus_track_unref(s->track); +} + +static int on_bus_track(sd_bus_track *track, void *userdata) { + Session *s = ASSERT_PTR(userdata); + + assert(track); + + session_drop_controller(s); + + return 0; +} + +int session_set_controller(Session *s, const char *sender, bool force, bool prepare) { + _cleanup_free_ char *name = NULL; + int r; + + assert(s); + assert(sender); + + if (session_is_controller(s, sender)) + return 0; + if (s->controller && !force) + return -EBUSY; + + name = strdup(sender); + if (!name) + return -ENOMEM; + + s->track = sd_bus_track_unref(s->track); + r = sd_bus_track_new(s->manager->bus, &s->track, on_bus_track, s); + if (r < 0) + return r; + + r = sd_bus_track_add_name(s->track, name); + if (r < 0) + return r; + + /* When setting a session controller, we forcibly mute the VT and set + * it into graphics-mode. Applications can override that by changing + * VT state after calling TakeControl(). However, this serves as a good + * default and well-behaving controllers can now ignore VTs entirely. + * Note that we reset the VT on ReleaseControl() and if the controller + * exits. + * If logind crashes/restarts, we restore the controller during restart + * (without preparing the VT since the controller has probably overridden + * VT state by now) or reset the VT in case it crashed/exited, too. */ + if (prepare) { + r = session_prepare_vt(s); + if (r < 0) { + s->track = sd_bus_track_unref(s->track); + return r; + } + } + + session_release_controller(s, true); + s->controller = TAKE_PTR(name); + session_save(s); + + return 0; +} + +void session_drop_controller(Session *s) { + assert(s); + + if (!s->controller) + return; + + s->track = sd_bus_track_unref(s->track); + session_set_type(s, s->original_type); + session_release_controller(s, false); + session_save(s); + session_restore_vt(s); +} + +static const char* const session_state_table[_SESSION_STATE_MAX] = { + [SESSION_OPENING] = "opening", + [SESSION_ONLINE] = "online", + [SESSION_ACTIVE] = "active", + [SESSION_CLOSING] = "closing", +}; + +DEFINE_STRING_TABLE_LOOKUP(session_state, SessionState); + +static const char* const session_type_table[_SESSION_TYPE_MAX] = { + [SESSION_UNSPECIFIED] = "unspecified", + [SESSION_TTY] = "tty", + [SESSION_X11] = "x11", + [SESSION_WAYLAND] = "wayland", + [SESSION_MIR] = "mir", + [SESSION_WEB] = "web", +}; + +DEFINE_STRING_TABLE_LOOKUP(session_type, SessionType); + +static const char* const session_class_table[_SESSION_CLASS_MAX] = { + [SESSION_USER] = "user", + [SESSION_GREETER] = "greeter", + [SESSION_LOCK_SCREEN] = "lock-screen", + [SESSION_BACKGROUND] = "background", +}; + +DEFINE_STRING_TABLE_LOOKUP(session_class, SessionClass); + +static const char* const kill_who_table[_KILL_WHO_MAX] = { + [KILL_LEADER] = "leader", + [KILL_ALL] = "all", +}; + +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); |