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:
committed by
Luca Boccassi
parent
d69d919379
commit
851ee29a54
+99
-3
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user