test: add integration test for LUO
This commit is contained in:
@@ -369,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,
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
/* Helper for TEST-91-LIVEUPDATE: creates memfds and stores them in the fd store,
|
||||
* or verifies that inherited fd store entries contain the expected content.
|
||||
*
|
||||
* Usage:
|
||||
* test-luo store - create memfds with test data and push them to the fd store
|
||||
* test-luo check - verify fd store content matches expectations
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "sd-daemon.h"
|
||||
|
||||
#include "fd-util.h"
|
||||
#include "log.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"
|
||||
|
||||
static int do_store(void) {
|
||||
_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.");
|
||||
|
||||
/* 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(void) {
|
||||
const char *e;
|
||||
_cleanup_strv_free_ char **names = NULL;
|
||||
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.");
|
||||
|
||||
/* 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)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Usage: %s store|check", argv[0]);
|
||||
|
||||
if (streq(argv[1], "store"))
|
||||
return do_store();
|
||||
if (streq(argv[1], "check"))
|
||||
return do_check();
|
||||
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown command: %s", argv[1]);
|
||||
}
|
||||
|
||||
DEFINE_MAIN_FUNCTION(run);
|
||||
@@ -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
|
||||
|
||||
Executable
+199
@@ -0,0 +1,199 @@
|
||||
#!/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 2
|
||||
write_late_unit user TEST-91-LIVEUPDATE-user-late \
|
||||
"/usr/lib/systemd/tests/unit-tests/manual/test-luo check user-late"
|
||||
systemctl restart "${TESTUSER_USER_SVC}"
|
||||
timeout 30s bash -c "until systemctl is-active --quiet '${TESTUSER_USER_SVC}'; do sleep 0.5; done"
|
||||
n_user_unit_fds=$(run0 -u testuser systemctl --user show -P NFileDescriptorStore TEST-91-LIVEUPDATE-user-late.service)
|
||||
test "${n_user_unit_fds}" -eq 2
|
||||
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"
|
||||
|
||||
n_fds=$(systemctl show -P NFileDescriptorStore TEST-91-LIVEUPDATE-late.service)
|
||||
test "$n_fds" -eq 2
|
||||
|
||||
systemctl daemon-reload
|
||||
|
||||
# Verify the late unit doesn't get GC'ed during daemon-reload
|
||||
n_fds=$(systemctl show -P NFileDescriptorStore TEST-91-LIVEUPDATE-late.service)
|
||||
test "$n_fds" -eq 2
|
||||
|
||||
systemctl daemon-reexec
|
||||
|
||||
# Verify the late unit doesn't get GC'ed during daemon-reexec
|
||||
n_fds=$(systemctl show -P NFileDescriptorStore TEST-91-LIVEUPDATE-late.service)
|
||||
test "$n_fds" -eq 2
|
||||
|
||||
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 2
|
||||
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.
|
||||
/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 2
|
||||
n_user_at_fds=$(systemctl show -P NFileDescriptorStore "${TESTUSER_USER_SVC}")
|
||||
test "${n_user_at_fds}" -ge 2
|
||||
|
||||
# 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"
|
||||
systemctl start "TEST-91-LIVEUPDATE-${variant}.service"
|
||||
|
||||
n_fds=$(systemctl show -P NFileDescriptorStore "TEST-91-LIVEUPDATE-${variant}.service")
|
||||
test "$n_fds" -eq 2
|
||||
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