]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
core: add LUOSession= unit setting 42530/head
authorLuca Boccassi <luca.boccassi@gmail.com>
Tue, 14 Apr 2026 14:22:34 +0000 (15:22 +0100)
committerLuca Boccassi <luca.boccassi@gmail.com>
Wed, 17 Jun 2026 12:11:15 +0000 (13:11 +0100)
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.

13 files changed:
man/org.freedesktop.systemd1.xml
man/systemd.service.xml
src/core/dbus-service.c
src/core/load-fragment-gperf.gperf.in
src/core/load-fragment.c
src/core/load-fragment.h
src/core/service.c
src/core/service.h
src/shared/bus-unit-util.c
src/shared/luo-util.c
src/shared/luo-util.h
src/test/test-luo.c
test/units/TEST-91-LIVEUPDATE.sh

index 2d63050a68620980a7fb7ea3e9d9c0e9dda98852..42455ef6c7a635b4fa750a82014927880da7a5c7 100644 (file)
@@ -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 {
 
     <!--property FileDescriptorStorePreserve is not documented!-->
 
+    <!--property LUOSession is not documented!-->
+
     <!--property ReloadResult is not documented!-->
 
     <!--property CleanResult is not documented!-->
@@ -4247,6 +4251,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
 
     <variablelist class="dbus-property" generated="True" extra-ref="FileDescriptorStorePreserve"/>
 
+    <variablelist class="dbus-property" generated="True" extra-ref="LUOSession"/>
+
     <variablelist class="dbus-property" generated="True" extra-ref="StatusText"/>
 
     <variablelist class="dbus-property" generated="True" extra-ref="StatusErrno"/>
@@ -12840,6 +12846,7 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \
       <varname>IOPressureWatch</varname>,
       <varname>CPUSetPartition</varname>, and
       <varname>OOMRules</varname> were added in version 261.</para>
+      <para><varname>LUOSession</varname> was added in version 262.</para>
     </refsect2>
     <refsect2>
       <title>Socket Unit Objects</title>
index ed0f9476a00b95008dfe864ca98e6a7bf7c2a32a..0ae6086feb8a9bda6873d294afcdd2b796ead54d 100644 (file)
@@ -1297,6 +1297,42 @@ RestartMaxDelaySec=160s</programlisting>
         <xi:include href="version-info.xml" xpointer="v254"/></listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><varname>LUOSession=</varname></term>
+        <listitem><para>Takes a whitespace-separated list of names. For each name, when the service is
+        started the service manager creates a LUO session via the <ulink
+        url="https://docs.kernel.org/userspace-api/liveupdate.html">Live Update Orchestrator</ulink> (LUO,
+        i.e. <filename>/dev/liveupdate</filename>) and hands the resulting session file descriptor to the
+        service through the file descriptor store, using the configured name as its
+        <literal>FDNAME=</literal>. The session is hence passed to the service's processes via
+        <citerefentry><refentrytitle>sd_listen_fds</refentrytitle><manvolnum>3</manvolnum></citerefentry>,
+        like any other file descriptor store entry. The service may then preserve additional file
+        descriptors (such as <citerefentry
+        project='man-pages'><refentrytitle>memfd_create</refentrytitle><manvolnum>2</manvolnum></citerefentry>
+        memory file descriptors) in the session, which survive a <literal>kexec</literal>-based reboot and
+        can be retrieved again on the other side.</para>
+
+        <para>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
+        <literal>kexec</literal>, 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.</para>
+
+        <para>This setting implies <varname>FileDescriptorStoreMax=</varname> is set to at least the number
+        of configured sessions. To ensure the sessions are kept pinned and preserved across a
+        <literal>kexec</literal>, combine this with <varname>FileDescriptorStorePreserve=yes</varname> (see
+        above). If <filename>/dev/liveupdate</filename> 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.</para>
+
+        <para>For further information on the file descriptor store see the <ulink
+        url="https://systemd.io/FILE_DESCRIPTOR_STORE">File Descriptor Store</ulink> overview.</para>
+
+        <xi:include href="system-only.xml" xpointer="singular"/>
+        <xi:include href="version-info.xml" xpointer="v262"/></listitem>
+      </varlistentry>
+
       <varlistentry>
         <term><varname>USBFunctionDescriptors=</varname></term>
         <listitem><para>Configure the location of a file containing
index 9b1b1f772183361da5ac2cea54ef6b00c22ef2a2..5fc06df714b7f77a15bc3fb990da08ee50113b97 100644 (file)
@@ -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);
 
