From 2baca6c22b2d75b8ba2d0bd8a9e7f4a8579752ed Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 19 Nov 2024 22:21:47 +0100 Subject: [PATCH 1/2] logind: add basic Varlink API For now this only covers CreateSession() and ReleaseSession(), i.e. the two operations pam_systemd cares about. --- src/login/logind-session.c | 10 +- src/login/logind-session.h | 2 + src/login/logind-varlink.c | 385 ++++++++++++++++++++++++++ src/login/logind-varlink.h | 12 + src/login/logind.c | 9 +- src/login/logind.h | 3 + src/login/meson.build | 1 + src/shared/meson.build | 1 + src/shared/varlink-io.systemd.Login.c | 119 ++++++++ src/shared/varlink-io.systemd.Login.h | 6 + src/test/test-varlink-idl.c | 3 + test/units/TEST-35-LOGIN.sh | 4 + 12 files changed, 553 insertions(+), 2 deletions(-) create mode 100644 src/login/logind-varlink.c create mode 100644 src/login/logind-varlink.h create mode 100644 src/shared/varlink-io.systemd.Login.c create mode 100644 src/shared/varlink-io.systemd.Login.h diff --git a/src/login/logind-session.c b/src/login/logind-session.c index 5d13191af9..bc48609c26 100644 --- a/src/login/logind-session.c +++ b/src/login/logind-session.c @@ -29,6 +29,7 @@ #include "logind-session-dbus.h" #include "logind-session.h" #include "logind-user-dbus.h" +#include "logind-varlink.h" #include "mkdir-label.h" #include "parse-util.h" #include "path-util.h" @@ -192,6 +193,8 @@ Session* session_free(Session *s) { sd_bus_message_unref(s->create_message); sd_bus_message_unref(s->upgrade_message); + sd_varlink_unref(s->create_link); + free(s->tty); free(s->display); free(s->remote_host); @@ -1662,6 +1665,8 @@ bool session_job_pending(Session *s) { } int session_send_create_reply(Session *s, const sd_bus_error *error) { + int r; + assert(s); /* If error occurred, return it immediately. Otherwise let's wait for all jobs to finish before @@ -1669,7 +1674,10 @@ int session_send_create_reply(Session *s, const sd_bus_error *error) { if (!sd_bus_error_is_set(error) && session_job_pending(s)) return 0; - return session_send_create_reply_bus(s, error); + r = 0; + RET_GATHER(r, session_send_create_reply_bus(s, error)); + RET_GATHER(r, session_send_create_reply_varlink(s, error)); + return r; } static const char* const session_state_table[_SESSION_STATE_MAX] = { diff --git a/src/login/logind-session.h b/src/login/logind-session.h index 1f15b57f38..8ecd90b7ae 100644 --- a/src/login/logind-session.h +++ b/src/login/logind-session.h @@ -151,6 +151,8 @@ struct Session { sd_bus_message *create_message; /* The D-Bus message used to create the session, which we haven't responded to yet */ sd_bus_message *upgrade_message; /* The D-Bus message used to upgrade the session class user-incomplete → user, which we haven't responded to yet */ + sd_varlink *create_link; /* The Varlink connection used to create session, which we haven't responded to yet */ + /* Set up when a client requested to release the session via the bus */ sd_event_source *timer_event_source; diff --git a/src/login/logind-varlink.c b/src/login/logind-varlink.c new file mode 100644 index 0000000000..a8a2542e18 --- /dev/null +++ b/src/login/logind-varlink.c @@ -0,0 +1,385 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "cgroup-util.h" +#include "fd-util.h" +#include "json-util.h" +#include "logind.h" +#include "logind-dbus.h" +#include "logind-session-dbus.h" +#include "logind-varlink.h" +#include "terminal-util.h" +#include "user-util.h" +#include "varlink-io.systemd.Login.h" +#include "varlink-util.h" + +static int manager_varlink_get_session_by_peer( + Manager *m, + sd_varlink *link, + bool consult_display, + Session **ret) { + + int r; + + assert(m); + assert(link); + assert(ret); + + /* Determines the session of the peer. If the peer is not part of a session, but consult_display is + * true, then will return the display session of the peer's owning user */ + + _cleanup_(pidref_done) PidRef pidref = PIDREF_NULL; + r = varlink_get_peer_pidref(link, &pidref); + if (r < 0) + return log_error_errno(r, "Failed to acquire peer PID: %m"); + + Session *session = NULL; + _cleanup_free_ char *name = NULL; + r = cg_pidref_get_session(&pidref, &name); + if (r < 0) { + if (!consult_display) + log_debug_errno(r, "Failed to acquire session of peer, giving up: %m"); + else { + log_debug_errno(r, "Failed to acquire session of peer, trying to find owner UID: %m"); + + uid_t uid; + r = cg_pidref_get_owner_uid(&pidref, &uid); + if (r < 0) + log_debug_errno(r, "Failed to acquire owning UID of peer, giving up: %m"); + else { + User *user = hashmap_get(m->users, UID_TO_PTR(uid)); + if (user) + session = user->display; + } + } + } else + session = hashmap_get(m->sessions, name); + + if (!session) + return sd_varlink_error(link, "io.systemd.Login.NoSuchSession", /* parameters= */ NULL); + + *ret = session; + return 0; +} + +static int manager_varlink_get_session_by_name( + Manager *m, + sd_varlink *link, + const char *name, + Session **ret) { + + assert(m); + assert(link); + assert(ret); + + /* Resolves a session name to a session object. Supports resolving the special names "self" and "auto". */ + + if (SESSION_IS_SELF(name)) + return manager_varlink_get_session_by_peer(m, link, /* consult_display= */ false, ret); + if (SESSION_IS_AUTO(name)) + return manager_varlink_get_session_by_peer(m, link, /* consult_display= */ true, ret); + + Session *session = hashmap_get(m->sessions, name); + if (!session) + return sd_varlink_error(link, "io.systemd.Login.NoSuchSession", /* parameters= */ NULL); + + *ret = session; + return 0; +} + +int session_send_create_reply_varlink(Session *s, const sd_bus_error *error) { + assert(s); + + /* This is called after the session scope and the user service were successfully created, and + * finishes where manager_create_session() left off. */ + + _cleanup_(sd_varlink_unrefp) sd_varlink *vl = TAKE_PTR(s->create_link); + if (!vl) + return 0; + + if (sd_bus_error_is_set(error)) + return sd_varlink_error(vl, "io.systemd.Login.UnitAllocationFailed", /* parameters= */ NULL); + + _cleanup_close_ int fifo_fd = session_create_fifo(s); + if (fifo_fd < 0) + return fifo_fd; + + /* Update the session state file before we notify the client about the result. */ + session_save(s); + + log_debug("Sending Varlink reply about created session: " + "id=%s uid=" UID_FMT " runtime_path=%s " + "session_fd=%d seat=%s vtnr=%u", + s->id, + s->user->user_record->uid, + s->user->runtime_path, + fifo_fd, + s->seat ? s->seat->id : "", + s->vtnr); + + int fifo_fd_idx = sd_varlink_push_fd(vl, fifo_fd); + if (fifo_fd_idx < 0) { + log_error_errno(fifo_fd_idx, "Failed to push FIFO fd to Varlink: %m"); + return sd_varlink_error_errno(vl, fifo_fd_idx); + } + + TAKE_FD(fifo_fd); + + return sd_varlink_replybo( + vl, + SD_JSON_BUILD_PAIR_STRING("Id", s->id), + SD_JSON_BUILD_PAIR_STRING("RuntimePath", s->user->runtime_path), + SD_JSON_BUILD_PAIR_UNSIGNED("SessionFileDescriptor", fifo_fd_idx), + SD_JSON_BUILD_PAIR_UNSIGNED("UID", s->user->user_record->uid), + SD_JSON_BUILD_PAIR_CONDITION(!!s->seat, "Seat", SD_JSON_BUILD_STRING(s->seat ? s->seat->id : NULL)), + SD_JSON_BUILD_PAIR_CONDITION(s->vtnr > 0, "VTNr", SD_JSON_BUILD_UNSIGNED(s->vtnr)), + SD_JSON_BUILD_PAIR_STRING("Class", session_class_to_string(s->class)), + SD_JSON_BUILD_PAIR_STRING("Type", session_type_to_string(s->type))); +} + +static JSON_DISPATCH_ENUM_DEFINE(json_dispatch_session_class, SessionClass, session_class_from_string); +static JSON_DISPATCH_ENUM_DEFINE(json_dispatch_session_type, SessionType, session_type_from_string); + +typedef struct CreateSessionParameters { + uid_t uid; + PidRef pid; + const char *service; + SessionType type; + SessionClass class; + const char *desktop; + const char *seat; + unsigned vtnr; + const char *tty; + const char *display; + int remote; + const char *remote_user; + const char *remote_host; +} CreateSessionParameters; + +static void create_session_parameters_done(CreateSessionParameters *p) { + pidref_done(&p->pid); +} + +static int vl_method_create_session(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + int r; + + static const sd_json_dispatch_field dispatch_table[] = { + { "UID", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, offsetof(CreateSessionParameters, uid), SD_JSON_MANDATORY }, + { "PID", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_pidref, offsetof(CreateSessionParameters, pid), SD_JSON_RELAX }, + { "Service", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(CreateSessionParameters, service), 0 }, + { "Type", SD_JSON_VARIANT_STRING, json_dispatch_session_type, offsetof(CreateSessionParameters, type), SD_JSON_MANDATORY }, + { "Class", SD_JSON_VARIANT_STRING, json_dispatch_session_class, offsetof(CreateSessionParameters, class), SD_JSON_MANDATORY }, + { "Desktop", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(CreateSessionParameters, desktop), SD_JSON_STRICT }, + { "Seat", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(CreateSessionParameters, seat), 0 }, + { "VTNr", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, offsetof(CreateSessionParameters, vtnr), 0 }, + { "TTY", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(CreateSessionParameters, tty), 0 }, + { "Display", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(CreateSessionParameters, display), 0 }, + { "Remote", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(CreateSessionParameters, remote), 0 }, + { "RemoteUser", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(CreateSessionParameters, remote_user), 0 }, + { "RemoteHost", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(CreateSessionParameters, remote_host), 0 }, + {} + }; + + _cleanup_(create_session_parameters_done) CreateSessionParameters p = { + .uid = UID_INVALID, + .pid = PIDREF_NULL, + .class = _SESSION_CLASS_INVALID, + .type = _SESSION_TYPE_INVALID, + .remote = -1, + }; + + r = sd_varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + Seat *seat = NULL; + if (p.seat) { + seat = hashmap_get(m->seats, p.seat); + if (!seat) + return sd_varlink_error(link, "io.systemd.Login.NoSuchSeat", /* parameters= */ NULL); + } + + if (p.tty) { + if (tty_is_vc(p.tty)) { + if (!seat) + seat = m->seat0; + else if (seat != m->seat0) + return sd_varlink_error_invalid_parameter_name(link, "Seat"); + + int v = vtnr_from_tty(p.tty); + if (v <= 0) + return sd_varlink_error_invalid_parameter_name(link, "TTY"); + + if (p.vtnr == 0) + p.vtnr = v; + else if (p.vtnr != (unsigned) v) + return sd_varlink_error_invalid_parameter_name(link, "VTNr"); + + } else if (tty_is_console(p.tty)) { + if (!seat) + seat = m->seat0; + else if (seat != m->seat0) + return sd_varlink_error_invalid_parameter_name(link, "Seat"); + + if (p.vtnr != 0) + return sd_varlink_error_invalid_parameter_name(link, "VTNr"); + } + } + + if (seat) { + if (seat_has_vts(seat)) { + if (!vtnr_is_valid(p.vtnr)) + return sd_varlink_error_invalid_parameter_name(link, "VTNr"); + } else { + if (p.vtnr != 0) + return sd_varlink_error_invalid_parameter_name(link, "VTNr"); + } + } + + if (p.remote < 0) + p.remote = p.remote_user || p.remote_host; + + /* Before we continue processing this, let's ensure the peer is privileged */ + uid_t peer_uid; + r = sd_varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return log_debug_errno(r, "Failed to get peer UID: %m"); + if (peer_uid != 0) + return sd_varlink_error(link, SD_VARLINK_ERROR_PERMISSION_DENIED, /* parameters= */ NULL); + + if (!pidref_is_set(&p.pid)) { + r = varlink_get_peer_pidref(link, &p.pid); + if (r < 0) + return log_debug_errno(r, "Failed to get peer pidref: %m"); + } + + Session *session; + r = manager_create_session( + m, + p.uid, + &p.pid, + p.service, + p.type, + p.class, + p.desktop, + seat, + p.vtnr, + p.tty, + p.display, + p.remote, + p.remote_user, + p.remote_host, + &session); + if (r == -EBUSY) + return sd_varlink_error(link, "io.systemd.Login.AlreadySessionMember", /* parameters= */ NULL); + if (r == -EADDRNOTAVAIL) + return sd_varlink_error(link, "io.systemd.Login.VirtualTerminalAlreadyTaken", /* parameters= */ NULL); + if (r == -EUSERS) + return sd_varlink_error(link, "io.systemd.Login.TooManySessions", /* parameters= */ NULL); + if (r < 0) + return r; + + r = session_start(session, /* properties= */ NULL, /* error= */ NULL); + if (r < 0) + goto fail; + + session->create_link = sd_varlink_ref(link); + + /* Let's check if this is complete now */ + r = session_send_create_reply(session, /* error= */ NULL); + if (r < 0) + goto fail; + + return 1; + +fail: + if (session) + session_add_to_gc_queue(session); + + return r; +} + +static int vl_method_release_session(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + int r; + + struct { + const char *id; + } p; + + static const sd_json_dispatch_field dispatch_table[] = { + { "Id", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, id), SD_JSON_MANDATORY }, + {} + }; + + r = sd_varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + Session *session; + r = manager_varlink_get_session_by_name(m, link, p.id, &session); + if (r < 0) + return r; + + Session *peer_session; + r = manager_varlink_get_session_by_peer(m, link, /* consult_display= */ false, &peer_session); + if (r < 0) + return r; + + if (session != peer_session) + return sd_varlink_error(link, SD_VARLINK_ERROR_PERMISSION_DENIED, /* parameters= */ NULL); + + r = session_release(session); + if (r < 0) + return r; + + return sd_varlink_reply(link, NULL); +} + +int manager_varlink_init(Manager *m) { + _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *s = NULL; + int r; + + assert(m); + + if (m->varlink_server) + return 0; + + r = sd_varlink_server_new( + &s, + SD_VARLINK_SERVER_ACCOUNT_UID| + SD_VARLINK_SERVER_INHERIT_USERDATA| + SD_VARLINK_SERVER_ALLOW_FD_PASSING_OUTPUT); + if (r < 0) + return log_error_errno(r, "Failed to allocate varlink server object: %m"); + + sd_varlink_server_set_userdata(s, m); + + r = sd_varlink_server_add_interface(s, &vl_interface_io_systemd_Login); + if (r < 0) + return log_error_errno(r, "Failed to add Login interface to varlink server: %m"); + + r = sd_varlink_server_bind_method_many( + s, + "io.systemd.Login.CreateSession", vl_method_create_session, + "io.systemd.Login.ReleaseSession", vl_method_release_session); + if (r < 0) + return log_error_errno(r, "Failed to register varlink methods: %m"); + + r = sd_varlink_server_listen_address(s, "/run/systemd/io.systemd.Login", 0666); + if (r < 0) + return log_error_errno(r, "Failed to bind to varlink socket: %m"); + + r = sd_varlink_server_attach_event(s, m->event, SD_EVENT_PRIORITY_NORMAL); + if (r < 0) + return log_error_errno(r, "Failed to attach varlink connection to event loop: %m"); + + m->varlink_server = TAKE_PTR(s); + return 0; +} + +void manager_varlink_done(Manager *m) { + assert(m); + + m->varlink_server = sd_varlink_server_unref(m->varlink_server); +} diff --git a/src/login/logind-varlink.h b/src/login/logind-varlink.h new file mode 100644 index 0000000000..bcf2af4fed --- /dev/null +++ b/src/login/logind-varlink.h @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-bus.h" + +#include "logind.h" +#include "logind-session.h" + +int manager_varlink_init(Manager *m); +void manager_varlink_done(Manager *m); + +int session_send_create_reply_varlink(Session *s, const sd_bus_error *error); diff --git a/src/login/logind.c b/src/login/logind.c index 186778677b..ef1952b0cc 100644 --- a/src/login/logind.c +++ b/src/login/logind.c @@ -24,11 +24,12 @@ #include "fd-util.h" #include "format-util.h" #include "fs-util.h" +#include "logind.h" #include "logind-dbus.h" #include "logind-seat-dbus.h" #include "logind-session-dbus.h" #include "logind-user-dbus.h" -#include "logind.h" +#include "logind-varlink.h" #include "main-func.h" #include "mkdir-label.h" #include "parse-util.h" @@ -154,6 +155,8 @@ static Manager* manager_free(Manager *m) { hashmap_free(m->polkit_registry); + manager_varlink_done(m); + sd_bus_flush_close_unref(m->bus); sd_event_unref(m->event); @@ -1115,6 +1118,10 @@ static int manager_startup(Manager *m) { if (r < 0) return r; + r = manager_varlink_init(m); + if (r < 0) + return r; + /* Instantiate magic seat 0 */ r = manager_add_seat(m, "seat0", &m->seat0); if (r < 0) diff --git a/src/login/logind.h b/src/login/logind.h index ce7e76e761..b19fbb7f3e 100644 --- a/src/login/logind.h +++ b/src/login/logind.h @@ -7,6 +7,7 @@ #include "sd-bus.h" #include "sd-device.h" #include "sd-event.h" +#include "sd-varlink.h" #include "calendarspec.h" #include "conf-parser.h" @@ -147,6 +148,8 @@ struct Manager { CalendarSpec *maintenance_time; dual_timestamp init_ts; + + sd_varlink_server *varlink_server; }; void manager_reset_config(Manager *m); diff --git a/src/login/meson.build b/src/login/meson.build index 43db03184c..86879b4723 100644 --- a/src/login/meson.build +++ b/src/login/meson.build @@ -26,6 +26,7 @@ liblogind_core_sources = files( 'logind-session.c', 'logind-user-dbus.c', 'logind-user.c', + 'logind-varlink.c', 'logind-wall.c', ) diff --git a/src/shared/meson.build b/src/shared/meson.build index 403c304338..5936e20de6 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -184,6 +184,7 @@ shared_sources = files( 'varlink-io.systemd.Hostname.c', 'varlink-io.systemd.Import.c', 'varlink-io.systemd.Journal.c', + 'varlink-io.systemd.Login.c', 'varlink-io.systemd.Machine.c', 'varlink-io.systemd.MachineImage.c', 'varlink-io.systemd.ManagedOOM.c', diff --git a/src/shared/varlink-io.systemd.Login.c b/src/shared/varlink-io.systemd.Login.c new file mode 100644 index 0000000000..9076d47456 --- /dev/null +++ b/src/shared/varlink-io.systemd.Login.c @@ -0,0 +1,119 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "bus-polkit.h" +#include "varlink-idl-common.h" +#include "varlink-io.systemd.Login.h" + +static SD_VARLINK_DEFINE_ENUM_TYPE( + SessionType, + SD_VARLINK_DEFINE_ENUM_VALUE(unspecified), + SD_VARLINK_DEFINE_ENUM_VALUE(tty), + SD_VARLINK_DEFINE_ENUM_VALUE(x11), + SD_VARLINK_DEFINE_ENUM_VALUE(wayland), + SD_VARLINK_DEFINE_ENUM_VALUE(mir), + SD_VARLINK_DEFINE_ENUM_VALUE(web)); + +static SD_VARLINK_DEFINE_ENUM_TYPE( + SessionClass, + SD_VARLINK_FIELD_COMMENT("Regular user sessions"), + SD_VARLINK_DEFINE_ENUM_VALUE(user), + SD_VARLINK_FIELD_COMMENT("Session of the root user that shall be open for login from earliest moment on, and not be delayed for /run/nologin"), + SD_VARLINK_DEFINE_ENUM_VALUE(user_early), + SD_VARLINK_FIELD_COMMENT("Regular user session whose home directory is not available right now, but will be later, at which point the session class can be upgraded to 'user'"), + SD_VARLINK_DEFINE_ENUM_VALUE(user_incomplete), + SD_VARLINK_FIELD_COMMENT("Display manager greeter screen used for login"), + SD_VARLINK_DEFINE_ENUM_VALUE(greeter), + SD_VARLINK_FIELD_COMMENT("Similar, but a a lock screen"), + SD_VARLINK_DEFINE_ENUM_VALUE(lock_screen), + SD_VARLINK_FIELD_COMMENT("Background session (that has no TTY, VT, Seat)"), + SD_VARLINK_DEFINE_ENUM_VALUE(background), + SD_VARLINK_FIELD_COMMENT("Similar, but for which no service manager is invoked"), + SD_VARLINK_DEFINE_ENUM_VALUE(background_light), + SD_VARLINK_FIELD_COMMENT("The special session of the service manager"), + SD_VARLINK_DEFINE_ENUM_VALUE(manager), + SD_VARLINK_FIELD_COMMENT("The special session of the service manager for the root user"), + SD_VARLINK_DEFINE_ENUM_VALUE(manager_early)); + +static SD_VARLINK_DEFINE_METHOD( + CreateSession, + SD_VARLINK_FIELD_COMMENT("Numeric UNIX UID of the user this session shall be owned by"), + SD_VARLINK_DEFINE_INPUT(UID, SD_VARLINK_INT, 0), + SD_VARLINK_FIELD_COMMENT("Process that shall become the leader of the session. If null defaults to the IPC client."), + SD_VARLINK_DEFINE_INPUT_BY_TYPE(PID, ProcessId, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("PAM service name of the program requesting the session"), + SD_VARLINK_DEFINE_INPUT(Service, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The type of the session"), + SD_VARLINK_DEFINE_INPUT_BY_TYPE(Type, SessionType, 0), + SD_VARLINK_FIELD_COMMENT("The class of the session"), + SD_VARLINK_DEFINE_INPUT_BY_TYPE(Class, SessionClass, 0), + SD_VARLINK_FIELD_COMMENT("An identifier for the chosen desktop"), + SD_VARLINK_DEFINE_INPUT(Desktop, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The name of the seat to assign this session to"), + SD_VARLINK_DEFINE_INPUT(Seat, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The virtual terminal number to assign this session to"), + SD_VARLINK_DEFINE_INPUT(VTNr, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The TTY device to assign this session to, if applicable"), + SD_VARLINK_DEFINE_INPUT(TTY, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The X11 display for this session"), + SD_VARLINK_DEFINE_INPUT(Display, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("If true this is a remote session"), + SD_VARLINK_DEFINE_INPUT(Remote, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("User name on the remote site, if known"), + SD_VARLINK_DEFINE_INPUT(RemoteUser, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Host name of the remote host"), + SD_VARLINK_DEFINE_INPUT(RemoteHost, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The identifier string of the session of the user."), + SD_VARLINK_DEFINE_OUTPUT(Id, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("The runtime path ($XDG_RUNTIME_DIR) of the user."), + SD_VARLINK_DEFINE_OUTPUT(RuntimePath, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("Index into the file descriptor table of this reply with the session tracking fd for this session."), + SD_VARLINK_DEFINE_OUTPUT(SessionFileDescriptor, SD_VARLINK_INT, 0), + SD_VARLINK_FIELD_COMMENT("The original UID of this session."), + SD_VARLINK_DEFINE_OUTPUT(UID, SD_VARLINK_INT, 0), + SD_VARLINK_FIELD_COMMENT("The seat this session has been assigned to"), + SD_VARLINK_DEFINE_OUTPUT(Seat, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The virtual terminal number the session has been assigned to"), + SD_VARLINK_DEFINE_OUTPUT(VTNr, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The assigned session type"), + SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(Type, SessionType, 0), + SD_VARLINK_FIELD_COMMENT("The assigned session class"), + SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(Class, SessionClass, 0)); + +static SD_VARLINK_DEFINE_METHOD( + ReleaseSession, + SD_VARLINK_FIELD_COMMENT("The identifier string of the session to release. If unspecified or 'self', will return the callers session."), + SD_VARLINK_DEFINE_INPUT(Id, SD_VARLINK_STRING, SD_VARLINK_NULLABLE)); + +static SD_VARLINK_DEFINE_ERROR(NoSuchSession); +static SD_VARLINK_DEFINE_ERROR(NoSuchSeat); +static SD_VARLINK_DEFINE_ERROR(AlreadySessionMember); +static SD_VARLINK_DEFINE_ERROR(VirtualTerminalAlreadyTaken); +static SD_VARLINK_DEFINE_ERROR(TooManySessions); +static SD_VARLINK_DEFINE_ERROR(UnitAllocationFailed); + +SD_VARLINK_DEFINE_INTERFACE( + io_systemd_Login, + "io.systemd.Login", + SD_VARLINK_INTERFACE_COMMENT("APIs for managing login sessions."), + SD_VARLINK_SYMBOL_COMMENT("Process identifier"), + &vl_type_ProcessId, + SD_VARLINK_SYMBOL_COMMENT("Various types of sessions"), + &vl_type_SessionType, + SD_VARLINK_SYMBOL_COMMENT("Various classes of sessions"), + &vl_type_SessionClass, + SD_VARLINK_SYMBOL_COMMENT("Allocates a new session."), + &vl_method_CreateSession, + SD_VARLINK_SYMBOL_COMMENT("Releases an existing session. Currently, will be refuses unless originating from the session to release itself."), + &vl_method_ReleaseSession, + SD_VARLINK_SYMBOL_COMMENT("No session by this name found"), + &vl_error_NoSuchSession, + SD_VARLINK_SYMBOL_COMMENT("No seat by this name found"), + &vl_error_NoSuchSeat, + SD_VARLINK_SYMBOL_COMMENT("Process already member of a session"), + &vl_error_AlreadySessionMember, + SD_VARLINK_SYMBOL_COMMENT("The specified virtual terminal (VT) is already taken by another session"), + &vl_error_VirtualTerminalAlreadyTaken, + SD_VARLINK_SYMBOL_COMMENT("Maximum number of sessions reached"), + &vl_error_TooManySessions, + SD_VARLINK_SYMBOL_COMMENT("Failed to allocate a unit for the session"), + &vl_error_UnitAllocationFailed); diff --git a/src/shared/varlink-io.systemd.Login.h b/src/shared/varlink-io.systemd.Login.h new file mode 100644 index 0000000000..5453cdfb25 --- /dev/null +++ b/src/shared/varlink-io.systemd.Login.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-varlink-idl.h" + +extern const sd_varlink_interface vl_interface_io_systemd_Login; diff --git a/src/test/test-varlink-idl.c b/src/test/test-varlink-idl.c index 2426779505..41dbfbdb5b 100644 --- a/src/test/test-varlink-idl.c +++ b/src/test/test-varlink-idl.c @@ -15,6 +15,7 @@ #include "varlink-io.systemd.Credentials.h" #include "varlink-io.systemd.Import.h" #include "varlink-io.systemd.Journal.h" +#include "varlink-io.systemd.Login.h" #include "varlink-io.systemd.Machine.h" #include "varlink-io.systemd.MachineImage.h" #include "varlink-io.systemd.ManagedOOM.h" @@ -200,6 +201,8 @@ TEST(parse_format) { print_separator(); test_parse_format_one(&vl_interface_io_systemd_Udev); print_separator(); + test_parse_format_one(&vl_interface_io_systemd_Login); + print_separator(); test_parse_format_one(&vl_interface_xyz_test); } diff --git a/test/units/TEST-35-LOGIN.sh b/test/units/TEST-35-LOGIN.sh index 1fc49e75b4..060e1fb18a 100755 --- a/test/units/TEST-35-LOGIN.sh +++ b/test/units/TEST-35-LOGIN.sh @@ -747,6 +747,10 @@ EOF systemctl stop user@"$uid".service } +testcase_varlink() { + varlinkctl introspect /run/systemd/io.systemd.Login +} + setup_test_user test_write_dropin run_testcases From 4582f8d7a59d774a92b1da333996766aa9c9be76 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 18 Nov 2024 11:25:20 +0100 Subject: [PATCH 2/2] pam-systemd: talk to logind via varlink This makes sure we now use Varlink per default as transport for allocating sessions. This reduces the time it takes to do one run0 cycle by roughly ~10% on my completely synthetic test setup (assuming the target user's service manager is already started) The D-Bus codepaths are kept in place for two reasons: * To make upgrades easy * If the user actually sets resource properties on the PAM session we fall back to the D-Bus codepaths, as we currently have no way to encode the scope properties in JSON, this is only supported for D-Bus serialization. The latter should be revisited once it is possible to allocate a scope unit from PID1 via varlink. --- src/login/pam_systemd.c | 318 +++++++++++++++++++++++++++++----------- 1 file changed, 234 insertions(+), 84 deletions(-) diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index e0861f934c..fbc172e9a5 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -18,6 +18,9 @@ #include #include +#include "sd-bus.h" +#include "sd-varlink.h" + #include "alloc-util.h" #include "audit-util.h" #include "bus-common-errors.h" @@ -35,6 +38,7 @@ #include "format-util.h" #include "fs-util.h" #include "hostname-util.h" +#include "json-util.h" #include "locale-util.h" #include "login-util.h" #include "macro.h" @@ -1022,6 +1026,16 @@ static void session_context_mangle( c->remote = !isempty(c->remote_host) && !is_localhost(c->remote_host); } +static bool can_use_varlink(const SessionContext *c) { + /* Since PID 1 currently doesn't do Varlink right now, we cannot directly set properties for the + * scope, for now. */ + return !c->memory_max && + !c->runtime_max_sec && + !c->tasks_max && + !c->cpu_weight && + !c->io_weight; +} + static int register_session( pam_handle_t *handle, SessionContext *c, @@ -1029,13 +1043,6 @@ static int register_session( bool debug, char **ret_seat) { - /* Let's release the D-Bus connection once this function exits, after all the session might live - * quite a long time, and we are not going to process the bus connection in that time, so let's - * better close before the daemon kicks us off because we are not processing anything. */ - _cleanup_(pam_bus_data_disconnectp) PamBusData *d = NULL; - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL; - _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; int r; assert(handle); @@ -1045,18 +1052,17 @@ static int register_session( /* We don't register session class none with logind */ if (streq(c->class, "none")) { - pam_debug_syslog(handle, debug, "Skipping logind registration for session class none"); - goto skip; + pam_debug_syslog(handle, debug, "Skipping logind registration for session class none."); + *ret_seat = NULL; + return PAM_SUCCESS; } /* Make most of this a NOP on non-logind systems */ - if (!logind_running()) - goto skip; - - /* Talk to logind over the message bus */ - r = pam_acquire_bus_connection(handle, "pam-systemd", debug, &bus, &d); - if (r != PAM_SUCCESS) - return r; + if (!logind_running()) { + pam_debug_syslog(handle, debug, "Skipping logind registration as logind is not running."); + *ret_seat = NULL; + return PAM_SUCCESS; + } pam_debug_syslog(handle, debug, "Asking logind to create session: " @@ -1071,68 +1077,187 @@ static int register_session( "memory_max=%s tasks_max=%s cpu_weight=%s io_weight=%s runtime_max_sec=%s", strna(c->memory_max), strna(c->tasks_max), strna(c->cpu_weight), strna(c->io_weight), strna(c->runtime_max_sec)); - r = create_session_message( - bus, - handle, - ur, - c, - /* avoid_pidfd = */ false, - &m); - if (r < 0) - return pam_bus_log_create_error(handle, r); + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; /* the following variables point into this message, hence pin it for longer */ + _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; /* similar */ + const char *id = NULL, *object_path = NULL, *runtime_path = NULL, *real_seat = NULL; + int session_fd = -EBADF, existing = false; + uint32_t original_uid = UID_INVALID, real_vtnr = 0; - r = sd_bus_call(bus, m, LOGIN_SLOW_BUS_CALL_TIMEOUT_USEC, &error, &reply); - if (r < 0 && sd_bus_error_has_name(&error, SD_BUS_ERROR_UNKNOWN_METHOD)) { - sd_bus_error_free(&error); - pam_debug_syslog(handle, debug, - "CreateSessionWithPIDFD() API is not available, retrying with CreateSession()."); + bool done = false; + if (can_use_varlink(c)) { - m = sd_bus_message_unref(m); - r = create_session_message(bus, - handle, - ur, - c, - /* avoid_pidfd = */ true, - &m); + r = sd_varlink_connect_address(&vl, "/run/systemd/io.systemd.Login"); + if (r < 0) + log_debug_errno(r, "Failed to connect to logind via Varlink, falling back to D-Bus: %m"); + else { + r = sd_varlink_set_allow_fd_passing_input(vl, true); + if (r < 0) + return pam_syslog_errno(handle, LOG_ERR, r, "Failed to enable input fd passing on Varlink socket: %m"); + + r = sd_varlink_set_allow_fd_passing_output(vl, true); + if (r < 0) + return pam_syslog_errno(handle, LOG_ERR, r, "Failed to enable output fd passing on Varlink socket: %m"); + + r = sd_varlink_set_relative_timeout(vl, LOGIN_SLOW_BUS_CALL_TIMEOUT_USEC); + if (r < 0) + return pam_syslog_errno(handle, LOG_ERR, r, "Failed to set relative timeout on Varlink socket: %m"); + + _cleanup_(pidref_done) PidRef pidref = PIDREF_NULL; + r = pidref_set_self(&pidref); + if (r < 0) + return pam_syslog_errno(handle, LOG_ERR, r, "Failed to acquire PID reference on ourselves: %m"); + + sd_json_variant *vreply = NULL; + const char *error_id = NULL; + r = sd_varlink_callbo( + vl, + "io.systemd.Login.CreateSession", + &vreply, + &error_id, + SD_JSON_BUILD_PAIR_UNSIGNED("UID", ur->uid), + JSON_BUILD_PAIR_PIDREF("PID", &pidref), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Service", c->service), + SD_JSON_BUILD_PAIR("Type", JSON_BUILD_STRING_UNDERSCORIFY(c->type)), + SD_JSON_BUILD_PAIR("Class", JSON_BUILD_STRING_UNDERSCORIFY(c->class)), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Desktop", c->desktop), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Seat", c->seat), + SD_JSON_BUILD_PAIR_CONDITION(c->vtnr > 0, "VTNr", SD_JSON_BUILD_UNSIGNED(c->vtnr)), + JSON_BUILD_PAIR_STRING_NON_EMPTY("TTY", c->tty), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Display", c->display), + SD_JSON_BUILD_PAIR_BOOLEAN("Remote", c->remote), + JSON_BUILD_PAIR_STRING_NON_EMPTY("RemoteUser", c->remote_user), + JSON_BUILD_PAIR_STRING_NON_EMPTY("RemoteHost", c->remote_host)); + if (r < 0) + return pam_syslog_errno(handle, LOG_ERR, r, + "Failed to register session: %s", error_id); + if (streq_ptr(error_id, "io.systemd.Login.AlreadySessionMember")) { + /* We are already in a session, don't do anything */ + pam_debug_syslog(handle, debug, "Not creating session: %s", error_id); + *ret_seat = NULL; + return PAM_SUCCESS; + } + if (error_id) + return pam_syslog_errno(handle, LOG_ERR, sd_varlink_error_to_errno(error_id, vreply), + "Failed to issue CreateSession() varlink call: %s", error_id); + + struct { + const char *id; + const char *runtime_path; + unsigned session_fd_idx; + uid_t uid; + const char *seat; + unsigned vtnr; + bool existing; + } p = { + .session_fd_idx = UINT_MAX, + .uid = UID_INVALID, + }; + + static const sd_json_dispatch_field dispatch_table[] = { + { "Id", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, id), SD_JSON_MANDATORY }, + { "RuntimePath", SD_JSON_VARIANT_STRING, json_dispatch_const_path, voffsetof(p, runtime_path), SD_JSON_MANDATORY }, + { "SessionFileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, voffsetof(p, session_fd_idx), SD_JSON_MANDATORY }, + { "UID", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, voffsetof(p, uid), SD_JSON_MANDATORY }, + { "Seat", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, seat), 0 }, + { "VTNr", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, voffsetof(p, vtnr), 0 }, + {} + }; + + r = sd_json_dispatch(vreply, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p); + if (r < 0) + return pam_syslog_errno(handle, LOG_ERR, r, "Failed to parse CreateSession() reply: %m"); + + session_fd = sd_varlink_peek_fd(vl, p.session_fd_idx); + if (session_fd < 0) + return pam_syslog_errno(handle, LOG_ERR, session_fd, "Failed to extract session fd from CreateSession() reply: %m"); + + id = p.id; + runtime_path = p.runtime_path; + original_uid = p.uid; + real_seat = p.seat; + real_vtnr = p.vtnr; + existing = false; /* Even on D-Bus logind only returns false these days */ + + done = true; + } + } + + if (!done) { + /* Let's release the D-Bus connection once we are done here, after all the session might live + * quite a long time, and we are not going to process the bus connection in that time, so + * let's better close before the daemon kicks us off because we are not processing + * anything. */ + _cleanup_(pam_bus_data_disconnectp) PamBusData *d = NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + + /* Talk to logind over the message bus */ + r = pam_acquire_bus_connection(handle, "pam-systemd", debug, &bus, &d); + if (r != PAM_SUCCESS) + return r; + + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + r = create_session_message( + bus, + handle, + ur, + c, + /* avoid_pidfd = */ false, + &m); if (r < 0) return pam_bus_log_create_error(handle, r); + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; r = sd_bus_call(bus, m, LOGIN_SLOW_BUS_CALL_TIMEOUT_USEC, &error, &reply); - } - if (r < 0) { - if (sd_bus_error_has_name(&error, BUS_ERROR_SESSION_BUSY)) { - /* We are already in a session, don't do anything */ + if (r < 0 && sd_bus_error_has_name(&error, SD_BUS_ERROR_UNKNOWN_METHOD)) { + sd_bus_error_free(&error); pam_debug_syslog(handle, debug, - "Not creating session: %s", bus_error_message(&error, r)); - goto skip; + "CreateSessionWithPIDFD() API is not available, retrying with CreateSession()."); + + m = sd_bus_message_unref(m); + r = create_session_message(bus, + handle, + ur, + c, + /* avoid_pidfd = */ true, + &m); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + r = sd_bus_call(bus, m, LOGIN_SLOW_BUS_CALL_TIMEOUT_USEC, &error, &reply); + } + if (r < 0) { + if (sd_bus_error_has_name(&error, BUS_ERROR_SESSION_BUSY)) { + /* We are already in a session, don't do anything */ + pam_debug_syslog(handle, debug, + "Not creating session: %s", bus_error_message(&error, r)); + *ret_seat = NULL; + return PAM_SUCCESS; + } + + pam_syslog(handle, LOG_ERR, + "Failed to create session: %s", bus_error_message(&error, r)); + return PAM_SESSION_ERR; } - pam_syslog(handle, LOG_ERR, - "Failed to create session: %s", bus_error_message(&error, r)); - return PAM_SESSION_ERR; + r = sd_bus_message_read( + reply, + "soshusub", + &id, + &object_path, + &runtime_path, + &session_fd, + &original_uid, + &real_seat, + &real_vtnr, + &existing); + if (r < 0) + return pam_bus_log_parse_error(handle, r); } - const char *id, *object_path, *runtime_path, *real_seat; - int session_fd = -EBADF, existing; - uint32_t original_uid, real_vtnr; - r = sd_bus_message_read( - reply, - "soshusub", - &id, - &object_path, - &runtime_path, - &session_fd, - &original_uid, - &real_seat, - &real_vtnr, - &existing); - if (r < 0) - return pam_bus_log_parse_error(handle, r); - pam_debug_syslog(handle, debug, "Reply from logind: " "id=%s object_path=%s runtime_path=%s session_fd=%d seat=%s vtnr=%u original_uid=%u", - id, object_path, runtime_path, session_fd, real_seat, real_vtnr, original_uid); + id, strna(object_path), runtime_path, session_fd, real_seat, real_vtnr, original_uid); /* Please update manager_default_environment() in core/manager.c accordingly if more session envvars * shall be added. */ @@ -1197,17 +1322,15 @@ static int register_session( /* Everything worked, hence let's patch in the data we learned. Since 'real_set' points into the * D-Bus message, let's copy it and return it as a buffer */ - char *rs = strdup(real_seat); - if (!rs) - return pam_log_oom(handle); + char *rs = NULL; + if (real_seat) { + rs = strdup(real_seat); + if (!rs) + return pam_log_oom(handle); + } c->seat = *ret_seat = rs; c->vtnr = real_vtnr; - - return PAM_SUCCESS; - -skip: - *ret_seat = NULL; return PAM_SUCCESS; } @@ -1343,20 +1466,47 @@ _public_ PAM_EXTERN int pam_sm_close_session( id = pam_getenv(handle, "XDG_SESSION_ID"); if (id && !existing) { - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; + bool done = false; - /* Before we go and close the FIFO we need to tell logind that this is a clean session - * shutdown, so that it doesn't just go and slaughter us immediately after closing the fd */ - - r = pam_acquire_bus_connection(handle, "pam-systemd", debug, &bus, NULL); - if (r != PAM_SUCCESS) - return r; - - r = bus_call_method(bus, bus_login_mgr, "ReleaseSession", &error, NULL, "s", id); + r = sd_varlink_connect_address(&vl, "/run/systemd/io.systemd.Login"); if (r < 0) - return pam_syslog_pam_error(handle, LOG_ERR, PAM_SESSION_ERR, - "Failed to release session: %s", bus_error_message(&error, r)); + log_debug_errno(r, "Failed to connect to logind via Varlink, falling back to D-Bus: %m"); + else { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *vreply = NULL; + const char *error_id = NULL; + r = sd_varlink_callbo( + vl, + "io.systemd.Login.ReleaseSession", + /* ret_reply= */ NULL, + &error_id, + SD_JSON_BUILD_PAIR_STRING("Id", id)); + if (r < 0) + return pam_syslog_errno(handle, LOG_ERR, r, "Failed to register session: %s", error_id); + if (error_id) + return pam_syslog_errno(handle, LOG_ERR, sd_varlink_error_to_errno(error_id, vreply), + "Failed to issue ReleaseSession() varlink call: %s", error_id); + + done = true; + } + + if (!done) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(pam_bus_data_disconnectp) PamBusData *d = NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + + /* Before we go and close the FIFO we need to tell logind that this is a clean session + * shutdown, so that it doesn't just go and slaughter us immediately after closing the fd */ + + r = pam_acquire_bus_connection(handle, "pam-systemd", debug, &bus, &d); + if (r != PAM_SUCCESS) + return r; + + r = bus_call_method(bus, bus_login_mgr, "ReleaseSession", &error, NULL, "s", id); + if (r < 0) + return pam_syslog_pam_error(handle, LOG_ERR, PAM_SESSION_ERR, + "Failed to release session: %s", bus_error_message(&error, r)); + } } /* Note that we are knowingly leaking the FIFO fd here. This way, logind can watch us die. If we