otherwise in case multiple fds are stored, it will be impossible to
distinguish them, as they will all carry the default name (`stored`).
+Services that need to preserve additional kernel state may also create their
+own LUO sessions by opening `/dev/liveupdate` directly (see the kernel
+documentation linked above) and pushing the obtained session fd into their
+fdstore (it is recommended to use a `FDNAME=…` string, as above). systemd
+detects such fds and arranges for them to survive the kexec as well, so that
+the session, and any supported file descriptors preserved inside it, is
+handed back to the service on the other side of the reboot.
+
## Initrd Transitions
The fdstore may also be used to pass file descriptors for resources from the
const char *type;
const char *name;
uint64_t token;
+ const char *session_name;
} p = {
.token = UINT64_MAX,
};
{ "type", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, type), SD_JSON_MANDATORY },
{ "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, name), SD_JSON_MANDATORY },
{ "token", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, voffsetof(p, token), 0 },
+ { "sessionName", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, session_name), 0 },
{}
};
unit_id, p.name, p.token);
continue;
}
+ } else if (streq(p.type, "luo_session")) {
+ if (!p.session_name) {
+ log_warning("LUO mapping for unit '%s' fd '%s': missing sessionName.", unit_id, p.name);
+ continue;
+ }
+
+ fd = luo_retrieve_session(device_fd, p.session_name);
+ if (fd < 0) {
+ log_warning_errno(fd, "Failed to retrieve LUO session '%s' for unit '%s' name '%s': %m",
+ p.session_name, unit_id, p.name);
+ continue;
+ }
+
+ log_debug("Retrieved LUO session '%s' for unit fd store '%s'.", p.session_name, p.name);
} else {
log_warning("LUO mapping for unit '%s' fd '%s': unknown type '%s', skipping.",
unit_id, p.name, p.type);
if (!fds)
return log_oom();
- /* Build a JSON object: { "unit_id": [ { "type": "fd", "name": "...", "fd": N }, ... ], ... }
+ /* Build a JSON object: { "unit_id": [ { "type": "fd", "name": "...", "fd": N },
+ * { "type": "luo_session", "name": "...", "fd": N, "sessionName": "..." } ], ... }
* This is passed to systemd-shutdown which will create a LUO session and preserve the fds. */
HASHMAP_FOREACH(u, m->units) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *entries = NULL;
continue;
LIST_FOREACH(fd_store, fs, s->fd_store) {
+ _cleanup_free_ char *session_name = NULL;
int copy;
+ /* Check if this fd is itself a LUO session, as those cannot be nested and need
+ * special handling */
+ r = fd_get_luo_session_name(fs->fd, &session_name);
+ if (r < 0 && r != -EMEDIUMTYPE) {
+ log_warning_errno(r, "Failed to check if fd '%s' of unit '%s' is a LUO session, skipping: %m",
+ fs->fdname, u->id);
+ continue;
+ }
+
+ /* Ensure nobody tries to hijack our session, as we will create this later before
+ * kexec */
+ if (streq_ptr(session_name, LUO_SESSION_NAME)) {
+ log_warning("Skipping fd '%s' of unit '%s' for LUO serialization, as the session name '%s' infringes systemd's namespace.",
+ fs->fdname, u->id, session_name);
+ continue;
+ }
+
copy = fdset_put_dup(fds, fs->fd);
if (copy < 0)
return log_error_errno(copy, "Failed to duplicate fd for LUO serialization: %m");
r = sd_json_variant_append_arraybo(
&entries,
- SD_JSON_BUILD_PAIR_STRING("type", "fd"),
+ SD_JSON_BUILD_PAIR_STRING("type", session_name ? "luo_session" : "fd"),
SD_JSON_BUILD_PAIR_STRING("name", fs->fdname),
- SD_JSON_BUILD_PAIR_INTEGER("fd", copy));
+ SD_JSON_BUILD_PAIR_INTEGER("fd", copy),
+ JSON_BUILD_PAIR_STRING_NON_EMPTY("sessionName", session_name));
if (r < 0)
return log_error_errno(r, "Failed to build JSON for LUO serialization: %m");
#include <fcntl.h>
#include <linux/liveupdate.h>
+#include <linux/magic.h>
#include <sys/ioctl.h>
+#include <sys/vfs.h>
#include "sd-json.h"
#include "luo-util.h"
#include "memfd-util.h"
#include "parse-util.h"
+#include "stat-util.h"
#include "string-util.h"
/* Kernel API defined at https://docs.kernel.org/userspace-api/liveupdate.html The /dev/liveupdate is a
return log_error_errno(session_fd, "Failed to create LUO session '%s': %m", LUO_SESSION_NAME);
/* Build the mapping JSON for the new kernel's PID 1 and preserve each fd.
- * JSON format: { "unit_id": [ {"type": "fd", "name": "...", "token": N}, ... ], ... }
+ * JSON format: { "unit_id": [ {"type": "fd", "name": "...", "token": N},
+ * {"type": "luo_session", "name": "...", "sessionName": "..."} ], ... }
*
- * For regular fds: type=fd, preserved in the systemd session with the given LUO token. */
+ * For regular fds: type=fd, preserved in the systemd session with the given LUO token.
+ * For LUO session fds: type=luo_session, the session survives kexec independently, as it cannot be
+ * nested. */
JSON_VARIANT_OBJECT_FOREACH(unit_id, entries, serialization) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *fd_list = NULL;
sd_json_variant *entry;
return log_error_errno(r, "Failed to build LUO mapping: %m");
++token;
+ } else if (streq(p.type, "luo_session")) {
+ if (!p.session_name) {
+ log_warning("LUO mapping for unit '%s' fd '%s': missing sessionName, skipping.", unit_id, p.name);
+ continue;
+ }
+
+ /* Remember the FDStore name to session name mapping */
+ r = sd_json_variant_append_arraybo(
+ &fd_list,
+ SD_JSON_BUILD_PAIR_STRING("type", "luo_session"),
+ SD_JSON_BUILD_PAIR_STRING("name", p.name),
+ SD_JSON_BUILD_PAIR_STRING("sessionName", p.session_name));
+ if (r < 0)
+ return log_error_errno(r, "Failed to build LUO mapping for session fd: %m");
+
+ log_debug("LUO session fd '%s' (session '%s') recorded in mapping.",
+ p.name, p.session_name);
} else
log_warning("Unknown fd type '%s' for unit '%s' fd '%s', skipping.", p.type, unit_id, p.name);
}
*ret_session_fd = TAKE_FD(session_fd);
return 1;
}
+
+int fd_get_luo_session_name(int fd, char **ret) {
+ _cleanup_free_ char *path = NULL;
+ int r;
+
+ assert(fd >= 0);
+
+ // TODO: switch to LUO specific inode magic once available
+ r = fd_is_fs_type(fd, ANON_INODE_FS_MAGIC);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return -EMEDIUMTYPE;
+
+ r = fd_get_path(fd, &path);
+ if (r < 0)
+ return r;
+
+ /* Path is "anon_inode:[luo_session] <session_name>" */
+ const char *suffix = startswith(path, "anon_inode:[luo_session] ");
+ if (isempty(suffix))
+ return -EMEDIUMTYPE;
+
+ if (ret)
+ return strdup_to(ret, suffix);
+
+ return 0;
+}
+
+int fd_is_luo_session(int fd) {
+ int r;
+
+ r = fd_get_luo_session_name(fd, /* ret= */ NULL);
+ if (r == -EMEDIUMTYPE)
+ return false;
+ if (r < 0)
+ return r;
+
+ return true;
+}
* "unit-name.service": [
* { "type": "fd", "name": "fdname1", "token": 1 },
* { "type": "fd", "name": "fdname2", "token": 2 },
+ * { "type": "luo_session", "name": "fdname3", "sessionName": "unit.service/myapp" }
* ],
* "other-unit.service": [
* { "type": "fd", "name": "stored", "token": 3 }
* }
*
* type=fd: the fd was preserved in the "systemd" LUO session with the given token.
+ * type=luo_session: a service-owned LUO session that survives kexec independently,
+ * retrieved by session_name on the next boot.
*/
#define LUO_MAPPING_INDEX UINT64_C(0)
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);
+
+int fd_is_luo_session(int fd);
+int fd_get_luo_session_name(int fd, char **ret);
/* SPDX-License-Identifier: LGPL-2.1-or-later */
/* Helper for TEST-91-LIVEUPDATE: creates memfds and stores them in the fd store,
- * or verifies that inherited fd store entries contain the expected content.
+ * creates a LUO session directly via /dev/liveupdate and stores a memfd in it,
+ * or verifies everything after kexec.
*
* Usage:
- * test-luo store - create memfds with test data and push them to the fd store
- * test-luo check - verify fd store content matches expectations
+ * test-luo store - create memfds and a LUO session, push all to the fd store
+ * test-luo check - verify fd store content and LUO session memfd after kexec
*/
#include <stdlib.h>
#include "fd-util.h"
#include "log.h"
+#include "luo-util.h"
#include "main-func.h"
#include "memfd-util.h"
#include "parse-util.h"
#define TEST_DATA_1 "liveupdate-test-data-1"
#define TEST_DATA_2 "liveupdate-test-data-2"
+#define SESSION_MEMFD_DATA "luo-session-memfd-test-data"
+#define SESSION_MEMFD_TOKEN UINT64_C(42)
-static int do_store(void) {
+static int do_store(const char *prefix) {
_cleanup_close_ int fd1 = -EBADF, fd2 = -EBADF;
int r;
log_info("Stored 2 memfds in fd store.");
+ /* Create a LUO session directly via /dev/liveupdate, put a memfd in it, and store the session fd */
+ _cleanup_close_ int device_fd = -EBADF, session_fd = -EBADF, session_memfd = -EBADF;
+ const char *session_name = strjoina(prefix, "-direct");
+
+ device_fd = luo_open_device();
+ if (device_fd < 0)
+ return log_error_errno(device_fd, "Failed to open /dev/liveupdate: %m");
+
+ session_fd = luo_create_session(device_fd, session_name);
+ if (session_fd < 0)
+ return log_error_errno(session_fd, "Failed to create LUO session '%s': %m", session_name);
+
+ session_memfd = memfd_new_and_seal("session-test", SESSION_MEMFD_DATA, strlen(SESSION_MEMFD_DATA));
+ if (session_memfd < 0)
+ return log_error_errno(session_memfd, "Failed to create session memfd: %m");
+
+ r = luo_session_preserve_fd(session_fd, session_memfd, SESSION_MEMFD_TOKEN);
+ if (r < 0)
+ return log_error_errno(r, "Failed to preserve memfd in session: %m");
+
+ r = sd_pid_notifyf_with_fds(0, false, &session_fd, 1, "FDSTORE=1\nFDNAME=%s-direct", prefix);
+ if (r < 0)
+ return log_error_errno(r, "Failed to store session fd in fd store: %m");
+ TAKE_FD(session_fd);
+
+ log_info("Stored LUO session '%s' with memfd in fd store.", session_name);
+
/* Wait for PID 1 to actually process all our FDSTORE notifications before we exit, otherwise
* the cgroup-based pidref to unit lookup may fail once we're gone, and the fds end up closed. */
r = sd_notify_barrier(0, 5 * USEC_PER_SEC);
return 0;
}
-static int do_check(void) {
+static int do_check(const char *prefix) {
const char *e;
_cleanup_strv_free_ char **names = NULL;
+ const char *session_fdname = strjoina(prefix, "-direct");
size_t n_fds;
int r;
log_info("All fd store checks passed.");
+ /* Verify the LUO session fd survived and its memfd content is intact */
+ int session_fd = -EBADF;
+ size_t idx = 0;
+ STRV_FOREACH(name, names) {
+ if (idx >= n_fds)
+ break;
+ if (streq(*name, session_fdname)) {
+ session_fd = SD_LISTEN_FDS_START + idx;
+ break;
+ }
+ idx++;
+ }
+
+ if (session_fd < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
+ "LUO session fd '%s' not found in fd store", session_fdname);
+
+ r = fd_is_luo_session(session_fd);
+ if (r < 0)
+ return log_error_errno(r, "Failed to check if fd is LUO session: %m");
+ if (r == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EBADMSG),
+ "fd '%s' is not a LUO session!", session_fdname);
+
+ _cleanup_close_ int session_memfd = luo_session_retrieve_fd(session_fd, SESSION_MEMFD_TOKEN);
+ if (session_memfd < 0)
+ return log_error_errno(session_memfd, "Failed to retrieve memfd from session: %m");
+
+ char sbuf[256];
+ ssize_t sn = pread(session_memfd, sbuf, sizeof(sbuf) - 1, 0);
+ if (sn < 0)
+ return log_error_errno(errno, "Failed to read session memfd: %m");
+ sbuf[sn] = '\0';
+
+ if (!streq(sbuf, SESSION_MEMFD_DATA))
+ return log_error_errno(SYNTHETIC_ERRNO(EBADMSG),
+ "Session memfd content mismatch: expected '%s', got '%s'",
+ SESSION_MEMFD_DATA, sbuf);
+
+ /* Remove the LUO session fd from the fd store as well. */
+ r = sd_pid_notifyf(0, /* unset_environment= */ false,
+ "FDSTOREREMOVE=1\nFDNAME=%s", session_fdname);
+ if (r < 0)
+ return log_error_errno(r, "Failed to remove fd '%s' from fd store: %m", session_fdname);
+
+ log_info("Verified LUO session memfd content matches.");
+
/* Wait for PID 1 to actually process all our FDSTORE notifications before we exit, otherwise
* the cgroup-based pidref to unit lookup may fail once we're gone, and the fds end up closed. */
r = sd_notify_barrier(0, 5 * USEC_PER_SEC);
}
static int run(int argc, char *argv[]) {
- if (argc != 2)
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Usage: %s store|check", argv[0]);
+ if (argc < 2 || argc > 3)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Usage: %s store|check [PREFIX]", argv[0]);
+
+ const char *prefix = argc > 2 ? argv[2] : "luosession";
if (streq(argv[1], "store"))
- return do_store();
+ return do_store(prefix);
if (streq(argv[1], "check"))
- return do_check();
+ return do_check(prefix);
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown command: %s", argv[1]);
}
# Verify that the user manager also preserved its FD store
n_user_at_fds=$(systemctl show -P NFileDescriptorStore "${TESTUSER_USER_SVC}")
- test "${n_user_at_fds}" -ge 2
+ test "${n_user_at_fds}" -ge 3
write_late_unit user TEST-91-LIVEUPDATE-user-late \
"/usr/lib/systemd/tests/unit-tests/manual/test-luo check user-late"
systemctl restart "${TESTUSER_USER_SVC}"
timeout 30s bash -c "until systemctl is-active --quiet '${TESTUSER_USER_SVC}'; do sleep 0.5; done"
n_user_unit_fds=$(run0 -u testuser systemctl --user show -P NFileDescriptorStore TEST-91-LIVEUPDATE-user-late.service)
- test "${n_user_unit_fds}" -eq 2
+ test "${n_user_unit_fds}" -eq 3
run0 -u testuser systemctl --user start TEST-91-LIVEUPDATE-user-late.service
# nspawn fdstore variant: after kexec, PID 1 propagated the
# late.service: rewrite the fragment with the second-boot ExecStart and
# exercise the daemon-reload + daemon-reexec preservation paths.
write_late_unit system TEST-91-LIVEUPDATE-late \
- "/usr/lib/systemd/tests/unit-tests/manual/test-luo check"
+ "/usr/lib/systemd/tests/unit-tests/manual/test-luo check late"
n_fds=$(systemctl show -P NFileDescriptorStore TEST-91-LIVEUPDATE-late.service)
- test "$n_fds" -eq 2
+ test "$n_fds" -eq 3
systemctl daemon-reload
# Verify the late unit doesn't get GC'ed during daemon-reload
n_fds=$(systemctl show -P NFileDescriptorStore TEST-91-LIVEUPDATE-late.service)
- test "$n_fds" -eq 2
+ test "$n_fds" -eq 3
systemctl daemon-reexec
# Verify the late unit doesn't get GC'ed during daemon-reexec
n_fds=$(systemctl show -P NFileDescriptorStore TEST-91-LIVEUPDATE-late.service)
- test "$n_fds" -eq 2
+ test "$n_fds" -eq 3
systemctl start TEST-91-LIVEUPDATE-late.service
write_late_unit system TEST-91-LIVEUPDATE-late-noreload \
"/usr/lib/systemd/tests/unit-tests/manual/test-luo check late-noreload"
n_fds=$(systemctl show -P NFileDescriptorStore TEST-91-LIVEUPDATE-late-noreload.service)
- test "$n_fds" -eq 2
+ test "$n_fds" -eq 3
systemctl start TEST-91-LIVEUPDATE-late-noreload.service
# Zero-fds variant: fragment on second boot sets FileDescriptorStoreMax=0,
systemctl start TEST-91-LIVEUPDATE-late-zerofds.service
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.
/usr/lib/systemd/tests/unit-tests/manual/test-luo store
# Exercise the user manager FD preservation across kexec too
"/usr/lib/systemd/tests/unit-tests/manual/test-luo store user-late"
run0 -u testuser systemctl --user start TEST-91-LIVEUPDATE-user-late.service
n_user_unit_fds=$(run0 -u testuser systemctl --user show -P NFileDescriptorStore TEST-91-LIVEUPDATE-user-late.service)
- test "${n_user_unit_fds}" -eq 2
+ test "${n_user_unit_fds}" -eq 3
n_user_at_fds=$(systemctl show -P NFileDescriptorStore "${TESTUSER_USER_SVC}")
- test "${n_user_at_fds}" -ge 2
+ test "${n_user_at_fds}" -ge 3
# Exercise the FD-store preservation chain across a kexec for a privileged
# nspawn container managed as a system service:
# to avoid collisions in the LUO session namespace.
for variant in late late-noreload late-zerofds; do
write_late_unit system "TEST-91-LIVEUPDATE-${variant}" \
- "/usr/lib/systemd/tests/unit-tests/manual/test-luo store"
+ "/usr/lib/systemd/tests/unit-tests/manual/test-luo store ${variant}"
systemctl start "TEST-91-LIVEUPDATE-${variant}.service"
n_fds=$(systemctl show -P NFileDescriptorStore "TEST-91-LIVEUPDATE-${variant}.service")
- test "$n_fds" -eq 2
+ test "$n_fds" -eq 3
done
# 'systemctl kexec' auto-loads the default boot entry (i.e. the booted UKI,