From 4d2bc332f338db8bb85947ab1259f70e4e3f8c82 Mon Sep 17 00:00:00 2001 From: Yaping Li <202858510+YapingLi04@users.noreply.github.com> Date: Tue, 28 Apr 2026 15:45:05 -0700 Subject: [PATCH 1/6] sd-varlink: skip IDL validation on synthetic empty terminator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a streaming method's callback returns 0 with no replies enqueued and sd_varlink_set_sentinel() was passed NULL (POINTER_MAX), the framework emits an empty parameters reply via sd_varlink_reply(v, NULL) as a clean stream terminator. That call validated the empty parameters against the method's output IDL — which always failed when the IDL declared any mandatory output field, generating a spurious 'didn't pass validation' warning even though the empty terminator is correct by construction. Move the body of sd_varlink_reply() into a static varlink_reply_internal() that takes a skip_validation flag. The public sd_varlink_reply() keeps its existing behavior (validation enabled). Add a static varlink_reply_terminator() helper that wraps varlink_reply_internal(v, NULL, /* skip_validation= */ true), and route varlink_dispatch_sentinel() through it for the synthetic empty terminator, so both the synchronous dispatch path (varlink_dispatch_method()) and the fiber path (varlink_fiber_entry()) skip validation in one place. Without this, callers who want 'streaming method, possibly empty result' semantics had to either fire a logically wrong 'NotFound'-style error sentinel on empty (sysext.List, BootControl.ListBootEntries pattern) or mark every output field NULLABLE just to placate the validator (the io.systemd.Manager.EnqueueMarkedJobs approach), misrepresenting the contract — exactly the tradeoff the FIXME in pcrlock.c flagged. With this change, the empty terminator is invisible to the IDL validator and the FIXME is no longer needed. Co-developed-by: Claude Opus 4.7 (1M context) --- src/libsystemd/sd-varlink/sd-varlink.c | 24 +++++++++++++++++++----- src/pcrlock/pcrlock.c | 1 - 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/libsystemd/sd-varlink/sd-varlink.c b/src/libsystemd/sd-varlink/sd-varlink.c index af9dd30973..1b0efa3992 100644 --- a/src/libsystemd/sd-varlink/sd-varlink.c +++ b/src/libsystemd/sd-varlink/sd-varlink.c @@ -73,6 +73,7 @@ static const char* const varlink_state_table[_VARLINK_STATE_MAX] = { DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(varlink_state, VarlinkState); static void varlink_server_test_exit_on_idle(sd_varlink_server *s); +static int varlink_reply_terminator(sd_varlink *v); static void varlink_set_state(sd_varlink *v, VarlinkState state) { assert(v); @@ -989,7 +990,9 @@ static int varlink_dispatch_sentinel(sd_varlink *v) { /* Propagate the sentinel to the client if one was configured and no replies were enqueued by * the callback. */ if (sentinel == POINTER_MAX) - r = sd_varlink_reply(v, NULL); + /* Synthetic empty terminator. Skip IDL validation since the empty parameters wouldn't + * satisfy any mandatory output fields the method declares. */ + r = varlink_reply_terminator(v); else { r = sd_varlink_error(v, sentinel, NULL); /* sd_varlink_error() deliberately returns a negative @@ -2298,10 +2301,10 @@ _public_ int sd_varlink_collectb( return sd_varlink_collect_full(v, method, parameters, ret_parameters, ret_error_id, NULL); } -_public_ int sd_varlink_reply(sd_varlink *v, sd_json_variant *parameters) { +static int varlink_reply_internal(sd_varlink *v, sd_json_variant *parameters, bool skip_validation) { int r; - assert_return(v, -EINVAL); + assert(v); if (v->state == VARLINK_DISCONNECTED) return varlink_log_errno(v, SYNTHETIC_ERRNO(ENOTCONN), "Not connected."); @@ -2313,8 +2316,9 @@ _public_ int sd_varlink_reply(sd_varlink *v, sd_json_variant *parameters) { bool more = IN_SET(v->state, VARLINK_PROCESSING_METHOD_MORE, VARLINK_PENDING_METHOD_MORE); - /* Validate parameters BEFORE sanitization */ - if (v->current_method) { + /* Validate parameters BEFORE sanitization. Validation should be skipped for the synthetic + * empty terminator. */ + if (!skip_validation && v->current_method) { const char *bad_field = NULL; r = varlink_idl_validate_method_reply(v->current_method, parameters, more && v->sentinel ? SD_VARLINK_REPLY_CONTINUES : 0, &bad_field); @@ -2373,6 +2377,16 @@ _public_ int sd_varlink_reply(sd_varlink *v, sd_json_variant *parameters) { return 1; } +static int varlink_reply_terminator(sd_varlink *v) { + return varlink_reply_internal(v, /* parameters= */ NULL, /* skip_validation= */ true); +} + +_public_ int sd_varlink_reply(sd_varlink *v, sd_json_variant *parameters) { + assert_return(v, -EINVAL); + + return varlink_reply_internal(v, parameters, /* skip_validation= */ false); +} + _public_ int sd_varlink_replyb(sd_varlink *v, ...) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *parameters = NULL; va_list ap; diff --git a/src/pcrlock/pcrlock.c b/src/pcrlock/pcrlock.c index a0d415209c..247c36cca1 100644 --- a/src/pcrlock/pcrlock.c +++ b/src/pcrlock/pcrlock.c @@ -5406,7 +5406,6 @@ static int vl_method_read_event_log(sd_varlink *link, sd_json_variant *parameter if (r < 0) return r; - // FIXME: We can't use a NULL sentinel here because the output fields in the IDL are non-nullable. r = sd_varlink_set_sentinel(link, NULL); if (r < 0) return r; From ecf309cab84ef894bf826039d61d387122c8fb37 Mon Sep 17 00:00:00 2001 From: Yaping Li <202858510+YapingLi04@users.noreply.github.com> Date: Sun, 10 May 2026 14:50:12 +0000 Subject: [PATCH 2/6] logind: let {session,user,seat}_get_idle_hint() return bool and always set output timestamp These helpers returned int but in practice only ever produced 0/1. session_get_idle_hint() silently swallowed negative returns from get_tty_atime()/get_process_ctty_atime() and fell through to return false; user_get_idle_hint() and seat_get_idle_hint() propagated errors from session_get_idle_hint() that could no longer occur. Change all three to return bool and always set *t on non-NULL output. session_get_idle_hint() previously did not set *t on the !SESSION_CLASS_CAN_IDLE() early return; fix by defaulting *t at the top of the function. Callers that read *t after a false return now see DUAL_TIMESTAMP_NULL rather than uninitialized memory. Update all callers: drop dead < 0 error checks, drop > 0 coercion, drop DUAL_TIMESTAMP_NULL pre-init at sites that only pass &t to the helpers. --- src/login/logind-core.c | 13 +++++-------- src/login/logind-dbus.c | 9 +++------ src/login/logind-seat-dbus.c | 7 ++----- src/login/logind-seat.c | 12 ++++-------- src/login/logind-seat.h | 2 +- src/login/logind-session-dbus.c | 9 +++------ src/login/logind-session.c | 20 ++++++++++++-------- src/login/logind-session.h | 2 +- src/login/logind-user-dbus.c | 4 ++-- src/login/logind-user.c | 12 ++++-------- src/login/logind-user.h | 2 +- src/login/logind.c | 5 +++-- src/login/logind.h | 2 +- 13 files changed, 42 insertions(+), 57 deletions(-) diff --git a/src/login/logind-core.c b/src/login/logind-core.c index c8846d592b..29ca81eec9 100644 --- a/src/login/logind-core.c +++ b/src/login/logind-core.c @@ -447,7 +447,7 @@ int manager_get_user_by_pid(Manager *m, pid_t pid, User **ret) { return !!u; } -int manager_get_idle_hint(Manager *m, dual_timestamp *t) { +bool manager_get_idle_hint(Manager *m, dual_timestamp *ret_timestamp) { Session *s; bool idle_hint; dual_timestamp ts; @@ -458,19 +458,16 @@ int manager_get_idle_hint(Manager *m, dual_timestamp *t) { * unreasonable large idle periods starting with the Unix epoch. */ ts = m->init_ts; - idle_hint = !manager_is_inhibited(m, INHIBIT_IDLE, t, /* flags= */ 0, UID_INVALID, NULL); + idle_hint = !manager_is_inhibited(m, INHIBIT_IDLE, /* since= */ NULL, /* flags= */ 0, UID_INVALID, NULL); HASHMAP_FOREACH(s, m->sessions) { dual_timestamp k; - int ih; + bool ih; if (!SESSION_CLASS_CAN_IDLE(s->class)) continue; ih = session_get_idle_hint(s, &k); - if (ih < 0) - return ih; - if (!ih) { if (!idle_hint) { if (k.monotonic < ts.monotonic) @@ -486,8 +483,8 @@ int manager_get_idle_hint(Manager *m, dual_timestamp *t) { } } - if (t) - *t = ts; + if (ret_timestamp) + *ret_timestamp = ts; return idle_hint; } diff --git a/src/login/logind-dbus.c b/src/login/logind-dbus.c index 5ff5a1f656..4623e1a749 100644 --- a/src/login/logind-dbus.c +++ b/src/login/logind-dbus.c @@ -282,7 +282,7 @@ static int property_get_idle_hint( assert(bus); assert(reply); - return sd_bus_message_append(reply, "b", manager_get_idle_hint(m, NULL) > 0); + return sd_bus_message_append(reply, "b", manager_get_idle_hint(m, /* ret_timestamp= */ NULL)); } static int property_get_idle_since_hint( @@ -295,7 +295,7 @@ static int property_get_idle_since_hint( sd_bus_error *error) { Manager *m = ASSERT_PTR(userdata); - dual_timestamp t = DUAL_TIMESTAMP_NULL; + dual_timestamp t; assert(bus); assert(reply); @@ -701,10 +701,7 @@ static int method_list_sessions_ex(sd_bus_message *message, void *userdata, sd_b if (!path) return -ENOMEM; - r = session_get_idle_hint(s, &idle_ts); - if (r < 0) - return r; - idle = r > 0; + idle = session_get_idle_hint(s, &idle_ts); r = sd_bus_message_append(reply, "(sussussbto)", s->id, diff --git a/src/login/logind-seat-dbus.c b/src/login/logind-seat-dbus.c index a07a0e49ee..3e236e8bb7 100644 --- a/src/login/logind-seat-dbus.c +++ b/src/login/logind-seat-dbus.c @@ -100,7 +100,7 @@ static int property_get_idle_hint( assert(bus); assert(reply); - return sd_bus_message_append(reply, "b", seat_get_idle_hint(s, NULL) > 0); + return sd_bus_message_append(reply, "b", seat_get_idle_hint(s, /* ret_timestamp= */ NULL)); } static int property_get_idle_since_hint( @@ -115,14 +115,11 @@ static int property_get_idle_since_hint( Seat *s = ASSERT_PTR(userdata); dual_timestamp t; uint64_t u; - int r; assert(bus); assert(reply); - r = seat_get_idle_hint(s, &t); - if (r < 0) - return r; + seat_get_idle_hint(s, &t); u = streq(property, "IdleSinceHint") ? t.realtime : t.monotonic; diff --git a/src/login/logind-seat.c b/src/login/logind-seat.c index 66c27d3d73..c20c6bb3c7 100644 --- a/src/login/logind-seat.c +++ b/src/login/logind-seat.c @@ -773,7 +773,7 @@ bool seat_can_graphical(Seat *s) { return seat_has_master_device(s); } -int seat_get_idle_hint(Seat *s, dual_timestamp *t) { +bool seat_get_idle_hint(Seat *s, dual_timestamp *ret_timestamp) { bool idle_hint = true; dual_timestamp ts = DUAL_TIMESTAMP_NULL; @@ -781,12 +781,9 @@ int seat_get_idle_hint(Seat *s, dual_timestamp *t) { LIST_FOREACH(sessions_by_seat, session, s->sessions) { dual_timestamp k; - int ih; + bool ih; ih = session_get_idle_hint(session, &k); - if (ih < 0) - return ih; - if (!ih) { if (!idle_hint) { if (k.monotonic > ts.monotonic) @@ -796,14 +793,13 @@ int seat_get_idle_hint(Seat *s, dual_timestamp *t) { ts = k; } } else if (idle_hint) { - if (k.monotonic > ts.monotonic) ts = k; } } - if (t) - *t = ts; + if (ret_timestamp) + *ret_timestamp = ts; return idle_hint; } diff --git a/src/login/logind-seat.h b/src/login/logind-seat.h index 323cbe546e..5ca657cafb 100644 --- a/src/login/logind-seat.h +++ b/src/login/logind-seat.h @@ -55,7 +55,7 @@ bool seat_can_tty(Seat *s); bool seat_has_master_device(Seat *s); bool seat_can_graphical(Seat *s); -int seat_get_idle_hint(Seat *s, dual_timestamp *t); +bool seat_get_idle_hint(Seat *s, dual_timestamp *ret_timestamp); int seat_start(Seat *s); int seat_stop(Seat *s, bool force); diff --git a/src/login/logind-session-dbus.c b/src/login/logind-session-dbus.c index 4da9bb2425..90cc576fec 100644 --- a/src/login/logind-session-dbus.c +++ b/src/login/logind-session-dbus.c @@ -117,7 +117,7 @@ static int property_get_idle_hint( assert(bus); assert(reply); - return sd_bus_message_append(reply, "b", session_get_idle_hint(s, NULL) > 0); + return sd_bus_message_append(reply, "b", session_get_idle_hint(s, /* ret_timestamp= */ NULL)); } static int property_get_can_idle( @@ -164,16 +164,13 @@ static int property_get_idle_since_hint( sd_bus_error *error) { Session *s = ASSERT_PTR(userdata); - dual_timestamp t = DUAL_TIMESTAMP_NULL; + dual_timestamp t; uint64_t u; - int r; assert(bus); assert(reply); - r = session_get_idle_hint(s, &t); - if (r < 0) - return r; + session_get_idle_hint(s, &t); u = streq(property, "IdleSinceHint") ? t.realtime : t.monotonic; diff --git a/src/login/logind-session.c b/src/login/logind-session.c index 420ec81008..1b8731e2c6 100644 --- a/src/login/logind-session.c +++ b/src/login/logind-session.c @@ -1153,19 +1153,23 @@ static int get_process_ctty_atime(pid_t pid, usec_t *atime) { return get_tty_atime(p, atime); } -int session_get_idle_hint(Session *s, dual_timestamp *t) { +bool session_get_idle_hint(Session *s, dual_timestamp *ret_timestamp) { usec_t atime = 0, dtime = 0; int r; assert(s); - if (!SESSION_CLASS_CAN_IDLE(s->class)) + if (!SESSION_CLASS_CAN_IDLE(s->class)) { + if (ret_timestamp) + *ret_timestamp = DUAL_TIMESTAMP_NULL; + return false; + } /* Graphical sessions have an explicit idle hint */ if (SESSION_TYPE_IS_GRAPHICAL(s->type)) { - if (t) - *t = s->idle_hint_timestamp; + if (ret_timestamp) + *ret_timestamp = s->idle_hint_timestamp; return s->idle_hint; } @@ -1187,14 +1191,14 @@ int session_get_idle_hint(Session *s, dual_timestamp *t) { } } - if (t) - *t = DUAL_TIMESTAMP_NULL; + if (ret_timestamp) + *ret_timestamp = DUAL_TIMESTAMP_NULL; return false; found_atime: - if (t) - dual_timestamp_from_realtime(t, atime); + if (ret_timestamp) + dual_timestamp_from_realtime(ret_timestamp, 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); diff --git a/src/login/logind-session.h b/src/login/logind-session.h index f51eed24fa..da8c52510a 100644 --- a/src/login/logind-session.h +++ b/src/login/logind-session.h @@ -179,7 +179,7 @@ bool session_may_gc(Session *s, bool drop_not_started); void session_add_to_gc_queue(Session *s); int session_activate(Session *s); bool session_is_active(Session *s); -int session_get_idle_hint(Session *s, dual_timestamp *t); +bool session_get_idle_hint(Session *s, dual_timestamp *ret_timestamp); int session_set_idle_hint(Session *s, bool b); int session_get_locked_hint(Session *s); int session_set_locked_hint(Session *s, bool b); diff --git a/src/login/logind-user-dbus.c b/src/login/logind-user-dbus.c index b67e21e052..ca32e8b056 100644 --- a/src/login/logind-user-dbus.c +++ b/src/login/logind-user-dbus.c @@ -144,7 +144,7 @@ static int property_get_idle_hint( assert(bus); assert(reply); - return sd_bus_message_append(reply, "b", user_get_idle_hint(u, NULL) > 0); + return sd_bus_message_append(reply, "b", user_get_idle_hint(u, /* ret_timestamp= */ NULL)); } static int property_get_idle_since_hint( @@ -163,7 +163,7 @@ static int property_get_idle_since_hint( assert(bus); assert(reply); - (void) user_get_idle_hint(u, &t); + 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 11ca04dc8d..a9e56352f8 100644 --- a/src/login/logind-user.c +++ b/src/login/logind-user.c @@ -621,7 +621,7 @@ int user_finalize(User *u) { return r; } -int user_get_idle_hint(User *u, dual_timestamp *t) { +bool user_get_idle_hint(User *u, dual_timestamp *ret_timestamp) { bool idle_hint = true; dual_timestamp ts = DUAL_TIMESTAMP_NULL; @@ -629,15 +629,12 @@ int user_get_idle_hint(User *u, dual_timestamp *t) { LIST_FOREACH(sessions_by_user, s, u->sessions) { dual_timestamp k; - int ih; + bool ih; if (!SESSION_CLASS_CAN_IDLE(s->class)) continue; ih = session_get_idle_hint(s, &k); - if (ih < 0) - return ih; - if (!ih) { if (!idle_hint) { if (k.monotonic < ts.monotonic) @@ -647,14 +644,13 @@ int user_get_idle_hint(User *u, dual_timestamp *t) { ts = k; } } else if (idle_hint) { - if (k.monotonic > ts.monotonic) ts = k; } } - if (t) - *t = ts; + if (ret_timestamp) + *ret_timestamp = ts; return idle_hint; } diff --git a/src/login/logind-user.h b/src/login/logind-user.h index b38af08744..5e04e25a87 100644 --- a/src/login/logind-user.h +++ b/src/login/logind-user.h @@ -75,7 +75,7 @@ void user_add_to_gc_queue(User *u); int user_stop(User *u, bool force); int user_finalize(User *u); UserState user_get_state(User *u); -int user_get_idle_hint(User *u, dual_timestamp *t); +bool user_get_idle_hint(User *u, dual_timestamp *ret_timestamp); int user_save(User *u); int user_load(User *u); int user_kill(User *u, int signo); diff --git a/src/login/logind.c b/src/login/logind.c index c19c1cb791..973506a892 100644 --- a/src/login/logind.c +++ b/src/login/logind.c @@ -1116,6 +1116,7 @@ static int manager_dispatch_idle_action(sd_event_source *s, uint64_t t, void *us Manager *m = ASSERT_PTR(userdata); struct dual_timestamp since; usec_t n, elapse; + bool idle; int r; if (m->idle_action == HANDLE_IGNORE || @@ -1124,8 +1125,8 @@ static int manager_dispatch_idle_action(sd_event_source *s, uint64_t t, void *us n = now(CLOCK_MONOTONIC); - r = manager_get_idle_hint(m, &since); - if (r <= 0) { + idle = manager_get_idle_hint(m, &since); + if (!idle) { /* Not idle. Let's check if after a timeout it might be idle then. */ elapse = n + m->idle_action_usec; m->was_idle = false; diff --git a/src/login/logind.h b/src/login/logind.h index 4e3c15de52..d96bb753dd 100644 --- a/src/login/logind.h +++ b/src/login/logind.h @@ -159,7 +159,7 @@ int manager_spawn_autovt(Manager *m, unsigned vtnr); bool manager_shall_kill(Manager *m, const char *user); -int manager_get_idle_hint(Manager *m, dual_timestamp *t); +bool manager_get_idle_hint(Manager *m, dual_timestamp *ret_timestamp); int manager_get_user_by_pid(Manager *m, pid_t pid, User **ret); int manager_get_session_by_pidref(Manager *m, const PidRef *pid, Session **ret); From 1f9fb6300b3294904e430ba78dd6c9a0fbf301fe Mon Sep 17 00:00:00 2001 From: Yaping Li <202858510+YapingLi04@users.noreply.github.com> Date: Sun, 10 May 2026 14:50:12 +0000 Subject: [PATCH 3/6] logind: add ListSessions Varlink method The Varlink ListSessions method accepts optional Id and PID filters, folding in the D-Bus GetSession(s) and GetSessionByPID(u) lookups. Passing a unique-key filter (Id and/or PID) yields a single reply on match, or NoSuchSession on miss. Passing no filter streams the full list (requires the 'more' flag). Specifying both Id and PID acts as a consistency check: both must refer to the same session, otherwise NoSuchSession is returned. The Id filter supports the special names "self" and "auto" which resolve to the caller's session. The SessionInfo type in the io.systemd.Login Varlink IDL carries all session properties matching the D-Bus org.freedesktop.login1.Session interface. --- src/login/logind-session.c | 60 ++++++++ src/login/logind-session.h | 2 + src/login/logind-varlink.c | 188 ++++++++++++++++++++++++-- src/login/pam_systemd.c | 2 +- src/shared/varlink-io.systemd.Login.c | 82 ++++++++++- src/shared/varlink-io.systemd.Login.h | 3 + src/test/meson.build | 4 + src/test/test-varlink-idl-login.c | 15 ++ test/units/TEST-35-LOGIN.sh | 107 ++++++++++++++- 9 files changed, 444 insertions(+), 19 deletions(-) create mode 100644 src/test/test-varlink-idl-login.c diff --git a/src/login/logind-session.c b/src/login/logind-session.c index 1b8731e2c6..e62a5768da 100644 --- a/src/login/logind-session.c +++ b/src/login/logind-session.c @@ -28,6 +28,7 @@ #include "format-util.h" #include "fs-util.h" #include "hashmap.h" +#include "json-util.h" #include "login-util.h" #include "logind.h" #include "logind-dbus.h" @@ -1684,6 +1685,65 @@ bool session_is_auto(const char *name) { return streq_ptr(name, "auto"); } +static int session_context_build_json(sd_json_variant **ret, const char *name, void *userdata) { + Session *s = ASSERT_PTR(userdata); + + assert(ret); + assert(s->user); + + return sd_json_buildo( + ret, + SD_JSON_BUILD_PAIR_UNSIGNED("UID", s->user->user_record->uid), + JSON_BUILD_PAIR_PIDREF_NON_NULL("PID", &s->leader), + JSON_BUILD_PAIR_ENUM("Type", session_type_to_string(s->type)), + JSON_BUILD_PAIR_ENUM("Class", session_class_to_string(s->class)), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Service", s->service), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Desktop", s->desktop), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Seat", s->seat ? s->seat->id : NULL), + JSON_BUILD_PAIR_UNSIGNED_NON_ZERO("VTNr", s->vtnr), + JSON_BUILD_PAIR_STRING_NON_EMPTY("TTY", s->tty), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Display", s->display), + SD_JSON_BUILD_PAIR_BOOLEAN("Remote", s->remote), + JSON_BUILD_PAIR_STRING_NON_EMPTY("RemoteHost", s->remote_host), + JSON_BUILD_PAIR_STRING_NON_EMPTY("RemoteUser", s->remote_user), + JSON_BUILD_PAIR_STRV_NON_EMPTY("ExtraDeviceAccess", s->extra_device_access)); +} + +static int session_runtime_build_json(sd_json_variant **ret, const char *name, void *userdata) { + Session *s = ASSERT_PTR(userdata); + + assert(ret); + + dual_timestamp idle_ts; + bool idle = session_get_idle_hint(s, &idle_ts); + + return sd_json_buildo( + ret, + SD_JSON_BUILD_PAIR_STRING("Id", s->id), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Scope", s->scope), + SD_JSON_BUILD_PAIR_CONDITION(audit_session_is_valid(s->audit_id), + "Audit", SD_JSON_BUILD_UNSIGNED(s->audit_id)), + JSON_BUILD_PAIR_DUAL_TIMESTAMP_NON_NULL("Timestamp", &s->timestamp), + JSON_BUILD_PAIR_ENUM("State", session_state_to_string(session_get_state(s))), + SD_JSON_BUILD_PAIR_BOOLEAN("Active", session_is_active(s)), + SD_JSON_BUILD_PAIR_BOOLEAN("IdleHint", idle), + SD_JSON_BUILD_PAIR_CONDITION(idle, "IdleSinceHint", JSON_BUILD_DUAL_TIMESTAMP(&idle_ts)), + SD_JSON_BUILD_PAIR_BOOLEAN("LockedHint", s->locked_hint), + SD_JSON_BUILD_PAIR_BOOLEAN("CanIdle", SESSION_CLASS_CAN_IDLE(s->class)), + SD_JSON_BUILD_PAIR_BOOLEAN("CanLock", SESSION_CLASS_CAN_LOCK(s->class))); +} + +int session_build_json(Session *s, sd_json_variant **ret) { + assert(s); + assert(s->user); + assert(ret); + + return sd_json_buildo( + ret, + SD_JSON_BUILD_PAIR_CALLBACK("context", session_context_build_json, s), + SD_JSON_BUILD_PAIR_CALLBACK("runtime", session_runtime_build_json, s)); +} + static const char* const session_state_table[_SESSION_STATE_MAX] = { [SESSION_OPENING] = "opening", [SESSION_ONLINE] = "online", diff --git a/src/login/logind-session.h b/src/login/logind-session.h index da8c52510a..b0749134af 100644 --- a/src/login/logind-session.h +++ b/src/login/logind-session.h @@ -219,3 +219,5 @@ int session_send_create_reply(Session *s, const sd_bus_error *error); bool session_is_self(const char *name); bool session_is_auto(const char *name); + +int session_build_json(Session *s, sd_json_variant **ret); diff --git a/src/login/logind-varlink.c b/src/login/logind-varlink.c index 71a445e900..9b58791991 100644 --- a/src/login/logind-varlink.c +++ b/src/login/logind-varlink.c @@ -41,7 +41,8 @@ static int manager_varlink_get_session_by_peer( 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 */ + * true, then will return the display session of the peer's owning user. Returns 0 with *ret set to + * NULL if no session could be determined; the caller decides which error to report to the client. */ _cleanup_(pidref_done) PidRef pidref = PIDREF_NULL; r = varlink_get_peer_pidref(link, &pidref); @@ -70,35 +71,114 @@ static int manager_varlink_get_session_by_peer( } else session = hashmap_get(m->sessions, name); + *ret = session; + return 0; +} + +static int lookup_session_by_name(Manager *m, sd_varlink *link, const char *name, Session **ret) { + int r; + + assert(m); + assert(link); + assert(name); + assert(ret); + + if (session_is_self(name) || session_is_auto(name)) { + Session *peer; + r = manager_varlink_get_session_by_peer(m, link, /* consult_display= */ session_is_auto(name), &peer); + if (r < 0) + return r; + if (!peer) + return -ESRCH; + + *ret = peer; + return 0; + } + + if (!session_id_valid(name)) + return -EINVAL; + + Session *session = hashmap_get(m->sessions, name); if (!session) - return sd_varlink_error(link, "io.systemd.Login.NoSuchSession", /* parameters= */ NULL); + return -ESRCH; *ret = session; return 0; } -static int manager_varlink_get_session_by_name( +static int lookup_session_by_pidref(Manager *m, const PidRef *pidref, Session **ret) { + int r; + + assert(m); + assert(pidref); + assert(ret); + + Session *session; + r = manager_get_session_by_pidref(m, pidref, &session); + if (r < 0) + return log_debug_errno(r, "Failed to look up session by PID " PID_FMT ": %m", pidref->pid); + if (!session) + return -ESRCH; + + *ret = session; + return 0; +} + +static int manager_varlink_get_session_by_name_or_pidref( Manager *m, sd_varlink *link, const char *name, + const PidRef *pidref, Session **ret) { + Session *by_name = NULL, *by_pid = NULL; + int r; + assert(m); assert(link); assert(ret); - /* Resolves a session name to a session object. Supports resolving the special names "self" and "auto". */ + /* Resolves a session by name and/or PID. Supports the special names "self" and "auto" for the name + * argument. If both name and pidref are unset, resolves to the caller's session. If both name and + * pidref are set they must refer to the same session, otherwise -ESRCH is returned. Returns -ESRCH + * on "not found". Caller is expected to turn that into a varlink error, typically via + * sd_varlink_set_sentinel(). Returns negative errno on other failures. */ - 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); + if (name) { + r = lookup_session_by_name(m, link, name, &by_name); + if (r == -EINVAL) + return sd_varlink_error_invalid_parameter_name(link, "Id"); + if (r < 0) + return r; + } - Session *session = hashmap_get(m->sessions, name); - if (!session) - return sd_varlink_error(link, "io.systemd.Login.NoSuchSession", /* parameters= */ NULL); + if (pidref && pidref_is_set(pidref)) { + r = lookup_session_by_pidref(m, pidref, &by_pid); + if (r == -EINVAL) + return sd_varlink_error_invalid_parameter_name(link, "PID"); + if (r < 0) + return r; + } - *ret = session; + if (by_name && by_pid && by_name != by_pid) + return log_debug_errno(SYNTHETIC_ERRNO(ESRCH), + "Search by session id '%s' and PID " PID_FMT " resulted in two different sessions", + name, pidref->pid); + + if (by_name || by_pid) { + *ret = by_name ?: by_pid; + return 0; + } + + /* Neither filter set — fall back to caller's session. */ + Session *by_peer; + r = manager_varlink_get_session_by_peer(m, link, /* consult_display= */ true, &by_peer); + if (r < 0) + return r; + if (!by_peer) + return -ESRCH; + + *ret = by_peer; return 0; } @@ -129,7 +209,7 @@ int session_send_create_reply_varlink(Session *s, const sd_bus_error *error) { SD_JSON_BUILD_PAIR_STRING("RuntimePath", s->user->runtime_path), 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)), + JSON_BUILD_PAIR_UNSIGNED_NON_ZERO("VTNr", s->vtnr), JSON_BUILD_PAIR_ENUM("Class", session_class_to_string(s->class)), JSON_BUILD_PAIR_ENUM("Type", session_type_to_string(s->type))); } @@ -304,6 +384,81 @@ fail: return r; } +static int emit_session_reply(sd_varlink *link, Session *session) { + assert(link); + assert(session); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + int r = session_build_json(session, &v); + if (r < 0) + return r; + + return sd_varlink_reply(link, v); +} + +typedef struct ListSessionsParameters { + const char *id; + PidRef pidref; +} ListSessionsParameters; + +static void list_sessions_parameters_done(ListSessionsParameters *p) { + assert(p); + pidref_done(&p->pidref); +} + +static int vl_method_list_sessions(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[] = { + { "Id", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(ListSessionsParameters, id), 0 }, + { "PID", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_pidref, offsetof(ListSessionsParameters, pidref), 0 }, + {} + }; + + _cleanup_(list_sessions_parameters_done) ListSessionsParameters p = { + .pidref = PIDREF_NULL, + }; + + r = sd_varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + /* Unique-key path: Id and/or PID provided. Single reply or NoSuchSession. */ + if (p.id || pidref_is_set(&p.pidref)) { + r = sd_varlink_set_sentinel(link, "io.systemd.Login.NoSuchSession"); + if (r < 0) + return r; + + Session *session; + r = manager_varlink_get_session_by_name_or_pidref(m, link, p.id, &p.pidref, &session); + if (r == -ESRCH) + return 0; /* triggers NoSuchSession sentinel */ + if (r < 0) + return r; + + return emit_session_reply(link, session); + } + + /* Streaming path: no filter. Full list, requires 'more' flag. */ + if (!FLAGS_SET(flags, SD_VARLINK_METHOD_MORE)) + return sd_varlink_error(link, SD_VARLINK_ERROR_EXPECTED_MORE, /* parameters= */ NULL); + + /* Empty hashmap is a valid empty stream, not "not found" — see vl_method_list_inhibitors. */ + r = sd_varlink_set_sentinel(link, /* error_id= */ NULL); + if (r < 0) + return r; + + Session *session; + HASHMAP_FOREACH(session, m->sessions) { + r = emit_session_reply(link, session); + if (r < 0) + return r; + } + + return 0; +} + 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; @@ -322,7 +477,9 @@ static int vl_method_release_session(sd_varlink *link, sd_json_variant *paramete return r; Session *session; - r = manager_varlink_get_session_by_name(m, link, p.id, &session); + r = manager_varlink_get_session_by_name_or_pidref(m, link, p.id, /* pidref= */ NULL, &session); + if (r == -ESRCH) + return sd_varlink_error(link, "io.systemd.Login.NoSuchSession", /* parameters= */ NULL); if (r < 0) return r; @@ -330,6 +487,8 @@ static int vl_method_release_session(sd_varlink *link, sd_json_variant *paramete r = manager_varlink_get_session_by_peer(m, link, /* consult_display= */ false, &peer_session); if (r < 0) return r; + if (!peer_session) + return sd_varlink_error(link, "io.systemd.Login.NoSuchSession", /* parameters= */ NULL); if (session != peer_session) return sd_varlink_error(link, SD_VARLINK_ERROR_PERMISSION_DENIED, /* parameters= */ NULL); @@ -467,6 +626,7 @@ int manager_varlink_init(Manager *m, int fd) { s, "io.systemd.Login.CreateSession", vl_method_create_session, "io.systemd.Login.ReleaseSession", vl_method_release_session, + "io.systemd.Login.ListSessions", vl_method_list_sessions, "io.systemd.Shutdown.PowerOff", vl_method_power_off, "io.systemd.Shutdown.Reboot", vl_method_reboot, "io.systemd.Shutdown.Halt", vl_method_halt, diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index 1b0adfae9e..b6f7ddd38c 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -1147,7 +1147,7 @@ static int register_session( JSON_BUILD_PAIR_ENUM("Class", 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_UNSIGNED_NON_ZERO("VTNr", 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), diff --git a/src/shared/varlink-io.systemd.Login.c b/src/shared/varlink-io.systemd.Login.c index 9a07155ef2..42cd0aa95a 100644 --- a/src/shared/varlink-io.systemd.Login.c +++ b/src/shared/varlink-io.systemd.Login.c @@ -3,7 +3,7 @@ #include "varlink-idl-common.h" #include "varlink-io.systemd.Login.h" -static SD_VARLINK_DEFINE_ENUM_TYPE( +SD_VARLINK_DEFINE_ENUM_TYPE( SessionType, SD_VARLINK_DEFINE_ENUM_VALUE(unspecified), SD_VARLINK_DEFINE_ENUM_VALUE(tty), @@ -12,7 +12,7 @@ static SD_VARLINK_DEFINE_ENUM_TYPE( SD_VARLINK_DEFINE_ENUM_VALUE(mir), SD_VARLINK_DEFINE_ENUM_VALUE(web)); -static SD_VARLINK_DEFINE_ENUM_TYPE( +SD_VARLINK_DEFINE_ENUM_TYPE( SessionClass, SD_VARLINK_FIELD_COMMENT("Regular user sessions"), SD_VARLINK_DEFINE_ENUM_VALUE(user), @@ -37,6 +37,62 @@ static SD_VARLINK_DEFINE_ENUM_TYPE( 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_STRUCT_TYPE( + SessionContext, + SD_VARLINK_FIELD_COMMENT("Numeric UNIX UID of the user owning this session"), + SD_VARLINK_DEFINE_FIELD(UID, SD_VARLINK_INT, 0), + SD_VARLINK_FIELD_COMMENT("PID of the session leader"), + SD_VARLINK_DEFINE_FIELD_BY_TYPE(PID, ProcessId, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The type of the session"), + SD_VARLINK_DEFINE_FIELD_BY_TYPE(Type, SessionType, 0), + SD_VARLINK_FIELD_COMMENT("The class of the session"), + SD_VARLINK_DEFINE_FIELD_BY_TYPE(Class, SessionClass, 0), + SD_VARLINK_FIELD_COMMENT("PAM service name"), + SD_VARLINK_DEFINE_FIELD(Service, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Desktop identifier"), + SD_VARLINK_DEFINE_FIELD(Desktop, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Seat this session is assigned to"), + SD_VARLINK_DEFINE_FIELD(Seat, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Virtual terminal number of the session, if applicable"), + SD_VARLINK_DEFINE_FIELD(VTNr, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("TTY device of the session"), + SD_VARLINK_DEFINE_FIELD(TTY, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("X11 display of the session"), + SD_VARLINK_DEFINE_FIELD(Display, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Whether this is a remote session"), + SD_VARLINK_DEFINE_FIELD(Remote, SD_VARLINK_BOOL, 0), + SD_VARLINK_FIELD_COMMENT("Remote host, if this is a remote session"), + SD_VARLINK_DEFINE_FIELD(RemoteHost, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Remote user, if this is a remote session"), + SD_VARLINK_DEFINE_FIELD(RemoteUser, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Device paths this session has special access to"), + SD_VARLINK_DEFINE_FIELD(ExtraDeviceAccess, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY)); + +static SD_VARLINK_DEFINE_STRUCT_TYPE( + SessionRuntime, + SD_VARLINK_FIELD_COMMENT("The session identifier"), + SD_VARLINK_DEFINE_FIELD(Id, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("systemd scope unit name"), + SD_VARLINK_DEFINE_FIELD(Scope, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Audit session ID"), + SD_VARLINK_DEFINE_FIELD(Audit, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The session timestamps"), + SD_VARLINK_DEFINE_FIELD_BY_TYPE(Timestamp, Timestamp, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Current state of the session"), + SD_VARLINK_DEFINE_FIELD(State, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("Whether the session is active (i.e. in the foreground)"), + SD_VARLINK_DEFINE_FIELD(Active, SD_VARLINK_BOOL, 0), + SD_VARLINK_FIELD_COMMENT("Whether the session is idle"), + SD_VARLINK_DEFINE_FIELD(IdleHint, SD_VARLINK_BOOL, 0), + SD_VARLINK_FIELD_COMMENT("Timestamp when the session went idle, only present when IdleHint is true"), + SD_VARLINK_DEFINE_FIELD_BY_TYPE(IdleSinceHint, Timestamp, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Whether the session is currently locked"), + SD_VARLINK_DEFINE_FIELD(LockedHint, SD_VARLINK_BOOL, 0), + SD_VARLINK_FIELD_COMMENT("Whether this session class supports idle tracking"), + SD_VARLINK_DEFINE_FIELD(CanIdle, SD_VARLINK_BOOL, 0), + SD_VARLINK_FIELD_COMMENT("Whether this session class supports locking"), + SD_VARLINK_DEFINE_FIELD(CanLock, SD_VARLINK_BOOL, 0)); + static SD_VARLINK_DEFINE_METHOD( CreateSession, SD_VARLINK_FIELD_COMMENT("Numeric UNIX UID of the user this session shall be owned by"), @@ -83,6 +139,18 @@ static SD_VARLINK_DEFINE_METHOD( SD_VARLINK_FIELD_COMMENT("The assigned session class"), SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(Class, SessionClass, 0)); +static SD_VARLINK_DEFINE_METHOD_FULL( + ListSessions, + SD_VARLINK_SUPPORTS_MORE, + SD_VARLINK_FIELD_COMMENT("If non-null, the identifier string of a session to look up. Supports the special names 'self' and 'auto' which resolve to the caller's session."), + SD_VARLINK_DEFINE_INPUT(Id, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("If non-null, return the session containing the process with this PID. If both Id and PID are specified they must reference the same session, otherwise NoSuchSession is returned."), + SD_VARLINK_DEFINE_INPUT_BY_TYPE(PID, ProcessId, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Configuration of the session"), + SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(context, SessionContext, 0), + SD_VARLINK_FIELD_COMMENT("Runtime information of the session"), + SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(runtime, SessionRuntime, 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."), @@ -102,14 +170,22 @@ SD_VARLINK_DEFINE_INTERFACE( SD_VARLINK_INTERFACE_COMMENT("APIs for managing login sessions."), SD_VARLINK_SYMBOL_COMMENT("Process identifier"), &vl_type_ProcessId, + SD_VARLINK_SYMBOL_COMMENT("Dual timestamp"), + &vl_type_Timestamp, 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("Configuration aspects of a session (suitable for reuse as creation input)"), + &vl_type_SessionContext, + SD_VARLINK_SYMBOL_COMMENT("Runtime state and dynamic information of a session"), + &vl_type_SessionRuntime, 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."), + SD_VARLINK_SYMBOL_COMMENT("Releases an existing session. Currently, will be refused unless originating from the session to release itself."), &vl_method_ReleaseSession, + SD_VARLINK_SYMBOL_COMMENT("Lists current sessions. If an ID or PID filter is provided, returns the single matching session; otherwise streams all current sessions (requires the 'more' flag)."), + &vl_method_ListSessions, SD_VARLINK_SYMBOL_COMMENT("No session by this name found"), &vl_error_NoSuchSession, SD_VARLINK_SYMBOL_COMMENT("No seat by this name found"), diff --git a/src/shared/varlink-io.systemd.Login.h b/src/shared/varlink-io.systemd.Login.h index 5453cdfb25..52a2ddd974 100644 --- a/src/shared/varlink-io.systemd.Login.h +++ b/src/shared/varlink-io.systemd.Login.h @@ -3,4 +3,7 @@ #include "sd-varlink-idl.h" +extern const sd_varlink_symbol vl_type_SessionType; +extern const sd_varlink_symbol vl_type_SessionClass; + extern const sd_varlink_interface vl_interface_io_systemd_Login; diff --git a/src/test/meson.build b/src/test/meson.build index 0c8dba64d5..8a25cc03dc 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -494,6 +494,10 @@ executables += [ 'sources' : files('test-varlink-idl-machine.c'), 'objects' : ['systemd-machined'], }, + test_template + { + 'sources' : files('test-varlink-idl-login.c'), + 'objects' : ['systemd-logind'], + }, test_template + { 'sources' : files('test-watchdog.c'), 'type' : 'unsafe', diff --git a/src/test/test-varlink-idl-login.c b/src/test/test-varlink-idl-login.c new file mode 100644 index 0000000000..7dff80178f --- /dev/null +++ b/src/test/test-varlink-idl-login.c @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "logind-session.h" +#include "tests.h" +#include "test-varlink-idl-util.h" +#include "varlink-io.systemd.Login.h" + +TEST(login_enums_idl) { + TEST_IDL_ENUM(SessionType, session_type, vl_type_SessionType); + /* SessionClass has SESSION_NONE ("none") as an internal sentinel that is intentionally not exposed + * via Varlink. Only validate the IDL→C direction. */ + TEST_IDL_ENUM_FROM_STRING(SessionClass, session_class, vl_type_SessionClass); +} + +DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/test/units/TEST-35-LOGIN.sh b/test/units/TEST-35-LOGIN.sh index 58b3bcca07..74e83c79d4 100755 --- a/test/units/TEST-35-LOGIN.sh +++ b/test/units/TEST-35-LOGIN.sh @@ -797,8 +797,113 @@ EOF systemctl stop "$RUN0UNIT3" } +teardown_varlink() ( + set +ex + + cleanup_session + + return 0 +) + testcase_varlink() { - varlinkctl introspect /run/systemd/io.systemd.Login + local session uid session_out + + if [[ ! -c /dev/tty2 ]]; then + echo "/dev/tty2 does not exist, skipping test ${FUNCNAME[0]}." + return + fi + + trap teardown_varlink RETURN + + local VARLINK_SOCKET="/run/systemd/io.systemd.Login" + + : "--- Introspect ---" + varlinkctl introspect "$VARLINK_SOCKET" + varlinkctl introspect "$VARLINK_SOCKET" | grep "method ListSessions" >/dev/null + + : "--- Setup test session ---" + create_session + session=$(loginctl --no-legend | grep -v manager | awk '$3 == "logind-test-user" { print $1 }') + uid=$(id -ru logind-test-user) + local leader_pid + leader_pid=$(loginctl show-session "$session" -p Leader --value) + loginctl activate "$session" + + : "--- ListSessions: Id filter (single reply) ---" + session_out=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListSessions "{\"Id\":\"$session\"}") + echo "$session_out" | jq -e ".runtime.Id == \"$session\"" >/dev/null + echo "$session_out" | jq -e ".context.UID == $uid" >/dev/null + echo "$session_out" | jq -e ".context.PID.pid == $leader_pid" >/dev/null + echo "$session_out" | jq -e '.context.TTY == "tty2"' >/dev/null + echo "$session_out" | jq -e '.context.Remote == false' >/dev/null + echo "$session_out" | jq -e '.context.Type == "tty"' >/dev/null + echo "$session_out" | jq -e '.context.Class == "user"' >/dev/null + echo "$session_out" | jq -e '.runtime.CanIdle == true' >/dev/null + echo "$session_out" | jq -e '.runtime.CanLock == true' >/dev/null + echo "$session_out" | jq -e '.runtime.State == "active"' >/dev/null + echo "$session_out" | jq -e '.runtime.Active == true' >/dev/null + # ExtraDeviceAccess may be absent (empty) but must be an array if present. + echo "$session_out" | jq -e '(.context | has("ExtraDeviceAccess") | not) or (.context.ExtraDeviceAccess | type == "array")' >/dev/null + + # nonexistent session id + (! varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListSessions '{"Id":"nonexistent"}') + + : "--- ListSessions: PID filter (single reply) ---" + # Look up the session containing the session leader's PID. + local pid_out + pid_out=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListSessions "{\"PID\":{\"pid\":$leader_pid}}") + echo "$pid_out" | jq -e ".runtime.Id == \"$session\"" >/dev/null + + : "--- ListSessions: Id+PID consistency check ---" + # Same session referenced two ways: must succeed. + local ok_out mismatch_err + ok_out=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListSessions \ + "{\"Id\":\"$session\",\"PID\":{\"pid\":$leader_pid}}") + echo "$ok_out" | jq -e ".runtime.Id == \"$session\"" >/dev/null + # Mismatched Id and PID (PID 1 is not in $session): must fail with NoSuchSession. + mismatch_err=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListSessions \ + "{\"Id\":\"$session\",\"PID\":{\"pid\":1}}" 2>&1 || true) + echo "$mismatch_err" | grep NoSuchSession >/dev/null + + : "--- ListSessions: streaming path ---" + # varlinkctl --more emits RFC 7464 JSON-seq (each record prefixed with RS/0x1e), so jq --seq is required. + varlinkctl call --more "$VARLINK_SOCKET" io.systemd.Login.ListSessions '{}' \ + | jq --seq -e --arg s "$session" 'select(.runtime.Id == $s)' >/dev/null + test "$(varlinkctl call --more "$VARLINK_SOCKET" io.systemd.Login.ListSessions '{}' | wc -l)" -ge 2 + # without --more and no filter: must fail with ExpectedMore (not assert()) so logind stays running. + local list_err + list_err=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListSessions '{}' 2>&1 || true) + # varlinkctl rewrites SD_VARLINK_ERROR_EXPECTED_MORE to the friendly description + # below (see src/varlinkctl/varlinkctl.c), so match on that substring rather than + # the raw error id. + echo "$list_err" | grep "'more' flag" >/dev/null + systemctl is-active systemd-logind.service >/dev/null + + : "--- ReleaseSession: NULL ID resolves to caller's session ---" + # A caller with a logind session calling ReleaseSession '{}' (no ID) must + # release its own session. We spawn the call inside a transient unit with + # PAM so pam_systemd creates a real session for the caller. + local PAMSERVICE_REL="pamserv-rel$RANDOM" + cat >/etc/pam.d/"$PAMSERVICE_REL" <&1 || true) + echo "$no_sess_rel" | grep NoSuchSession >/dev/null } testcase_restart() { From 355f73a564fc8c3d439a7e5e12d81e74ab296836 Mon Sep 17 00:00:00 2001 From: Yaping Li <202858510+YapingLi04@users.noreply.github.com> Date: Sun, 10 May 2026 14:50:13 +0000 Subject: [PATCH 4/6] logind: add ListUsers Varlink method The Varlink ListUsers method accepts optional UID and PID filters, folding in the D-Bus GetUser(u) and GetUserByPID(u) lookups. Passing a unique-key filter (UID and/or PID) yields a single reply on match, or NoSuchUser on miss. Passing no filter with the 'more' flag streams the full list; passing no filter without 'more' resolves to the caller's user (preserving the ergonomic default of GetUser). If both UID and PID are specified they must reference the same user, otherwise NoSuchUser is returned. The UserInfo type in the io.systemd.Login Varlink IDL carries all user properties matching the D-Bus org.freedesktop.login1.User interface. --- src/login/logind-user.c | 72 +++++++++++++++ src/login/logind-user.h | 2 + src/login/logind-varlink.c | 128 ++++++++++++++++++++++++++ src/shared/varlink-io.systemd.Login.c | 53 +++++++++++ test/units/TEST-35-LOGIN.sh | 46 ++++++++- 5 files changed, 300 insertions(+), 1 deletion(-) diff --git a/src/login/logind-user.c b/src/login/logind-user.c index a9e56352f8..422fcd1b51 100644 --- a/src/login/logind-user.c +++ b/src/login/logind-user.c @@ -17,6 +17,7 @@ #include "format-util.h" #include "fs-util.h" #include "hashmap.h" +#include "json-util.h" #include "limits-util.h" #include "logind-session.h" #include "logind.h" @@ -961,6 +962,77 @@ void user_update_last_session_timer(User *u) { FORMAT_TIMESPAN(user_stop_delay, USEC_PER_MSEC)); } +static int user_sessions_build_json(sd_json_variant **ret, const char *name, void *userdata) { + User *u = ASSERT_PTR(userdata); + int r; + + assert(ret); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + LIST_FOREACH(sessions_by_user, session, u->sessions) { + r = sd_json_variant_append_arrayb(&v, SD_JSON_BUILD_STRING(session->id)); + if (r < 0) + return r; + } + + *ret = TAKE_PTR(v); + return 0; +} + +static int user_context_build_json(sd_json_variant **ret, const char *name, void *userdata) { + User *u = ASSERT_PTR(userdata); + + assert(ret); + assert(u->user_record); + + int linger = user_check_linger_file(u); + if (linger == -ENOMEM) + return linger; + if (linger < 0) + log_warning_errno(linger, + "Failed to check linger file for user '%s', assuming disabled: %m", + u->user_record->user_name); + + return sd_json_buildo( + ret, + SD_JSON_BUILD_PAIR_UNSIGNED("UID", u->user_record->uid), + SD_JSON_BUILD_PAIR_UNSIGNED("GID", u->user_record->gid), + SD_JSON_BUILD_PAIR_STRING("Name", u->user_record->user_name), + SD_JSON_BUILD_PAIR_BOOLEAN("Linger", linger > 0), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Service", u->service_manager_unit), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Slice", u->slice), + JSON_BUILD_PAIR_STRING_NON_EMPTY("RuntimePath", u->runtime_path)); +} + +static int user_runtime_build_json(sd_json_variant **ret, const char *name, void *userdata) { + User *u = ASSERT_PTR(userdata); + + assert(ret); + + dual_timestamp idle_ts; + bool idle = user_get_idle_hint(u, &idle_ts); + + return sd_json_buildo( + ret, + JSON_BUILD_PAIR_STRING_NON_EMPTY("Display", u->display ? u->display->id : NULL), + JSON_BUILD_PAIR_ENUM("State", user_state_to_string(user_get_state(u))), + JSON_BUILD_PAIR_CALLBACK_NON_NULL("Sessions", user_sessions_build_json, u), + JSON_BUILD_PAIR_DUAL_TIMESTAMP_NON_NULL("Timestamp", &u->timestamp), + SD_JSON_BUILD_PAIR_BOOLEAN("IdleHint", idle), + SD_JSON_BUILD_PAIR_CONDITION(idle, "IdleSinceHint", JSON_BUILD_DUAL_TIMESTAMP(&idle_ts))); +} + +int user_build_json(User *u, sd_json_variant **ret) { + assert(u); + assert(u->user_record); + assert(ret); + + return sd_json_buildo( + ret, + SD_JSON_BUILD_PAIR_CALLBACK("context", user_context_build_json, u), + SD_JSON_BUILD_PAIR_CALLBACK("runtime", user_runtime_build_json, u)); +} + 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 5e04e25a87..b71f27aa75 100644 --- a/src/login/logind-user.h +++ b/src/login/logind-user.h @@ -83,6 +83,8 @@ int user_check_linger_file(const User *u); void user_elect_display(User *u); void user_update_last_session_timer(User *u); +int user_build_json(User *u, sd_json_variant **ret); + DECLARE_STRING_TABLE_LOOKUP(user_state, UserState); DECLARE_STRING_TABLE_LOOKUP(user_gc_mode, UserGCMode); diff --git a/src/login/logind-varlink.c b/src/login/logind-varlink.c index 9b58791991..e7fd4febd0 100644 --- a/src/login/logind-varlink.c +++ b/src/login/logind-varlink.c @@ -459,6 +459,133 @@ static int vl_method_list_sessions(sd_varlink *link, sd_json_variant *parameters return 0; } +static int manager_varlink_get_user_by_uid_or_pidref( + Manager *m, + uid_t uid, + const PidRef *pidref, + User **ret) { + + int r; + + assert(m); + assert(ret); + + /* Resolves a user by UID and/or PID. At least one filter must be set. If both are set they must + * reference the same user, otherwise -ESRCH is returned. Returns -ESRCH on "not found". Returns + * negative errno on other failures. */ + + User *by_uid = NULL; + if (uid_is_valid(uid)) { + by_uid = hashmap_get(m->users, UID_TO_PTR(uid)); + if (!by_uid) + return -ESRCH; + } + + User *by_pid = NULL; + if (pidref && pidref_is_set(pidref)) { + uid_t pid_uid; + r = cg_pidref_get_owner_uid(pidref, &pid_uid); + if (r < 0) { + if (!IN_SET(r, -ENXIO, -ENOENT)) + return log_debug_errno(r, "Failed to acquire owning UID of PID " PID_FMT ": %m", pidref->pid); + /* -ENXIO: PID not in a user-N.slice cgroup (e.g. PID 1). + * -ENOENT: cgroup gone (process terminated). + * Translate to -ESRCH so the caller maps to the NoSuchUser sentinel. */ + return -ESRCH; + } + + by_pid = hashmap_get(m->users, UID_TO_PTR(pid_uid)); + if (!by_pid) + return -ESRCH; + } + + if (by_uid && by_pid && by_uid != by_pid) + return log_debug_errno(SYNTHETIC_ERRNO(ESRCH), + "Search by UID " UID_FMT " and PID " PID_FMT " resulted in two different users", + uid, pidref->pid); + + assert(by_uid || by_pid); + + *ret = by_uid ?: by_pid; + return 0; +} + +static int emit_user_reply(sd_varlink *link, User *user) { + assert(link); + assert(user); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + int r = user_build_json(user, &v); + if (r < 0) + return r; + + return sd_varlink_reply(link, v); +} + +typedef struct ListUsersParameters { + uid_t uid; + PidRef pidref; +} ListUsersParameters; + +static void list_users_parameters_done(ListUsersParameters *p) { + assert(p); + pidref_done(&p->pidref); +} + +static int vl_method_list_users(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(ListUsersParameters, uid), 0 }, + { "PID", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_pidref, offsetof(ListUsersParameters, pidref), 0 }, + {} + }; + + _cleanup_(list_users_parameters_done) ListUsersParameters p = { + .uid = UID_INVALID, + .pidref = PIDREF_NULL, + }; + + r = sd_varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + /* Unique-key path: UID and/or PID provided. Single reply or NoSuchUser. */ + if (uid_is_valid(p.uid) || pidref_is_set(&p.pidref)) { + r = sd_varlink_set_sentinel(link, "io.systemd.Login.NoSuchUser"); + if (r < 0) + return r; + + User *user; + r = manager_varlink_get_user_by_uid_or_pidref(m, p.uid, &p.pidref, &user); + if (r == -ESRCH) + return 0; /* triggers NoSuchUser sentinel */ + if (r < 0) + return r; + + return emit_user_reply(link, user); + } + + /* Streaming path: no filter. Full list, requires 'more' flag. */ + if (!FLAGS_SET(flags, SD_VARLINK_METHOD_MORE)) + return sd_varlink_error(link, SD_VARLINK_ERROR_EXPECTED_MORE, /* parameters= */ NULL); + + /* Empty hashmap is a valid empty stream, not "not found" — see vl_method_list_inhibitors. */ + r = sd_varlink_set_sentinel(link, /* error_id= */ NULL); + if (r < 0) + return r; + + User *user; + HASHMAP_FOREACH(user, m->users) { + r = emit_user_reply(link, user); + if (r < 0) + return r; + } + + return 0; +} + 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; @@ -627,6 +754,7 @@ int manager_varlink_init(Manager *m, int fd) { "io.systemd.Login.CreateSession", vl_method_create_session, "io.systemd.Login.ReleaseSession", vl_method_release_session, "io.systemd.Login.ListSessions", vl_method_list_sessions, + "io.systemd.Login.ListUsers", vl_method_list_users, "io.systemd.Shutdown.PowerOff", vl_method_power_off, "io.systemd.Shutdown.Reboot", vl_method_reboot, "io.systemd.Shutdown.Halt", vl_method_halt, diff --git a/src/shared/varlink-io.systemd.Login.c b/src/shared/varlink-io.systemd.Login.c index 42cd0aa95a..1060552b21 100644 --- a/src/shared/varlink-io.systemd.Login.c +++ b/src/shared/varlink-io.systemd.Login.c @@ -156,7 +156,52 @@ static SD_VARLINK_DEFINE_METHOD( 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_STRUCT_TYPE( + UserContext, + SD_VARLINK_FIELD_COMMENT("Numeric UNIX UID"), + SD_VARLINK_DEFINE_FIELD(UID, SD_VARLINK_INT, 0), + SD_VARLINK_FIELD_COMMENT("Numeric UNIX GID"), + SD_VARLINK_DEFINE_FIELD(GID, SD_VARLINK_INT, 0), + SD_VARLINK_FIELD_COMMENT("User name"), + SD_VARLINK_DEFINE_FIELD(Name, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("Whether lingering is enabled for this user"), + SD_VARLINK_DEFINE_FIELD(Linger, SD_VARLINK_BOOL, 0), + SD_VARLINK_FIELD_COMMENT("Service manager unit name"), + SD_VARLINK_DEFINE_FIELD(Service, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("User slice unit name"), + SD_VARLINK_DEFINE_FIELD(Slice, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Path to the user's runtime directory"), + SD_VARLINK_DEFINE_FIELD(RuntimePath, SD_VARLINK_STRING, SD_VARLINK_NULLABLE)); + +static SD_VARLINK_DEFINE_STRUCT_TYPE( + UserRuntime, + SD_VARLINK_FIELD_COMMENT("Display session identifier"), + SD_VARLINK_DEFINE_FIELD(Display, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Current state of the user"), + SD_VARLINK_DEFINE_FIELD(State, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Identifiers of sessions belonging to this user"), + SD_VARLINK_DEFINE_FIELD(Sessions, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY), + SD_VARLINK_FIELD_COMMENT("Timestamp when the user was 'started' for the first time"), + SD_VARLINK_DEFINE_FIELD_BY_TYPE(Timestamp, Timestamp, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Whether the user is idle"), + SD_VARLINK_DEFINE_FIELD(IdleHint, SD_VARLINK_BOOL, 0), + SD_VARLINK_FIELD_COMMENT("Timestamp when the user went idle, only present when IdleHint is true"), + SD_VARLINK_DEFINE_FIELD_BY_TYPE(IdleSinceHint, Timestamp, SD_VARLINK_NULLABLE)); + +static SD_VARLINK_DEFINE_METHOD_FULL( + ListUsers, + SD_VARLINK_SUPPORTS_MORE, + SD_VARLINK_FIELD_COMMENT("If non-null, the UNIX UID of a user to look up."), + SD_VARLINK_DEFINE_INPUT(UID, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("If non-null, return the user owning the cgroup containing the process with this PID. If both UID and PID are specified they must reference the same user, otherwise NoSuchUser is returned."), + SD_VARLINK_DEFINE_INPUT_BY_TYPE(PID, ProcessId, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Configuration of the user"), + SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(context, UserContext, 0), + SD_VARLINK_FIELD_COMMENT("Runtime information of the user"), + SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(runtime, UserRuntime, 0)); + static SD_VARLINK_DEFINE_ERROR(NoSuchSession); +static SD_VARLINK_DEFINE_ERROR(NoSuchUser); static SD_VARLINK_DEFINE_ERROR(NoSuchSeat); static SD_VARLINK_DEFINE_ERROR(AlreadySessionMember); static SD_VARLINK_DEFINE_ERROR(VirtualTerminalAlreadyTaken); @@ -186,10 +231,18 @@ SD_VARLINK_DEFINE_INTERFACE( &vl_method_ReleaseSession, SD_VARLINK_SYMBOL_COMMENT("Lists current sessions. If an ID or PID filter is provided, returns the single matching session; otherwise streams all current sessions (requires the 'more' flag)."), &vl_method_ListSessions, + SD_VARLINK_SYMBOL_COMMENT("Configuration aspects of a user"), + &vl_type_UserContext, + SD_VARLINK_SYMBOL_COMMENT("Runtime state and dynamic information of a user"), + &vl_type_UserRuntime, + SD_VARLINK_SYMBOL_COMMENT("Lists current users. If a UID or PID filter is provided, returns the single matching user; otherwise streams all current users (requires the 'more' flag). If called with no parameters and no 'more' flag, resolves to the caller's user."), + &vl_method_ListUsers, 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("No user by this UID found"), + &vl_error_NoSuchUser, 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"), diff --git a/test/units/TEST-35-LOGIN.sh b/test/units/TEST-35-LOGIN.sh index 74e83c79d4..f9d6e853ff 100755 --- a/test/units/TEST-35-LOGIN.sh +++ b/test/units/TEST-35-LOGIN.sh @@ -806,7 +806,7 @@ teardown_varlink() ( ) testcase_varlink() { - local session uid session_out + local session uid session_out user_out if [[ ! -c /dev/tty2 ]]; then echo "/dev/tty2 does not exist, skipping test ${FUNCNAME[0]}." @@ -820,6 +820,7 @@ testcase_varlink() { : "--- Introspect ---" varlinkctl introspect "$VARLINK_SOCKET" varlinkctl introspect "$VARLINK_SOCKET" | grep "method ListSessions" >/dev/null + varlinkctl introspect "$VARLINK_SOCKET" | grep "method ListUsers" >/dev/null : "--- Setup test session ---" create_session @@ -879,6 +880,49 @@ testcase_varlink() { echo "$list_err" | grep "'more' flag" >/dev/null systemctl is-active systemd-logind.service >/dev/null + : "--- ListUsers: UID filter (single reply) ---" + user_out=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListUsers "{\"UID\":$uid}") + echo "$user_out" | jq -e ".context.UID == $uid" >/dev/null + echo "$user_out" | jq -e '.context.Name == "logind-test-user"' >/dev/null + echo "$user_out" | jq -e '.runtime.State' >/dev/null + echo "$user_out" | jq -e '.context.Linger == false' >/dev/null + # Sessions is now a flat array of session id strings (no wrapper object). + echo "$user_out" | jq -e ".runtime.Sessions[] | select(. == \"$session\")" >/dev/null + + : "--- ListUsers: PID filter (single reply) ---" + # PID of a process in the test user's session should map back to the user. + local pid_user_out + pid_user_out=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListUsers \ + "{\"PID\":{\"pid\":$leader_pid}}") + echo "$pid_user_out" | jq -e --argjson u "$uid" '.context.UID == $u' >/dev/null + + : "--- ListUsers: UID+PID consistency check ---" + # Same user referenced two ways: must succeed. + local ok_user_out mismatch_user_err + ok_user_out=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListUsers \ + "{\"UID\":$uid,\"PID\":{\"pid\":$leader_pid}}") + echo "$ok_user_out" | jq -e --argjson u "$uid" '.context.UID == $u' >/dev/null + # Mismatched UID and PID (PID 1 is systemd init, UID 0): must fail with NoSuchUser. + mismatch_user_err=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListUsers \ + "{\"UID\":$uid,\"PID\":{\"pid\":1}}" 2>&1 || true) + echo "$mismatch_user_err" | grep NoSuchUser >/dev/null + + : "--- ListUsers: empty input without --more must require --more ---" + # The DescribeUser-style caller-UID fallback was removed; a no-filter call without + # --more must fail with the EXPECTED_MORE error. + local empty_users_err + empty_users_err=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListUsers '{}' 2>&1 || true) + echo "$empty_users_err" | grep "'more' flag" >/dev/null + systemctl is-active systemd-logind.service >/dev/null + + # nonexistent UID + (! varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListUsers '{"UID":4294967294}') + + : "--- ListUsers: streaming path ---" + varlinkctl call --more "$VARLINK_SOCKET" io.systemd.Login.ListUsers '{}' \ + | jq --seq -e 'select(.context.Name == "logind-test-user")' >/dev/null + test "$(varlinkctl call --more "$VARLINK_SOCKET" io.systemd.Login.ListUsers '{}' | wc -l)" -ge 2 + : "--- ReleaseSession: NULL ID resolves to caller's session ---" # A caller with a logind session calling ReleaseSession '{}' (no ID) must # release its own session. We spawn the call inside a transient unit with From b4de3e5759963eaabeb96b7d17770214bc8700b9 Mon Sep 17 00:00:00 2001 From: Yaping Li <202858510+YapingLi04@users.noreply.github.com> Date: Sun, 10 May 2026 14:50:13 +0000 Subject: [PATCH 5/6] logind: add ListSeats Varlink method The Varlink ListSeats method accepts an optional Id filter, folding in the D-Bus GetSeat(s) lookup. Passing Id yields a single reply on match, or NoSuchSeat on miss. Passing no Id with the 'more' flag streams the full list; passing no Id without 'more' resolves to the caller's seat (preserving the ergonomic default of GetSeat). The Id filter supports the special names "self" and "auto" which resolve to the caller's seat. The SeatInfo type in the io.systemd.Login Varlink IDL carries all seat properties matching the D-Bus org.freedesktop.login1.Seat interface. --- src/login/logind-seat.c | 56 ++++++++++++++ src/login/logind-seat.h | 2 + src/login/logind-varlink.c | 105 ++++++++++++++++++++++++++ src/shared/varlink-io.systemd.Login.c | 36 +++++++++ test/units/TEST-35-LOGIN.sh | 35 ++++++++- 5 files changed, 233 insertions(+), 1 deletion(-) diff --git a/src/login/logind-seat.c b/src/login/logind-seat.c index c20c6bb3c7..1fe1eb41f4 100644 --- a/src/login/logind-seat.c +++ b/src/login/logind-seat.c @@ -16,6 +16,7 @@ #include "fs-util.h" #include "hashmap.h" #include "id128-util.h" +#include "json-util.h" #include "log.h" #include "logind.h" #include "logind-device.h" @@ -826,6 +827,61 @@ void seat_add_to_gc_queue(Seat *s) { s->in_gc_queue = true; } +static int seat_sessions_build_json(sd_json_variant **ret, const char *name, void *userdata) { + Seat *s = ASSERT_PTR(userdata); + int r; + + assert(ret); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + LIST_FOREACH(sessions_by_seat, session, s->sessions) { + r = sd_json_variant_append_arrayb(&v, SD_JSON_BUILD_STRING(session->id)); + if (r < 0) + return r; + } + + *ret = TAKE_PTR(v); + return 0; +} + +static int seat_context_build_json(sd_json_variant **ret, const char *name, void *userdata) { + Seat *s = ASSERT_PTR(userdata); + + assert(ret); + + return sd_json_buildo( + ret, + SD_JSON_BUILD_PAIR_STRING("Id", s->id)); +} + +static int seat_runtime_build_json(sd_json_variant **ret, const char *name, void *userdata) { + Seat *s = ASSERT_PTR(userdata); + + assert(ret); + + dual_timestamp idle_ts; + bool idle = seat_get_idle_hint(s, &idle_ts); + + return sd_json_buildo( + ret, + JSON_BUILD_PAIR_STRING_NON_EMPTY("ActiveSession", s->active ? s->active->id : NULL), + JSON_BUILD_PAIR_CALLBACK_NON_NULL("Sessions", seat_sessions_build_json, s), + SD_JSON_BUILD_PAIR_BOOLEAN("CanTTY", seat_can_tty(s)), + SD_JSON_BUILD_PAIR_BOOLEAN("CanGraphical", seat_can_graphical(s)), + SD_JSON_BUILD_PAIR_BOOLEAN("IdleHint", idle), + SD_JSON_BUILD_PAIR_CONDITION(idle, "IdleSinceHint", JSON_BUILD_DUAL_TIMESTAMP(&idle_ts))); +} + +int seat_build_json(Seat *s, sd_json_variant **ret) { + assert(s); + assert(ret); + + return sd_json_buildo( + ret, + SD_JSON_BUILD_PAIR_CALLBACK("context", seat_context_build_json, s), + SD_JSON_BUILD_PAIR_CALLBACK("runtime", seat_runtime_build_json, s)); +} + static bool seat_name_valid_char(char c) { return ascii_isalpha(c) || diff --git a/src/login/logind-seat.h b/src/login/logind-seat.h index 5ca657cafb..3f6d2aaf3c 100644 --- a/src/login/logind-seat.h +++ b/src/login/logind-seat.h @@ -64,6 +64,8 @@ int seat_stop_sessions(Seat *s, bool force); bool seat_may_gc(Seat *s, bool drop_not_started); void seat_add_to_gc_queue(Seat *s); +int seat_build_json(Seat *s, sd_json_variant **ret); + bool seat_name_is_valid(const char *name); bool seat_is_self(const char *name); bool seat_is_auto(const char *name); diff --git a/src/login/logind-varlink.c b/src/login/logind-varlink.c index e7fd4febd0..89dcbf8778 100644 --- a/src/login/logind-varlink.c +++ b/src/login/logind-varlink.c @@ -586,6 +586,110 @@ static int vl_method_list_users(sd_varlink *link, sd_json_variant *parameters, s return 0; } +static int manager_varlink_get_seat_by_name( + Manager *m, + sd_varlink *link, + const char *name, + Seat **ret) { + + int r; + + assert(m); + assert(link); + assert(name); + assert(ret); + + /* Resolves a seat name to a seat object. Supports the special names "self" and "auto" — these + * resolve to the seat of the caller's session. Returns -EINVAL on invalid seat name. Returns + * -ESRCH on "not found". Caller is expected to turn that into a varlink error. */ + + if (seat_is_self(name) || seat_is_auto(name)) { + Session *session; + r = manager_varlink_get_session_by_peer(m, link, /* consult_display= */ seat_is_auto(name), &session); + if (r < 0) + return r; + if (!session || !session->seat) + return -ESRCH; + + *ret = session->seat; + return 0; + } + + if (!seat_name_is_valid(name)) + return -EINVAL; + + Seat *seat = hashmap_get(m->seats, name); + if (!seat) + return -ESRCH; + + *ret = seat; + return 0; +} + +static int emit_seat_reply(sd_varlink *link, Seat *seat) { + assert(link); + assert(seat); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + int r = seat_build_json(seat, &v); + if (r < 0) + return r; + + return sd_varlink_reply(link, v); +} + +static int vl_method_list_seats(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + int r; + + const char *id = NULL; + + static const sd_json_dispatch_field dispatch_table[] = { + { "Id", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, 0, 0 }, + {} + }; + + r = sd_varlink_dispatch(link, parameters, dispatch_table, &id); + if (r != 0) + return r; + + /* Unique-key path: Id provided. Single reply or NoSuchSeat. */ + if (id) { + r = sd_varlink_set_sentinel(link, "io.systemd.Login.NoSuchSeat"); + if (r < 0) + return r; + + Seat *seat; + r = manager_varlink_get_seat_by_name(m, link, id, &seat); + if (r == -EINVAL) + return sd_varlink_error_invalid_parameter_name(link, "Id"); + if (r == -ESRCH) + return 0; /* triggers NoSuchSeat sentinel */ + if (r < 0) + return r; + + return emit_seat_reply(link, seat); + } + + /* Streaming path: no filter. Full list, requires 'more' flag. */ + if (!FLAGS_SET(flags, SD_VARLINK_METHOD_MORE)) + return sd_varlink_error(link, SD_VARLINK_ERROR_EXPECTED_MORE, /* parameters= */ NULL); + + /* Empty hashmap is a valid empty stream, not "not found" — see vl_method_list_inhibitors. */ + r = sd_varlink_set_sentinel(link, /* error_id= */ NULL); + if (r < 0) + return r; + + Seat *seat; + HASHMAP_FOREACH(seat, m->seats) { + r = emit_seat_reply(link, seat); + if (r < 0) + return r; + } + + return 0; +} + 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; @@ -755,6 +859,7 @@ int manager_varlink_init(Manager *m, int fd) { "io.systemd.Login.ReleaseSession", vl_method_release_session, "io.systemd.Login.ListSessions", vl_method_list_sessions, "io.systemd.Login.ListUsers", vl_method_list_users, + "io.systemd.Login.ListSeats", vl_method_list_seats, "io.systemd.Shutdown.PowerOff", vl_method_power_off, "io.systemd.Shutdown.Reboot", vl_method_reboot, "io.systemd.Shutdown.Halt", vl_method_halt, diff --git a/src/shared/varlink-io.systemd.Login.c b/src/shared/varlink-io.systemd.Login.c index 1060552b21..5771c52c4c 100644 --- a/src/shared/varlink-io.systemd.Login.c +++ b/src/shared/varlink-io.systemd.Login.c @@ -200,6 +200,36 @@ static SD_VARLINK_DEFINE_METHOD_FULL( SD_VARLINK_FIELD_COMMENT("Runtime information of the user"), SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(runtime, UserRuntime, 0)); +static SD_VARLINK_DEFINE_STRUCT_TYPE( + SeatContext, + SD_VARLINK_FIELD_COMMENT("The seat identifier"), + SD_VARLINK_DEFINE_FIELD(Id, SD_VARLINK_STRING, 0)); + +static SD_VARLINK_DEFINE_STRUCT_TYPE( + SeatRuntime, + SD_VARLINK_FIELD_COMMENT("The currently active session on this seat, if any"), + SD_VARLINK_DEFINE_FIELD(ActiveSession, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Identifiers of sessions assigned to this seat"), + SD_VARLINK_DEFINE_FIELD(Sessions, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY), + SD_VARLINK_FIELD_COMMENT("Whether this seat supports text terminal sessions"), + SD_VARLINK_DEFINE_FIELD(CanTTY, SD_VARLINK_BOOL, 0), + SD_VARLINK_FIELD_COMMENT("Whether this seat supports graphical sessions"), + SD_VARLINK_DEFINE_FIELD(CanGraphical, SD_VARLINK_BOOL, 0), + SD_VARLINK_FIELD_COMMENT("Whether the seat is idle"), + SD_VARLINK_DEFINE_FIELD(IdleHint, SD_VARLINK_BOOL, 0), + SD_VARLINK_FIELD_COMMENT("Timestamp when the seat went idle, only present when IdleHint is true"), + SD_VARLINK_DEFINE_FIELD_BY_TYPE(IdleSinceHint, Timestamp, SD_VARLINK_NULLABLE)); + +static SD_VARLINK_DEFINE_METHOD_FULL( + ListSeats, + SD_VARLINK_SUPPORTS_MORE, + SD_VARLINK_FIELD_COMMENT("If non-null, the identifier string of a seat to look up."), + SD_VARLINK_DEFINE_INPUT(Id, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Configuration of the seat"), + SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(context, SeatContext, 0), + SD_VARLINK_FIELD_COMMENT("Runtime information of the seat"), + SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(runtime, SeatRuntime, 0)); + static SD_VARLINK_DEFINE_ERROR(NoSuchSession); static SD_VARLINK_DEFINE_ERROR(NoSuchUser); static SD_VARLINK_DEFINE_ERROR(NoSuchSeat); @@ -237,6 +267,12 @@ SD_VARLINK_DEFINE_INTERFACE( &vl_type_UserRuntime, SD_VARLINK_SYMBOL_COMMENT("Lists current users. If a UID or PID filter is provided, returns the single matching user; otherwise streams all current users (requires the 'more' flag). If called with no parameters and no 'more' flag, resolves to the caller's user."), &vl_method_ListUsers, + SD_VARLINK_SYMBOL_COMMENT("Configuration aspects of a seat"), + &vl_type_SeatContext, + SD_VARLINK_SYMBOL_COMMENT("Runtime state and dynamic information of a seat"), + &vl_type_SeatRuntime, + SD_VARLINK_SYMBOL_COMMENT("Lists current seats. If an Id filter is provided (or the caller has a session), returns the single matching seat; otherwise streams all current seats (requires the 'more' flag). If called with no parameters and no 'more' flag, resolves to the caller's seat."), + &vl_method_ListSeats, SD_VARLINK_SYMBOL_COMMENT("No session by this name found"), &vl_error_NoSuchSession, SD_VARLINK_SYMBOL_COMMENT("No seat by this name found"), diff --git a/test/units/TEST-35-LOGIN.sh b/test/units/TEST-35-LOGIN.sh index f9d6e853ff..491b754b29 100755 --- a/test/units/TEST-35-LOGIN.sh +++ b/test/units/TEST-35-LOGIN.sh @@ -806,7 +806,7 @@ teardown_varlink() ( ) testcase_varlink() { - local session uid session_out user_out + local session uid session_out user_out seat_out self_err if [[ ! -c /dev/tty2 ]]; then echo "/dev/tty2 does not exist, skipping test ${FUNCNAME[0]}." @@ -821,6 +821,7 @@ testcase_varlink() { varlinkctl introspect "$VARLINK_SOCKET" varlinkctl introspect "$VARLINK_SOCKET" | grep "method ListSessions" >/dev/null varlinkctl introspect "$VARLINK_SOCKET" | grep "method ListUsers" >/dev/null + varlinkctl introspect "$VARLINK_SOCKET" | grep "method ListSeats" >/dev/null : "--- Setup test session ---" create_session @@ -923,6 +924,38 @@ testcase_varlink() { | jq --seq -e 'select(.context.Name == "logind-test-user")' >/dev/null test "$(varlinkctl call --more "$VARLINK_SOCKET" io.systemd.Login.ListUsers '{}' | wc -l)" -ge 2 + : "--- ListSeats: Id filter (single reply) ---" + seat_out=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListSeats '{"Id":"seat0"}') + echo "$seat_out" | jq -e '.context.Id == "seat0"' >/dev/null + echo "$seat_out" | jq -e '.runtime.CanTTY == true' >/dev/null + # Sessions is a flat array of session id strings. + echo "$seat_out" | jq -e ".runtime.Sessions[] | select(. == \"$session\")" >/dev/null + + # nonexistent seat + (! varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListSeats '{"Id":"seat-nonexistent"}') + + : "--- ListSeats: empty input without --more must require --more ---" + # The caller-seat fallback was removed; no filter + no --more = EXPECTED_MORE. + local empty_seats_err + empty_seats_err=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListSeats '{}' 2>&1 || true) + echo "$empty_seats_err" | grep "'more' flag" >/dev/null + systemctl is-active systemd-logind.service >/dev/null + + : "--- ListSeats: self/auto from session-less context yields NoSuchSeat ---" + # self/auto still resolves via peer session; running as root outside any session + # has no peer session, so we must get NoSuchSeat (not NoSuchSession leaked from + # the lookup helper). + local self_err + for id_arg in '{"Id":"self"}' '{"Id":"auto"}'; do + self_err=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListSeats "$id_arg" 2>&1 || true) + echo "$self_err" | grep NoSuchSeat >/dev/null + (! echo "$self_err" | grep NoSuchSession >/dev/null) + done + + : "--- ListSeats: streaming path ---" + varlinkctl call --more "$VARLINK_SOCKET" io.systemd.Login.ListSeats '{}' | grep "seat0" >/dev/null + test "$(varlinkctl call --more "$VARLINK_SOCKET" io.systemd.Login.ListSeats '{}' | wc -l)" -ge 1 + : "--- ReleaseSession: NULL ID resolves to caller's session ---" # A caller with a logind session calling ReleaseSession '{}' (no ID) must # release its own session. We spawn the call inside a transient unit with From d2bdaab55e8c875bae8efc6ca6328b968521ce7a Mon Sep 17 00:00:00 2001 From: Yaping Li <202858510+YapingLi04@users.noreply.github.com> Date: Sun, 10 May 2026 14:50:13 +0000 Subject: [PATCH 6/6] logind: add ListInhibitors Varlink method The Varlink ListInhibitors method is the counterpart of D-Bus ListInhibitors. Like its D-Bus counterpart it is zero-filter and streams the full list of currently registered inhibitors using the SD_VARLINK_METHOD_MORE pattern, returning InhibitorInfo objects with Id, What, Who, Why, Mode, UID, PID, and Since fields. There is no D-Bus GetInhibitor getter to fold in, so no unique-key filter is introduced here. --- src/login/logind-inhibit.c | 62 ++++++++++++++++++++++++ src/login/logind-inhibit.h | 2 + src/login/logind-varlink.c | 36 ++++++++++++++ src/shared/varlink-io.systemd.Login.c | 69 +++++++++++++++++++++++++-- src/shared/varlink-io.systemd.Login.h | 2 + src/test/test-varlink-idl-login.c | 6 +++ test/units/TEST-35-LOGIN.sh | 31 +++++++++++- 7 files changed, 204 insertions(+), 4 deletions(-) diff --git a/src/login/logind-inhibit.c b/src/login/logind-inhibit.c index 30b43404b1..99ec0a3c99 100644 --- a/src/login/logind-inhibit.c +++ b/src/login/logind-inhibit.c @@ -17,6 +17,7 @@ #include "fs-util.h" #include "hashmap.h" #include "io-util.h" +#include "json-util.h" #include "log.h" #include "logind-session.h" #include "logind.h" @@ -528,6 +529,67 @@ int inhibit_what_from_string(const char *s) { } } +static int inhibit_what_build_json(sd_json_variant **ret, const char *name, void *userdata) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + InhibitWhat *what = ASSERT_PTR(userdata); + int r; + + assert(ret); + assert(inhibit_what_is_valid(*what)); + + for (unsigned bit = 0; (1U << bit) < (unsigned) _INHIBIT_WHAT_MAX; bit++) { + InhibitWhat w = 1U << bit; + + if (!FLAGS_SET(*what, w)) + continue; + + r = sd_json_variant_append_arrayb(&v, JSON_BUILD_STRING_UNDERSCORIFY(inhibit_what_to_string(w))); + if (r < 0) + return r; + } + + *ret = TAKE_PTR(v); + return 0; +} + +static int inhibitor_context_build_json(sd_json_variant **ret, const char *name, void *userdata) { + Inhibitor *i = ASSERT_PTR(userdata); + + assert(ret); + assert(i->mode >= 0 && i->mode < _INHIBIT_MODE_MAX); + assert(inhibit_what_is_valid(i->what)); + + return sd_json_buildo( + ret, + SD_JSON_BUILD_PAIR_STRING("Id", i->id), + SD_JSON_BUILD_PAIR_CALLBACK("What", inhibit_what_build_json, &i->what), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Who", i->who), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Why", i->why), + JSON_BUILD_PAIR_ENUM("Mode", inhibit_mode_to_string(i->mode)), + SD_JSON_BUILD_PAIR_UNSIGNED("UID", i->uid)); +} + +static int inhibitor_runtime_build_json(sd_json_variant **ret, const char *name, void *userdata) { + Inhibitor *i = ASSERT_PTR(userdata); + + assert(ret); + + return sd_json_buildo( + ret, + JSON_BUILD_PAIR_PIDREF_NON_NULL("PID", &i->pid), + JSON_BUILD_PAIR_DUAL_TIMESTAMP_NON_NULL("Since", &i->since)); +} + +int inhibitor_build_json(Inhibitor *i, sd_json_variant **ret) { + assert(i); + assert(ret); + + return sd_json_buildo( + ret, + SD_JSON_BUILD_PAIR_CALLBACK("context", inhibitor_context_build_json, i), + SD_JSON_BUILD_PAIR_CALLBACK("runtime", inhibitor_runtime_build_json, i)); +} + static const char* const inhibit_mode_table[_INHIBIT_MODE_MAX] = { [INHIBIT_BLOCK] = "block", [INHIBIT_BLOCK_WEAK] = "block-weak", diff --git a/src/login/logind-inhibit.h b/src/login/logind-inhibit.h index 1cb4ed4605..c606582df2 100644 --- a/src/login/logind-inhibit.h +++ b/src/login/logind-inhibit.h @@ -80,6 +80,8 @@ bool manager_is_inhibited( uid_t uid_to_ignore, Inhibitor **ret_offending); +int inhibitor_build_json(Inhibitor *i, sd_json_variant **ret); + static inline bool inhibit_what_is_valid(InhibitWhat w) { return w > 0 && w < _INHIBIT_WHAT_MAX; } diff --git a/src/login/logind-varlink.c b/src/login/logind-varlink.c index 89dcbf8778..382f8fea22 100644 --- a/src/login/logind-varlink.c +++ b/src/login/logind-varlink.c @@ -14,6 +14,7 @@ #include "login-util.h" #include "logind.h" #include "logind-dbus.h" +#include "logind-inhibit.h" #include "logind-seat.h" #include "logind-session.h" #include "logind-shutdown.h" @@ -690,6 +691,40 @@ static int vl_method_list_seats(sd_varlink *link, sd_json_variant *parameters, s return 0; } +static int vl_method_list_inhibitors(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + int r; + + /* IDL uses SD_VARLINK_REQUIRES_MORE, so the framework rejects non-more calls before this handler. */ + + r = sd_varlink_dispatch(link, parameters, /* dispatch_table= */ NULL, /* userdata= */ NULL); + if (r != 0) + return r; + + /* Required for multi-reply streaming: sd_varlink_reply() only stashes the previous reply (for + * the 'continues' flag machinery) when v->sentinel is set. Pass NULL to send an empty reply + * (not an error) when the inhibitor hashmap is empty — this method has no point-lookup, so an + * empty list is a valid result, not a "not found" failure. */ + r = sd_varlink_set_sentinel(link, /* error_id= */ NULL); + if (r < 0) + return r; + + Inhibitor *inhibitor; + HASHMAP_FOREACH(inhibitor, m->inhibitors) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + + r = inhibitor_build_json(inhibitor, &v); + if (r < 0) + return r; + + r = sd_varlink_reply(link, v); + if (r < 0) + return r; + } + + return 0; +} + 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; @@ -860,6 +895,7 @@ int manager_varlink_init(Manager *m, int fd) { "io.systemd.Login.ListSessions", vl_method_list_sessions, "io.systemd.Login.ListUsers", vl_method_list_users, "io.systemd.Login.ListSeats", vl_method_list_seats, + "io.systemd.Login.ListInhibitors", vl_method_list_inhibitors, "io.systemd.Shutdown.PowerOff", vl_method_power_off, "io.systemd.Shutdown.Reboot", vl_method_reboot, "io.systemd.Shutdown.Halt", vl_method_halt, diff --git a/src/shared/varlink-io.systemd.Login.c b/src/shared/varlink-io.systemd.Login.c index 5771c52c4c..7d7f5f2a4a 100644 --- a/src/shared/varlink-io.systemd.Login.c +++ b/src/shared/varlink-io.systemd.Login.c @@ -230,9 +230,60 @@ static SD_VARLINK_DEFINE_METHOD_FULL( SD_VARLINK_FIELD_COMMENT("Runtime information of the seat"), SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(runtime, SeatRuntime, 0)); +SD_VARLINK_DEFINE_ENUM_TYPE( + InhibitMode, + SD_VARLINK_FIELD_COMMENT("Hard inhibition; blocks the operation entirely"), + SD_VARLINK_DEFINE_ENUM_VALUE(block), + SD_VARLINK_FIELD_COMMENT("Weak hard inhibition; like block, but the user who set it can override it without polkit authorization"), + SD_VARLINK_DEFINE_ENUM_VALUE(block_weak), + SD_VARLINK_FIELD_COMMENT("Soft inhibition; delays the operation but ultimately permits it"), + SD_VARLINK_DEFINE_ENUM_VALUE(delay)); + +SD_VARLINK_DEFINE_ENUM_TYPE( + InhibitWhat, + SD_VARLINK_DEFINE_ENUM_VALUE(shutdown), + SD_VARLINK_DEFINE_ENUM_VALUE(sleep), + SD_VARLINK_DEFINE_ENUM_VALUE(idle), + SD_VARLINK_DEFINE_ENUM_VALUE(handle_power_key), + SD_VARLINK_DEFINE_ENUM_VALUE(handle_suspend_key), + SD_VARLINK_DEFINE_ENUM_VALUE(handle_hibernate_key), + SD_VARLINK_DEFINE_ENUM_VALUE(handle_lid_switch), + SD_VARLINK_DEFINE_ENUM_VALUE(handle_reboot_key)); + +static SD_VARLINK_DEFINE_STRUCT_TYPE( + InhibitorContext, + SD_VARLINK_FIELD_COMMENT("The inhibitor identifier"), + SD_VARLINK_DEFINE_FIELD(Id, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("What is being inhibited"), + SD_VARLINK_DEFINE_FIELD_BY_TYPE(What, InhibitWhat, SD_VARLINK_ARRAY), + SD_VARLINK_FIELD_COMMENT("A human-readable descriptive string of who is taking the inhibition"), + SD_VARLINK_DEFINE_FIELD(Who, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("A human-readable descriptive string of why the inhibition is taken"), + SD_VARLINK_DEFINE_FIELD(Why, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The inhibition mode"), + SD_VARLINK_DEFINE_FIELD_BY_TYPE(Mode, InhibitMode, 0), + SD_VARLINK_FIELD_COMMENT("The UID of the user taking the inhibition"), + SD_VARLINK_DEFINE_FIELD(UID, SD_VARLINK_INT, 0)); + +static SD_VARLINK_DEFINE_STRUCT_TYPE( + InhibitorRuntime, + SD_VARLINK_FIELD_COMMENT("The PID of the process taking the inhibition"), + SD_VARLINK_DEFINE_FIELD_BY_TYPE(PID, ProcessId, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The inhibitor timestamps"), + SD_VARLINK_DEFINE_FIELD_BY_TYPE(Since, Timestamp, SD_VARLINK_NULLABLE)); + +static SD_VARLINK_DEFINE_METHOD_FULL( + ListInhibitors, + SD_VARLINK_REQUIRES_MORE, + SD_VARLINK_FIELD_COMMENT("Configuration of the inhibitor"), + SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(context, InhibitorContext, 0), + SD_VARLINK_FIELD_COMMENT("Runtime information of the inhibitor"), + SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(runtime, InhibitorRuntime, 0)); + static SD_VARLINK_DEFINE_ERROR(NoSuchSession); static SD_VARLINK_DEFINE_ERROR(NoSuchUser); static SD_VARLINK_DEFINE_ERROR(NoSuchSeat); +static SD_VARLINK_DEFINE_ERROR(NoSuchInhibitor); static SD_VARLINK_DEFINE_ERROR(AlreadySessionMember); static SD_VARLINK_DEFINE_ERROR(VirtualTerminalAlreadyTaken); static SD_VARLINK_DEFINE_ERROR(TooManySessions); @@ -259,26 +310,38 @@ SD_VARLINK_DEFINE_INTERFACE( &vl_method_CreateSession, SD_VARLINK_SYMBOL_COMMENT("Releases an existing session. Currently, will be refused unless originating from the session to release itself."), &vl_method_ReleaseSession, - SD_VARLINK_SYMBOL_COMMENT("Lists current sessions. If an ID or PID filter is provided, returns the single matching session; otherwise streams all current sessions (requires the 'more' flag)."), + SD_VARLINK_SYMBOL_COMMENT("Lists current sessions. If an Id or PID filter is provided, returns the single matching session; otherwise streams all current sessions (requires the 'more' flag)."), &vl_method_ListSessions, SD_VARLINK_SYMBOL_COMMENT("Configuration aspects of a user"), &vl_type_UserContext, SD_VARLINK_SYMBOL_COMMENT("Runtime state and dynamic information of a user"), &vl_type_UserRuntime, - SD_VARLINK_SYMBOL_COMMENT("Lists current users. If a UID or PID filter is provided, returns the single matching user; otherwise streams all current users (requires the 'more' flag). If called with no parameters and no 'more' flag, resolves to the caller's user."), + SD_VARLINK_SYMBOL_COMMENT("Lists current users. If a UID or PID filter is provided, returns the single matching user; otherwise streams all current users (requires the 'more' flag)."), &vl_method_ListUsers, SD_VARLINK_SYMBOL_COMMENT("Configuration aspects of a seat"), &vl_type_SeatContext, SD_VARLINK_SYMBOL_COMMENT("Runtime state and dynamic information of a seat"), &vl_type_SeatRuntime, - SD_VARLINK_SYMBOL_COMMENT("Lists current seats. If an Id filter is provided (or the caller has a session), returns the single matching seat; otherwise streams all current seats (requires the 'more' flag). If called with no parameters and no 'more' flag, resolves to the caller's seat."), + SD_VARLINK_SYMBOL_COMMENT("Lists current seats. If an Id filter is provided, returns the single matching seat; otherwise streams all current seats (requires the 'more' flag)."), &vl_method_ListSeats, + SD_VARLINK_SYMBOL_COMMENT("Inhibition mode"), + &vl_type_InhibitMode, + SD_VARLINK_SYMBOL_COMMENT("Operations that can be inhibited"), + &vl_type_InhibitWhat, + SD_VARLINK_SYMBOL_COMMENT("Configuration aspects of an inhibitor"), + &vl_type_InhibitorContext, + SD_VARLINK_SYMBOL_COMMENT("Runtime state and dynamic information of an inhibitor"), + &vl_type_InhibitorRuntime, + SD_VARLINK_SYMBOL_COMMENT("Lists all current inhibitors."), + &vl_method_ListInhibitors, 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("No user by this UID found"), &vl_error_NoSuchUser, + SD_VARLINK_SYMBOL_COMMENT("No inhibitor found"), + &vl_error_NoSuchInhibitor, 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"), diff --git a/src/shared/varlink-io.systemd.Login.h b/src/shared/varlink-io.systemd.Login.h index 52a2ddd974..2d751eebdf 100644 --- a/src/shared/varlink-io.systemd.Login.h +++ b/src/shared/varlink-io.systemd.Login.h @@ -5,5 +5,7 @@ extern const sd_varlink_symbol vl_type_SessionType; extern const sd_varlink_symbol vl_type_SessionClass; +extern const sd_varlink_symbol vl_type_InhibitMode; +extern const sd_varlink_symbol vl_type_InhibitWhat; extern const sd_varlink_interface vl_interface_io_systemd_Login; diff --git a/src/test/test-varlink-idl-login.c b/src/test/test-varlink-idl-login.c index 7dff80178f..fb8ef81363 100644 --- a/src/test/test-varlink-idl-login.c +++ b/src/test/test-varlink-idl-login.c @@ -1,5 +1,6 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include "logind-inhibit.h" #include "logind-session.h" #include "tests.h" #include "test-varlink-idl-util.h" @@ -10,6 +11,11 @@ TEST(login_enums_idl) { /* SessionClass has SESSION_NONE ("none") as an internal sentinel that is intentionally not exposed * via Varlink. Only validate the IDL→C direction. */ TEST_IDL_ENUM_FROM_STRING(SessionClass, session_class, vl_type_SessionClass); + TEST_IDL_ENUM(InhibitMode, inhibit_mode, vl_type_InhibitMode); + /* InhibitWhat is a bit-flag enum: inhibit_what_to_string() accepts a bitmask and + * the index does not correspond to a sequential integer, so the TEST_IDL_ENUM + * round-trip macro doesn't apply. The flag→string mapping is exercised through + * ListInhibitors integration tests in TEST-35-LOGIN.sh. */ } DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/test/units/TEST-35-LOGIN.sh b/test/units/TEST-35-LOGIN.sh index 491b754b29..e4f9621c8d 100755 --- a/test/units/TEST-35-LOGIN.sh +++ b/test/units/TEST-35-LOGIN.sh @@ -800,13 +800,14 @@ EOF teardown_varlink() ( set +ex + systemctl stop test-varlink-inhibit.service 2>/dev/null cleanup_session return 0 ) testcase_varlink() { - local session uid session_out user_out seat_out self_err + local session uid session_out user_out seat_out self_err inhibitor_out if [[ ! -c /dev/tty2 ]]; then echo "/dev/tty2 does not exist, skipping test ${FUNCNAME[0]}." @@ -822,6 +823,7 @@ testcase_varlink() { varlinkctl introspect "$VARLINK_SOCKET" | grep "method ListSessions" >/dev/null varlinkctl introspect "$VARLINK_SOCKET" | grep "method ListUsers" >/dev/null varlinkctl introspect "$VARLINK_SOCKET" | grep "method ListSeats" >/dev/null + varlinkctl introspect "$VARLINK_SOCKET" | grep "method ListInhibitors" >/dev/null : "--- Setup test session ---" create_session @@ -956,6 +958,33 @@ testcase_varlink() { varlinkctl call --more "$VARLINK_SOCKET" io.systemd.Login.ListSeats '{}' | grep "seat0" >/dev/null test "$(varlinkctl call --more "$VARLINK_SOCKET" io.systemd.Login.ListSeats '{}' | wc -l)" -ge 1 + : "--- ListInhibitors ---" + systemd-run --unit=test-varlink-inhibit.service --service-type=exec \ + systemd-inhibit --what=shutdown --who="varlink-test" --why="testing varlink" --mode=block \ + sleep infinity + timeout 10 bash -c "until varlinkctl call --more '$VARLINK_SOCKET' io.systemd.Login.ListInhibitors '{}' 2>/dev/null | grep varlink-test >/dev/null; do sleep 0.5; done" + + inhibitor_out=$(varlinkctl call --more "$VARLINK_SOCKET" io.systemd.Login.ListInhibitors '{}') + # What is now an array of strings; pick our test record and validate fields with jq. + echo "$inhibitor_out" | jq --seq -e 'select(.context.Who == "varlink-test") | .context.What | type == "array" and contains(["shutdown"])' >/dev/null + echo "$inhibitor_out" | jq --seq -e 'select(.context.Who == "varlink-test") | .context.Mode == "block"' >/dev/null + echo "$inhibitor_out" | jq --seq -e 'select(.context.Who == "varlink-test") | .context.Why == "testing varlink"' >/dev/null + + # without --more should fail (ListInhibitors has no filter, --more required) + (! varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListInhibitors '{}') + + systemctl stop test-varlink-inhibit.service + + # Best-effort empty-list check: stopping the test inhibitor doesn't guarantee + # zero entries (other system inhibitors may be registered), but the contract + # we verify holds regardless — ListInhibitors has no single-lookup path, so + # it must never emit a NoSuchInhibitor sentinel. An empty result is just an + # empty parameters reply as the stream terminator; the IDL-validation skip + # in the sentinel handler ensures this passes validation. + local empty_inhibitor_err + empty_inhibitor_err=$(varlinkctl call --more "$VARLINK_SOCKET" io.systemd.Login.ListInhibitors '{}' 2>&1 || true) + (! echo "$empty_inhibitor_err" | grep NoSuchInhibitor >/dev/null) + : "--- ReleaseSession: NULL ID resolves to caller's session ---" # A caller with a logind session calling ReleaseSession '{}' (no ID) must # release its own session. We spawn the call inside a transient unit with