test: cover LUO serialize-side anti-hijack guard in TEST-91

manager_luo_serialize_fd_stores() refuses to serialize a unit fd store
entry that holds a child LUO session named like PID 1's own ("systemd"),
to stop a service from hijacking PID 1's reserved session namespace
across kexec. That guard had no test coverage.

Add a test-luo store-hijack/check-hijack subcommand pair: on the first
boot a system service preserves a child LUO session named "systemd" in
its fd store; after kexec the test asserts the entry was not restored --
the unit's NFileDescriptorStore is 0, and check-hijack, run as the unit's
own second-boot ExecStart, confirms the hijack fd is absent from its
restored LISTEN_FDNAMES -- proving PID 1 skipped it during serialization.

The restore-side guards (corrupt mapping, reserved token 0, invalid unit
name, missing child session) are intentionally not covered: they only run
against PID 1's own "systemd" session built by luo_preserve_fd_stores(),
which a cooperating userspace helper cannot corrupt without racing or
displacing PID 1 (it single-owns /dev/liveupdate at shutdown). Triggering
them reliably would need kernel fault injection.

Co-developed-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Rocker Zhang
2026-05-22 12:00:05 +01:00
committed by Luca Boccassi
parent d69d919379
commit 851ee29a54
2 changed files with 124 additions and 3 deletions
+99 -3
View File
@@ -5,8 +5,11 @@
* 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
* 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
* test-luo store-hijack - store a fd store entry holding a child LUO session named like
* PID 1's own ("systemd"), to exercise the serialize-side anti-hijack guard
* test-luo check-hijack - verify the hijacking session fd was NOT serialized/restored after kexec
*/
#include <stdlib.h>
@@ -30,6 +33,14 @@
#define SESSION_MEMFD_DATA "luo-session-memfd-test-data"
#define SESSION_MEMFD_TOKEN UINT64_C(42)
/* Name PID 1 reserves for its own LUO session (must match LUO_SESSION_NAME). A unit that tries to
* preserve a child session under this name is an attempt to hijack PID 1's namespace, and the
* serialize-side guard in manager_luo_serialize_fd_stores() must refuse to serialize it. */
#define HIJACK_SESSION_NAME LUO_SESSION_NAME
#define HIJACK_FDNAME "hijackfd"
#define HIJACK_MEMFD_DATA "luo-hijack-memfd-test-data"
#define HIJACK_MEMFD_TOKEN UINT64_C(99)
static int do_store(const char *prefix) {
_cleanup_close_ int fd1 = -EBADF, fd2 = -EBADF;
int r;
@@ -234,9 +245,90 @@ static int do_check(const char *prefix) {
return 0;
}
static int do_store_hijack(void) {
int r;
/* Create a child LUO session named exactly like PID 1's own session, put a memfd in it, and push the
* session fd into our fd store. On kexec, PID 1's manager_luo_serialize_fd_stores() must detect the
* reserved session name and refuse to serialize this entry (anti-hijack guard). */
_cleanup_close_ int device_fd = -EBADF, session_fd = -EBADF, session_memfd = -EBADF;
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, HIJACK_SESSION_NAME);
if (session_fd < 0)
return log_error_errno(session_fd, "Failed to create hijacking LUO session '%s': %m", HIJACK_SESSION_NAME);
session_memfd = memfd_new_and_seal("hijack-test", HIJACK_MEMFD_DATA, strlen(HIJACK_MEMFD_DATA));
if (session_memfd < 0)
return log_error_errno(session_memfd, "Failed to create hijack session memfd: %m");
r = luo_session_preserve_fd(session_fd, session_memfd, HIJACK_MEMFD_TOKEN);
if (r < 0)
return log_error_errno(r, "Failed to preserve memfd in hijack session: %m");
r = sd_pid_notify_with_fds(0, /* unset_environment= */ false, "FDSTORE=1\nFDNAME=" HIJACK_FDNAME, &session_fd, 1);
if (r < 0)
return log_error_errno(r, "Failed to store hijack session fd in fd store: %m");
TAKE_FD(session_fd);
log_info("Stored hijacking LUO session '%s' with memfd in fd store.", HIJACK_SESSION_NAME);
/* Wait for PID 1 to actually process the FDSTORE notification before we exit, otherwise
* the cgroup-based pidref to unit lookup may fail once we're gone, and the fd ends 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_hijack(void) {
_cleanup_strv_free_ char **names = NULL;
const char *e;
size_t n_fds;
int r;
/* The hijacking session fd ("hijackfd") must NOT have survived kexec: PID 1 refused to serialize it
* because its session name infringes PID 1's reserved namespace. So it must be absent from
* LISTEN_FDNAMES here. */
e = getenv("LISTEN_FDS");
if (!e) {
log_info("No LISTEN_FDS set after kexec, hijack fd correctly not restored.");
return 0;
}
r = safe_atozu(e, &n_fds);
if (r < 0)
return log_error_errno(r, "Failed to parse LISTEN_FDS='%s': %m", e);
e = getenv("LISTEN_FDNAMES");
if (!e) {
if (n_fds == 0) {
log_info("No fds restored after kexec, hijack fd correctly not restored.");
return 0;
}
return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "LISTEN_FDS=%zu but no LISTEN_FDNAMES set", n_fds);
}
names = strv_split(e, ":");
if (!names)
return log_oom();
if (strv_contains(names, HIJACK_FDNAME))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG),
"Hijacking session fd '%s' was restored after kexec, anti-hijack guard failed!",
HIJACK_FDNAME);
log_info("Verified hijacking session fd '%s' was not restored after kexec.", HIJACK_FDNAME);
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]);
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Usage: %s store|check|store-hijack|check-hijack [PREFIX]", argv[0]);
const char *prefix = argc > 2 ? argv[2] : "luosession";
@@ -244,6 +336,10 @@ static int run(int argc, char *argv[]) {
return do_store(prefix);
if (streq(argv[1], "check"))
return do_check(prefix);
if (streq(argv[1], "store-hijack"))
return do_store_hijack();
if (streq(argv[1], "check-hijack"))
return do_check_hijack();
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown command: %s", argv[1]);
}
+25
View File
@@ -67,6 +67,22 @@ 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
# Negative path: a unit stored a child LUO session named like PID 1's own
# ("systemd") on the first boot. PID 1's serialize step must have refused to
# serialize that fd store entry (anti-hijack guard in
# manager_luo_serialize_fd_stores()), so it must NOT have been restored: the
# unit's fd store must be empty here.
n_hijack_fds=$(systemctl show -P NFileDescriptorStore TEST-91-LIVEUPDATE-hijack.service)
assert_eq "${n_hijack_fds}" "0"
# Rewrite the unit with a second-boot ExecStart and start it, so check-hijack
# runs inside the unit and inspects its own restored LISTEN_FDS, failing if
# the hijack fd came back. Mirrors the late.service variants below.
write_late_unit system TEST-91-LIVEUPDATE-hijack \
"/usr/lib/systemd/tests/unit-tests/manual/test-luo check-hijack"
systemctl start TEST-91-LIVEUPDATE-hijack.service
rm -f /run/systemd/system/TEST-91-LIVEUPDATE-hijack.service
systemctl daemon-reload
# 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
@@ -249,6 +265,15 @@ EOF
timeout 30s bash -c \
"until [[ \"\$(systemctl show -P NFileDescriptorStore systemd-nspawn@fdstore.service)\" -ge 2 ]]; do sleep 0.5; done"
# Negative path: store a fd store entry that holds a child LUO session named
# like PID 1's own ("systemd"). On kexec PID 1 must refuse to serialize it
# (anti-hijack guard), so it must not be restored on the next boot.
write_late_unit system TEST-91-LIVEUPDATE-hijack \
"/usr/lib/systemd/tests/unit-tests/manual/test-luo store-hijack"
systemctl start TEST-91-LIVEUPDATE-hijack.service
timeout 30s bash -c \
"until [[ \"\$(systemctl show -P NFileDescriptorStore TEST-91-LIVEUPDATE-hijack.service)\" -ge 1 ]]; 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