From: Rocker Zhang Date: Thu, 21 May 2026 16:02:51 +0000 (+0800) Subject: test: cover LUO serialize-side anti-hijack guard in TEST-91 X-Git-Tag: v261-rc1~12 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=851ee29a548f639e03050a7ea77c85b0a4f7df52;p=thirdparty%2Fsystemd.git 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 --- diff --git a/src/test/test-luo.c b/src/test/test-luo.c index 97ba66459d2..7c2fe73d969 100644 --- a/src/test/test-luo.c +++ b/src/test/test-luo.c @@ -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 @@ -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]); } diff --git a/test/units/TEST-91-LIVEUPDATE.sh b/test/units/TEST-91-LIVEUPDATE.sh index cd4bc3bc370..8932b5e4dd0 100755 --- a/test/units/TEST-91-LIVEUPDATE.sh +++ b/test/units/TEST-91-LIVEUPDATE.sh @@ -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