]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
test: cover LUO serialize-side anti-hijack guard in TEST-91
authorRocker Zhang <zhang.rocker.liyuan@gmail.com>
Thu, 21 May 2026 16:02:51 +0000 (00:02 +0800)
committerLuca Boccassi <luca.boccassi@gmail.com>
Fri, 22 May 2026 11:00:05 +0000 (12:00 +0100)
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>
src/test/test-luo.c
test/units/TEST-91-LIVEUPDATE.sh

index 97ba66459d236b126d3a2f442a02b800d566a194..7c2fe73d969dd1c5bc8ebf0fa9e1669434b5163e 100644 (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>
 #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]);
 }
index cd4bc3bc37054fd2ce142768584bcaa1be311687..8932b5e4dd03ad78883dfcf98d0b12fae66f30b1 100755 (executable)
@@ -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