core: support FD Store propagation through manager instances, and preservation through kexec via LUO (#41683)
First of all, FD store propagation is enabled between manager instances, and nspawn instances, via LISTEN_FDS and sd_notify. These can be nested arbitrarily deeply all the way up to the system manager, and on restart will be propagated all the way down to the origin. The FDs payload of an nspawn container running as a user unit will be preserved, all the way up to the system manager, and then down again. The kernel Live Update Orchestrator (LUO) exposes /dev/liveupdate, which lets userspace hand a set of "preservable" kernel objects to the new kernel across a kexec-based reboot. For now it only supports memfds, with more object types (virtio devices, etc.) expected to be added later. This is a natural fit for systemd's FD Store feature: services hand memfds (containing serialized state or other service data) to PID 1 via FDSTORE=1 sd_notify() messages, and get them back on their next start. Today this works across service restarts, soft reboots and initrd→rootfs transitions. With LUO, this series extends the same mechanism to work across kexec too. The nesting preservation of FD stores thus now is extended across kexec. All preservable fds are collected into a single LUO session named "systemd". Each fd is uploaded with an index (token). Token 0 is reserved for a "mapping" memfd, which carries a JSON object describing how to dispatch the other tokens back to units on the next boot. Unit names are used as the unit identifier, as they are stable across daemon-reexec, switch-root and kexec. token refers to the LUO index assigned to the object in the session. On shutdown for MANAGER_KEXEC, just before manager_free(), systemd walks all services and serializes their persistent fd store contents (fds + FDNAMEs + cgroup paths) into a JSON memfd. The fds themselves are gathered into an FDSet. The fdset and the serialization memfd are passed to systemd-shutdown via the SYSTEMD_LUO_SERIALIZE_FD environment variable providing the fd number, so the actual LUO session creation and ioctls happen as the very last step before kexec. On boot, manager_luo_restore_fd_stores() opens /dev/liveupdate, tries to retrieve the "systemd" session, reads the mapping memfd, then for each entry retrieves the fd from the session and attempts to attach it to the matching unit's fd store. Because the initrd-stage PID 1 runs before the real rootfs units are loaded, fds whose target unit is not (yet) known are not dropped: they are stashed in a new luo_held_fds hashmap keyed by cgroup path. They are re-tried in two places: after deserialization, and from unit_load(), so fds land in the correct fd store as soon as the owning unit is parsed, allowing units to be plugged in at runtime. Non-kexec shutdown paths are unaffected: if MANAGER_KEXEC is not the final objective, no serialization file is produced and no LUO session is ever created. Likewise if /dev/liveupdate does not exist, nothing happens. The LUO session creation is performed by systemd-shutdown, rather than by PID 1, deliberately: it is the last point where we can be sure all other processes have already been killed, so nothing else can race us into creating (or worse, hijacking) the "systemd" session. /dev/liveupdate is a singleton and session names are global. In addition, any kernel-visible side effects of preserving objects (memory pinning, etc.) are delayed until the absolute last moment, minimizing the window in which they could affect the running system. There is no behaviour change for shutdown paths other than kexec, or for kexec when systemd didn't hand over a serialization fd (e.g. because no service had any fds stored, or because LUO wasn't supported at serialization time). Finally, since LUO sessions cannot be nested under other sessions, third-party sessions need to be handled explicitly and held open in the shutdown binary alongside our own internal session, to allow services to create and preserve their own sessions. The requirement comes from VMMs that wish to preserve VM state across kexec: some file descriptors (e.g. KVM's vmfd from the KVM_CREATE_VM ioctl) cannot be transferred between processes via SCM_RIGHTS, so they cannot be stashed in the FD Store directly. Additionally, some file descriptors must be handled all-or-nothing, again tied to KVM, where a VM and its associated devices are one indivisible group. https://docs.kernel.org/userspace-api/liveupdate.html https://docs.kernel.org/core-api/liveupdate.html https://docs.kernel.org/admin-guide/mm/kho.html
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -428,6 +428,17 @@
|
||||
<para>For further information on the file descriptor store see the <ulink
|
||||
url="https://systemd.io/FILE_DESCRIPTOR_STORE">File Descriptor Store</ulink> overview.</para>
|
||||
|
||||
<para>On kernels that support the <ulink
|
||||
url="https://docs.kernel.org/userspace-api/liveupdate.html">Live Update Orchestrator</ulink>,
|
||||
compatible file descriptors stored this way (such as <citerefentry
|
||||
project='man-pages'><refentrytitle>memfd_create</refentrytitle><manvolnum>2</manvolnum></citerefentry>)
|
||||
are additionally preserved across <literal>kexec</literal>-based reboots and handed back to the
|
||||
service on the other side, provided <varname>FileDescriptorStorePreserve=yes</varname> is set on
|
||||
the service (see
|
||||
<citerefentry><refentrytitle>systemd.service</refentrytitle><manvolnum>5</manvolnum></citerefentry>).
|
||||
See the <ulink url="https://systemd.io/FILE_DESCRIPTOR_STORE">File Descriptor Store</ulink>
|
||||
overview for details.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v219"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
|
||||
@@ -681,6 +681,20 @@
|
||||
<citerefentry><refentrytitle>systemd-vmspawn</refentrytitle><manvolnum>1</manvolnum></citerefentry>
|
||||
that defaults to true.)</para>
|
||||
|
||||
<para>If <command>systemd-nspawn</command> itself is invoked with a <varname>$NOTIFY_SOCKET</varname>
|
||||
set in its environment (i.e. it is itself supervised by a service manager that uses the
|
||||
<citerefentry><refentrytitle>sd_notify</refentrytitle><manvolnum>3</manvolnum></citerefentry>
|
||||
protocol), <literal>FDSTORE=1</literal> and <literal>FDSTOREREMOVE=1</literal> messages received
|
||||
from the container payload (along with any accompanying file descriptors and
|
||||
<literal>FDNAME=</literal> 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
|
||||
<varname>FileDescriptorStoreMax=</varname>/<varname>FileDescriptorStorePreserve=yes</varname> are
|
||||
configured on the unit running <command>systemd-nspawn</command>. See the
|
||||
<ulink url="https://systemd.io/FILE_DESCRIPTOR_STORE">File Descriptor Store</ulink> documentation
|
||||
for details.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v231"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
|
||||
@@ -1255,6 +1255,26 @@ RestartMaxDelaySec=160s</programlisting>
|
||||
is removed, the service manager exits, or the file descriptors get <constant>EPOLLHUP</constant> or
|
||||
<constant>EPOLLERR</constant>.</para>
|
||||
|
||||
<para>When set to <constant>yes</constant>, and the service is itself running under another service
|
||||
manager (e.g. a service of <filename>user@.service</filename>, or a payload inside
|
||||
<citerefentry><refentrytitle>systemd-nspawn</refentrytitle><manvolnum>1</manvolnum></citerefentry>),
|
||||
file descriptors pushed into the store are also forwarded one level up via the enveloping manager's
|
||||
<varname>$NOTIFY_SOCKET</varname>, 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
|
||||
<varname>FileDescriptorStoreMax=</varname> and <varname>FileDescriptorStorePreserve=yes</varname>.
|
||||
See the <ulink url="https://systemd.io/FILE_DESCRIPTOR_STORE">File Descriptor Store</ulink>
|
||||
overview for details.</para>
|
||||
|
||||
<para>Setting this to <constant>yes</constant> also ensures the file descriptor store is kept loaded
|
||||
across a <literal>kexec</literal>-based reboot on kernels supporting the <ulink
|
||||
url="https://docs.kernel.org/userspace-api/liveupdate.html">Live Update Orchestrator</ulink>,
|
||||
so that compatible file descriptors (such as <citerefentry
|
||||
project='man-pages'><refentrytitle>memfd_create</refentrytitle><manvolnum>2</manvolnum></citerefentry>)
|
||||
are preserved and handed back to the service on the other side. See the <ulink
|
||||
url="https://systemd.io/FILE_DESCRIPTOR_STORE">File Descriptor Store</ulink> overview for
|
||||
details.</para>
|
||||
|
||||
<para>Use <command>systemctl clean --what=fdstore …</command> to release the file descriptor store
|
||||
explicitly.</para>
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[Service]
|
||||
FileDescriptorStoreMax=16
|
||||
FileDescriptorStorePreserve=yes
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
[Service]
|
||||
FileDescriptorStoreMax=16
|
||||
FileDescriptorStorePreserve=yes
|
||||
+5
@@ -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
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
+288
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
+279
-4
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
+103
-1
@@ -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 */
|
||||
|
||||
+20
-1
@@ -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);
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
+258
-14
@@ -6,6 +6,7 @@
|
||||
#include <unistd.h>
|
||||
|
||||
#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,
|
||||
|
||||
|
||||
@@ -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-<index>" 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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 <pasha.tatashin@soleen.com>
|
||||
*/
|
||||
|
||||
#ifndef _UAPI_LIVEUPDATE_H
|
||||
#define _UAPI_LIVEUPDATE_H
|
||||
|
||||
#include <linux/ioctl.h>
|
||||
#include <linux/types.h>
|
||||
|
||||
/**
|
||||
* 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 */
|
||||
+81
-4
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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...");
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <linux/liveupdate.h>
|
||||
#include <linux/magic.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/vfs.h>
|
||||
|
||||
#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] <session_name>" */
|
||||
const char *suffix = startswith(path, "anon_inode:[luo_session] ");
|
||||
if (isempty(suffix))
|
||||
return -EMEDIUMTYPE;
|
||||
|
||||
if (ret)
|
||||
return strdup_to(ret, suffix);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int fd_is_luo_session(int fd) {
|
||||
int r;
|
||||
|
||||
r = fd_get_luo_session_name(fd, /* ret= */ NULL);
|
||||
if (r == -EMEDIUMTYPE)
|
||||
return false;
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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',
|
||||
|
||||
+15
-2
@@ -10,6 +10,7 @@
|
||||
#include <unistd.h>
|
||||
|
||||
#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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -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"));
|
||||
|
||||
@@ -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 <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
]
|
||||
@@ -103,6 +103,7 @@ foreach dirname : [
|
||||
'TEST-88-UPGRADE',
|
||||
'TEST-89-RESOLVED-MDNS',
|
||||
'TEST-90-RESTRICT-FSACCESS',
|
||||
'TEST-91-LIVEUPDATE',
|
||||
]
|
||||
subdir(dirname)
|
||||
endforeach
|
||||
|
||||
@@ -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@<UID>.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 <<EOF
|
||||
[Exec]
|
||||
KillSignal=SIGKILL
|
||||
EOF"
|
||||
|
||||
run0 -u testuser mkdir -p ".config/systemd/user/systemd-nspawn@fdstore.service.d/"
|
||||
run0 -u testuser -i "cat >.config/systemd/user/systemd-nspawn@fdstore.service.d/fdstore.conf <<EOF
|
||||
[Service]
|
||||
FileDescriptorStoreMax=8
|
||||
FileDescriptorStorePreserve=yes
|
||||
EOF"
|
||||
run0 -u testuser systemctl --user daemon-reload
|
||||
|
||||
run0 -u testuser systemctl start --user systemd-nspawn@fdstore.service
|
||||
timeout 30s bash -c \
|
||||
"until [[ \"\$(run0 -u testuser systemctl --user show -P NFileDescriptorStore systemd-nspawn@fdstore.service)\" -ge 2 ]]; do sleep 0.5; done"
|
||||
|
||||
# 1) Payload -> 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@<UID>.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
|
||||
|
||||
Executable
+200
@@ -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" <<EOF
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
FileDescriptorStoreMax=${maxfd}
|
||||
FileDescriptorStorePreserve=yes
|
||||
ExecStart=${cmd}
|
||||
EOF
|
||||
}
|
||||
|
||||
if grep -qw luo_nboot=1 /proc/cmdline; then
|
||||
# Verify that the fd store of the main test service survived the kexec.
|
||||
/usr/lib/systemd/tests/unit-tests/manual/test-luo check
|
||||
|
||||
# Verify that the user manager also preserved its FD store
|
||||
n_user_at_fds=$(systemctl show -P NFileDescriptorStore "${TESTUSER_USER_SVC}")
|
||||
test "${n_user_at_fds}" -ge 3
|
||||
write_late_unit user TEST-91-LIVEUPDATE-user-late \
|
||||
"/usr/lib/systemd/tests/unit-tests/manual/test-luo check user-late"
|
||||
systemctl restart "${TESTUSER_USER_SVC}"
|
||||
timeout 30s bash -c "until systemctl is-active --quiet '${TESTUSER_USER_SVC}'; do sleep 0.5; done"
|
||||
n_user_unit_fds=$(run0 -u testuser systemctl --user show -P NFileDescriptorStore TEST-91-LIVEUPDATE-user-late.service)
|
||||
test "${n_user_unit_fds}" -eq 3
|
||||
run0 -u testuser systemctl --user start TEST-91-LIVEUPDATE-user-late.service
|
||||
|
||||
# nspawn fdstore variant: after kexec, PID 1 propagated the
|
||||
# systemd-nspawn@fdstore.service fdstore through LUO. Starting the service
|
||||
# then forwards the preserved fds via LISTEN_FDS to a fresh nspawn payload,
|
||||
# which verifies the content is intact.
|
||||
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 check
|
||||
EOF
|
||||
chmod +x /var/lib/machines/fdstore/sbin/init
|
||||
mkdir -p /run/systemd/nspawn
|
||||
cat >/run/systemd/nspawn/fdstore.nspawn <<EOF
|
||||
[Exec]
|
||||
KillSignal=SIGKILL
|
||||
EOF
|
||||
n_nspawn_fds=$(systemctl show -P NFileDescriptorStore systemd-nspawn@fdstore.service)
|
||||
test "${n_nspawn_fds}" -ge 2
|
||||
systemctl start systemd-nspawn@fdstore.service
|
||||
systemctl is-active systemd-nspawn@fdstore.service
|
||||
|
||||
# late.service: rewrite the fragment with the second-boot ExecStart and
|
||||
# exercise the daemon-reload + daemon-reexec preservation paths.
|
||||
write_late_unit system TEST-91-LIVEUPDATE-late \
|
||||
"/usr/lib/systemd/tests/unit-tests/manual/test-luo check late"
|
||||
|
||||
n_fds=$(systemctl show -P NFileDescriptorStore TEST-91-LIVEUPDATE-late.service)
|
||||
test "$n_fds" -eq 3
|
||||
|
||||
systemctl daemon-reload
|
||||
|
||||
# Verify the late unit doesn't get GC'ed during daemon-reload
|
||||
n_fds=$(systemctl show -P NFileDescriptorStore TEST-91-LIVEUPDATE-late.service)
|
||||
test "$n_fds" -eq 3
|
||||
|
||||
systemctl daemon-reexec
|
||||
|
||||
# Verify the late unit doesn't get GC'ed during daemon-reexec
|
||||
n_fds=$(systemctl show -P NFileDescriptorStore TEST-91-LIVEUPDATE-late.service)
|
||||
test "$n_fds" -eq 3
|
||||
|
||||
systemctl start TEST-91-LIVEUPDATE-late.service
|
||||
|
||||
# No-reload variant: drop a brand-new fragment file but never call
|
||||
# daemon-reload. Lazy load via systemctl start must pick it up while
|
||||
# preserving the LUO-restored fds.
|
||||
write_late_unit system TEST-91-LIVEUPDATE-late-noreload \
|
||||
"/usr/lib/systemd/tests/unit-tests/manual/test-luo check late-noreload"
|
||||
n_fds=$(systemctl show -P NFileDescriptorStore TEST-91-LIVEUPDATE-late-noreload.service)
|
||||
test "$n_fds" -eq 3
|
||||
systemctl start TEST-91-LIVEUPDATE-late-noreload.service
|
||||
|
||||
# Zero-fds variant: fragment on second boot sets FileDescriptorStoreMax=0,
|
||||
# so the LUO-restored fds must be dropped on (lazy) load.
|
||||
write_late_unit system TEST-91-LIVEUPDATE-late-zerofds \
|
||||
"bash -c 'test \"\${LISTEN_FDS:-0}\" -eq 0'" 0
|
||||
systemctl daemon-reload
|
||||
n_fds=$(systemctl show -P NFileDescriptorStore TEST-91-LIVEUPDATE-late-zerofds.service)
|
||||
test "$n_fds" -eq 0
|
||||
systemctl start TEST-91-LIVEUPDATE-late-zerofds.service
|
||||
else
|
||||
# Create memfds with known content and push them to our fd store.
|
||||
# Also request a LUO session, store a memfd in it, and push the session fd to the fd store.
|
||||
/usr/lib/systemd/tests/unit-tests/manual/test-luo store
|
||||
|
||||
# Exercise the user manager FD preservation across kexec too
|
||||
loginctl enable-linger testuser
|
||||
timeout 30s bash -c "until systemctl is-active --quiet '${TESTUSER_USER_SVC}'; do sleep 0.5; done"
|
||||
write_late_unit user TEST-91-LIVEUPDATE-user-late \
|
||||
"/usr/lib/systemd/tests/unit-tests/manual/test-luo store user-late"
|
||||
run0 -u testuser systemctl --user start TEST-91-LIVEUPDATE-user-late.service
|
||||
n_user_unit_fds=$(run0 -u testuser systemctl --user show -P NFileDescriptorStore TEST-91-LIVEUPDATE-user-late.service)
|
||||
test "${n_user_unit_fds}" -eq 3
|
||||
n_user_at_fds=$(systemctl show -P NFileDescriptorStore "${TESTUSER_USER_SVC}")
|
||||
test "${n_user_at_fds}" -ge 3
|
||||
|
||||
# Exercise the FD-store preservation chain across a kexec for a privileged
|
||||
# nspawn container managed as a system service:
|
||||
# payload (inside container) -> 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 <<EOF
|
||||
[Exec]
|
||||
KillSignal=SIGKILL
|
||||
EOF
|
||||
|
||||
systemctl start systemd-nspawn@fdstore.service
|
||||
timeout 30s bash -c \
|
||||
"until [[ \"\$(systemctl show -P NFileDescriptorStore systemd-nspawn@fdstore.service)\" -ge 2 ]]; do sleep 0.5; done"
|
||||
|
||||
# Write and start each late unit with distinct session name prefixes
|
||||
# to avoid collisions in the LUO session namespace.
|
||||
for variant in late late-noreload late-zerofds; do
|
||||
write_late_unit system "TEST-91-LIVEUPDATE-${variant}" \
|
||||
"/usr/lib/systemd/tests/unit-tests/manual/test-luo store ${variant}"
|
||||
systemctl start "TEST-91-LIVEUPDATE-${variant}.service"
|
||||
|
||||
n_fds=$(systemctl show -P NFileDescriptorStore "TEST-91-LIVEUPDATE-${variant}.service")
|
||||
test "$n_fds" -eq 3
|
||||
done
|
||||
|
||||
# 'systemctl kexec' auto-loads the default boot entry (i.e. the booted UKI,
|
||||
# via EFI LoaderEntrySelected/LoaderEntryDefault). Append a marker to the
|
||||
# kernel command line so we can tell the two boots apart, and also the current
|
||||
# cmdline that is added by mkosi, otherwise the test framework will break.
|
||||
systemctl kexec --kernel-cmdline="$(cat /proc/cmdline) luo_nboot=1"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
touch /testok
|
||||
systemctl --no-block exit 123
|
||||
Reference in New Issue
Block a user