[RFC PATCH 22/46] selftests: liveupdate: Add physical CPU preservation live update tests
From: Pasha Tatashin
Date: Sun Sep 20 2026 - 15:49:26 EST
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 <pasha.tatashin@xxxxxxxxxx>
---
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 <pasha.tatashin@xxxxxxxxxx>
+ *
+ * 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