diff --git a/docs/FILE_DESCRIPTOR_STORE.md b/docs/FILE_DESCRIPTOR_STORE.md index 231af87c91..c333645378 100644 --- a/docs/FILE_DESCRIPTOR_STORE.md +++ b/docs/FILE_DESCRIPTOR_STORE.md @@ -181,6 +181,39 @@ continuously). For further details see [Resource Pass-Through](https://www.freedesktop.org/software/systemd/man/latest/systemd-soft-reboot.service.html#Resource%20Pass-Through). +## Kernel Live Update (kexec) + +On kernels that support the [Live Update +Orchestrator](https://docs.kernel.org/userspace-api/liveupdate.html) +(LUO), the fdstore may also be preserved across a `kexec`-based reboot into a +new kernel. This allows updating the kernel itself without losing pinned +resources such as serialized service state, analogous to soft reboot, but for +the kernel. + +Only file descriptors that reference LUO-compatible kernel objects can be +preserved this way. Currently the kernel supports `memfd` only for LUO, but +more types are being worked on. Other kinds of file descriptors (sockets, +regular files, etc.) will be dropped from the store during the kexec transition. + +LUO preservation of the fdstore is triggered automatically whenever a +kexec-based reboot is initiated on an LUO-capable kernel, and is gated by a +similar rule as soft-reboot: the service must have +`FileDescriptorStorePreserve=yes` set, so that its fdstore remains loaded. On +the other side of the kexec, the system manager rebuilds the mapping of fds +back to their original service units, so that when those services are +re-activated the fds are passed to them using the normal fdstore protocol. +Adding a `FDNAME=…` string identifying the fd is also highly recommended, +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 @@ -198,6 +231,27 @@ The soft reboot cycle transition and the initrd→host transition are semantically very similar, hence similar rules apply, and in both cases it is recommended to use the fdstore if pinned resources shall be passed over. +## Propagation Across Manager Boundaries + +When a service that has `FileDescriptorStorePreserve=yes` set is itself running +under another service manager, for example a service of the per-user manager +(`user@.service`), or a payload running inside a +[`systemd-nspawn`](https://www.freedesktop.org/software/systemd/man/latest/systemd-nspawn.html) +container, fds pushed into its fdstore are automatically forwarded one level up +the supervisor chain via the enveloping manager's `$NOTIFY_SOCKET`. This allows +the fdstore contents of inner services to be preserved across restarts, re-execs, +soft-reboots, etc. of the *outer* manager, even when the inner manager (or the +container payload) is itself restarted along the way. On the way up, each fd is +tagged with its originating unit id and the original `FDNAME=…` value, so that +when the fds are eventually handed back down (via the regular +`$LISTEN_FDS`/`$LISTEN_FDNAMES` protocol), each manager along the chain can +route them back to the correct unit's fdstore. `FDSTOREREMOVE=1` notifications +are forwarded the same way, so that explicit removals propagate all the way up too. + +For this to work the enveloping unit must itself enable the fdstore (i.e. set +`FileDescriptorStoreMax=` to a sufficiently large value and +`FileDescriptorStorePreserve=yes`). + ## Debugging The diff --git a/man/sd_notify.xml b/man/sd_notify.xml index f9bb56b2d4..52685c404b 100644 --- a/man/sd_notify.xml +++ b/man/sd_notify.xml @@ -428,6 +428,17 @@ For further information on the file descriptor store see the File Descriptor Store overview. + On kernels that support the Live Update Orchestrator, + compatible file descriptors stored this way (such as memfd_create2) + are additionally preserved across kexec-based reboots and handed back to the + service on the other side, provided FileDescriptorStorePreserve=yes is set on + the service (see + systemd.service5). + See the File Descriptor Store + overview for details. + diff --git a/man/systemd-nspawn.xml b/man/systemd-nspawn.xml index c93fad3776..f8b82a4a6e 100644 --- a/man/systemd-nspawn.xml +++ b/man/systemd-nspawn.xml @@ -681,6 +681,20 @@ systemd-vmspawn1 that defaults to true.) + If systemd-nspawn itself is invoked with a $NOTIFY_SOCKET + set in its environment (i.e. it is itself supervised by a service manager that uses the + sd_notify3 + protocol), FDSTORE=1 and FDSTOREREMOVE=1 messages received + from the container payload (along with any accompanying file descriptors and + FDNAME= tag) are forwarded one level up to the enveloping service manager. This + allows the file descriptor store of services running inside the container to be preserved across + container restarts (and, transitively, across restarts, re-execs, soft-reboots and LUO-based kexecs + of any outer service manager), provided + FileDescriptorStoreMax=/FileDescriptorStorePreserve=yes are + configured on the unit running systemd-nspawn. See the + File Descriptor Store documentation + for details. + diff --git a/man/systemd.service.xml b/man/systemd.service.xml index d4a1978523..028c1144b0 100644 --- a/man/systemd.service.xml +++ b/man/systemd.service.xml @@ -1255,6 +1255,26 @@ RestartMaxDelaySec=160s is removed, the service manager exits, or the file descriptors get EPOLLHUP or EPOLLERR. + When set to yes, and the service is itself running under another service + manager (e.g. a service of user@.service, or a payload inside + systemd-nspawn1), + file descriptors pushed into the store are also forwarded one level up via the enveloping manager's + $NOTIFY_SOCKET, tagged with the originating unit id, so that they are preserved + across restarts of the inner manager and handed back to the originating unit when it is started + again. For this to take effect, the enveloping unit must itself enable + FileDescriptorStoreMax= and FileDescriptorStorePreserve=yes. + See the File Descriptor Store + overview for details. + + Setting this to yes also ensures the file descriptor store is kept loaded + across a kexec-based reboot on kernels supporting the Live Update Orchestrator, + so that compatible file descriptors (such as memfd_create2) + are preserved and handed back to the service on the other side. See the File Descriptor Store overview for + details. + Use systemctl clean --what=fdstore … to release the file descriptor store explicitly. diff --git a/mkosi/mkosi.extra/usr/lib/systemd/system/systemd-nspawn@.service.d/fdstore.conf b/mkosi/mkosi.extra/usr/lib/systemd/system/systemd-nspawn@.service.d/fdstore.conf new file mode 100644 index 0000000000..3b023f7832 --- /dev/null +++ b/mkosi/mkosi.extra/usr/lib/systemd/system/systemd-nspawn@.service.d/fdstore.conf @@ -0,0 +1,3 @@ +[Service] +FileDescriptorStoreMax=16 +FileDescriptorStorePreserve=yes diff --git a/mkosi/mkosi.extra/usr/lib/systemd/system/user@.service.d/fdstore.conf b/mkosi/mkosi.extra/usr/lib/systemd/system/user@.service.d/fdstore.conf new file mode 100644 index 0000000000..8a0b417e97 --- /dev/null +++ b/mkosi/mkosi.extra/usr/lib/systemd/system/user@.service.d/fdstore.conf @@ -0,0 +1,5 @@ +# For tests exercising the fd store we need the unit in the rootfs to have these +# settings, or the fdstore content will be dropped in the initrd -> rootfs transition +[Service] +FileDescriptorStoreMax=20 +FileDescriptorStorePreserve=yes diff --git a/mkosi/mkosi.images/minimal-base/mkosi.postinst b/mkosi/mkosi.images/minimal-base/mkosi.postinst index 6feaebc19a..ba3f4aec31 100755 --- a/mkosi/mkosi.images/minimal-base/mkosi.postinst +++ b/mkosi/mkosi.images/minimal-base/mkosi.postinst @@ -16,3 +16,8 @@ chmod +x "$BUILDROOT/sbin/init" if [ ! -e "$BUILDROOT/etc/os-release" ]; then ln -s ../usr/lib/os-release "$BUILDROOT/etc/os-release" fi + +# For use in the minimal containers, only needs libsystemd and libc +if [[ -x "$BUILDDIR/test-fdstore" ]]; then + cp "$BUILDDIR/test-fdstore" "$BUILDROOT/usr/bin/test-fdstore" +fi diff --git a/mkosi/mkosi.initrd.conf/mkosi.extra/usr/lib/systemd/system/systemd-nspawn@.service.d/fdstore.conf b/mkosi/mkosi.initrd.conf/mkosi.extra/usr/lib/systemd/system/systemd-nspawn@.service.d/fdstore.conf new file mode 100644 index 0000000000..3b023f7832 --- /dev/null +++ b/mkosi/mkosi.initrd.conf/mkosi.extra/usr/lib/systemd/system/systemd-nspawn@.service.d/fdstore.conf @@ -0,0 +1,3 @@ +[Service] +FileDescriptorStoreMax=16 +FileDescriptorStorePreserve=yes diff --git a/mkosi/mkosi.initrd.conf/mkosi.extra/usr/lib/systemd/system/user@.service.d/fdstore.conf b/mkosi/mkosi.initrd.conf/mkosi.extra/usr/lib/systemd/system/user@.service.d/fdstore.conf new file mode 100644 index 0000000000..311feddad0 --- /dev/null +++ b/mkosi/mkosi.initrd.conf/mkosi.extra/usr/lib/systemd/system/user@.service.d/fdstore.conf @@ -0,0 +1,5 @@ +# For tests exercising the FD store we need the unit in the initrd to have these +# settings, or the fdstore content will be dropped in the initrd +[Service] +FileDescriptorStoreMax=20 +FileDescriptorStorePreserve=yes diff --git a/src/analyze/analyze-condition.c b/src/analyze/analyze-condition.c index a928f84ef4..5c5177c3be 100644 --- a/src/analyze/analyze-condition.c +++ b/src/analyze/analyze-condition.c @@ -98,7 +98,7 @@ static int verify_conditions(char **lines, RuntimeScope scope, const char *unit, return log_error_errno(r, "Failed to initialize manager: %m"); log_debug("Starting manager..."); - r = manager_startup(m, /* serialization= */ NULL, /* fds= */ NULL, root); + r = manager_startup(m, /* serialization= */ NULL, /* fds= */ NULL, /* named_listen_fds= */ NULL, root); if (r < 0) return r; diff --git a/src/analyze/analyze-security.c b/src/analyze/analyze-security.c index 08bff2cf9e..fdeaf69e17 100644 --- a/src/analyze/analyze-security.c +++ b/src/analyze/analyze-security.c @@ -2713,7 +2713,7 @@ static int offline_security_checks( log_debug("Starting manager..."); - r = manager_startup(m, /* serialization= */ NULL, /* fds= */ NULL, root); + r = manager_startup(m, /* serialization= */ NULL, /* fds= */ NULL, /* named_listen_fds= */ NULL, root); if (r < 0) return r; diff --git a/src/analyze/analyze-verify-util.c b/src/analyze/analyze-verify-util.c index e7ffae5a28..ad553078d4 100644 --- a/src/analyze/analyze-verify-util.c +++ b/src/analyze/analyze-verify-util.c @@ -315,7 +315,7 @@ int verify_units( log_debug("Starting manager..."); - r = manager_startup(m, /* serialization= */ NULL, /* fds= */ NULL, root); + r = manager_startup(m, /* serialization= */ NULL, /* fds= */ NULL, /* named_listen_fds= */ NULL, root); if (r < 0) return r; diff --git a/src/core/luo.c b/src/core/luo.c new file mode 100644 index 0000000000..9dc892d5d6 --- /dev/null +++ b/src/core/luo.c @@ -0,0 +1,288 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-json.h" + +#include "errno-util.h" +#include "fd-util.h" +#include "fdset.h" +#include "fileio.h" +#include "json-util.h" +#include "log.h" +#include "luo.h" +#include "luo-util.h" +#include "manager.h" +#include "serialize.h" +#include "service.h" +#include "unit.h" +#include "unit-name.h" + +static int luo_read_mapping(int session_fd, sd_json_variant **ret) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + _cleanup_close_ int mapping_fd = -EBADF; + _cleanup_fclose_ FILE *f = NULL; + int r; + + assert(session_fd >= 0); + assert(ret); + + mapping_fd = luo_session_retrieve_fd(session_fd, LUO_MAPPING_INDEX); + if (mapping_fd < 0) + return log_warning_errno(mapping_fd, "Failed to retrieve LUO mapping fd (fd_index 0): %m"); + + r = fdopen_independent(mapping_fd, "r", &f); + if (r < 0) + return log_warning_errno(r, "Failed to open LUO mapping fd for reading: %m"); + + r = sd_json_parse_file(f, "luo-mapping", SD_JSON_PARSE_MUST_BE_OBJECT, &v, /* reterr_line= */ NULL, /* reterr_column= */ NULL); + if (r < 0) + return log_warning_errno(r, "Failed to parse LUO mapping JSON: %m"); + + *ret = TAKE_PTR(v); + return 0; +} + +static void luo_session_finishp(int *fd) { + assert(fd); + + if (*fd >= 0) + (void) luo_session_finish(*fd); + safe_close(*fd); +} + +int manager_luo_restore_fd_stores(Manager *m) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *mapping = NULL; + _cleanup_close_ int device_fd = -EBADF; + _cleanup_(luo_session_finishp) int session_fd = -EBADF; + const char *unit_id; + sd_json_variant *fds_json; + int r, n_total = 0; + + assert(m); + + if (MANAGER_IS_USER(m)) + return 0; + + device_fd = luo_open_device(); + if (ERRNO_IS_NEG_DEVICE_ABSENT(device_fd)) { + log_debug_errno(device_fd, "No /dev/liveupdate device found, skipping LUO fd store restoration."); + return 0; + } + if (device_fd < 0) + return log_warning_errno(device_fd, "Failed to open /dev/liveupdate: %m"); + + session_fd = luo_retrieve_session(device_fd, LUO_SESSION_NAME); + if (session_fd == -ENOENT) { + log_debug("No LUO session '%s' found, skipping fd store restoration.", LUO_SESSION_NAME); + return 0; + } + if (session_fd < 0) + return log_warning_errno(session_fd, "Failed to retrieve LUO session '%s': %m", LUO_SESSION_NAME); + + log_debug("Found LUO session '%s', restoring fd stores.", LUO_SESSION_NAME); + + r = luo_read_mapping(session_fd, &mapping); + if (r < 0) + return r; + + /* Retrieve all fds from the session and dispatch each to the named unit, eagerly loading the + * unit if necessary. */ + JSON_VARIANT_OBJECT_FOREACH(unit_id, fds_json, mapping) { + sd_json_variant *entry; + + if (!unit_name_is_valid(unit_id, UNIT_NAME_ANY)) { + log_warning("Invalid unit name '%s' in LUO mapping, skipping.", unit_id); + continue; + } + + if (!sd_json_variant_is_array(fds_json)) { + log_warning("LUO mapping for unit '%s' is not a JSON array, skipping.", unit_id); + continue; + } + + JSON_VARIANT_ARRAY_FOREACH(entry, fds_json) { + struct { + const char *type; + const char *name; + uint64_t token; + const char *session_name; + } p = { + .token = UINT64_MAX, + }; + + static const sd_json_dispatch_field dispatch_table[] = { + { "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 }, + {} + }; + + _cleanup_close_ int fd = -EBADF; + + r = sd_json_dispatch(entry, dispatch_table, SD_JSON_ALLOW_EXTENSIONS|SD_JSON_LOG|SD_JSON_WARNING, &p); + if (r < 0) + continue; + + if (streq(p.type, "fd")) { + if (p.token == UINT64_MAX) { + log_warning("LUO mapping for unit '%s' fd '%s': missing 'token' field.", unit_id, p.name); + continue; + } + if (p.token == LUO_MAPPING_INDEX) { + log_warning("LUO mapping for unit '%s' fd '%s': token %" PRIu64 " is reserved for the mapping memfd.", unit_id, p.name, p.token); + continue; + } + + fd = luo_session_retrieve_fd(session_fd, p.token); + if (fd < 0) { + log_warning_errno(fd, "Failed to retrieve LUO fd for unit '%s' name '%s' token %" PRIu64 ": %m", + 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); + continue; + } + + r = manager_dispatch_external_fd_to_unit(m, unit_id, p.name, /* index= */ 0, TAKE_FD(fd), "LUO"); + if (r > 0) + n_total++; + /* On error fd is already consumed by manager_dispatch_external_fd_to_unit. */ + } + } + + if (n_total > 0) + log_debug("Restored %d fd(s) total from LUO session.", n_total); + + return n_total; +} + +int manager_luo_serialize_fd_stores(Manager *m, FILE **ret_f, FDSet **ret_fds) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *root = NULL; + _cleanup_fclose_ FILE *f = NULL; + _cleanup_fdset_free_ FDSet *fds = NULL; + Unit *u; + int r, n_serialized = 0; + + assert(m); + assert(ret_f); + assert(ret_fds); + + if (MANAGER_IS_USER(m)) { + *ret_f = NULL; + *ret_fds = NULL; + return 0; + } + + fds = fdset_new(); + if (!fds) + return log_oom(); + + /* 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; + Service *s; + + if (u->type != UNIT_SERVICE) + continue; + + s = SERVICE(u); + + if (s->fd_store_preserve_mode != EXEC_PRESERVE_YES) + continue; + + if (!s->fd_store) + 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", session_name ? "luo_session" : "fd"), + SD_JSON_BUILD_PAIR_STRING("name", fs->fdname), + 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"); + + n_serialized++; + } + + r = sd_json_variant_set_field(&root, u->id, entries); + if (r < 0) + return log_error_errno(r, "Failed to add unit to LUO serialization JSON: %m"); + } + + if (n_serialized == 0) { + log_debug("No fd store entries to serialize for LUO."); + *ret_f = NULL; + *ret_fds = NULL; + return 0; + } + + r = open_serialization_file("luo-fd-store", &f); + if (r < 0) + return log_error_errno(r, "Failed to create LUO serialization file: %m"); + + r = sd_json_variant_dump(root, /* flags= */ 0, f, /* prefix= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to dump LUO serialization JSON: %m"); + + r = finish_serialization_file(f); + if (r < 0) + return log_error_errno(r, "Failed to finish LUO serialization file: %m"); + + r = fd_cloexec(fileno(f), false); + if (r < 0) + return log_error_errno(r, "Failed to disable O_CLOEXEC for LUO serialization: %m"); + + r = fdset_cloexec(fds, false); + if (r < 0) + return log_error_errno(r, "Failed to disable O_CLOEXEC for LUO serialization fds: %m"); + + log_info("Serialized %d fd store entries for LUO.", n_serialized); + + *ret_f = TAKE_PTR(f); + *ret_fds = TAKE_PTR(fds); + return n_serialized; +} diff --git a/src/core/luo.h b/src/core/luo.h new file mode 100644 index 0000000000..314006c76b --- /dev/null +++ b/src/core/luo.h @@ -0,0 +1,7 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "core-forward.h" + +int manager_luo_restore_fd_stores(Manager *m); +int manager_luo_serialize_fd_stores(Manager *m, FILE **ret_f, FDSet **ret_fds); diff --git a/src/core/main.c b/src/core/main.c index c6d66c9ec7..f98d0edb2d 100644 --- a/src/core/main.c +++ b/src/core/main.c @@ -15,6 +15,7 @@ #include "sd-bus.h" #include "sd-daemon.h" +#include "sd-json.h" #include "sd-messages.h" #include "alloc-util.h" @@ -44,12 +45,15 @@ #include "emergency-action.h" #include "env-util.h" #include "escape.h" +#include "extract-word.h" #include "fd-util.h" #include "fdset.h" #include "fileio.h" #include "format-table.h" #include "format-util.h" #include "glyph-util.h" +#include "hash-funcs.h" +#include "hashmap.h" #include "help-util.h" #include "hexdecoct.h" #include "hostname-setup.h" @@ -59,6 +63,7 @@ #include "initrd-util.h" #include "io-util.h" #include "ipe-setup.h" +#include "json-util.h" #include "killall.h" #include "kmod-setup.h" #include "label-util.h" @@ -67,6 +72,7 @@ #include "load-fragment.h" #include "log.h" #include "loopback-setup.h" +#include "luo.h" #include "machine-id-setup.h" #include "main.h" #include "manager.h" @@ -82,6 +88,7 @@ #include "parse-argument.h" #include "parse-util.h" #include "path-util.h" +#include "pidfd-util.h" #include "proc-cmdline.h" #include "process-util.h" #include "random-util.h" @@ -91,6 +98,7 @@ #include "selinux-setup.h" #include "selinux-util.h" #include "serialize.h" +#include "service.h" #include "set.h" #include "signal-util.h" #include "smack-setup.h" @@ -157,6 +165,7 @@ static RestrictFileSystemAccess arg_restrict_filesystem_access; static nsec_t arg_timer_slack_nsec; static Set* arg_syscall_archs; static FILE* arg_serialization; +static FILE* arg_luo_serialization; static sd_id128_t arg_machine_id; static bool arg_machine_id_from_firmware = false; static EmergencyAction arg_cad_burst_action; @@ -1799,6 +1808,13 @@ static int become_shutdown(int objective, int retval) { if (arg_minimum_uptime_usec != USEC_INFINITY) (void) strv_extendf(&env_block, "MINIMUM_UPTIME_USEC=" USEC_FMT, arg_minimum_uptime_usec); + /* If we have a LUO serialization file, pass the fd to systemd-shutdown so it can + * preserve FD store entries across kexec via the kernel Live Update Orchestrator. */ + if (arg_luo_serialization) { + log_debug("Passing LUO serialization fd to systemd-shutdown."); + (void) strv_extendf(&env_block, "SYSTEMD_LUO_SERIALIZE_FD=%i", fileno(arg_luo_serialization)); + } + (void) write_boot_or_shutdown_osc("shutdown"); execve(SYSTEMD_SHUTDOWN_BINARY_PATH, (char **) command_line, env_block); @@ -3069,10 +3085,254 @@ static int initialize_security( return 0; } -static int collect_fds(FDSet **ret_fds, const char **ret_error_message) { +static int parse_listen_fds_env(unsigned *ret_n_fds, char ***ret_names) { + _cleanup_strv_free_ char **names = NULL; + const char *e; + unsigned n_fds; + int r; + + assert(ret_n_fds); + assert(ret_names); + + /* Parse and validate the LISTEN_PID=/LISTEN_PIDFDID=/LISTEN_FDS=/LISTEN_FDNAMES= environment + * variables. */ + + e = secure_getenv("LISTEN_PID"); + if (!e) + return -ENXIO; + + pid_t pid; + r = parse_pid(e, &pid); + if (r < 0) + return log_debug_errno(r, "Failed to parse LISTEN_PID=%s: %m", e); + if (pid != getpid_cached()) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "LISTEN_PID=%s does not match our own PID " PID_FMT ", ignoring.", + e, + getpid_cached()); + + e = secure_getenv("LISTEN_PIDFDID"); + if (e) { + uint64_t own_pidfdid, pidfdid; + + r = safe_atou64(e, &pidfdid); + if (r < 0) + return log_debug_errno(r, "Failed to parse LISTEN_PIDFDID=%s: %m", e); + + if (pidfd_get_inode_id_self_cached(&own_pidfdid) >= 0 && pidfdid != own_pidfdid) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "LISTEN_PIDFDID=%s does not match our own pidfdid %" PRIu64 ", ignoring.", + e, + own_pidfdid); + } + + e = secure_getenv("LISTEN_FDS"); + if (!e) + return -ENXIO; + + r = safe_atou(e, &n_fds); + if (r < 0) + return log_debug_errno(r, "Failed to parse LISTEN_FDS= value '%s': %m", e); + if (n_fds == 0) + return -ENXIO; + if (n_fds > INT_MAX - SD_LISTEN_FDS_START) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid number of fds in LISTEN_FDS= value '%s'", e); + + e = secure_getenv("LISTEN_FDNAMES"); + if (!e) + return -ENXIO; + + r = strv_split_full(&names, e, ":", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r < 0) + return log_debug_errno(r, "Failed to parse LISTEN_FDNAMES=%s: %m", e); + if (strv_length(names) != (size_t) n_fds) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "Mismatch between number of LISTEN_FDS= and LISTEN_FDNAMES= entries: %u vs %zu", + n_fds, strv_length(names)); + + *ret_n_fds = n_fds; + *ret_names = TAKE_PTR(names); + return 0; +} + +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR( + index_to_tag_hash_ops, + uint64_t, uint64_hash_func, uint64_compare_func, + ListenFDsTag, listen_fds_tag_free); + +static int parse_listen_fds_mapping(int mapping_fd, Hashmap **ret_index_to_tag) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *root = NULL; + _cleanup_hashmap_free_ Hashmap *index_to_tag = NULL; + const char *unit_id; + sd_json_variant *fds_json; + int r; + + assert(mapping_fd >= 0); + assert(ret_index_to_tag); + + /* Parse the JSON mapping memfd that the downstream manager pushed alongside the indexed fds: + * { "unit-name.service": [ { "name": "fdname1", "index": 1 }, ... ], ... } + * Returns a hashmap keyed by stringified index ("1", "2", ...) with ListenFDsTag* values + * carrying the resolved (unit_id, original fdname, upstream index). */ + + _cleanup_fclose_ FILE *f = NULL; + r = fdopen_independent(mapping_fd, "r", &f); + if (r < 0) + return log_warning_errno(r, "Failed to open fdstore-mapping memfd: %m"); + + r = sd_json_parse_file(f, "fdstore-mapping", SD_JSON_PARSE_MUST_BE_OBJECT, &root, + /* reterr_line= */ NULL, /* reterr_column= */ NULL); + if (r < 0) + return log_warning_errno(r, "Failed to parse fdstore-mapping JSON: %m"); + + JSON_VARIANT_OBJECT_FOREACH(unit_id, fds_json, root) { + sd_json_variant *entry; + + if (!unit_name_is_valid(unit_id, UNIT_NAME_ANY)) { + log_warning("fdstore-mapping has invalid unit name '%s', skipping.", unit_id); + continue; + } + + JSON_VARIANT_ARRAY_FOREACH(entry, fds_json) { + struct { + const char *name; + uint64_t index; + } p = { }; + + static const sd_json_dispatch_field dispatch_table[] = { + { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, name), SD_JSON_MANDATORY }, + { "index", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, voffsetof(p, index), SD_JSON_MANDATORY }, + {} + }; + + r = sd_json_dispatch(entry, dispatch_table, SD_JSON_ALLOW_EXTENSIONS|SD_JSON_LOG|SD_JSON_WARNING, &p); + if (r < 0) + continue; + + if (p.index == 0) { + log_warning("fdstore-mapping entry for unit '%s' name '%s' has zero index, skipping.", unit_id, p.name); + continue; + } + + _cleanup_(listen_fds_tag_freep) ListenFDsTag *t = new(ListenFDsTag, 1); + if (!t) + return log_oom(); + + *t = (ListenFDsTag) { + .index = p.index, + }; + + t->unit_id = strdup(unit_id); + t->fdname = strdup(p.name); + if (!t->unit_id || !t->fdname) + return log_oom(); + + /* Key points into the value struct, so freeing the value frees the key. */ + r = hashmap_ensure_put(&index_to_tag, &index_to_tag_hash_ops, &t->index, t); + if (r < 0) + return log_warning_errno(r, "Failed to insert fdstore-mapping entry into hashmap: %m"); + if (r > 0) + TAKE_PTR(t); + } + } + + *ret_index_to_tag = TAKE_PTR(index_to_tag); + return 0; +} + +static int collect_listen_fds_named(FDSet *fds, Hashmap **ret_named_fds) { + _cleanup_hashmap_free_ Hashmap *named_fds = NULL, *index_to_tag = NULL; + _cleanup_strv_free_ char **names = NULL; + unsigned n_fds; + int r; + + assert(fds); + assert(ret_named_fds); + + /* Pull entries from the LISTEN_FDS=/LISTEN_FDNAMES= protocol out of 'fds' into a hashmap + * keyed by fd. Two flavours of named entries are recognized: + * + * - A single mapping memfd whose fdname matches SERVICE_FDSTORE_MAPPING_FDNAME, which + * contains a JSON map pairing numeric indices to (unit-id, original-fdname). + * - Numeric indices (matching entries in the mapping document) for the actual fds. + * + * The hashmap owns the fds (closed via destructor on cleanup) so any entries the dispatcher + * does not consume are correctly cleaned up. */ + + r = parse_listen_fds_env(&n_fds, &names); + if (r < 0) { + /* Fail gracefully here, just warn and ignore but otherwise proceed on parsing failure */ + if (r != -ENXIO) + log_warning_errno(r, "Failed to parse LISTEN_FDS environment, ignoring: %m"); + *ret_named_fds = NULL; + return 0; + } + + /* First pass: locate and parse the mapping memfd, if any. */ + for (unsigned i = 0; i < n_fds; i++) { + int fd = SD_LISTEN_FDS_START + i; + + if (!streq(names[i], SERVICE_FDSTORE_MAPPING_FDNAME)) + continue; + + if (!fdset_contains(fds, fd)) + continue; + + (void) parse_listen_fds_mapping(fd, &index_to_tag); + + /* The mapping memfd itself is not routed to any unit; close it and remove from fds + * so it doesn't get redistributed */ + assert_se(fdset_remove(fds, fd) == fd); + safe_close(fd); + break; + } + + /* Second pass: route fds whose name matches an entry in the mapping. */ + for (unsigned i = 0; i < n_fds; i++) { + int fd = SD_LISTEN_FDS_START + i; + const char *name = names[i], *suffix; + ListenFDsTag *t; + uint64_t idx; + + if (!fdset_contains(fds, fd)) + continue; + + if (!index_to_tag) + continue; + + suffix = startswith(name, SERVICE_FDSTORE_SUB_FDNAME_PREFIX); + if (!suffix || safe_atou64(suffix, &idx) < 0) + continue; + + /* Steal the matching mapping entry — we transfer ownership of the parsed + * (unit_id, fdname, index) struct into the per-fd hashmap that the manager + * will consume. */ + t = hashmap_remove(index_to_tag, &idx); + if (!t) + continue; + + _cleanup_(listen_fds_tag_freep) ListenFDsTag *t_owned = t; + + r = hashmap_ensure_put(&named_fds, &fd_to_listen_fds_tag_hash_ops, FD_TO_PTR(fd), t_owned); + if (r < 0) + return log_debug_errno(r, "Failed to insert named fd into hashmap: %m"); + if (r == 0) + continue; /* fd already inserted, cannot really happen */ + + TAKE_PTR(t_owned); + + assert_se(fdset_remove(fds, fd) == fd); + } + + *ret_named_fds = TAKE_PTR(named_fds); + return 1; +} + +static int collect_fds(FDSet **ret_fds, Hashmap **ret_named_fds, const char **ret_error_message) { int r; assert(ret_fds); + assert(ret_named_fds); assert(ret_error_message); /* Pick up all fds passed to us. We apply a filter here: we only take the fds that have O_CLOEXEC @@ -3094,6 +3354,8 @@ static int collect_fds(FDSet **ret_fds, const char **ret_error_message) { /* The serialization fd should have O_CLOEXEC turned on already, let's verify that we didn't pick it up here */ assert_se(!arg_serialization || !fdset_contains(*ret_fds, fileno(arg_serialization))); + (void) collect_listen_fds_named(*ret_fds, ret_named_fds); + return 0; } @@ -3151,6 +3413,7 @@ int main(int argc, char *argv[]) { * for the two that indicate whether * these fields are initialized! */ bool skip_setup, loaded_policy = false, queue_default_job = false, first_boot = false; + _cleanup_hashmap_free_ Hashmap *named_listen_fds = NULL; char *switch_root_dir = NULL, *switch_root_init = NULL; usec_t before_startup, after_startup; static char systemd[] = "systemd"; @@ -3396,7 +3659,7 @@ int main(int argc, char *argv[]) { log_close(); /* Remember open file descriptors for later deserialization */ - r = collect_fds(&fds, &error_message); + r = collect_fds(&fds, &named_listen_fds, &error_message); if (r < 0) goto finish; @@ -3471,7 +3734,7 @@ int main(int argc, char *argv[]) { before_startup = now(CLOCK_MONOTONIC); - r = manager_startup(m, arg_serialization, fds, /* root= */ NULL); + r = manager_startup(m, arg_serialization, fds, named_listen_fds, /* root= */ NULL); if (r < 0) { error_message = "Failed to start up manager"; goto finish; @@ -3523,6 +3786,12 @@ finish: if (m) { arg_reboot_watchdog = manager_get_watchdog(m, WATCHDOG_REBOOT); arg_kexec_watchdog = manager_get_watchdog(m, WATCHDOG_KEXEC); + + /* For kexec, serialize fd stores now. Services have stopped and sent + * their FDs to the store, but the manager (and its fd stores) is still alive. */ + if (r == MANAGER_KEXEC) + (void) manager_luo_serialize_fd_stores(m, &arg_luo_serialization, &fds); + m = manager_free(m); } @@ -3540,7 +3809,13 @@ finish: &error_message); /* This only returns if reexecution failed */ arg_serialization = safe_fclose(arg_serialization); - fds = fdset_free(fds); + + /* For kexec, the FDSet and LUO serialization file must survive until become_shutdown() calls + * execve() (CLOEXEC is already cleared on these FDs). For all other paths, free them now. */ + if (r != MANAGER_KEXEC) { + fds = fdset_free(fds); + arg_luo_serialization = safe_fclose(arg_luo_serialization); + } saved_env = strv_free(saved_env); diff --git a/src/core/manager-serialize.c b/src/core/manager-serialize.c index 6bc41f15f9..bef4021771 100644 --- a/src/core/manager-serialize.c +++ b/src/core/manager-serialize.c @@ -124,6 +124,7 @@ int manager_serialize( (void) serialize_item(f, "previous-objective", manager_objective_to_string(m->objective)); (void) serialize_item_format(f, "soft-reboots-count", "%u", m->soft_reboots_count); + (void) serialize_item_format(f, "fd-store-upstream-next-index", "%" PRIu64, m->fd_store_upstream_next_index); for (ManagerTimestamp q = 0; q < _MANAGER_TIMESTAMP_MAX; q++) { _cleanup_free_ char *joined = NULL; @@ -757,6 +758,9 @@ int manager_deserialize(Manager *m, FILE *f, FDSet *fds) { log_notice("Failed to parse soft reboots counter '%s', ignoring.", val); else m->soft_reboots_count = n; + } else if ((val = startswith(l, "fd-store-upstream-next-index="))) { + if (safe_atou64(val, &m->fd_store_upstream_next_index) < 0) + log_notice("Failed to parse fd-store-upstream-next-index '%s', ignoring.", val); } else if ((val = startswith(l, "previous-objective="))) { ManagerObjective objective; diff --git a/src/core/manager.c b/src/core/manager.c index 038690a808..accf9c8ff9 100644 --- a/src/core/manager.c +++ b/src/core/manager.c @@ -57,6 +57,7 @@ #include "libaudit-util.h" #include "locale-setup.h" #include "log.h" +#include "luo.h" #include "manager-dump.h" #include "manager-serialize.h" #include "manager.h" @@ -76,6 +77,7 @@ #include "rm-rf.h" #include "selinux-util.h" #include "serialize.h" +#include "service.h" #include "set.h" #include "signal-util.h" #include "socket-util.h" @@ -1878,6 +1880,93 @@ static void manager_catchup(Manager *m) { } } +ListenFDsTag* listen_fds_tag_free(ListenFDsTag *t) { + if (!t) + return NULL; + + free(t->unit_id); + free(t->fdname); + return mfree(t); +} + +DEFINE_HASH_OPS_FULL( + fd_to_listen_fds_tag_hash_ops, + void, trivial_hash_func, trivial_compare_func, close_fd_ptr, + ListenFDsTag, listen_fds_tag_free); + +int manager_dispatch_external_fd_to_unit( + Manager *m, + const char *unit_id, + const char *fdname, + uint64_t index, + int fd_in, + const char *log_context) { + + _cleanup_close_ int fd = ASSERT_FD(fd_in); + Unit *u = NULL; + int r; + + assert(m); + assert(unit_id); + assert(fdname); + assert(log_context); + + /* Load the unit eagerly: if the unit file exists this brings it into UNIT_LOADED, otherwise it + * lands in UNIT_NOT_FOUND. In both cases we want to attach the fd so it's preserved until the + * unit is fully stopped (or its file appears via daemon-reload). */ + r = manager_load_unit(m, unit_id, /* path= */ NULL, /* e= */ NULL, &u); + if (r < 0) + return log_warning_errno(r, "%s: failed to load unit '%s', closing fd '%s': %m", + log_context, unit_id, fdname); + + if (!UNIT_VTABLE(u)->attach_external_fd_to_fdstore) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: unit '%s' does not support fd restoration, closing fd '%s'.", + log_context, unit_id, fdname); + + r = UNIT_VTABLE(u)->attach_external_fd_to_fdstore(u, TAKE_FD(fd), fdname, index); + if (r < 0) + return log_unit_warning_errno(u, r, "%s: failed to attach fd '%s' to fd store: %m", + log_context, fdname); + + return 1; /* fd consumed */ +} + +static int manager_distribute_listen_fds_named(Manager *m, Hashmap *named_listen_fds) { + assert(m); + + /* Route fds whose LISTEN_FDNAMES name was a numeric index into the matching unit's fd store. + * The hashmap is built and owned by main.c's collect_fds(), keyed by fd, with ListenFDsTag* values + * that already carry the parsed unit-id, original fdname and index (resolved against the + * upstream-pushed fdstore-mapping memfd). We steal entries here so any leftover (skipped) entries + * are still cleaned up by the hashmap's destructor on the caller side. */ + + if (MANAGER_IS_TEST_RUN(m)) + return 0; + + for (;;) { + _cleanup_(listen_fds_tag_freep) ListenFDsTag *t = NULL; + _cleanup_close_ int fd = -EBADF; + void *key; + + t = hashmap_steal_first_key_and_value(named_listen_fds, &key); + if (!t) + break; + + fd = PTR_TO_FD(key); + + if (!t->unit_id || !t->fdname) + continue; + + if (!unit_name_is_valid(t->unit_id, UNIT_NAME_ANY)) + continue; + + (void) manager_dispatch_external_fd_to_unit(m, t->unit_id, t->fdname, t->index, TAKE_FD(fd), "LISTEN_FDS"); + } + + return 0; +} + static void manager_distribute_fds(Manager *m, FDSet *fds) { Unit *u; @@ -2034,7 +2123,7 @@ static int manager_make_runtime_dir(Manager *m) { return 0; } -int manager_startup(Manager *m, FILE *serialization, FDSet *fds, const char *root) { +int manager_startup(Manager *m, FILE *serialization, FDSet *fds, Hashmap *named_listen_fds, const char *root) { int r; assert(m); @@ -2103,6 +2192,15 @@ int manager_startup(Manager *m, FILE *serialization, FDSet *fds, const char *roo if (m->previous_objective == MANAGER_SOFT_REBOOT) m->soft_reboots_count++; + /* If a LUO (Live Update Orchestrator) session from a previous kexec is available, restore + * preserved file descriptors into the appropriate service fd stores now, before coldplug. */ + (void) manager_luo_restore_fd_stores(m); + + /* Pick up fds passed via the LISTEN_FDS=/LISTEN_FDNAMES= protocol that are tagged with a + * unit id ("unit-id|fdname"), and route them into the matching unit's fd store. Untagged + * fds remain in 'fds' and are handed to socket units below as before. */ + (void) manager_distribute_listen_fds_named(m, named_listen_fds); + /* Any fds left? Find some unit which wants them. This is useful to allow container managers to pass * some file descriptors to us pre-initialized. This enables socket-based activation of entire * containers. */ @@ -2142,6 +2240,10 @@ int manager_startup(Manager *m, FILE *serialization, FDSet *fds, const char *roo /* Clean up runtime objects */ manager_vacuum(m); + /* After deserialization, refresh the upstream JSON mapping memfd so the supervisor's + * view of our fd store stays consistent with the indices we just restored. */ + (void) service_propagate_fd_store_mapping_upstream(m); + if (serialization) /* Let's wait for the UnitNew/JobNew messages being sent, before we notify that the * reload is finished */ diff --git a/src/core/manager.h b/src/core/manager.h index d17693369a..4695112c04 100644 --- a/src/core/manager.h +++ b/src/core/manager.h @@ -513,6 +513,11 @@ typedef struct Manager { /* The number of successfully completed configuration reloads. */ uint64_t reload_count; + /* Monotonic counter for fdstore entries propagated to a NOTIFY_SOCKET supervisor. Each propagated + * fd is sent upstream using this index as the FDNAME. The mapping (index -> unit_id + original fdname) + * is pushed alongside as a JSON memfd named "systemd-fdstore-mapping". */ + uint64_t fd_store_upstream_next_index; + /* Original ambient capabilities when we were initialized */ uint64_t saved_ambient_set; } Manager; @@ -542,7 +547,20 @@ int manager_new(RuntimeScope scope, ManagerTestRunFlags test_run_flags, Manager Manager* manager_free(Manager *m); DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_free); -int manager_startup(Manager *m, FILE *serialization, FDSet *fds, const char *root); +/* One entry parsed out of the upstream "systemd-fdstore-mapping" memfd. Pairs the numeric index from the + * JSON map to the (unit-id, original fdname) the fd was originally stored as. */ +typedef struct ListenFDsTag { + char *unit_id; + char *fdname; + uint64_t index; +} ListenFDsTag; + +ListenFDsTag* listen_fds_tag_free(ListenFDsTag *t); +DEFINE_TRIVIAL_CLEANUP_FUNC(ListenFDsTag*, listen_fds_tag_free); + +extern const struct hash_ops fd_to_listen_fds_tag_hash_ops; + +int manager_startup(Manager *m, FILE *serialization, FDSet *fds, Hashmap *named_listen_fds, const char *root); Job *manager_get_job(Manager *m, uint32_t id); Unit *manager_get_unit(Manager *m, const char *name); @@ -552,6 +570,7 @@ int manager_get_job_from_dbus_path(Manager *m, const char *s, Job **_j); bool manager_unit_cache_should_retry_load(Unit *u); int manager_load_unit_prepare(Manager *m, const char *name, const char *path, sd_bus_error *e, Unit **ret); int manager_load_unit(Manager *m, const char *name, const char *path, sd_bus_error *e, Unit **ret); +int manager_dispatch_external_fd_to_unit(Manager *m, const char *unit_id, const char *fdname, uint64_t index, int fd, const char *log_context); int manager_load_startable_unit_or_warn(Manager *m, const char *name, const char *path, Unit **ret); int manager_load_unit_from_dbus_path(Manager *m, const char *s, sd_bus_error *e, Unit **_u); diff --git a/src/core/meson.build b/src/core/meson.build index 98cf02aef8..2bd8170c6a 100644 --- a/src/core/meson.build +++ b/src/core/meson.build @@ -43,6 +43,7 @@ libcore_sources = files( 'kill.c', 'load-dropin.c', 'load-fragment.c', + 'luo.c', 'manager-dump.c', 'manager-serialize.c', 'manager.c', diff --git a/src/core/service.c b/src/core/service.c index 445812f952..6a02ca8d6f 100644 --- a/src/core/service.c +++ b/src/core/service.c @@ -6,6 +6,7 @@ #include #include "sd-bus.h" +#include "sd-json.h" #include "sd-messages.h" #include "alloc-util.h" @@ -15,6 +16,7 @@ #include "bus-util.h" #include "cgroup.h" #include "chase.h" +#include "daemon-util.h" #include "dbus-service.h" #include "dbus-unit.h" #include "devnum-util.h" @@ -33,6 +35,7 @@ #include "image-policy.h" #include "log.h" #include "manager.h" +#include "memfd-util.h" #include "mount-util.h" #include "namespace.h" #include "open-file.h" @@ -462,12 +465,27 @@ static void service_override_watchdog_timeout(Service *s, usec_t watchdog_overri log_unit_debug(UNIT(s), "watchdog_override_usec="USEC_FMT, s->watchdog_override_usec); } -static ServiceFDStore* service_fd_store_unlink(ServiceFDStore *fs) { +static ServiceFDStore* service_fd_store_unlink_full(ServiceFDStore *fs, bool propagate_upstream) { if (!fs) return NULL; if (fs->service) { assert(fs->service->n_fd_store > 0); + + /* If we previously propagated this fd to an enveloping service/container manager via + * the FDSTORE=1 protocol on its NOTIFY_SOCKET (only done when persistence is on), + * tell that supervisor to drop it now too, so the upstream fd store stays in sync. + * Only do this for explicit removals (EPOLLHUP/EPOLLERR or app FDSTOREREMOVE), not + * for local cleanup like service shutdown or fdstore-limit truncation: in those + * cases we want the upstream copy to survive so it can be handed back to us later. */ + if (propagate_upstream && fs->index > 0) { + (void) notify_remove_fd_warnf(SERVICE_FDSTORE_SUB_FDNAME_PREFIX "%" PRIu64, fs->index); + fs->index = 0; + /* Refresh the upstream JSON mapping so the supervisor's view stays in sync + * with what fds are actually still around. */ + (void) service_propagate_fd_store_mapping_upstream(UNIT(fs->service)->manager); + } + LIST_REMOVE(fd_store, fs->service->fd_store, fs); fs->service->n_fd_store--; } @@ -479,6 +497,10 @@ static ServiceFDStore* service_fd_store_unlink(ServiceFDStore *fs) { return mfree(fs); } +static ServiceFDStore* service_fd_store_unlink(ServiceFDStore *fs) { + return service_fd_store_unlink_full(fs, /* propagate_upstream= */ false); +} + DEFINE_TRIVIAL_CLEANUP_FUNC(ServiceFDStore*, service_fd_store_unlink); static void service_release_fd_store(Service *s) { @@ -495,6 +517,21 @@ static void service_release_fd_store(Service *s) { assert(s->n_fd_store == 0); } +static void service_truncate_fd_store(Service *s) { + assert(s); + + /* Drop fds that exceed the (possibly newly lowered) n_fd_store_max, e.g. after the fragment was + * 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) { + 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); + service_fd_store_unlink(fs); + } +} + static void service_release_extra_fds(Service *s) { assert(s); @@ -512,6 +549,15 @@ static void service_release_extra_fds(Service *s) { s->n_extra_fds = 0; } +ServiceExtraFD* service_extra_fd_free(ServiceExtraFD *fd) { + if (!fd) + return NULL; + + safe_close(fd->fd); + free(fd->fdname); + return mfree(fd); +} + static void service_release_stdio_fd(Service *s) { assert(s); @@ -585,7 +631,7 @@ static int on_fd_store_io(sd_event_source *e, int fd, uint32_t revents, void *us "Received %s on stored fd %d (%s), closing.", revents & EPOLLERR ? "EPOLLERR" : "EPOLLHUP", fs->fd, strna(fs->fdname)); - service_fd_store_unlink(fs); + service_fd_store_unlink_full(fs, /* propagate_upstream= */ true); if (s->state == SERVICE_DEAD_RESOURCES_PINNED && !SERVICE_FD_STORE_POPULATED(s)) service_set_state(s, SERVICE_DEAD); @@ -593,7 +639,7 @@ static int on_fd_store_io(sd_event_source *e, int fd, uint32_t revents, void *us return 0; } -static int service_add_fd_store(Service *s, int fd_in, const char *name, bool do_poll) { +int service_add_fd_store(Service *s, int fd_in, const char *name, bool do_poll, bool propagate_upstream) { _cleanup_(service_fd_store_unlinkp) ServiceFDStore *fs = NULL; _cleanup_(asynchronous_closep) int fd = ASSERT_FD(fd_in); struct stat st; @@ -647,14 +693,43 @@ static int service_add_fd_store(Service *s, int fd_in, const char *name, bool do log_unit_debug(UNIT(s), "Added fd %i (%s) to fd store.", fs->fd, fs->fdname); + /* If fd-store persistence is enabled and we have an enveloping service/container manager (i.e. + * NOTIFY_SOCKET is set), forward the fd to it via sd_notify(FDSTORE=1) tagged with a fresh + * incrementing index, and (re-)push the JSON mapping memfd that pairs the index back to this + * unit and the original fdname. This way fdstore persistence chains all the way up to whichever + * entity is ultimately responsible for surviving across kexec/restart, regardless of fdname + * length or charset constraints. */ + if (propagate_upstream && s->fd_store_preserve_mode == EXEC_PRESERVE_YES) { + Manager *m = ASSERT_PTR(UNIT(s)->manager); + char idx_str[STRLEN(SERVICE_FDSTORE_SUB_FDNAME_PREFIX) + DECIMAL_STR_MAX(uint64_t)]; + + assert(m->fd_store_upstream_next_index < UINT64_MAX); + uint64_t idx = ++m->fd_store_upstream_next_index; + + xsprintf(idx_str, SERVICE_FDSTORE_SUB_FDNAME_PREFIX "%" PRIu64, idx); + + r = notify_push_fd(fs->fd, idx_str); + if (r < 0) + log_unit_debug_errno(UNIT(s), r, + "Failed to propagate fd '%s' to upstream supervisor as index %" PRIu64 ", ignoring: %m", + fs->fdname, idx); + else + fs->index = idx; + } + fs->service = s; LIST_PREPEND(fd_store, s->fd_store, TAKE_PTR(fs)); s->n_fd_store++; + if (propagate_upstream && s->fd_store_preserve_mode == EXEC_PRESERVE_YES) + /* Refresh the JSON mapping memfd so the supervisor can resolve the new index. Do this + * after LIST_PREPEND so the new entry is visible to the helper. */ + (void) service_propagate_fd_store_mapping_upstream(UNIT(s)->manager); + return 1; /* fd newly stored */ } -static int service_add_fd_store_set(Service *s, FDSet *fds, const char *name, bool do_poll) { +static int service_add_fd_store_set(Service *s, FDSet *fds, const char *name, bool do_poll, bool propagate_upstream) { int r; assert(s); @@ -666,7 +741,7 @@ static int service_add_fd_store_set(Service *s, FDSet *fds, const char *name, bo if (fd < 0) break; - r = service_add_fd_store(s, fd, name, do_poll); + r = service_add_fd_store(s, fd, name, do_poll, propagate_upstream); if (r == -EXFULL) return log_unit_warning_errno(UNIT(s), r, "Cannot store more fds than FileDescriptorStoreMax=%u, closing remaining.", @@ -678,6 +753,139 @@ static int service_add_fd_store_set(Service *s, FDSet *fds, const char *name, bo 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; + _cleanup_free_ char *text = NULL; + Unit *u; + int r; + + assert(m); + + /* Build a JSON object listing all fdstore entries that have been propagated upstream: + * + * { + * "unit-name.service": [ + * { "name": "fdname1", "index": 1 }, + * { "name": "fdname2", "index": 2 } + * ], + * ... + * } + * + * Push it as a sealed memfd to the upstream supervisor under a fixed FDNAME so it can resolve + * the per-fd numeric indices back to (unit_id, original fdname) at startup. The mapping is + * regenerated and re-pushed after every add/remove, so the supervisor's view stays in sync. */ + HASHMAP_FOREACH(u, m->units) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *entries = NULL; + Service *s; + + if (u->type != UNIT_SERVICE) + continue; + + s = SERVICE(u); + if (!s->fd_store) + continue; + + LIST_FOREACH(fd_store, fs, s->fd_store) { + if (fs->index == 0) + continue; + + r = sd_json_variant_append_arraybo( + &entries, + SD_JSON_BUILD_PAIR_STRING("name", fs->fdname), + SD_JSON_BUILD_PAIR_UNSIGNED("index", fs->index)); + if (r < 0) + return log_warning_errno(r, "Failed to build fdstore-mapping JSON entry: %m"); + } + + if (!entries) + continue; + + r = sd_json_variant_set_field(&root, u->id, entries); + if (r < 0) + return log_warning_errno(r, "Failed to add unit to fdstore-mapping JSON: %m"); + } + + if (!root) { + /* Nothing to map: tell the supervisor to drop any previously-pushed mapping memfd + * so it doesn't keep stale entries around. Only do this if we have actually pushed + * one in the past (i.e. we ever assigned an upstream index, either in this + * incarnation or in a previous one whose counter we deserialized), otherwise we + * might inadvertently remove a mapping that was just handed back to us via + * LISTEN_FDS during a fresh manager startup. */ + if (m->fd_store_upstream_next_index > 0) + (void) notify_remove_fd_warn(SERVICE_FDSTORE_MAPPING_FDNAME); + return 0; + } + + r = sd_json_variant_format(root, /* flags= */ 0, &text); + if (r < 0) + return log_warning_errno(r, "Failed to format fdstore-mapping JSON: %m"); + + fd = memfd_new_and_seal_string(SERVICE_FDSTORE_MAPPING_FDNAME, text); + if (fd < 0) + return log_warning_errno(fd, "Failed to create fdstore-mapping memfd: %m"); + + r = notify_push_fd(fd, SERVICE_FDSTORE_MAPPING_FDNAME); + if (r < 0) + return log_warning_errno(r, "Failed to propagate fdstore-mapping to upstream supervisor: %m"); + + return 0; +} + +static int service_attach_external_fd_to_fdstore(Unit *u, int fd, const char *fdname, uint64_t index) { + Service *s = ASSERT_PTR(SERVICE(u)); + int r; + + assert(u->type == UNIT_SERVICE); + + /* If the unit file is absent, bump the limit by one and force preserve so the fd is + * accepted and pins the unit until a daemon-reload picks up the unit file or it is + * explicitly stopped. */ + if (u->load_state == UNIT_NOT_FOUND) { + s->fd_store_preserve_mode = EXEC_PRESERVE_YES; + s->n_fd_store_max++; + } + + /* Don't propagate upstream: the fd just came back from upstream, forwarding it would loop. */ + r = service_add_fd_store(s, fd, fdname, /* do_poll= */ true, /* propagate_upstream= */ false); + if (r <= 0 && u->load_state == UNIT_NOT_FOUND) + s->n_fd_store_max--; + if (r < 0) + return log_unit_debug_errno(u, r, "Failed to add LUO fd '%s' to fd store: %m", fdname); + + /* If the fd was previously propagated to an upstream supervisor under a numeric index, + * preserve that index on the freshly-added entry so that future FDSTOREREMOVE messages + * (and the fdstore-mapping memfd we re-push to the supervisor) reference the same index + * the supervisor already knows about. service_add_fd_store() does LIST_PREPEND() on + * success, so the new entry is at the head. Also keep the manager's allocator counter + * past the highest restored index, to avoid collisions with newly allocated indices. */ + if (r > 0 && index > 0 && s->fd_store) { + Manager *m = ASSERT_PTR(u->manager); + + s->fd_store->index = index; + if (index > m->fd_store_upstream_next_index) + m->fd_store_upstream_next_index = index; + } + + /* If the unit is otherwise inactive (typical for LUO/upstream restore), pin its resources so it + * isn't garbage-collected before something explicitly stops it. Only flip the state when both + * runtime and deserialized state agree on DEAD, to avoid clobbering a just-deserialized live + * state (e.g. SERVICE_RUNNING after daemon-reload, where service_coldplug() will set the proper + * state later). */ + if (r > 0 && + s->state == SERVICE_DEAD && + s->deserialized_state == SERVICE_DEAD && + s->fd_store_preserve_mode == EXEC_PRESERVE_YES) { + service_set_state(s, SERVICE_DEAD_RESOURCES_PINNED); + s->deserialized_state = SERVICE_DEAD_RESOURCES_PINNED; + } + + if (r > 0) + log_unit_debug(u, "Restored fd '%s'.", fdname); + return r; +} + static void service_remove_fd_store(Service *s, const char *name) { assert(s); assert(name); @@ -687,7 +895,7 @@ static void service_remove_fd_store(Service *s, const char *name) { continue; log_unit_debug(UNIT(s), "Got explicit request to remove fd %i (%s), closing.", fs->fd, name); - service_fd_store_unlink(fs); + service_fd_store_unlink_full(fs, /* propagate_upstream= */ true); } } @@ -952,6 +1160,11 @@ static int service_load(Unit *u) { if (u->load_state != UNIT_LOADED) return 0; + /* The fragment may have lowered FileDescriptorStoreMax= below the number of fds currently in the + * store (e.g. fds restored from LUO into a synthesized UNIT_NOT_FOUND service that just got a real + * fragment via lazy reload, but which now disables the fd store). */ + service_truncate_fd_store(s); + /* This is a new unit? Then let's add in some extras */ r = service_add_extras(s); if (r < 0) @@ -1437,7 +1650,8 @@ static int service_coldplug(Unit *u) { int r; assert(s); - assert(s->state == SERVICE_DEAD); + /* Ensure we can insert FD store into units at boot */ + assert(IN_SET(s->state, SERVICE_DEAD, SERVICE_DEAD_RESOURCES_PINNED)); if (s->deserialized_state == s->state) return 0; @@ -3470,7 +3684,8 @@ static int service_serialize(Unit *u, FILE *f, FDSet *fds) { if (!c) return log_oom(); - (void) serialize_item_format(f, "fd-store-fd", "%i \"%s\" %s", copy, c, one_zero(fs->do_poll)); + (void) serialize_item_format(f, "fd-store-fd", "%i \"%s\" %s %" PRIu64, + copy, c, one_zero(fs->do_poll), fs->index); } FOREACH_ARRAY(i, s->extra_fds, s->n_extra_fds) { @@ -3740,12 +3955,13 @@ static int service_deserialize_item(Unit *u, const char *key, const char *value, s->socket_fd = deserialize_fd(fds, value); } else if (streq(key, "fd-store-fd")) { - _cleanup_free_ char *fdv = NULL, *fdn = NULL, *fdp = NULL; + _cleanup_free_ char *fdv = NULL, *fdn = NULL, *fdp = NULL, *fdi = NULL; _cleanup_close_ int fd = -EBADF; int do_poll; + uint64_t index = 0; - r = extract_many_words(&value, " ", EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE, &fdv, &fdn, &fdp); - if (r < 2 || r > 3) { + r = extract_many_words(&value, " ", EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE, &fdv, &fdn, &fdp, &fdi); + if (r < 2 || r > 4) { log_unit_debug(u, "Failed to deserialize fd-store-fd, ignoring: %s", value); return 0; } @@ -3754,19 +3970,45 @@ static int service_deserialize_item(Unit *u, const char *key, const char *value, if (fd < 0) return 0; - do_poll = r == 3 ? parse_boolean(fdp) : true; + do_poll = r >= 3 ? parse_boolean(fdp) : true; if (do_poll < 0) { log_unit_debug_errno(u, do_poll, "Failed to deserialize fd-store-fd do_poll, ignoring: %s", fdp); return 0; } - r = service_add_fd_store(s, TAKE_FD(fd), fdn, do_poll); + if (r == 4 && safe_atou64(fdi, &index) < 0) { + log_unit_debug(u, "Failed to parse fd-store-fd index '%s', ignoring.", fdi); + index = 0; + } + + /* If the unit file is currently absent (e.g. after switch-root, before the unit file is + * available in the new root), the synthesized service has n_fd_store_max=0 and + * preserve_mode=NO, which would reject the fd. Grow the limit by one per fd so it matches + * exactly what was handed back, and force EXEC_PRESERVE_YES, so the fd survives until + * either a daemon-reload picks up the unit file or the service is explicitly stopped. + * Same logic as in luo_dispatch_fd(). */ + if (u->load_state == UNIT_NOT_FOUND) { + s->fd_store_preserve_mode = EXEC_PRESERVE_YES; + s->n_fd_store_max++; + } + + /* Don't propagate upstream during deserialization: the upstream supervisor (if any) + * already has these fds from when they were originally pushed. */ + r = service_add_fd_store(s, TAKE_FD(fd), fdn, do_poll, /* propagate_upstream= */ false); + if (r <= 0 && u->load_state == UNIT_NOT_FOUND) + /* The fd was not actually stored, roll back the limit bump. */ + s->n_fd_store_max--; if (r < 0) { log_unit_debug_errno(u, r, "Failed to store deserialized fd '%s', ignoring: %m", fdn); return 0; } + /* If preservation is enabled then this fd was previously propagated upstream when it + * was first pushed. Restore the index so future removals can be forwarded upstream + * and the JSON mapping memfd can be regenerated. */ + if (r > 0 && s->fd_store && index > 0) + s->fd_store->index = index; } else if (streq(key, "extra-fd")) { _cleanup_free_ char *fdv = NULL, *fdn = NULL; _cleanup_close_ int fd = -EBADF; @@ -5280,7 +5522,7 @@ static void service_notify_message( name = NULL; } - (void) service_add_fd_store_set(s, fds, name, !strv_contains(tags, "FDPOLL=0")); + (void) service_add_fd_store_set(s, fds, name, !strv_contains(tags, "FDPOLL=0"), /* propagate_upstream= */ fdstore_detected()); } /* Notify clients about changed status or main pid */ @@ -6178,6 +6420,8 @@ const UnitVTable service_vtable = { .serialize = service_serialize, .deserialize_item = service_deserialize_item, + .attach_external_fd_to_fdstore = service_attach_external_fd_to_fdstore, + .active_state = service_active_state, .sub_state_to_string = service_sub_state_to_string, diff --git a/src/core/service.h b/src/core/service.h index 9750b19ce2..b57634cdb0 100644 --- a/src/core/service.h +++ b/src/core/service.h @@ -8,6 +8,18 @@ #include "pidref.h" #include "unit.h" +/* FDNAME used to push the JSON mapping memfd that pairs upstream-propagated fdstore indices with + * (unit-id, original fdname) tuples. The receiving manager looks for this fdname in LISTEN_FDNAMES + * to find the mapping document. */ +#define SERVICE_FDSTORE_MAPPING_FDNAME "systemd-fdstore-mapping" + +/* Prefix for the upstream FDNAME used when forwarding individual fd-store entries to a parent + * supervisor: the entries are exposed as "sub-fdstore-" so the supervisor's own fd-store + * namespace doesn't collide with names a downstream service manager assigns. The trailing index + * is matched up with an entry in the SERVICE_FDSTORE_MAPPING_FDNAME memfd to recover the original + * (unit, fdname) pair. */ +#define SERVICE_FDSTORE_SUB_FDNAME_PREFIX "sub-fdstore-" + typedef enum ServiceRestart { SERVICE_RESTART_NO, SERVICE_RESTART_ON_SUCCESS, @@ -112,6 +124,10 @@ typedef struct ServiceFDStore { char *fdname; sd_event_source *event_source; bool do_poll; + /* If non-zero, this fd was forwarded to the NOTIFY_SOCKET supervisor via FDSTORE=1, with the + * stringified value of this index as its FDNAME. The originating unit-id and original fdname + * are recorded in a JSON mapping memfd that is also pushed upstream. */ + uint64_t index; LIST_FIELDS(struct ServiceFDStore, fd_store); } ServiceFDStore; @@ -279,6 +295,12 @@ extern const UnitVTable service_vtable; int service_set_socket_fd(Service *s, int fd, struct Socket *socket, struct SocketPeer *peer, bool selinux_context_net); void service_release_socket_fd(Service *s); +int service_add_fd_store(Service *s, int fd_in, const char *name, bool do_poll, bool propagate_upstream); + +int service_propagate_fd_store_mapping_upstream(Manager *m); + +ServiceExtraFD* service_extra_fd_free(ServiceExtraFD *fd); + usec_t service_restart_usec_next(const Service *s) _pure_; int service_determine_exec_selinux_label(Service *s, char **ret); diff --git a/src/core/unit.h b/src/core/unit.h index d20e46ab57..f96ae27936 100644 --- a/src/core/unit.h +++ b/src/core/unit.h @@ -618,6 +618,13 @@ typedef struct UnitVTable { /* Try to match up fds with what we need for this unit */ void (*distribute_fds)(Unit *u, FDSet *fds); + /* Restore one file descriptor that PID 1 retrieved from a Live Update Orchestrator session into the + * unit's per-instance state (e.g. fd store). Always consumes 'fd', even on failure. If the fd + * was previously propagated to an upstream NOTIFY_SOCKET supervisor under a numeric index, + * 'index' carries that index so it can be re-claimed (avoiding collisions with newly allocated + * indices and keeping FDSTOREREMOVE messages routable). Pass 0 to indicate no preserved index. */ + int (*attach_external_fd_to_fdstore)(Unit *u, int fd, const char *fdname, uint64_t index); + /* Boils down the more complex internal state of this unit to * a simpler one that the engine can understand */ UnitActiveState (*active_state)(Unit *u); diff --git a/src/include/uapi/linux/liveupdate.h b/src/include/uapi/linux/liveupdate.h new file mode 100644 index 0000000000..30bc66ee94 --- /dev/null +++ b/src/include/uapi/linux/liveupdate.h @@ -0,0 +1,216 @@ +/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */ + +/* + * Userspace interface for /dev/liveupdate + * Live Update Orchestrator + * + * Copyright (c) 2025, Google LLC. + * Pasha Tatashin + */ + +#ifndef _UAPI_LIVEUPDATE_H +#define _UAPI_LIVEUPDATE_H + +#include +#include + +/** + * DOC: General ioctl format + * + * The ioctl interface follows a general format to allow for extensibility. Each + * ioctl is passed in a structure pointer as the argument providing the size of + * the structure in the first u32. The kernel checks that any structure space + * beyond what it understands is 0. This allows userspace to use the backward + * compatible portion while consistently using the newer, larger, structures. + * + * ioctls use a standard meaning for common errnos: + * + * - ENOTTY: The IOCTL number itself is not supported at all + * - E2BIG: The IOCTL number is supported, but the provided structure has + * non-zero in a part the kernel does not understand. + * - EOPNOTSUPP: The IOCTL number is supported, and the structure is + * understood, however a known field has a value the kernel does not + * understand or support. + * - EINVAL: Everything about the IOCTL was understood, but a field is not + * correct. + * - ENOENT: A provided token does not exist. + * - ENOMEM: Out of memory. + * - EOVERFLOW: Mathematics overflowed. + * + * As well as additional errnos, within specific ioctls. + */ + +/* The ioctl type, documented in ioctl-number.rst */ +#define LIVEUPDATE_IOCTL_TYPE 0xBA + +/* The maximum length of session name including null termination */ +#define LIVEUPDATE_SESSION_NAME_LENGTH 64 + +/* The /dev/liveupdate ioctl commands */ +enum { + LIVEUPDATE_CMD_BASE = 0x00, + LIVEUPDATE_CMD_CREATE_SESSION = LIVEUPDATE_CMD_BASE, + LIVEUPDATE_CMD_RETRIEVE_SESSION = 0x01, +}; + +/* ioctl commands for session file descriptors */ +enum { + LIVEUPDATE_CMD_SESSION_BASE = 0x40, + LIVEUPDATE_CMD_SESSION_PRESERVE_FD = LIVEUPDATE_CMD_SESSION_BASE, + LIVEUPDATE_CMD_SESSION_RETRIEVE_FD = 0x41, + LIVEUPDATE_CMD_SESSION_FINISH = 0x42, +}; + +/** + * struct liveupdate_ioctl_create_session - ioctl(LIVEUPDATE_IOCTL_CREATE_SESSION) + * @size: Input; sizeof(struct liveupdate_ioctl_create_session) + * @fd: Output; The new file descriptor for the created session. + * @name: Input; A null-terminated string for the session name, max + * length %LIVEUPDATE_SESSION_NAME_LENGTH including termination + * character. + * + * Creates a new live update session for managing preserved resources. + * This ioctl can only be called on the main /dev/liveupdate device. + * + * Return: 0 on success, negative error code on failure. + */ +struct liveupdate_ioctl_create_session { + __u32 size; + __s32 fd; + __u8 name[LIVEUPDATE_SESSION_NAME_LENGTH]; +}; + +#define LIVEUPDATE_IOCTL_CREATE_SESSION \ + _IO(LIVEUPDATE_IOCTL_TYPE, LIVEUPDATE_CMD_CREATE_SESSION) + +/** + * struct liveupdate_ioctl_retrieve_session - ioctl(LIVEUPDATE_IOCTL_RETRIEVE_SESSION) + * @size: Input; sizeof(struct liveupdate_ioctl_retrieve_session) + * @fd: Output; The new file descriptor for the retrieved session. + * @name: Input; A null-terminated string identifying the session to retrieve. + * The name must exactly match the name used when the session was + * created in the previous kernel. + * + * Retrieves a handle (a new file descriptor) for a preserved session by its + * name. This is the primary mechanism for a userspace agent to regain control + * of its preserved resources after a live update. + * + * The userspace application provides the null-terminated `name` of a session + * it created before the live update. If a preserved session with a matching + * name is found, the kernel instantiates it and returns a new file descriptor + * in the `fd` field. This new session FD can then be used for all file-specific + * operations, such as restoring individual file descriptors with + * LIVEUPDATE_SESSION_RETRIEVE_FD. + * + * It is the responsibility of the userspace application to know the names of + * the sessions it needs to retrieve. If no session with the given name is + * found, the ioctl will fail with -ENOENT. + * + * This ioctl can only be called on the main /dev/liveupdate device when the + * system is in the LIVEUPDATE_STATE_UPDATED state. + */ +struct liveupdate_ioctl_retrieve_session { + __u32 size; + __s32 fd; + __u8 name[LIVEUPDATE_SESSION_NAME_LENGTH]; +}; + +#define LIVEUPDATE_IOCTL_RETRIEVE_SESSION \ + _IO(LIVEUPDATE_IOCTL_TYPE, LIVEUPDATE_CMD_RETRIEVE_SESSION) + +/* Session specific IOCTLs */ + +/** + * struct liveupdate_session_preserve_fd - ioctl(LIVEUPDATE_SESSION_PRESERVE_FD) + * @size: Input; sizeof(struct liveupdate_session_preserve_fd) + * @fd: Input; The user-space file descriptor to be preserved. + * @token: Input; An opaque, unique token for preserved resource. + * + * Holds parameters for preserving a file descriptor. + * + * User sets the @fd field identifying the file descriptor to preserve + * (e.g., memfd, kvm, iommufd, VFIO). The kernel validates if this FD type + * and its dependencies are supported for preservation. If validation passes, + * the kernel marks the FD internally and *initiates the process* of preparing + * its state for saving. The actual snapshotting of the state typically occurs + * during the subsequent %LIVEUPDATE_IOCTL_PREPARE execution phase, though + * some finalization might occur during freeze. + * On successful validation and initiation, the kernel uses the @token + * field with an opaque identifier representing the resource being preserved. + * This token confirms the FD is targeted for preservation and is required for + * the subsequent %LIVEUPDATE_SESSION_RETRIEVE_FD call after the live update. + * + * Return: 0 on success (validation passed, preservation initiated), negative + * error code on failure (e.g., unsupported FD type, dependency issue, + * validation failed). + */ +struct liveupdate_session_preserve_fd { + __u32 size; + __s32 fd; + __aligned_u64 token; +}; + +#define LIVEUPDATE_SESSION_PRESERVE_FD \ + _IO(LIVEUPDATE_IOCTL_TYPE, LIVEUPDATE_CMD_SESSION_PRESERVE_FD) + +/** + * struct liveupdate_session_retrieve_fd - ioctl(LIVEUPDATE_SESSION_RETRIEVE_FD) + * @size: Input; sizeof(struct liveupdate_session_retrieve_fd) + * @fd: Output; The new file descriptor representing the fully restored + * kernel resource. + * @token: Input; An opaque, token that was used to preserve the resource. + * + * Retrieve a previously preserved file descriptor. + * + * User sets the @token field to the value obtained from a successful + * %LIVEUPDATE_IOCTL_FD_PRESERVE call before the live update. On success, + * the kernel restores the state (saved during the PREPARE/FREEZE phases) + * associated with the token and populates the @fd field with a new file + * descriptor referencing the restored resource in the current (new) kernel. + * This operation must be performed *before* signaling completion via + * %LIVEUPDATE_IOCTL_FINISH. + * + * Return: 0 on success, negative error code on failure (e.g., invalid token). + */ +struct liveupdate_session_retrieve_fd { + __u32 size; + __s32 fd; + __aligned_u64 token; +}; + +#define LIVEUPDATE_SESSION_RETRIEVE_FD \ + _IO(LIVEUPDATE_IOCTL_TYPE, LIVEUPDATE_CMD_SESSION_RETRIEVE_FD) + +/** + * struct liveupdate_session_finish - ioctl(LIVEUPDATE_SESSION_FINISH) + * @size: Input; sizeof(struct liveupdate_session_finish) + * @reserved: Input; Must be zero. Reserved for future use. + * + * Signals the completion of the restoration process for a retrieved session. + * This is the final operation that should be performed on a session file + * descriptor after a live update. + * + * This ioctl must be called once all required file descriptors for the session + * have been successfully retrieved (using %LIVEUPDATE_SESSION_RETRIEVE_FD) and + * are fully restored from the userspace and kernel perspective. + * + * Upon success, the kernel releases its ownership of the preserved resources + * associated with this session. This allows internal resources to be freed, + * typically by decrementing reference counts on the underlying preserved + * objects. + * + * If this operation fails, the resources remain preserved in memory. Userspace + * may attempt to call finish again. The resources will otherwise be reset + * during the next live update cycle. + * + * Return: 0 on success, negative error code on failure. + */ +struct liveupdate_session_finish { + __u32 size; + __u32 reserved; +}; + +#define LIVEUPDATE_SESSION_FINISH \ + _IO(LIVEUPDATE_IOCTL_TYPE, LIVEUPDATE_CMD_SESSION_FINISH) + +#endif /* _UAPI_LIVEUPDATE_H */ diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 0e532cf7b0..16ea48eaf8 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -3782,9 +3782,13 @@ static int setup_notify_child(const void *directory) { if (r < 0) log_debug_errno(r, "Failed to enable SO_PASSPIDFD, ignoring: %m"); - r = setsockopt_int(fd, SOL_SOCKET, SO_PASSRIGHTS, false); - if (r < 0) - log_debug_errno(r, "Failed to turn off SO_PASSRIGHTS, ignoring: %m"); + /* Only allow the container payload to pass file descriptors to us if we ourselves are + * supervised by a service manager that enabled the FD store. */ + if (!fdstore_detected()) { + r = setsockopt_int(fd, SOL_SOCKET, SO_PASSRIGHTS, false); + if (r < 0) + log_debug_errno(r, "Failed to turn off SO_PASSRIGHTS, ignoring: %m"); + } return TAKE_FD(fd); } @@ -4613,6 +4617,76 @@ static int setup_uid_map( return 0; } +static int forward_fd_store(char **tags, FDSet *fds) { + int r; + + /* Forward fd-store related messages to our own service manager, so that file descriptors stored + * by the inner payload propagate up the chain and are preserved across restarts. Skip entirely + * if we have no upstream supervisor (no NOTIFY_SOCKET) or no fd store available (no FDSTORE). + * + * Forwarded entries are namespaced with a "payload-" prefix on their FDNAME so that they + * cannot collide with fd-store entries that nspawn itself might want to push to its own + * upstream supervisor (the container payload and nspawn share a single upstream fdstore + * namespace, since there's only one init system per container). */ + if (!getenv("NOTIFY_SOCKET") || !fdstore_detected()) + return 0; + + if (strv_contains(tags, "FDSTOREREMOVE=1")) { + const char *fdname = strv_find_startswith(tags, "FDNAME="); + if (!fdname) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "Got FDSTOREREMOVE=1 from container payload without FDNAME=, ignoring."); + if (!fdname_is_valid(fdname)) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "Got FDSTOREREMOVE=1 from container payload with invalid FDNAME='%s', ignoring.", + fdname); + + r = sd_notifyf(/* unset_environment= */ false, + "FDSTOREREMOVE=1\nFDNAME=payload-%s", fdname); + if (r < 0) + return log_warning_errno(r, "Failed to forward FDSTOREREMOVE upstream, ignoring: %m"); + } else if (strv_contains(tags, "FDSTORE=1")) { + if (fdset_isempty(fds)) { + log_debug("Got FDSTORE=1 from container payload without any attached file descriptors, ignoring."); + return 0; + } + + _cleanup_free_ int *fds_array = NULL; + int n; + + n = fdset_to_array(fds, &fds_array); + if (n < 0) + return log_warning_errno(n, "Failed to convert fdset to array, ignoring FDSTORE forward: %m"); + + const char *fdname = strv_find_startswith(tags, "FDNAME="); + bool fdpoll_off = strv_contains(tags, "FDPOLL=0"); + _cleanup_free_ char *msg = NULL; + unsigned n_fds = (unsigned) n; + + if (fdname && !fdname_is_valid(fdname)) { + log_warning("Got FDSTORE=1 from container payload with invalid FDNAME='%s', ignoring name.", fdname); + fdname = NULL; + } + + if (asprintf(&msg, "FDSTORE=1\nFDNAME=payload-%s%s%s", + fdname ?: "stored", + fdpoll_off ? "\nFDPOLL=" : "", + fdpoll_off ? "0" : "") < 0) + return log_oom(); + + r = sd_pid_notify_with_fds( + 0, + /* unset_environment= */ false, + msg, + fds_array, + n_fds); + if (r < 0) + return log_warning_errno(r, "Failed to forward FDSTORE upstream, ignoring: %m"); + } + + return 0; +} + static int nspawn_dispatch_notify_fd(sd_event_source *source, int fd, uint32_t revents, void *userdata) { PidRef *inner_child_pid = ASSERT_PTR(userdata); int r; @@ -4621,7 +4695,8 @@ static int nspawn_dispatch_notify_fd(sd_event_source *source, int fd, uint32_t r _cleanup_(pidref_done) PidRef sender_pid = PIDREF_NULL; _cleanup_strv_free_ char **tags = NULL; - r = notify_recv_strv(fd, &tags, /* ret_ucred= */ NULL, &sender_pid); + _cleanup_(fdset_freep) FDSet *fds = NULL; + r = notify_recv_with_fds_strv(fd, &tags, /* ret_ucred= */ NULL, &sender_pid, &fds); if (r == -EAGAIN) return 0; if (r < 0) @@ -4656,6 +4731,8 @@ static int nspawn_dispatch_notify_fd(sd_event_source *source, int fd, uint32_t r (void) sd_notifyf(/* unset_environment= */ false, "STATUS=Container running."); } + (void) forward_fd_store(tags, fds); + return 0; } diff --git a/src/shared/daemon-util.c b/src/shared/daemon-util.c index 321a9e58da..d85e52bcb6 100644 --- a/src/shared/daemon-util.c +++ b/src/shared/daemon-util.c @@ -7,6 +7,7 @@ #include "errno-util.h" #include "fd-util.h" #include "log.h" +#include "parse-util.h" #include "string-util.h" #include "time-util.h" @@ -84,6 +85,27 @@ int notify_push_fdf(int fd, const char *format, ...) { return notify_push_fd(fd, name); } +bool fdstore_detected(void) { + static int cached = -1; + int r; + + if (cached >= 0) + return cached; + + const char *e = getenv("FDSTORE"); + if (isempty(e)) + return (cached = 0); + + unsigned u; + r = safe_atou(e, &u); + if (r < 0) { + log_debug_errno(r, "Failed to parse 'FDSTORE=%s', ignoring: %m", e); + return (cached = 0); + } + + return (cached = u > 0); +} + int notify_reloading_full(const char *status) { int r; diff --git a/src/shared/daemon-util.h b/src/shared/daemon-util.h index 708a32985c..089e418f7b 100644 --- a/src/shared/daemon-util.h +++ b/src/shared/daemon-util.h @@ -27,6 +27,8 @@ int close_and_notify_warn(int fd, const char *name); int notify_push_fd(int fd, const char *name); int notify_push_fdf(int fd, const char *format, ...) _printf_(2, 3); +bool fdstore_detected(void); + int notify_reloading_full(const char *status); static inline int notify_reloading(void) { return notify_reloading_full("Reloading configuration..."); diff --git a/src/shared/luo-util.c b/src/shared/luo-util.c new file mode 100644 index 0000000000..898e51efa5 --- /dev/null +++ b/src/shared/luo-util.c @@ -0,0 +1,376 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include +#include +#include +#include + +#include "sd-json.h" + +#include "alloc-util.h" +#include "errno-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "json-util.h" +#include "log.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 + * single-owner singleton, only a single process at any given time can open it. Callers can create named + * "sessions", and then add FDs to them. The session name can be used to retrieve the session after reboot. + * To identify an FD, a 64bit token (what we would call an 'index' in our codebase) is passed in, and the + * caller is responsible for coming up with the token and tracking them. */ + +int luo_open_device(void) { + return RET_NERRNO(open("/dev/liveupdate", O_RDWR|O_CLOEXEC)); +} + +int luo_create_session(int device_fd, const char *name) { + struct liveupdate_ioctl_create_session args = { + .size = sizeof(args), + .fd = -EBADF, + }; + + assert(device_fd >= 0); + assert(name); + + if (strlen(name) >= sizeof(args.name)) + return -ENAMETOOLONG; + + strncpy_exact((char *) args.name, name, sizeof(args.name)); + + if (ioctl(device_fd, LIVEUPDATE_IOCTL_CREATE_SESSION, &args) < 0) + return -errno; + + return args.fd; +} + +int luo_retrieve_session(int device_fd, const char *name) { + struct liveupdate_ioctl_retrieve_session args = { + .size = sizeof(args), + .fd = -EBADF, + }; + + assert(device_fd >= 0); + assert(name); + + if (strlen(name) >= sizeof(args.name)) + return -ENAMETOOLONG; + + strncpy_exact((char *) args.name, name, sizeof(args.name)); + + if (ioctl(device_fd, LIVEUPDATE_IOCTL_RETRIEVE_SESSION, &args) < 0) + return -errno; + + return args.fd; +} + +int luo_session_preserve_fd(int session_fd, int fd, uint64_t token) { + struct liveupdate_session_preserve_fd args = { + .size = sizeof(args), + .fd = fd, + .token = token, + }; + + assert(session_fd >= 0); + assert(fd >= 0); + + return RET_NERRNO(ioctl(session_fd, LIVEUPDATE_SESSION_PRESERVE_FD, &args)); +} + +int luo_session_retrieve_fd(int session_fd, uint64_t token) { + struct liveupdate_session_retrieve_fd args = { + .size = sizeof(args), + .fd = -EBADF, + .token = token, + }; + int r; + + assert(session_fd >= 0); + + if (ioctl(session_fd, LIVEUPDATE_SESSION_RETRIEVE_FD, &args) < 0) + return -errno; + + r = fd_cloexec(args.fd, true); + if (r < 0) { + safe_close(args.fd); + return r; + } + + return args.fd; +} + +int luo_session_finish(int session_fd) { + struct liveupdate_session_finish args = { + .size = sizeof(args), + }; + + assert(session_fd >= 0); + + return RET_NERRNO(ioctl(session_fd, LIVEUPDATE_SESSION_FINISH, &args)); +} + +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; + _cleanup_fclose_ FILE *f = NULL; + size_t n_fds = 0; + int serialize_fd = -EBADF, r; + + assert(ret); + assert(ret_fds); + assert(ret_n_fds); + + const char *luo_fd_str = secure_getenv("SYSTEMD_LUO_SERIALIZE_FD"); + if (!luo_fd_str) { + *ret = NULL; + *ret_fds = NULL; + *ret_n_fds = 0; + return 0; + } + + serialize_fd = parse_fd(luo_fd_str); + if (serialize_fd < 0) + return log_warning_errno(serialize_fd, + "Failed to parse SYSTEMD_LUO_SERIALIZE_FD='%s': %m", luo_fd_str); + + r = fdopen_independent(serialize_fd, "r", &f); + if (r < 0) + return log_warning_errno(r, "Failed to open LUO serialization fd %d: %m", serialize_fd); + + r = sd_json_parse_file(f, /* path= */ NULL, SD_JSON_PARSE_MUST_BE_OBJECT, &root, /* reterr_line= */ NULL, /* reterr_column= */ NULL); + if (r < 0) + return log_warning_errno(r, "Failed to parse LUO serialization JSON: %m"); + + /* Collect all fd numbers referenced in the JSON (plus the serialization fd itself) + * so the caller can protect them from close_all_fds(). */ + const char *unit_id _unused_; + sd_json_variant *unit_entries; + + JSON_VARIANT_OBJECT_FOREACH(unit_id, unit_entries, root) { + sd_json_variant *entry; + + JSON_VARIANT_ARRAY_FOREACH(entry, unit_entries) { + struct { + int fd; + } p = { + .fd = -EBADF, + }; + + static const sd_json_dispatch_field dispatch_table[] = { + { "fd", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_int, voffsetof(p, fd), 0 }, + {} + }; + + r = sd_json_dispatch(entry, dispatch_table, SD_JSON_ALLOW_EXTENSIONS|SD_JSON_LOG|SD_JSON_WARNING, &p); + if (r < 0) + continue; + + if (p.fd < 0) + continue; + + if (!GREEDY_REALLOC(fd_list, n_fds + 1)) + return log_oom(); + + fd_list[n_fds++] = p.fd; + } + } + + /* Also protect the serialization fd itself */ + if (!GREEDY_REALLOC(fd_list, n_fds + 1)) + return log_oom(); + fd_list[n_fds++] = serialize_fd; + + log_debug("Parsed LUO serialization with %zu fd(s) to preserve.", n_fds); + + *ret = TAKE_PTR(root); + *ret_fds = TAKE_PTR(fd_list); + *ret_n_fds = n_fds; + return 0; +} + +int luo_preserve_fd_stores(sd_json_variant *serialization, int *ret_session_fd) { + _cleanup_close_ int device_fd = -EBADF, session_fd = -EBADF; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *mapping = NULL; + const char *unit_id; + sd_json_variant *entries; + uint64_t token = LUO_MAPPING_INDEX + 1; + int r; + + assert(ret_session_fd); + + if (!serialization) { + *ret_session_fd = -EBADF; + return 0; /* No LUO serialization, nothing to preserve */ + } + + device_fd = luo_open_device(); + if (ERRNO_IS_NEG_DEVICE_ABSENT(device_fd)) { + *ret_session_fd = -EBADF; + return 0; /* LUO not supported, ignore */ + } + if (device_fd < 0) + return log_error_errno(device_fd, "Failed to open /dev/liveupdate: %m"); + + session_fd = luo_create_session(device_fd, LUO_SESSION_NAME); + if (session_fd < 0) + 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}, + * {"type": "luo_session", "name": "...", "sessionName": "..."} ], ... } + * + * 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; + + JSON_VARIANT_ARRAY_FOREACH(entry, entries) { + struct { + const char *type; + const char *name; + int fd; + const char *session_name; + } p = { + .fd = -EBADF, + }; + + static const sd_json_dispatch_field dispatch_table[] = { + { "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 }, + { "fd", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_int, voffsetof(p, fd), 0 }, + { "sessionName", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, session_name), 0 }, + {} + }; + + r = sd_json_dispatch(entry, dispatch_table, SD_JSON_ALLOW_EXTENSIONS|SD_JSON_LOG|SD_JSON_WARNING, &p); + if (r < 0) + continue; + + if (streq(p.type, "fd")) { + if (p.fd < 0) { + log_warning("LUO mapping for unit '%s' fd '%s': missing or negative fd, skipping.", unit_id, p.name); + continue; + } + + /* Regular fd, preserve in the systemd session */ + r = luo_session_preserve_fd(session_fd, p.fd, token); + if (r < 0) { + log_warning_errno(r, "Failed to preserve LUO fd %i (name '%s') with token %" PRIu64 ", will be lost across kexec: %m", p.fd, p.name, token); + continue; + } + + r = sd_json_variant_append_arraybo( + &fd_list, + SD_JSON_BUILD_PAIR_STRING("type", "fd"), + SD_JSON_BUILD_PAIR_STRING("name", p.name), + SD_JSON_BUILD_PAIR_UNSIGNED("token", token)); + if (r < 0) + 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); + } + + if (fd_list) { + r = sd_json_variant_set_field(&mapping, unit_id, fd_list); + if (r < 0) + return log_error_errno(r, "Failed to add unit to LUO mapping: %m"); + } + } + + if (!mapping) { + log_debug("No fds were preserved in LUO session."); + *ret_session_fd = -EBADF; + return 0; + } + + /* Store the mapping as a memfd at LUO token 0 */ + _cleanup_free_ char *mapping_text = NULL; + + r = sd_json_variant_format(mapping, /* flags= */ 0, &mapping_text); + if (r < 0) + return log_error_errno(r, "Failed to format LUO mapping JSON: %m"); + + _cleanup_close_ int mapping_fd = -EBADF; + + mapping_fd = memfd_new_and_seal_string("luo-mapping", mapping_text); + if (mapping_fd < 0) + return log_error_errno(mapping_fd, "Failed to create LUO mapping memfd: %m"); + + r = luo_session_preserve_fd(session_fd, mapping_fd, LUO_MAPPING_INDEX); + if (r < 0) + return log_error_errno(r, "Failed to preserve LUO mapping memfd: %m"); + + log_info("Preserved fd stores in LUO session '%s' for kexec.", LUO_SESSION_NAME); + + /* Return the session fd to the caller as it must stay open until the kexec syscall, + * otherwise the kernel discards the session. */ + *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] " */ + 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; +} diff --git a/src/shared/luo-util.h b/src/shared/luo-util.h new file mode 100644 index 0000000000..1ca7ca6d71 --- /dev/null +++ b/src/shared/luo-util.h @@ -0,0 +1,40 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "basic-forward.h" +#include "sd-forward.h" + +#define LUO_SESSION_NAME "systemd" + +/* Index (token) 0 in the LUO session is always the mapping memfd, which contains a JSON document mapping + * unit ids to arrays of fd store entries: + * + * { + * "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_open_device(void); +int luo_create_session(int device_fd, const char *name); +int luo_retrieve_session(int device_fd, const char *name); +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); + +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); diff --git a/src/shared/meson.build b/src/shared/meson.build index d4cd5ca431..9b539f7983 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -129,6 +129,7 @@ shared_sources = files( 'loop-util.c', 'loopback-setup.c', 'lsm-util.c', + 'luo-util.c', 'machine-bind-user.c', 'machine-credential.c', 'machine-id-setup.c', diff --git a/src/shutdown/shutdown.c b/src/shutdown/shutdown.c index c3c114a667..c310d5c981 100644 --- a/src/shutdown/shutdown.c +++ b/src/shutdown/shutdown.c @@ -10,6 +10,7 @@ #include #include "sd-daemon.h" +#include "sd-json.h" #include "sd-messages.h" #include "alloc-util.h" @@ -31,6 +32,7 @@ #include "initrd-util.h" #include "killall.h" #include "log.h" +#include "luo-util.h" #include "options.h" #include "parse-util.h" #include "pidref.h" @@ -363,14 +365,21 @@ int main(int argc, char *argv[]) { SYSTEM_SHUTDOWN_PATH, NULL }; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *luo_serialization = NULL; + _cleanup_close_ int luo_session_fd = -EBADF; + _cleanup_free_ int *luo_fds = NULL; _cleanup_free_ char *cgroup = NULL; + size_t n_luo_fds = 0; int cmd, r; + /* If PID 1 passed us an LUO serialization fd, parse it first so we know which fds to keep open. */ + (void) luo_parse_serialization(&luo_serialization, &luo_fds, &n_luo_fds); + /* Close random fds we might have get passed, just for paranoia, before we open any new fds, for * example for logging. After all this tool's purpose is about detaching any pinned resources, and * open file descriptors are the primary way to pin resources. Note that we don't really expect any - * fds to be passed here. */ - (void) close_all_fds(NULL, 0); + * fds to be passed here, except for LUO fds that need to survive until kexec. */ + (void) close_all_fds(luo_fds, n_luo_fds); /* The log target defaults to console, but the original systemd process will pass its log target in through a * command line argument, which will override this default. Also, ensure we'll never log to the journal or @@ -646,6 +655,10 @@ int main(int argc, char *argv[]) { case LINUX_REBOOT_CMD_KEXEC: if (!in_container) { + /* Preserve fd stores via the kernel Live Update Orchestrator before kexec. + * The session fd must stay open until the kexec syscall. */ + (void) luo_preserve_fd_stores(luo_serialization, &luo_session_fd); + log_info("Rebooting with kexec."); (void) kexec(); diff --git a/src/test/meson.build b/src/test/meson.build index faf075c103..20960d0b95 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -323,6 +323,11 @@ executables += [ 'sources' : files('test-fd-util.c'), 'dependencies' : libseccomp_cflags, }, + test_template + { + 'sources' : files('test-fdstore.c'), + 'link_with' : libsystemd, + 'type' : 'manual', + }, test_template + { 'sources' : files( 'test-hashmap.c', @@ -364,6 +369,10 @@ executables += [ 'sources' : files('test-loopback.c'), 'dependencies' : common_test_dependencies, }, + test_template + { + 'sources' : files('test-luo.c'), + 'type' : 'manual', + }, test_template + { 'sources' : files('test-math-util.c'), 'dependencies' : libm, diff --git a/src/test/test-bpf-firewall.c b/src/test/test-bpf-firewall.c index c3d8e7d5d5..e591d0d5ea 100644 --- a/src/test/test-bpf-firewall.c +++ b/src/test/test-bpf-firewall.c @@ -75,7 +75,7 @@ int main(int argc, char *argv[]) { /* The simple tests succeeded. Now let's try full unit-based use-case. */ ASSERT_OK(manager_new(RUNTIME_SCOPE_USER, MANAGER_TEST_RUN_BASIC, &m)); - ASSERT_OK(manager_startup(m, NULL, NULL, NULL)); + ASSERT_OK(manager_startup(m, NULL, NULL, NULL, NULL)); ASSERT_NOT_NULL(u = unit_new(m, sizeof(Service))); ASSERT_EQ(unit_add_name(u, "foo.service"), 0); diff --git a/src/test/test-bpf-foreign-programs.c b/src/test/test-bpf-foreign-programs.c index bf56451d25..9d9093bee7 100644 --- a/src/test/test-bpf-foreign-programs.c +++ b/src/test/test-bpf-foreign-programs.c @@ -303,7 +303,7 @@ int main(int argc, char *argv[]) { assert_se(runtime_dir = setup_fake_runtime_dir()); ASSERT_OK(manager_new(RUNTIME_SCOPE_USER, MANAGER_TEST_RUN_BASIC, &m)); - ASSERT_OK(manager_startup(m, NULL, NULL, NULL)); + ASSERT_OK(manager_startup(m, NULL, NULL, NULL, NULL)); ASSERT_OK(test_bpf_cgroup_programs(m, "single_prog.service", single_prog, ELEMENTSOF(single_prog))); diff --git a/src/test/test-bpf-restrict-fs.c b/src/test/test-bpf-restrict-fs.c index 9d54ad0337..8e41537f21 100644 --- a/src/test/test-bpf-restrict-fs.c +++ b/src/test/test-bpf-restrict-fs.c @@ -87,7 +87,7 @@ int main(int argc, char *argv[]) { ASSERT_NOT_NULL((runtime_dir = setup_fake_runtime_dir())); ASSERT_OK(manager_new(RUNTIME_SCOPE_SYSTEM, MANAGER_TEST_RUN_BASIC, &m)); - ASSERT_OK(manager_startup(m, NULL, NULL, NULL)); + ASSERT_OK(manager_startup(m, NULL, NULL, NULL, NULL)); /* We need to enable access to the filesystem where the binary is so we * add @common-block and @application */ diff --git a/src/test/test-cgroup-mask.c b/src/test/test-cgroup-mask.c index 6b123f8761..9673415080 100644 --- a/src/test/test-cgroup-mask.c +++ b/src/test/test-cgroup-mask.c @@ -51,7 +51,7 @@ TEST_RET(cgroup_mask, .sd_booted = true) { m->defaults.tasks_accounting = false; m->defaults.tasks_max = CGROUP_TASKS_MAX_UNSET; - assert_se(manager_startup(m, NULL, NULL, NULL) >= 0); + assert_se(manager_startup(m, NULL, NULL, NULL, NULL) >= 0); /* Load units and verify hierarchy. */ ASSERT_OK(manager_load_startable_unit_or_warn(m, "parent.slice", NULL, &parent)); diff --git a/src/test/test-engine.c b/src/test/test-engine.c index e1a2f7ea04..5a6a1a4204 100644 --- a/src/test/test-engine.c +++ b/src/test/test-engine.c @@ -97,7 +97,7 @@ int main(int argc, char *argv[]) { if (manager_errno_skip_test(r)) return log_tests_skipped_errno(r, "manager_new"); assert_se(r >= 0); - assert_se(manager_startup(m, NULL, NULL, NULL) >= 0); + assert_se(manager_startup(m, NULL, NULL, NULL, NULL) >= 0); printf("Load1:\n"); assert_se(manager_load_startable_unit_or_warn(m, "a.service", NULL, &a) >= 0); diff --git a/src/test/test-execute.c b/src/test/test-execute.c index e14205bdf8..3a124f9674 100644 --- a/src/test/test-execute.c +++ b/src/test/test-execute.c @@ -1435,7 +1435,7 @@ static void run_tests(RuntimeScope scope, char **patterns) { ASSERT_OK(r); m->defaults.std_output = EXEC_OUTPUT_INHERIT; /* don't rely on host journald */ - ASSERT_OK(manager_startup(m, NULL, NULL, NULL)); + ASSERT_OK(manager_startup(m, NULL, NULL, NULL, NULL)); /* Uncomment below if you want to make debugging logs stored to journal. */ //manager_override_log_target(m, LOG_TARGET_AUTO); diff --git a/src/test/test-fdstore.c b/src/test/test-fdstore.c new file mode 100644 index 0000000000..6469d396e1 --- /dev/null +++ b/src/test/test-fdstore.c @@ -0,0 +1,166 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +/* In 'store' mode pushes a couple of memfds with known content into the supervisor's fd store via FDSTORE=1 + * sd_notify() messages. In 'check' mode reads back the fds passed via LISTEN_FDS and verifies the content + * matches what was pushed. + * + * This binary is intentionally linked against libsystemd only so that it can go in the minimal image. */ + +#include +#include +#include +#include +#include +#include + +#include "sd-daemon.h" + +#define DATA_A "fdstore-data-a" +#define DATA_B "fdstore-data-b" + +#define _cleanup_(f) __attribute__((cleanup(f))) + +static void closep(int *fd) { + if (!fd || *fd < 0) + return; + + close(*fd); + *fd = -EBADF; +} + +static int push_one(const char *fdname, const char *content) { + _cleanup_(closep) int fd = -EBADF; + int r; + + assert(fdname); + assert(content); + + fd = memfd_create(fdname, MFD_CLOEXEC | MFD_ALLOW_SEALING); + if (fd < 0) { + fprintf(stderr, "memfd_create(%s) failed: %m\n", fdname); + return -errno; + } + + size_t len = strlen(content); + if (write(fd, content, len) != (ssize_t) len) { + fprintf(stderr, "write(%s) failed: %m\n", fdname); + return -errno; + } + + char msg[256]; + r = snprintf(msg, sizeof(msg), "FDSTORE=1\nFDNAME=%s", fdname); + if (r < 0 || (size_t) r >= sizeof(msg)) { + if (r >= 0) + errno = ENOBUFS; + fprintf(stderr, "FDSTORE message for fdname=%s did not fit in buffer\n", fdname); + return -errno; + } + + r = sd_pid_notify_with_fds(0, /* unset_environment= */ 0, msg, &fd, 1); + if (r < 0) { + errno = -r; + fprintf(stderr, "sd_pid_notify_with_fds(%s) failed: %m\n", fdname); + return r; + } + if (r == 0) { + fprintf(stderr, "NOTIFY_SOCKET not set\n"); + return -ENOENT; + } + + return 0; +} + +static int do_store(void) { + int r; + + if (push_one("test-fd-a", DATA_A) < 0) + return EXIT_FAILURE; + + if (push_one("test-fd-b", DATA_B) < 0) + return EXIT_FAILURE; + + /* Wait for our supervisor to actually process the FDSTORE messages before we exit, otherwise + * the cgroup-based pidref to unit lookup may fail once we're gone. */ + r = sd_notify_barrier(0, 5 * 1000 * 1000); + if (r < 0) { + errno = -r; + fprintf(stderr, "sd_notify_barrier failed: %m\n"); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} + +static int do_check(void) { + bool seen_a = false, seen_b = false; + int n; + + n = sd_listen_fds(/* unset_environment= */ 0); + if (n < 0) { + errno = -n; + fprintf(stderr, "sd_listen_fds failed: %m\n"); + return EXIT_FAILURE; + } + if (n < 2) { + fprintf(stderr, "Expected at least 2 fds via LISTEN_FDS, got %d\n", n); + return EXIT_FAILURE; + } + + for (int i = 0; i < n; i++) { + int fd = SD_LISTEN_FDS_START + i; + char buf[256] = {}; + ssize_t k; + + if (lseek(fd, 0, SEEK_SET) < 0) { + fprintf(stderr, "lseek(fd=%d) failed: %m\n", fd); + return EXIT_FAILURE; + } + k = read(fd, buf, sizeof(buf) - 1); + if (k < 0) { + fprintf(stderr, "read(fd=%d) failed: %m\n", fd); + return EXIT_FAILURE; + } + buf[k] = 0; + + if (strcmp(buf, DATA_A) == 0) + seen_a = true; + else if (strcmp(buf, DATA_B) == 0) + seen_b = true; + else + fprintf(stderr, "Unexpected fd content: '%s'\n", buf); + } + + if (!seen_a || !seen_b) { + fprintf(stderr, "Missing expected fds: seen_a=%d seen_b=%d\n", seen_a, seen_b); + return EXIT_FAILURE; + } + + printf("Payload received both preserved fds with matching content.\n"); + return EXIT_SUCCESS; +} + +int main(int argc, char *argv[]) { + int r; + + if (argc < 2) { + fprintf(stderr, "Usage: %s store|check\n", argv[0]); + return EXIT_FAILURE; + } + + if (strcmp(argv[1], "store") == 0) + r = do_store(); + else if (strcmp(argv[1], "check") == 0) + r = do_check(); + else { + fprintf(stderr, "Unknown verb: %s\n", argv[1]); + return EXIT_FAILURE; + } + + if (r != EXIT_SUCCESS) + return r; + + /* On success, become sleep so if we are a container payload it can stay alive. */ + execlp("sleep", "sleep", "infinity", (char *) NULL); + fprintf(stderr, "execlp(sleep) failed: %m\n"); + return EXIT_FAILURE; +} diff --git a/src/test/test-load-fragment.c b/src/test/test-load-fragment.c index 3e318a36c9..259bd142e4 100644 --- a/src/test/test-load-fragment.c +++ b/src/test/test-load-fragment.c @@ -108,7 +108,7 @@ TEST(config_parse_exec) { } ASSERT_OK(r); - ASSERT_OK(manager_startup(m, NULL, NULL, NULL)); + ASSERT_OK(manager_startup(m, NULL, NULL, NULL, NULL)); ASSERT_NOT_NULL(u = unit_new(m, sizeof(Service))); @@ -430,7 +430,7 @@ TEST(config_parse_log_extra_fields) { } ASSERT_OK(r); - ASSERT_OK(manager_startup(m, NULL, NULL, NULL)); + ASSERT_OK(manager_startup(m, NULL, NULL, NULL, NULL)); ASSERT_NOT_NULL(u = unit_new(m, sizeof(Service))); @@ -788,7 +788,7 @@ TEST(config_parse_unit_env_file) { } ASSERT_OK(r); - ASSERT_OK(manager_startup(m, NULL, NULL, NULL)); + ASSERT_OK(manager_startup(m, NULL, NULL, NULL, NULL)); ASSERT_NOT_NULL(u = unit_new(m, sizeof(Service))); ASSERT_OK_ZERO(unit_add_name(u, "foobar.service")); @@ -912,7 +912,7 @@ TEST(unit_is_recursive_template_dependency) { } ASSERT_OK(r); - ASSERT_OK(manager_startup(m, NULL, NULL, NULL)); + ASSERT_OK(manager_startup(m, NULL, NULL, NULL, NULL)); ASSERT_NOT_NULL(u = unit_new(m, sizeof(Service))); ASSERT_OK_ZERO(unit_add_name(u, "foobar@1.service")); @@ -1006,7 +1006,7 @@ TEST(config_parse_open_file) { } ASSERT_OK(r); - ASSERT_OK(manager_startup(m, NULL, NULL, NULL)); + ASSERT_OK(manager_startup(m, NULL, NULL, NULL, NULL)); ASSERT_NOT_NULL(u = unit_new(m, sizeof(Service))); ASSERT_OK_ZERO(unit_add_name(u, "foobar.service")); @@ -1065,7 +1065,7 @@ TEST(config_parse_service_refresh_on_reload) { } ASSERT_OK(r); - ASSERT_OK(manager_startup(m, NULL, NULL, NULL)); + ASSERT_OK(manager_startup(m, NULL, NULL, NULL, NULL)); ASSERT_NOT_NULL(u = unit_new(m, sizeof(Service))); ASSERT_OK_ZERO(unit_add_name(u, "foobar.service")); diff --git a/src/test/test-luo.c b/src/test/test-luo.c new file mode 100644 index 0000000000..97ba66459d --- /dev/null +++ b/src/test/test-luo.c @@ -0,0 +1,251 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +/* Helper for TEST-91-LIVEUPDATE: creates memfds and stores them in the fd store, + * 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 and a LUO session, push all to the fd store + * test-luo check - verify fd store content and LUO session memfd after kexec + */ + +#include +#include +#include + +#include "sd-daemon.h" + +#include "fd-util.h" +#include "log.h" +#include "luo-util.h" +#include "main-func.h" +#include "memfd-util.h" +#include "parse-util.h" +#include "string-util.h" +#include "strv.h" +#include "time-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(const char *prefix) { + _cleanup_close_ int fd1 = -EBADF, fd2 = -EBADF; + int r; + + fd1 = memfd_new_and_seal("luo-test-1", TEST_DATA_1, strlen(TEST_DATA_1)); + if (fd1 < 0) + return log_error_errno(fd1, "Failed to create memfd 1: %m"); + + fd2 = memfd_new_and_seal("luo-test-2", TEST_DATA_2, strlen(TEST_DATA_2)); + if (fd2 < 0) + return log_error_errno(fd2, "Failed to create memfd 2: %m"); + + r = sd_pid_notify_with_fds(0, /* unset_environment= */ false, "FDSTORE=1\nFDNAME=testfd1", &fd1, 1); + if (r < 0) + return log_error_errno(r, "Failed to store memfd 1 in fd store: %m"); + + r = sd_pid_notify_with_fds(0, /* unset_environment= */ false, "FDSTORE=1\nFDNAME=testfd2", &fd2, 1); + if (r < 0) + return log_error_errno(r, "Failed to store memfd 2 in fd store: %m"); + + 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); + if (r < 0) + return log_error_errno(r, "Failed to wait for notification barrier: %m"); + + return 0; +} + +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; + + /* sd_listen_fds_with_names() checks LISTEN_PID which won't match since we're a child process. + * Read LISTEN_FDS and LISTEN_FDNAMES directly from the environment instead. */ + e = getenv("LISTEN_FDS"); + if (!e) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No LISTEN_FDS environment variable set"); + + r = safe_atozu(e, &n_fds); + if (r < 0) + return log_error_errno(r, "Failed to parse LISTEN_FDS='%s': %m", e); + if (n_fds == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No file descriptors in fd store after kexec"); + + log_info("Got %zu fd(s) in fd store after kexec.", n_fds); + + /* Parse LISTEN_FDNAMES to match fds by name, not position */ + e = getenv("LISTEN_FDNAMES"); + if (!e) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No LISTEN_FDNAMES environment variable set"); + + names = strv_split(e, ":"); + if (!names) + return log_oom(); + assert(n_fds == strv_length(names)); + + static const struct { + const char *name; + const char *expected; + } checks[] = { + { "testfd1", TEST_DATA_1 }, + { "testfd2", TEST_DATA_2 }, + }; + + if (n_fds < ELEMENTSOF(checks)) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), + "Not enough fds in fd store after kexec: expected at least %zu, got %zu", + ELEMENTSOF(checks), n_fds); + + for (size_t i = 0; i < ELEMENTSOF(checks); i++) { + char buf[256]; + ssize_t n; + size_t idx = 0; + int fd = -EBADF; + + /* Find the fd by name */ + STRV_FOREACH(name, names) { + if (idx >= n_fds) + break; + if (streq(*name, checks[i].name)) { + fd = SD_LISTEN_FDS_START + idx; + break; + } + idx++; + } + + if (fd < 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), + "fd '%s' not found in LISTEN_FDNAMES", checks[i].name); + + /* memfds are sealed; pread() avoids needing a separate lseek() */ + n = pread(fd, buf, sizeof(buf) - 1, 0); + if (n < 0) + return log_error_errno(errno, "Failed to read fd %d: %m", fd); + + buf[n] = '\0'; + + if (!streq(buf, checks[i].expected)) + return log_error_errno( + SYNTHETIC_ERRNO(EBADMSG), + "Content mismatch for '%s': expected '%s', got '%s'", + checks[i].name, checks[i].expected, buf); + + /* Remove the fd from the fd store so we don't keep accumulating duplicates across + * repeated invocations (and across repeated kexec cycles). */ + r = sd_pid_notifyf(0, /* unset_environment= */ false, + "FDSTOREREMOVE=1\nFDNAME=%s", checks[i].name); + if (r < 0) + return log_error_errno(r, "Failed to remove fd '%s' from fd store: %m", checks[i].name); + + log_info("Verified fd '%s': content matches.", checks[i].name); + } + + 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); + if (r < 0) + return log_error_errno(r, "Failed to wait for notification barrier: %m"); + + 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 [PREFIX]", argv[0]); + + const char *prefix = argc > 2 ? argv[2] : "luosession"; + + if (streq(argv[1], "store")) + return do_store(prefix); + if (streq(argv[1], "check")) + return do_check(prefix); + + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown command: %s", argv[1]); +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/test/test-path.c b/src/test/test-path.c index 8b02f5d0ff..512eb96ead 100644 --- a/src/test/test-path.c +++ b/src/test/test-path.c @@ -34,7 +34,7 @@ static int setup_test(Manager **m) { if (manager_errno_skip_test(r)) return log_tests_skipped_errno(r, "manager_new"); assert_se(r >= 0); - assert_se(manager_startup(tmp, NULL, NULL, NULL) >= 0); + assert_se(manager_startup(tmp, NULL, NULL, NULL, NULL) >= 0); STRV_FOREACH(test_path, tests_path) { _cleanup_free_ char *p = NULL; diff --git a/src/test/test-sched-prio.c b/src/test/test-sched-prio.c index c1305ac4ab..a523c01e8f 100644 --- a/src/test/test-sched-prio.c +++ b/src/test/test-sched-prio.c @@ -33,7 +33,7 @@ int main(int argc, char *argv[]) { if (manager_errno_skip_test(r)) return log_tests_skipped_errno(r, "manager_new"); assert_se(r >= 0); - assert_se(manager_startup(m, NULL, NULL, NULL) >= 0); + assert_se(manager_startup(m, NULL, NULL, NULL, NULL) >= 0); /* load idle ok */ assert_se(manager_load_startable_unit_or_warn(m, "sched_idle_ok.service", NULL, &idle_ok) >= 0); diff --git a/src/test/test-socket-bind.c b/src/test/test-socket-bind.c index 4e4fdbedd0..63d249d6b4 100644 --- a/src/test/test-socket-bind.c +++ b/src/test/test-socket-bind.c @@ -133,7 +133,7 @@ int main(int argc, char *argv[]) { assert_se(runtime_dir = setup_fake_runtime_dir()); assert_se(manager_new(RUNTIME_SCOPE_USER, MANAGER_TEST_RUN_BASIC, &m) >= 0); - assert_se(manager_startup(m, NULL, NULL, NULL) >= 0); + assert_se(manager_startup(m, NULL, NULL, NULL, NULL) >= 0); assert_se(test_socket_bind(m, "socket_bind_test.service", netcat_path, "2000", STRV_MAKE("2000"), STRV_MAKE("any")) >= 0); assert_se(test_socket_bind(m, "socket_bind_test.service", netcat_path, "2000", STRV_MAKE("ipv6:2001-2002"), STRV_MAKE("any")) >= 0); diff --git a/src/test/test-watch-pid.c b/src/test/test-watch-pid.c index 0a325dfb08..59f043f49c 100644 --- a/src/test/test-watch-pid.c +++ b/src/test/test-watch-pid.c @@ -21,7 +21,7 @@ TEST(watch_pid) { ASSERT_NOT_NULL(runtime_dir = setup_fake_runtime_dir()); ASSERT_OK(manager_new(RUNTIME_SCOPE_USER, MANAGER_TEST_RUN_BASIC, &m)); - ASSERT_OK(manager_startup(m, NULL, NULL, NULL)); + ASSERT_OK(manager_startup(m, NULL, NULL, NULL, NULL)); ASSERT_NOT_NULL(a = unit_new(m, sizeof(Service))); ASSERT_OK(unit_add_name(a, "a.service")); diff --git a/test/integration-tests/TEST-91-LIVEUPDATE/meson.build b/test/integration-tests/TEST-91-LIVEUPDATE/meson.build new file mode 100644 index 0000000000..460ef32476 --- /dev/null +++ b/test/integration-tests/TEST-91-LIVEUPDATE/meson.build @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +integration_tests += [ + integration_test_template + { + 'name' : 'TEST-91-LIVEUPDATE', + 'configuration' : integration_test_template['configuration'] + { + 'unit' : integration_test_template['configuration']['unit'] + { + 'DefaultDependencies' : 'no', + }, + 'service' : integration_test_template['configuration']['service'] + { + 'FileDescriptorStoreMax' : '20', + 'FileDescriptorStorePreserve' : 'yes', + 'NotifyAccess' : 'all', + }, + }, + 'cmdline' : ['kho=on', 'liveupdate=on', 'systemd.minimum_uptime_sec=0'], + 'storage' : 'persistent', + 'vm' : true, + 'firmware' : 'uefi', + }, +] diff --git a/test/integration-tests/meson.build b/test/integration-tests/meson.build index dd9eba9b67..867fdecda2 100644 --- a/test/integration-tests/meson.build +++ b/test/integration-tests/meson.build @@ -103,6 +103,7 @@ foreach dirname : [ 'TEST-88-UPGRADE', 'TEST-89-RESOLVED-MDNS', 'TEST-90-RESTRICT-FSACCESS', + 'TEST-91-LIVEUPDATE', ] subdir(dirname) endforeach diff --git a/test/units/TEST-13-NSPAWN.unpriv.sh b/test/units/TEST-13-NSPAWN.unpriv.sh index e0516449c7..19b1d445c8 100755 --- a/test/units/TEST-13-NSPAWN.unpriv.sh +++ b/test/units/TEST-13-NSPAWN.unpriv.sh @@ -22,14 +22,17 @@ at_exit() { rm -rf /home/testuser/.local/state/machines/inodetest ||: rm -rf /home/testuser/.local/state/machines/inodetest2 ||: rm -rf /home/testuser/.local/state/machines/mangletest ||: + rm -rf /home/testuser/.local/state/machines/fdstore ||: machinectl terminate zurps ||: machinectl terminate exfiltrate ||: systemctl --user --machine testuser@ stop exfiltrate.service ||: + systemctl --user --machine testuser@ stop systemd-nspawn@fdstore.service ||: rm -f /etc/polkit-1/rules.d/registermachinetest.rules machinectl terminate nurps ||: machinectl terminate kurps ||: machinectl terminate wumms ||: machinectl terminate wamms ||: + machinectl terminate fdstore ||: rm -f /usr/share/polkit-1/rules.d/registermachinetest.rules rm -rf /var/tmp/mangletest rm -f /var/tmp/mangletest.tar.gz @@ -307,4 +310,85 @@ tar -C /var/tmp/mangletest/ -cvzf /var/tmp/mangletest.tar.gz mangletest-0.1 run0 --pipe -u testuser importctl -m --user import-tar /var/tmp/mangletest.tar.gz cmp /var/tmp/mangletest/mangletest-0.1/usr/lib/os-release /home/testuser/.local/state/machines/mangletest/usr/lib/os-release +# Verify the fd-store preservation chain works end-to-end across: +# payload (inside container) -> systemd-nspawn (user manager) -> user manager +# -> system PID 1 (user@.service fd store) +# Then restart the nspawn service and verify the inner payload actually +# receives the preserved fds back via LISTEN_FDS, with their original content. +create_dummy_container /home/testuser/.local/state/machines/fdstore +# The container init execs the helper directly so the FDSTORE notification is +# sent from PID 1 (nspawn rejects notify messages from anyone but the inner +# payload's init). The helper itself execs sleep on success to keep the +# container alive, and on failure it exits non-zero making the systemd-nspawn +# service fail. +cat >/home/testuser/.local/state/machines/fdstore/sbin/init <<'EOF' +#!/usr/bin/env bash +set -e +if [[ "${LISTEN_FDS:-0}" -gt 0 ]]; then + exec /usr/bin/test-fdstore check +else + exec /usr/bin/test-fdstore store +fi +EOF +chmod +x /home/testuser/.local/state/machines/fdstore/sbin/init +systemd-dissect --shift /home/testuser/.local/state/machines/fdstore foreign + +run0 -u testuser mkdir -p .config/systemd/nspawn/ +run0 -u testuser -i "cat >.config/systemd/nspawn/fdstore.nspawn <.config/systemd/user/systemd-nspawn@fdstore.service.d/fdstore.conf < nspawn (user-side systemd-nspawn@fdstore.service fd store) +n_nspawn_fds=$(run0 -u testuser systemctl --user show -P NFileDescriptorStore systemd-nspawn@fdstore.service) +test "${n_nspawn_fds}" -ge 2 + +# 2) nspawn -> user manager -> system PID 1 (user@.service fd store) +TESTUSER_UID=$(id -u testuser) +timeout 30s bash -c \ + "until [[ \"\$(systemctl show -P NFileDescriptorStore user@${TESTUSER_UID}.service)\" -ge 2 ]]; do sleep 0.5; done" +n_user_at_fds=$(systemctl show -P NFileDescriptorStore "user@${TESTUSER_UID}.service") +test "${n_user_at_fds}" -ge 2 + +# 3) Stop the nspawn service: payload is gone but FileDescriptorStorePreserve=yes +# must keep the fds in the user-side fdstore (and propagated copy in PID 1). +run0 -u testuser systemctl --user stop systemd-nspawn@fdstore.service +n_nspawn_fds=$(run0 -u testuser systemctl --user show -P NFileDescriptorStore systemd-nspawn@fdstore.service) +test "${n_nspawn_fds}" -ge 2 + +# 4) Restart the service: nspawn must receive the preserved fds via LISTEN_FDS +# and forward them into the inner payload, which verifies the content matches. +run0 -u testuser systemctl start --user systemd-nspawn@fdstore.service +run0 -u testuser systemctl is-active --user systemd-nspawn@fdstore.service + +# 5) Stop the nspawn service and the user session +run0 -u testuser systemctl --user stop systemd-nspawn@fdstore.service +n_nspawn_fds=$(run0 -u testuser systemctl --user show -P NFileDescriptorStore systemd-nspawn@fdstore.service) +test "${n_nspawn_fds}" -ge 2 +systemctl stop "user@${TESTUSER_UID}.service" +n_user_at_fds=$(systemctl show -P NFileDescriptorStore "user@${TESTUSER_UID}.service") +test "${n_user_at_fds}" -ge 2 + +# 6) Restart the user session and container payload +systemctl start "user@${TESTUSER_UID}.service" +timeout 30s bash -c \ + "until systemctl is-active 'user@${TESTUSER_UID}.service' >/dev/null; do sleep 0.5; done" +run0 -u testuser systemctl --user start systemd-nspawn@fdstore.service +run0 -u testuser systemctl is-active --user systemd-nspawn@fdstore.service + +run0 -u testuser systemctl --user stop systemd-nspawn@fdstore.service +machinectl terminate fdstore 2>/dev/null || true + loginctl disable-linger testuser diff --git a/test/units/TEST-91-LIVEUPDATE.sh b/test/units/TEST-91-LIVEUPDATE.sh new file mode 100755 index 0000000000..1d2df255cf --- /dev/null +++ b/test/units/TEST-91-LIVEUPDATE.sh @@ -0,0 +1,200 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +export SYSTEMD_LOG_LEVEL=debug + +# This test verifies that the Live Update Orchestrator (LUO) integration works: +# - PID 1 can serialize fd stores and pass them to systemd-shutdown +# - systemd-shutdown can preserve fds in a LUO session before kexec +# - After kexec, PID 1 restores the fd stores from the LUO session +# +# The test requires KHO (Kexec HandOver) and LUO (Live Update Orchestrator) kernel support. + +if [[ ! -e /dev/liveupdate ]]; then + echo "/dev/liveupdate not available, skipping test" + exit 77 +fi + +# Ensure user units can also manage sessions +chmod 666 /dev/liveupdate + +TESTUSER_UID=$(id -u testuser) +TESTUSER_USER_SVC="user@${TESTUSER_UID}.service" + +at_exit() { + systemctl stop systemd-nspawn@fdstore.service ||: + machinectl terminate fdstore ||: + rm -rf /var/lib/machines/fdstore ||: + rm -f /run/systemd/nspawn/fdstore.nspawn +} + +trap at_exit EXIT + +# To test the late-load path also create units that appear at runtime. +# Three variants exercise different fragment scenarios on second boot: +# - late.service: fragment present before fds are observed (daemon-reload triggered) +# - late-noreload.service: fragment dropped only after kexec, never daemon-reloaded explicitly +# to exercise lazy load via systemctl start +# - late-zerofds.service: fragment on second boot sets FileDescriptorStoreMax=0, +# the previously stored fds must be dropped +write_late_unit() { + local scope="${1:?}" name="${2:?}" cmd="${3:?}" maxfd="${4:-20}" + local dir + + case "${scope}" in + system) dir=/run/systemd/system ;; + user) dir=/run/systemd/user ;; + *) echo "unknown scope: ${scope}" >&2; return 1 ;; + esac + + mkdir -p "${dir}" + cat >"${dir}/${name}.service" </var/lib/machines/fdstore/sbin/init <<'EOF' +#!/usr/bin/env bash +set -e +exec /usr/bin/test-fdstore check +EOF + chmod +x /var/lib/machines/fdstore/sbin/init + mkdir -p /run/systemd/nspawn + cat >/run/systemd/nspawn/fdstore.nspawn < systemd-nspawn@fdstore.service fdstore + # -> LUO -> after kexec PID 1 restores the fdstore -> systemd-nspawn -> + # payload verifies content matches. + create_dummy_container /var/lib/machines/fdstore + cat >/var/lib/machines/fdstore/sbin/init <<'EOF' +#!/usr/bin/env bash +set -e +exec /usr/bin/test-fdstore store +EOF + chmod +x /var/lib/machines/fdstore/sbin/init + + mkdir -p /run/systemd/nspawn + cat >/run/systemd/nspawn/fdstore.nspawn <