index 60fec56f46a7b52cd9777689e4efac3f2b9f2035..fcfdbd3169f91d69a203fe20bd07169a5c3b4a35 100644 (file)
@@ -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
index 9b2fa71be398f6ae16437da81b10a8a84d52090a..ff8bded569e274b27bf9419062493f6bd1448bdf 100644 (file)
@@ -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);
+}
index 99b53626203ec1e227845017f6658565284f6a41..48129f960299ad1dff4e9dcb4d4f93c4bc6dc635 100644 (file)
@@ -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);
index 6997866f883f0cd5a340d2bdc5d0eaf0cff3c6b5..3ae59df7ab76d0d9588288de722e079ee67216fe 100644 (file)
@@ -1,11 +1,13 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 
 #include <linux/audit.h>        /* IWYU pragma: keep */
+#include <linux/liveupdate.h>
 #include <math.h>
 #include <sys/stat.h>
 #include <unistd.h>
 
 #include "sd-bus.h"
+#include "sd-id128.h"
 #include "sd-json.h"
 #include "sd-messages.h"
 
 #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] <name>". 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;
index b57634cdb0f41c7ff151ff0371a68c9c4d0b338e..c8a09ca8293abf313283e182a02751d68577e8c8 100644 (file)
@@ -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;
index e3a1fcd8934b2a94bea866ee678ce70edea2dfae..c36ada61db9cda37620831730ceaa04f3e2fdd9d 100644 (file)
@@ -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                      },
index e6de4c25c2d76eb29cae562126dbec462b28af8c..172735daf4047c9d687158d8a4bffd630f7700fc 100644 (file)
@@ -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;
index 3e820190613e623d7e2ea2da806051c0b1c99b4f..a7f28bb27317457b50c97bbe49e89acb907b7a8a 100644 (file)
@@ -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);
 
index 7c2fe73d969dd1c5bc8ebf0fa9e1669434b5163e..9fd7ec80042a4506cb9049eb2a5fbe326f2eb1cc 100644 (file)
@@ -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]);
 }
index 8932b5e4dd03ad78883dfcf98d0b12fae66f30b1..27f734748ac938ec30c1caf373e8f29b7605ea9c 100755 (executable)
@@ -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" <<EOF
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+FileDescriptorStoreMax=10
+FileDescriptorStorePreserve=yes
+LUOSession=test-session-1 test-session-2
+ExecStart=/usr/lib/systemd/tests/unit-tests/manual/test-luo check-sessions test-session-1 test-session-2
 EOF
 }
 
@@ -226,6 +237,16 @@ EOF
           /run/TEST-91-LIVEUPDATE-failure.attempt \
           /run/TEST-91-LIVEUPDATE-failure.preserve-broken
     systemctl daemon-reload
+
+    # Verify the LUOSession= sessions was restored. Starting the unit again must
+    # reuse the restored sessions rather than handing out fresh ones (the names are
+    # already present in the fd store), and the payload verifies they are valid.
+    n_fds=$(systemctl show -P NFileDescriptorStore TEST-91-LIVEUPDATE-session.service)
+    echo "Session unit fd store count after kexec: $n_fds"
+    test "$n_fds" -eq 2
+    systemctl start TEST-91-LIVEUPDATE-session.service
+    n_fds=$(systemctl show -P NFileDescriptorStore TEST-91-LIVEUPDATE-session.service)
+    test "$n_fds" -eq 2
 else
     # Create memfds with known content and push them to our fd store.
     # Also request a LUO session, store a memfd in it, and push the session fd to the fd store.
@@ -285,6 +306,12 @@ EOF
         test "$n_fds" -eq 3
     done
 
+    # Test LUOSession= setting: start the unit and verify sessions are in its fd store
+    systemctl start TEST-91-LIVEUPDATE-session.service
+    n_fds=$(systemctl show -P NFileDescriptorStore TEST-91-LIVEUPDATE-session.service)
+    echo "Session unit fd store count: $n_fds"
+    test "$n_fds" -eq 2  # test-session-1 and test-session-2
+
     # 'systemctl kexec' auto-loads the default boot entry (i.e. the booted UKI,
     # via EFI LoaderEntrySelected/LoaderEntryDefault). Append a marker to the
     # kernel command line so we can tell the two boots apart, and also the current