From: Luca Boccassi Date: Tue, 14 Apr 2026 14:22:34 +0000 (+0100) Subject: core: add LUOSession= unit setting X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=82b8615463c306f8f7eeaec13600c89a7bbef151;p=thirdparty%2Fsystemd.git core: add LUOSession= unit setting Acquiring a LUO session from /dev/liveupdate requires privileges, and also the device is a single-owner driver so only a single process can open it at any given time. Add a LUOSession= service settings that allows units running without privileges to get a session assigned to them. The kernel imposes a 64 chars limit on session names, which is too short to avoid clashes, so derive a hash from joining the unit name with the parameter name, that way two units using the same setting don't clash. --- diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml index 2d63050a686..42455ef6c7a 100644 --- a/man/org.freedesktop.systemd1.xml +++ b/man/org.freedesktop.systemd1.xml @@ -2881,6 +2881,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { readonly u NFileDescriptorStore = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("false") readonly s FileDescriptorStorePreserve = '...'; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly as LUOSession = ['...', ...]; readonly s StatusText = '...'; readonly i StatusErrno = ...; readonly s StatusBusError = '...'; @@ -3599,6 +3601,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { + + @@ -4247,6 +4251,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { + + @@ -12840,6 +12846,7 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \ IOPressureWatch, CPUSetPartition, and OOMRules were added in version 261. + LUOSession was added in version 262. Socket Unit Objects diff --git a/man/systemd.service.xml b/man/systemd.service.xml index ed0f9476a00..0ae6086feb8 100644 --- a/man/systemd.service.xml +++ b/man/systemd.service.xml @@ -1297,6 +1297,42 @@ RestartMaxDelaySec=160s + + LUOSession= + Takes a whitespace-separated list of names. For each name, when the service is + started the service manager creates a LUO session via the Live Update Orchestrator (LUO, + i.e. /dev/liveupdate) and hands the resulting session file descriptor to the + service through the file descriptor store, using the configured name as its + FDNAME=. The session is hence passed to the service's processes via + sd_listen_fds3, + like any other file descriptor store entry. The service may then preserve additional file + descriptors (such as memfd_create2 + memory file descriptors) in the session, which survive a kexec-based reboot and + can be retrieved again on the other side. + + A session is only handed out if one with the same name is not already present in the service's + file descriptor store. This way a session handed out on first start, or restored across a + kexec, is reused on the next start rather than replaced. The kernel-level session + name is derived deterministically from the unit name and the configured name, so that it remains + stable and unique, and it fits within the length limit imposed by the kernel. + + This setting implies FileDescriptorStoreMax= is set to at least the number + of configured sessions. To ensure the sessions are kept pinned and preserved across a + kexec, combine this with FileDescriptorStorePreserve=yes (see + above). If /dev/liveupdate is not available no session is handed out, and the + service is started normally. This setting may be specified more than once, in which case the lists + are combined. If the empty string is assigned the list is reset and all prior assignments have no + effect. + + For further information on the file descriptor store see the File Descriptor Store overview. + + + + + USBFunctionDescriptors= Configure the location of a file containing diff --git a/src/core/dbus-service.c b/src/core/dbus-service.c index 9b1b1f77218..5fc06df714b 100644 --- a/src/core/dbus-service.c +++ b/src/core/dbus-service.c @@ -21,6 +21,7 @@ #include "fd-util.h" #include "glyph-util.h" #include "locale-util.h" +#include "luo-util.h" #include "manager.h" #include "mount-util.h" #include "open-file.h" @@ -380,6 +381,7 @@ const sd_bus_vtable bus_service_vtable[] = { SD_BUS_PROPERTY("FileDescriptorStoreMax", "u", bus_property_get_unsigned, offsetof(Service, n_fd_store_max), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("NFileDescriptorStore", "u", property_get_size_as_uint32, offsetof(Service, n_fd_store), 0), SD_BUS_PROPERTY("FileDescriptorStorePreserve", "s", bus_property_get_exec_preserve_mode, offsetof(Service, fd_store_preserve_mode), 0), + SD_BUS_PROPERTY("LUOSession", "as", NULL, offsetof(Service, luo_sessions), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("StatusText", "s", NULL, offsetof(Service, status_text), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), SD_BUS_PROPERTY("StatusErrno", "i", bus_property_get_int, offsetof(Service, status_errno), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), SD_BUS_PROPERTY("StatusBusError", "s", NULL, offsetof(Service, status_bus_error), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), @@ -648,6 +650,37 @@ static int bus_service_set_transient_property( if (streq(name, "FileDescriptorStorePreserve")) return bus_set_transient_exec_preserve_mode(u, name, &s->fd_store_preserve_mode, message, flags, reterr_error); + if (streq(name, "LUOSession")) { + _cleanup_strv_free_ char **l = NULL; + + r = sd_bus_message_read_strv(message, &l); + if (r < 0) + return r; + + STRV_FOREACH(p, l) + if (!luo_session_name_is_valid(*p)) + return sd_bus_error_setf(reterr_error, SD_BUS_ERROR_INVALID_ARGS, + "Invalid LUO session name: %s", *p); + + if (!UNIT_WRITE_FLAGS_NOOP(flags)) { + if (strv_isempty(l)) { + s->luo_sessions = strv_free(s->luo_sessions); + unit_write_settingf(u, flags, name, "%s=", name); + } else { + r = strv_extend_strv(&s->luo_sessions, l, /* filter_duplicates= */ true); + if (r < 0) + return r; + + strv_sort_uniq(s->luo_sessions); + + STRV_FOREACH(p, l) + unit_write_settingf(u, flags, name, "%s=%s", name, *p); + } + } + + return 1; + } + if (streq(name, "NotifyAccess")) return bus_set_transient_notify_access(u, name, &s->notify_access, message, flags, reterr_error); diff --git a/src/core/load-fragment-gperf.gperf.in b/src/core/load-fragment-gperf.gperf.in index 60fec56f46a..fcfdbd3169f 100644 --- a/src/core/load-fragment-gperf.gperf.in +++ b/src/core/load-fragment-gperf.gperf.in @@ -484,6 +484,7 @@ Service.NonBlocking, config_parse_bool, Service.BusName, config_parse_bus_name, 0, offsetof(Service, bus_name) Service.FileDescriptorStoreMax, config_parse_unsigned, 0, offsetof(Service, n_fd_store_max) Service.FileDescriptorStorePreserve, config_parse_exec_preserve_mode, 0, offsetof(Service, fd_store_preserve_mode) +Service.LUOSession, config_parse_luo_sessions, 0, offsetof(Service, luo_sessions) Service.NotifyAccess, config_parse_notify_access, 0, offsetof(Service, notify_access) Service.Sockets, config_parse_service_sockets, 0, 0 Service.BusPolicy, config_parse_warn_compat, DISABLED_LEGACY, 0 diff --git a/src/core/load-fragment.c b/src/core/load-fragment.c index 9b2fa71be39..ff8bded569e 100644 --- a/src/core/load-fragment.c +++ b/src/core/load-fragment.c @@ -41,6 +41,7 @@ #include "limits-util.h" #include "load-fragment.h" #include "log.h" +#include "luo-util.h" #include "manager.h" #include "mountpoint-util.h" #include "nsflags.h" @@ -6726,3 +6727,61 @@ int config_parse_protect_hostname( free_and_replace(c->private_hostname, h); return 1; } + +int config_parse_luo_sessions( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + char ***sessions = ASSERT_PTR(data); + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + *sessions = strv_free(*sessions); + return 0; + } + + for (const char *p = rvalue;;) { + _cleanup_free_ char *word = NULL; + int r; + + r = extract_first_word(&p, &word, /* separators= */ NULL, /* flags= */ 0); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse LUOSession= value, ignoring: %s", rvalue); + return 0; + } + if (r == 0) + return 0; + + if (!luo_session_name_is_valid(word)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "LUO session name contains invalid characters, ignoring: %s", word); + continue; + } + + if (strv_contains(*sessions, word)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Duplicate LUO session name, ignoring: %s", word); + continue; + } + + r = strv_extend(sessions, word); + if (r < 0) + return log_oom(); + } + + strv_sort(*sessions); +} diff --git a/src/core/load-fragment.h b/src/core/load-fragment.h index 99b53626203..48129f96029 100644 --- a/src/core/load-fragment.h +++ b/src/core/load-fragment.h @@ -172,6 +172,7 @@ CONFIG_PARSER_PROTOTYPE(config_parse_concurrency_max); CONFIG_PARSER_PROTOTYPE(config_parse_bind_network_interface); CONFIG_PARSER_PROTOTYPE(config_parse_exec_memory_thp); CONFIG_PARSER_PROTOTYPE(config_parse_cpuset_partition); +CONFIG_PARSER_PROTOTYPE(config_parse_luo_sessions); /* gperf prototypes */ const struct ConfigPerfItem* load_fragment_gperf_lookup(const char *key, GPERF_LEN_TYPE length); diff --git a/src/core/service.c b/src/core/service.c index 6997866f883..3ae59df7ab7 100644 --- a/src/core/service.c +++ b/src/core/service.c @@ -1,11 +1,13 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #include /* IWYU pragma: keep */ +#include #include #include #include #include "sd-bus.h" +#include "sd-id128.h" #include "sd-json.h" #include "sd-messages.h" @@ -32,8 +34,10 @@ #include "fileio.h" #include "format-util.h" #include "glyph-util.h" +#include "id128-util.h" #include "image-policy.h" #include "log.h" +#include "luo-util.h" #include "manager.h" #include "memfd-util.h" #include "mount-util.h" @@ -524,7 +528,7 @@ static void service_truncate_fd_store(Service *s) { * parsed and FileDescriptorStoreMax= shrunk the configured limit. Newest entries are at the head * of the list, so drop from the head (newest first). */ - while (s->n_fd_store > s->n_fd_store_max) { + while (s->n_fd_store > s->n_fd_store_max + strv_length(s->luo_sessions)) { ServiceFDStore *fs = ASSERT_PTR(s->fd_store); log_unit_debug(UNIT(s), "Dropping stored fd '%s' to honor FileDescriptorStoreMax=%u.", strna(fs->fdname), s->n_fd_store_max); @@ -617,6 +621,8 @@ static void service_done(Unit *u) { service_release_extra_fds(s); s->root_directory_fd = asynchronous_close(s->root_directory_fd); + s->luo_sessions = strv_free(s->luo_sessions); + s->mount_request = sd_bus_message_unref(s->mount_request); } @@ -753,6 +759,102 @@ static int service_add_fd_store_set(Service *s, FDSet *fds, const char *name, bo return 0; } +/* Build a deterministic LUO session name from the unit's name and the session name, with stable length. */ +static int service_build_luo_session_name(Service *s, const char *name, char **ret) { + _cleanup_free_ char *full = NULL, *result = NULL; + + assert(s); + assert(name); + assert(ret); + + full = strjoin(UNIT(s)->id, "/", name); + if (!full) + return -ENOMEM; + + /* The kernel embeds the session name in the anon_inode path shown in /proc/self/fd/, i.e. + * "anon_inode:[luo_session] ". On kernels lacking commit 97b67e64affb ("dcache: permit + * dynamic_dname()s up to NAME_MAX") that path must fit dynamic_dname()'s historical 64 byte limit, + * otherwise reading it back will fail. Can be simplified once Ubuntu 26.04 support is dropped. */ + // FIXME: allow longer prefix once Ubuntu 26.04 support is dropped + size_t name_max = 64U - STRLEN("anon_inode:[luo_session] ") - 1U; /* = 38 */ + size_t digest_chars = 24U; /* 96 bit, leaves room for a useful unit id prefix within name_max */ + + assert_cc(64U - STRLEN("anon_inode:[luo_session] ") - 1U < LIVEUPDATE_SESSION_NAME_LENGTH); + assert_cc(24U < SD_ID128_STRING_MAX); + assert_cc(24U + 1U < 64U - STRLEN("anon_inode:[luo_session] ") - 1U); + + char digest[SD_ID128_STRING_MAX]; + sd_id128_to_string(id128_digest(full, SIZE_MAX), digest); + + if (asprintf(&result, "%.*s-%.*s", + (int) (name_max - digest_chars - 1U), UNIT(s)->id, + (int) digest_chars, digest) < 0) + return -ENOMEM; + + *ret = TAKE_PTR(result); + return 0; +} + +static int service_setup_luo_sessions(Service *s) { + int r; + + assert(s); + + /* For each configured LUOSession=, create a fresh LUO session and hand it to the service via the fd + * store (and thus LISTEN_FDS, with the configured name as FDNAME). The kernel-level session name is + * derived deterministically from the unit and the configured name so it stays stable and fits the + * kernel's length limit. */ + + if (strv_isempty(s->luo_sessions)) + return 0; + + if (!MANAGER_IS_SYSTEM(UNIT(s)->manager)) { + log_unit_debug(UNIT(s), "LUOSession= is only supported in the system manager, ignoring."); + return 0; + } + + _cleanup_close_ int device_fd = luo_open_device(); + if (device_fd < 0) { + if (ERRNO_IS_NEG_DEVICE_ABSENT(device_fd)) { + log_unit_debug_errno(UNIT(s), device_fd, "No /dev/liveupdate device found, not handing out LUO sessions."); + return 0; + } + return log_unit_warning_errno(UNIT(s), device_fd, "Failed to open /dev/liveupdate: %m"); + } + + STRV_FOREACH(name, s->luo_sessions) { + _cleanup_free_ char *session_name = NULL; + _cleanup_close_ int session_fd = -EBADF; + bool already_given_out = false; + + LIST_FOREACH(fd_store, fs, s->fd_store) + if (streq_ptr(fs->fdname, *name) && fd_is_luo_session(fs->fd) > 0) { + already_given_out = true; + break; + } + if (already_given_out) + continue; + + r = service_build_luo_session_name(s, *name, &session_name); + if (r < 0) + return log_unit_warning_errno(UNIT(s), r, "Failed to build LUO session name for '%s': %m", *name); + + session_fd = luo_create_session(device_fd, session_name); + if (session_fd < 0) { + log_unit_warning_errno(UNIT(s), session_fd, "Failed to create LUO session '%s', ignoring: %m", session_name); + continue; + } + + r = service_add_fd_store(s, TAKE_FD(session_fd), *name, /* do_poll= */ false, /* propagate_upstream= */ false); + if (r < 0) + return log_unit_warning_errno(UNIT(s), r, "Failed to hand out LUO session '%s': %m", *name); + + log_unit_debug(UNIT(s), "Handed out LUO session '%s' (kernel name '%s').", *name, session_name); + } + + return 0; +} + int service_propagate_fd_store_mapping_upstream(Manager *m) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *root = NULL; _cleanup_close_ int fd = -EBADF; @@ -1119,6 +1221,10 @@ static int service_add_extras(Service *s) { if (r < 0) return r; + /* Each configured LUOSession= is handed to the service through the fd store, hence make sure the + * store is large enough to hold them all. */ + s->n_fd_store_max += strv_length(s->luo_sessions); + /* If the service needs the notify socket, let's enable it automatically. */ if (s->notify_access == NOTIFY_NONE && (IN_SET(s->type, SERVICE_NOTIFY, SERVICE_NOTIFY_RELOAD) || s->watchdog_usec > 0 || s->n_fd_store_max > 0)) @@ -2749,6 +2855,10 @@ static void service_enter_start(Service *s) { service_unwatch_control_pid(s); service_unwatch_main_pid(s); + r = service_setup_luo_sessions(s); + if (r < 0) + goto fail; + r = service_adverse_to_leftover_processes(s); if (r < 0) goto fail; diff --git a/src/core/service.h b/src/core/service.h index b57634cdb0f..c8a09ca8293 100644 --- a/src/core/service.h +++ b/src/core/service.h @@ -246,6 +246,8 @@ typedef struct Service { unsigned n_fd_store_max; ExecPreserveMode fd_store_preserve_mode; + char **luo_sessions; /* LUOSession= setting — list of session names to create/manage */ + int stdin_fd; int stdout_fd; int stderr_fd; diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c index e3a1fcd8934..c36ada61db9 100644 --- a/src/shared/bus-unit-util.c +++ b/src/shared/bus-unit-util.c @@ -2688,6 +2688,7 @@ static const BusProperty service_properties[] = { { "TimeoutStartFailureMode", bus_append_string }, { "TimeoutStopFailureMode", bus_append_string }, { "FileDescriptorStorePreserve", bus_append_string }, + { "LUOSession", bus_append_strv }, { "PermissionsStartOnly", bus_append_parse_boolean }, { "RootDirectoryStartOnly", bus_append_parse_boolean }, { "RemainAfterExit", bus_append_parse_boolean }, diff --git a/src/shared/luo-util.c b/src/shared/luo-util.c index e6de4c25c2d..172735daf40 100644 --- a/src/shared/luo-util.c +++ b/src/shared/luo-util.c @@ -115,6 +115,11 @@ int luo_session_finish(int session_fd) { return RET_NERRNO(ioctl(session_fd, LIVEUPDATE_SESSION_FINISH, &args)); } +bool luo_session_name_is_valid(const char *name) { + /* Used for FDNAME: no whitespace, no ":", no control characters, etc. */ + return fdname_is_valid(name) && string_is_safe(name, STRING_DISALLOW_WHITESPACE); +} + int luo_parse_serialization(sd_json_variant **ret, int **ret_fds, size_t *ret_n_fds) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *root = NULL; _cleanup_free_ int *fd_list = NULL; diff --git a/src/shared/luo-util.h b/src/shared/luo-util.h index 3e820190613..a7f28bb2731 100644 --- a/src/shared/luo-util.h +++ b/src/shared/luo-util.h @@ -36,6 +36,8 @@ int luo_session_preserve_fd(int session_fd, int fd, uint64_t token); int luo_session_retrieve_fd(int session_fd, uint64_t token); int luo_session_finish(int session_fd); +bool luo_session_name_is_valid(const char *name); + int luo_parse_serialization(sd_json_variant **ret, int **ret_fds, size_t *ret_n_fds); int luo_preserve_fd_stores(sd_json_variant *serialization, int *ret_session_fd); diff --git a/src/test/test-luo.c b/src/test/test-luo.c index 7c2fe73d969..9fd7ec80042 100644 --- a/src/test/test-luo.c +++ b/src/test/test-luo.c @@ -326,9 +326,64 @@ static int do_check_hijack(void) { return 0; } +static int do_check_sessions(int argc, char *argv[]) { + const char *e; + _cleanup_strv_free_ char **names = NULL; + int n_fds, n_verified = 0, r; + + /* Verify that named LUO sessions are present in the fd store */ + e = getenv("LISTEN_FDS"); + if (!e) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No LISTEN_FDS environment variable set!"); + + r = safe_atoi(e, &n_fds); + if (r < 0) + return log_error_errno(r, "Failed to parse LISTEN_FDS='%s': %m", e); + + e = getenv("LISTEN_FDNAMES"); + if (e) { + names = strv_split(e, ":"); + if (!names) + return log_oom(); + } + + for (int i = 2; i < argc; i++) { + const char *session_name = argv[i]; + int fd = -EBADF; + + /* Find the fd by name */ + STRV_FOREACH(name, names) { + int idx = (int) (name - names); + if (idx >= n_fds) + break; + if (streq(*name, session_name)) { + fd = SD_LISTEN_FDS_START + idx; + break; + } + } + + if (fd < 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), + "LUO session '%s' not found in fd store!", session_name); + + r = fd_is_luo_session(fd); + if (r < 0) + return log_error_errno(r, "Failed to check if fd '%s' is a LUO session: %m", session_name); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), + "fd '%s' is not a LUO session!", session_name); + + log_info("Verified LUO session '%s' is present and valid.", session_name); + n_verified++; + } + + log_info("All %d LUO session(s) verified.", n_verified); + return 0; +} + static int run(int argc, char *argv[]) { - if (argc < 2 || argc > 3) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Usage: %s store|check|store-hijack|check-hijack [PREFIX]", argv[0]); + if (argc < 2) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Usage: %s store|check|store-hijack|check-hijack [PREFIX] | check-sessions NAME...", argv[0]); const char *prefix = argc > 2 ? argv[2] : "luosession"; @@ -340,6 +395,8 @@ static int run(int argc, char *argv[]) { return do_store_hijack(); if (streq(argv[1], "check-hijack")) return do_check_hijack(); + if (streq(argv[1], "check-sessions")) + return do_check_sessions(argc, argv); return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown command: %s", argv[1]); } diff --git a/test/units/TEST-91-LIVEUPDATE.sh b/test/units/TEST-91-LIVEUPDATE.sh index 8932b5e4dd0..27f734748ac 100755 --- a/test/units/TEST-91-LIVEUPDATE.sh +++ b/test/units/TEST-91-LIVEUPDATE.sh @@ -60,6 +60,17 @@ RemainAfterExit=yes FileDescriptorStoreMax=${maxfd} FileDescriptorStorePreserve=yes ExecStart=${cmd} +EOF + + # Create a unit with LUOSession= to test server-managed session lifecycle + cat >"/run/systemd/system/TEST-91-LIVEUPDATE-session.service" <