Add integration tests performing kexec live updates with preserved
CPUs:
- Preserve physical cores across kexec reboot.
- Verify cores remain preserved and isolated in incoming kernel.
- Verify unpreserve and online restoration.

Signed-off-by: Pasha Tatashin <[email protected]>
---
 tools/testing/selftests/liveupdate/Makefile   |   1 +
 tools/testing/selftests/liveupdate/init.c     |  27 +++-
 .../selftests/liveupdate/luo_cpu_preserve.c   | 145 ++++++++++++++++++
 .../selftests/liveupdate/run-vmtests.sh       |   1 +
 tools/testing/selftests/liveupdate/vmtest.sh  |   4 +-
 5 files changed, 175 insertions(+), 3 deletions(-)
 create mode 100644 tools/testing/selftests/liveupdate/luo_cpu_preserve.c

diff --git a/tools/testing/selftests/liveupdate/Makefile 
b/tools/testing/selftests/liveupdate/Makefile
index 634211c66652..0fa80f26a191 100644
--- a/tools/testing/selftests/liveupdate/Makefile
+++ b/tools/testing/selftests/liveupdate/Makefile
@@ -3,6 +3,7 @@
 TEST_GEN_PROGS += liveupdate
 
 TEST_GEN_PROGS_EXTENDED += luo_kexec_simple
+TEST_GEN_PROGS_EXTENDED += luo_cpu_preserve
 TEST_GEN_PROGS_EXTENDED += luo_multi_session
 TEST_GEN_PROGS_EXTENDED += luo_stress_sessions
 TEST_GEN_PROGS_EXTENDED += luo_stress_files
diff --git a/tools/testing/selftests/liveupdate/init.c 
b/tools/testing/selftests/liveupdate/init.c
index fb08bd58b9b9..a3850ff95c00 100644
--- a/tools/testing/selftests/liveupdate/init.c
+++ b/tools/testing/selftests/liveupdate/init.c
@@ -11,6 +11,7 @@
 #include <string.h>
 #include <sys/mount.h>
 #include <sys/reboot.h>
+#include <sys/stat.h>
 #include <sys/syscall.h>
 #include <sys/wait.h>
 #include <unistd.h>
@@ -27,11 +28,15 @@ static int mount_filesystems(void)
                return -1;
        }
 
-       if (mount("debugfs", "/debugfs", "debugfs", 0, NULL) < 0) {
-               fprintf(stderr, "INIT: Failed to mount debugfs\n");
+       if (mount("sysfs", "/sys", "sysfs", 0, NULL) < 0) {
+               fprintf(stderr, "INIT: Failed to mount sysfs\n");
                return -1;
        }
 
+       mkdir("/sys/kernel/debug", 0755);
+       mount("debugfs", "/sys/kernel/debug", "debugfs", 0, NULL);
+       mount("debugfs", "/debugfs", "debugfs", 0, NULL);
+
        if (mount("proc", "/proc", "proc", 0, NULL) < 0) {
                fprintf(stderr, "INIT: Failed to mount proc\n");
                return -1;
@@ -116,7 +121,23 @@ static int run_test(int stage)
        }
 
        waitpid(pid, &status, 0);
+       if (WIFEXITED(status) && WEXITSTATUS(status) == 0)
+               return 0;
+
+       /* If test binary does not accept -s (e.g. kselftest harness), run 
without -s */
+       pid = fork();
+       if (pid < 0)
+               return -1;
+
+       if (!pid) {
+               char *const argv[] = {TEST_BINARY, NULL};
 
+               execve(TEST_BINARY, argv, NULL);
+               fprintf(stderr, "INIT: execve failed\n");
+               _exit(1);
+       }
+
+       waitpid(pid, &status, 0);
        return (WIFEXITED(status) && WEXITSTATUS(status) == 0) ? 0 : -1;
 }
 
@@ -168,6 +189,8 @@ int main(int argc, char *argv[])
        }
 
        printf("INIT: Stage %d completed successfully.\n", current_stage);
+       if (current_stage == 2)
+               printf("\n--- TEST PASSED ---\n");
        reboot(current_stage == 1 ? RB_KEXEC : RB_AUTOBOOT);
 
        return 0;
diff --git a/tools/testing/selftests/liveupdate/luo_cpu_preserve.c 
b/tools/testing/selftests/liveupdate/luo_cpu_preserve.c
new file mode 100644
index 000000000000..85d17ccf3d90
--- /dev/null
+++ b/tools/testing/selftests/liveupdate/luo_cpu_preserve.c
@@ -0,0 +1,145 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+/*
+ * Copyright (c) 2026, Google LLC.
+ * Pasha Tatashin <[email protected]>
+ *
+ * A selftest to validate the lifecycle of preserved CPUs across Live Update
+ * and within a local session.
+ */
+
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+
+#include <libliveupdate.h>
+
+#define TEST_SESSION_NAME "cpu-preserve-session"
+#define STATE_SESSION_NAME "cpu_preserve_state"
+#define STATE_MEMFD_TOKEN 999
+#define TEST_CPU_TOKEN 0x100
+
+static int find_target_cpu(void)
+{
+       char path[128];
+       int cpu;
+
+       for (cpu = 1; cpu < 256; cpu++) {
+               snprintf(path, sizeof(path), 
"/sys/devices/system/cpu/cpu%d/preserve", cpu);
+               if (access(path, R_OK) == 0) {
+                       snprintf(path, sizeof(path), 
"/sys/devices/system/cpu/cpu%d/online", cpu);
+                       if (access(path, R_OK) == 0)
+                               return cpu;
+               }
+       }
+       return -1;
+}
+
+static int read_cpu_online(int cpu)
+{
+       char path[128], buf[16];
+       int fd, val = -1;
+
+       snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%d/online", 
cpu);
+       fd = open(path, O_RDONLY);
+       if (fd < 0)
+               return -1;
+       if (read(fd, buf, sizeof(buf) - 1) > 0)
+               val = atoi(buf);
+       close(fd);
+       return val;
+}
+
+static int preserve_cpu_fd(int session_fd, int cpu, int token)
+{
+       struct liveupdate_session_preserve_fd pfd = {};
+       char path[128];
+       int cpu_fd, ret;
+
+       snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%d/preserve", 
cpu);
+       cpu_fd = open(path, O_RDONLY);
+       if (cpu_fd < 0)
+               return -1;
+
+       pfd.size = sizeof(pfd);
+       pfd.fd = cpu_fd;
+       pfd.token = token;
+
+       ret = ioctl(session_fd, LIVEUPDATE_SESSION_PRESERVE_FD, &pfd);
+       close(cpu_fd);
+       return ret;
+}
+
+/* Stage 1: Executed before kexec */
+static void run_stage_1(int luo_fd)
+{
+       int target_cpu, session_fd;
+
+       target_cpu = find_target_cpu();
+       if (target_cpu < 0)
+               fail_exit("No hotpluggable CPU with preserve attribute found");
+
+       ksft_print_msg("[STAGE 1] Target CPU for preservation: %d\n", 
target_cpu);
+
+       create_state_file(luo_fd, STATE_SESSION_NAME, STATE_MEMFD_TOKEN, 
target_cpu);
+
+       session_fd = luo_create_session(luo_fd, TEST_SESSION_NAME);
+       if (session_fd < 0)
+               fail_exit("luo_create_session for '%s'", TEST_SESSION_NAME);
+
+       if (preserve_cpu_fd(session_fd, target_cpu, TEST_CPU_TOKEN) < 0)
+               fail_exit("preserve_cpu_fd for cpu %d", target_cpu);
+
+       if (read_cpu_online(target_cpu) != 0)
+               fail_exit("CPU %d was expected to be offline after 
preservation", target_cpu);
+
+       ksft_print_msg("[STAGE 1] CPU %d successfully preserved and 
offlined\n", target_cpu);
+
+       close(luo_fd);
+       daemonize_and_wait();
+}
+
+/* Stage 2: Executed after kexec */
+static void run_stage_2(int luo_fd, int state_session_fd)
+{
+       struct liveupdate_session_retrieve_fd rfd = {};
+       int target_cpu, session_fd;
+
+       restore_and_read_stage(state_session_fd, STATE_MEMFD_TOKEN, 
&target_cpu);
+       ksft_print_msg("[STAGE 2] Restored target CPU: %d\n", target_cpu);
+
+       session_fd = luo_retrieve_session(luo_fd, TEST_SESSION_NAME);
+       if (session_fd < 0)
+               fail_exit("luo_retrieve_session for '%s'", TEST_SESSION_NAME);
+
+       rfd.size = sizeof(rfd);
+       rfd.token = TEST_CPU_TOKEN;
+       if (ioctl(session_fd, LIVEUPDATE_SESSION_RETRIEVE_FD, &rfd) < 0)
+               fail_exit("LIVEUPDATE_SESSION_RETRIEVE_FD for cpu token %#x", 
TEST_CPU_TOKEN);
+
+       close(rfd.fd);
+
+       if (read_cpu_online(target_cpu) != 1)
+               fail_exit("CPU %d was expected to be online after retrieval", 
target_cpu);
+
+       ksft_print_msg("[STAGE 2] CPU %d successfully retrieved and online\n", 
target_cpu);
+
+       if (luo_session_finish(session_fd) < 0)
+               fail_exit("luo_session_finish for test session");
+       close(session_fd);
+
+       if (luo_session_finish(state_session_fd) < 0)
+               fail_exit("luo_session_finish for state session");
+       close(state_session_fd);
+
+       ksft_print_msg("\n--- CPU PRESERVATION KEXEC TEST PASSED ---\n");
+}
+
+int main(int argc, char *argv[])
+{
+       return luo_test(argc, argv, STATE_SESSION_NAME,
+                       run_stage_1, run_stage_2);
+}
diff --git a/tools/testing/selftests/liveupdate/run-vmtests.sh 
b/tools/testing/selftests/liveupdate/run-vmtests.sh
index d656ce58c5a7..1b90d56ec4a2 100755
--- a/tools/testing/selftests/liveupdate/run-vmtests.sh
+++ b/tools/testing/selftests/liveupdate/run-vmtests.sh
@@ -20,6 +20,7 @@ TEST_NAMES=(
        "luo_multi_session"
        "luo_stress_files"
        "luo_stress_sessions"
+       "luo_cpu_preserve"
 )
 
 function usage() {
diff --git a/tools/testing/selftests/liveupdate/vmtest.sh 
b/tools/testing/selftests/liveupdate/vmtest.sh
index 64fd2ab55b76..c934c76c00a0 100755
--- a/tools/testing/selftests/liveupdate/vmtest.sh
+++ b/tools/testing/selftests/liveupdate/vmtest.sh
@@ -145,6 +145,7 @@ function mkinitrd() {
        cat > "$workspace_dir/cpio_list_inner" <<EOF
 dir /dev 0755 0 0
 dir /proc 0755 0 0
+dir /sys 0755 0 0
 dir /debugfs 0755 0 0
 nod /dev/console 0600 0 0 c 5 1
 file /init $workspace_dir/init 0755 0 0
@@ -157,6 +158,7 @@ EOF
        cat > "$workspace_dir/cpio_list" <<EOF
 dir /dev 0755 0 0
 dir /proc 0755 0 0
+dir /sys 0755 0 0
 dir /debugfs 0755 0 0
 nod /dev/console 0600 0 0 c 5 1
 file /init $workspace_dir/init 0755 0 0
@@ -179,7 +181,7 @@ function run_qemu() {
 
        echo "# Serial Log: $serial"
        timeout 30s \
-       $qemu_cmd -m 1G -smp 2 -no-reboot -nographic -nodefaults \
+       $qemu_cmd -m 1G -smp 4 -no-reboot -nographic -nodefaults \
                  -accel tcg -accel hvf -accel kvm \
                  -serial file:"$serial" \
                  -append "$cmdline" \
-- 
2.55.0.1082.g2b9226bbc0-goog


Reply via email to