summaryrefslogtreecommitdiff
path: root/tools/testing/selftests/liveupdate
diff options
context:
space:
mode:
Diffstat (limited to 'tools/testing/selftests/liveupdate')
-rw-r--r--tools/testing/selftests/liveupdate/Makefile2
-rw-r--r--tools/testing/selftests/liveupdate/liveupdate.c229
-rw-r--r--tools/testing/selftests/liveupdate/luo_stress_files.c97
-rw-r--r--tools/testing/selftests/liveupdate/luo_stress_sessions.c102
-rw-r--r--tools/testing/selftests/liveupdate/luo_test_utils.c24
-rw-r--r--tools/testing/selftests/liveupdate/luo_test_utils.h2
6 files changed, 456 insertions, 0 deletions
diff --git a/tools/testing/selftests/liveupdate/Makefile b/tools/testing/selftests/liveupdate/Makefile
index 080754787ede..30689d22cb02 100644
--- a/tools/testing/selftests/liveupdate/Makefile
+++ b/tools/testing/selftests/liveupdate/Makefile
@@ -6,6 +6,8 @@ TEST_GEN_PROGS += liveupdate
TEST_GEN_PROGS_EXTENDED += luo_kexec_simple
TEST_GEN_PROGS_EXTENDED += luo_multi_session
+TEST_GEN_PROGS_EXTENDED += luo_stress_sessions
+TEST_GEN_PROGS_EXTENDED += luo_stress_files
TEST_FILES += do_kexec.sh
diff --git a/tools/testing/selftests/liveupdate/liveupdate.c b/tools/testing/selftests/liveupdate/liveupdate.c
index c2878e3d5ef9..502fb3567e38 100644
--- a/tools/testing/selftests/liveupdate/liveupdate.c
+++ b/tools/testing/selftests/liveupdate/liveupdate.c
@@ -26,6 +26,7 @@
#include <linux/liveupdate.h>
+#include "luo_test_utils.h"
#include "../kselftest.h"
#include "../kselftest_harness.h"
@@ -102,6 +103,22 @@ static int create_session(int lu_fd, const char *name)
return args.fd;
}
+/* Helper function to get a session name via ioctl. */
+static int get_session_name(int session_fd, char *name, size_t name_len)
+{
+ struct liveupdate_session_get_name args = {};
+
+ args.size = sizeof(args);
+
+ if (ioctl(session_fd, LIVEUPDATE_SESSION_GET_NAME, &args))
+ return -errno;
+
+ strncpy(name, (char *)args.name, name_len - 1);
+ name[name_len - 1] = '\0';
+
+ return 0;
+}
+
/*
* Test Case: Create Duplicate Session
*
@@ -345,4 +362,216 @@ TEST_F(liveupdate_device, preserve_unsupported_fd)
ASSERT_EQ(close(session_fd), 0);
}
+/*
+ * Test Case: Prevent Double Preservation
+ *
+ * Verifies that a file (memfd) can only be preserved once across all active
+ * sessions. Attempting to preserve it a second time, whether in the same or
+ * a different session, should fail with EBUSY.
+ */
+TEST_F(liveupdate_device, prevent_double_preservation)
+{
+ int session_fd1, session_fd2, mem_fd;
+ int ret;
+
+ self->fd1 = open(LIVEUPDATE_DEV, O_RDWR);
+ if (self->fd1 < 0 && errno == ENOENT)
+ SKIP(return, "%s does not exist", LIVEUPDATE_DEV);
+ ASSERT_GE(self->fd1, 0);
+
+ session_fd1 = create_session(self->fd1, "double-preserve-session-1");
+ ASSERT_GE(session_fd1, 0);
+ session_fd2 = create_session(self->fd1, "double-preserve-session-2");
+ ASSERT_GE(session_fd2, 0);
+
+ mem_fd = memfd_create("test-memfd", 0);
+ ASSERT_GE(mem_fd, 0);
+
+ /* First preservation should succeed */
+ ASSERT_EQ(preserve_fd(session_fd1, mem_fd, 0x1111), 0);
+
+ /* Second preservation in a different session should fail with EBUSY */
+ ret = preserve_fd(session_fd2, mem_fd, 0x2222);
+ EXPECT_EQ(ret, -EBUSY);
+
+ /* Second preservation in the same session (different token) should fail with EBUSY */
+ ret = preserve_fd(session_fd1, mem_fd, 0x3333);
+ EXPECT_EQ(ret, -EBUSY);
+
+ ASSERT_EQ(close(mem_fd), 0);
+ ASSERT_EQ(close(session_fd1), 0);
+ ASSERT_EQ(close(session_fd2), 0);
+}
+
+/*
+ * Test Case: Create Session with No Null Termination
+ *
+ * Verifies that filling the entire 64-byte name field with non-null characters
+ * (no '\0' terminator) is rejected by the kernel with EINVAL.
+ */
+TEST_F(liveupdate_device, create_session_no_null_termination)
+{
+ struct liveupdate_ioctl_create_session args = {};
+
+ self->fd1 = open(LIVEUPDATE_DEV, O_RDWR);
+ if (self->fd1 < 0 && errno == ENOENT)
+ SKIP(return, "%s does not exist", LIVEUPDATE_DEV);
+ ASSERT_GE(self->fd1, 0);
+
+ /* Fill entire name field with 'X', no null terminator */
+ args.size = sizeof(args);
+ memset(args.name, 'X', sizeof(args.name));
+
+ EXPECT_LT(ioctl(self->fd1, LIVEUPDATE_IOCTL_CREATE_SESSION, &args), 0);
+ EXPECT_EQ(errno, EINVAL);
+}
+
+/*
+ * Test Case: Create Session with Empty Name
+ *
+ * Verifies that creating a session with an empty string name fails
+ * with EINVAL.
+ */
+TEST_F(liveupdate_device, create_session_empty_name)
+{
+ int session_fd;
+
+ self->fd1 = open(LIVEUPDATE_DEV, O_RDWR);
+ if (self->fd1 < 0 && errno == ENOENT)
+ SKIP(return, "%s does not exist", LIVEUPDATE_DEV);
+ ASSERT_GE(self->fd1, 0);
+
+ session_fd = create_session(self->fd1, "");
+ EXPECT_EQ(session_fd, -EINVAL);
+}
+
+/*
+ * Test Case: Get Session Name
+ *
+ * Verifies that the full session name can be retrieved from a session file
+ * descriptor via ioctl.
+ */
+TEST_F(liveupdate_device, get_session_name)
+{
+ char name_buf[LIVEUPDATE_SESSION_NAME_LENGTH] = {};
+ const char *session_name = "get-name-test-session";
+ int session_fd;
+
+ self->fd1 = open(LIVEUPDATE_DEV, O_RDWR);
+ if (self->fd1 < 0 && errno == ENOENT)
+ SKIP(return, "%s does not exist", LIVEUPDATE_DEV);
+ ASSERT_GE(self->fd1, 0);
+
+ session_fd = create_session(self->fd1, session_name);
+ ASSERT_GE(session_fd, 0);
+
+ ASSERT_EQ(get_session_name(session_fd, name_buf, sizeof(name_buf)), 0);
+ ASSERT_STREQ(name_buf, session_name);
+
+ ASSERT_EQ(close(session_fd), 0);
+}
+
+/*
+ * Test Case: Get Session Name at Maximum Length
+ *
+ * Verifies that a session name using the full LIVEUPDATE_SESSION_NAME_LENGTH
+ * (minus the null terminator) can be correctly retrieved.
+ */
+TEST_F(liveupdate_device, get_session_name_max_length)
+{
+ char name_buf[LIVEUPDATE_SESSION_NAME_LENGTH] = {};
+ char long_name[LIVEUPDATE_SESSION_NAME_LENGTH];
+ int session_fd;
+
+ memset(long_name, 'A', sizeof(long_name) - 1);
+ long_name[sizeof(long_name) - 1] = '\0';
+
+ self->fd1 = open(LIVEUPDATE_DEV, O_RDWR);
+ if (self->fd1 < 0 && errno == ENOENT)
+ SKIP(return, "%s does not exist", LIVEUPDATE_DEV);
+ ASSERT_GE(self->fd1, 0);
+
+ session_fd = create_session(self->fd1, long_name);
+ ASSERT_GE(session_fd, 0);
+
+ ASSERT_EQ(get_session_name(session_fd, name_buf, sizeof(name_buf)), 0);
+ ASSERT_STREQ(name_buf, long_name);
+
+ ASSERT_EQ(close(session_fd), 0);
+}
+
+/*
+ * Test Case: Manage Many Sessions
+ *
+ * Verifies that a large number of sessions can be created and then
+ * destroyed during normal system operation. This specifically tests the
+ * dynamic block allocation and reuse logic for session metadata management
+ * without preserving any files.
+ */
+TEST_F(liveupdate_device, preserve_many_sessions)
+{
+#define MANY_SESSIONS 2000
+ int session_fds[MANY_SESSIONS];
+ int ret, i;
+
+ self->fd1 = open(LIVEUPDATE_DEV, O_RDWR);
+ if (self->fd1 < 0 && errno == ENOENT)
+ SKIP(return, "%s does not exist", LIVEUPDATE_DEV);
+ ASSERT_GE(self->fd1, 0);
+
+ ret = luo_ensure_nofile_limit(MANY_SESSIONS);
+ if (ret == -EPERM)
+ SKIP(return, "Insufficient privileges to set RLIMIT_NOFILE");
+ ASSERT_EQ(ret, 0);
+
+ for (i = 0; i < MANY_SESSIONS; i++) {
+ char name[64];
+
+ snprintf(name, sizeof(name), "many-session-%d", i);
+ session_fds[i] = create_session(self->fd1, name);
+ ASSERT_GE(session_fds[i], 0);
+ }
+
+ for (i = 0; i < MANY_SESSIONS; i++)
+ ASSERT_EQ(close(session_fds[i]), 0);
+}
+
+/*
+ * Test Case: Preserve Many Files
+ *
+ * Verifies that a large number of files can be preserved in a single session
+ * and then destroyed during normal system operation. This tests the dynamic
+ * block allocation and management for outgoing files.
+ */
+TEST_F(liveupdate_device, preserve_many_files)
+{
+#define MANY_FILES 500
+ int mem_fds[MANY_FILES];
+ int session_fd, ret, i;
+
+ self->fd1 = open(LIVEUPDATE_DEV, O_RDWR);
+ if (self->fd1 < 0 && errno == ENOENT)
+ SKIP(return, "%s does not exist", LIVEUPDATE_DEV);
+ ASSERT_GE(self->fd1, 0);
+
+ session_fd = create_session(self->fd1, "many-files-test");
+ ASSERT_GE(session_fd, 0);
+
+ ret = luo_ensure_nofile_limit(MANY_FILES + 10);
+ if (ret == -EPERM)
+ SKIP(return, "Insufficient privileges to set RLIMIT_NOFILE");
+ ASSERT_EQ(ret, 0);
+
+ for (i = 0; i < MANY_FILES; i++) {
+ mem_fds[i] = memfd_create("test-memfd", 0);
+ ASSERT_GE(mem_fds[i], 0);
+ ASSERT_EQ(preserve_fd(session_fd, mem_fds[i], i), 0);
+ }
+
+ for (i = 0; i < MANY_FILES; i++)
+ ASSERT_EQ(close(mem_fds[i]), 0);
+
+ ASSERT_EQ(close(session_fd), 0);
+}
+
TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/liveupdate/luo_stress_files.c b/tools/testing/selftests/liveupdate/luo_stress_files.c
new file mode 100644
index 000000000000..0cdf9cd4bac7
--- /dev/null
+++ b/tools/testing/selftests/liveupdate/luo_stress_files.c
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+/*
+ * Copyright (c) 2026, Google LLC.
+ * Pasha Tatashin <pasha.tatashin@soleen.com>
+ *
+ * Validate that LUO can handle a large number of files per session across
+ * a kexec reboot.
+ */
+
+#include <stdio.h>
+#include <unistd.h>
+#include "luo_test_utils.h"
+
+#define NUM_FILES 500
+#define STATE_SESSION_NAME "kexec_many_files_state"
+#define STATE_MEMFD_TOKEN 9999
+#define TEST_SESSION_NAME "many_files_session"
+
+/* Stage 1: Executed before the kexec reboot. */
+static void run_stage_1(int luo_fd)
+{
+ int session_fd, i;
+
+ ksft_print_msg("[STAGE 1] Creating state file for next stage (2)...\n");
+ create_state_file(luo_fd, STATE_SESSION_NAME, STATE_MEMFD_TOKEN, 2);
+
+ ksft_print_msg("[STAGE 1] Creating test session '%s'...\n", TEST_SESSION_NAME);
+ session_fd = luo_create_session(luo_fd, TEST_SESSION_NAME);
+ if (session_fd < 0)
+ fail_exit("luo_create_session");
+
+ ksft_print_msg("[STAGE 1] Preserving %d files...\n", NUM_FILES);
+ for (i = 0; i < NUM_FILES; i++) {
+ char data[64];
+
+ snprintf(data, sizeof(data), "file-data-%d", i);
+ if (create_and_preserve_memfd(session_fd, i, data) < 0)
+ fail_exit("create_and_preserve_memfd for index %d", i);
+ }
+
+ ksft_print_msg("[STAGE 1] Successfully preserved %d files.\n", NUM_FILES);
+
+ close(luo_fd);
+ daemonize_and_wait();
+}
+
+/* Stage 2: Executed after the kexec reboot. */
+static void run_stage_2(int luo_fd, int state_session_fd)
+{
+ int session_fd;
+ int i, stage;
+
+ ksft_print_msg("[STAGE 2] Starting post-kexec verification...\n");
+
+ restore_and_read_stage(state_session_fd, STATE_MEMFD_TOKEN, &stage);
+ if (stage != 2) {
+ fail_exit("Expected stage 2, but state file contains %d",
+ stage);
+ }
+
+ ksft_print_msg("[STAGE 2] Retrieving test session '%s'...\n", TEST_SESSION_NAME);
+ session_fd = luo_retrieve_session(luo_fd, TEST_SESSION_NAME);
+ if (session_fd < 0)
+ fail_exit("luo_retrieve_session");
+
+ ksft_print_msg("[STAGE 2] Verifying %d files...\n", NUM_FILES);
+ for (i = 0; i < NUM_FILES; i++) {
+ char data[64];
+ int fd;
+
+ snprintf(data, sizeof(data), "file-data-%d", i);
+ fd = restore_and_verify_memfd(session_fd, i, data);
+ if (fd < 0)
+ fail_exit("restore_and_verify_memfd for index %d", i);
+ close(fd);
+ }
+
+ ksft_print_msg("[STAGE 2] Finishing test session...\n");
+ if (luo_session_finish(session_fd) < 0)
+ fail_exit("luo_session_finish for test session");
+ close(session_fd);
+
+ ksft_print_msg("[STAGE 2] Finalizing state session...\n");
+ if (luo_session_finish(state_session_fd) < 0)
+ fail_exit("luo_session_finish for state session");
+ close(state_session_fd);
+
+ ksft_print_msg("\n--- MANY-FILES KEXEC TEST PASSED (%d files) ---\n",
+ NUM_FILES);
+}
+
+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/luo_stress_sessions.c b/tools/testing/selftests/liveupdate/luo_stress_sessions.c
new file mode 100644
index 000000000000..f201b1839d1d
--- /dev/null
+++ b/tools/testing/selftests/liveupdate/luo_stress_sessions.c
@@ -0,0 +1,102 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+/*
+ * Copyright (c) 2026, Google LLC.
+ * Pasha Tatashin <pasha.tatashin@soleen.com>
+ *
+ * Validate that LUO can handle a large number of sessions across a kexec
+ * reboot.
+ */
+
+#include <stdio.h>
+#include <unistd.h>
+#include "luo_test_utils.h"
+
+#define NUM_SESSIONS 2000
+#define STATE_SESSION_NAME "kexec_many_state"
+#define STATE_MEMFD_TOKEN 999
+
+/* Stage 1: Executed before the kexec reboot. */
+static void run_stage_1(int luo_fd)
+{
+ int ret, i;
+
+ ksft_print_msg("[STAGE 1] Increasing ulimit for open files...\n");
+ ret = luo_ensure_nofile_limit(NUM_SESSIONS);
+ if (ret == -EPERM)
+ ksft_exit_skip("Insufficient privileges to set RLIMIT_NOFILE\n");
+ if (ret < 0)
+ ksft_exit_fail_msg("luo_ensure_nofile_limit failed: %s\n", strerror(-ret));
+
+ ksft_print_msg("[STAGE 1] Creating state file for next stage (2)...\n");
+ create_state_file(luo_fd, STATE_SESSION_NAME, STATE_MEMFD_TOKEN, 2);
+
+ ksft_print_msg("[STAGE 1] Creating %d sessions...\n", NUM_SESSIONS);
+
+ for (i = 0; i < NUM_SESSIONS; i++) {
+ char name[LIVEUPDATE_SESSION_NAME_LENGTH];
+ int s_fd;
+
+ snprintf(name, sizeof(name), "many-test-%d", i);
+ s_fd = luo_create_session(luo_fd, name);
+ if (s_fd < 0) {
+ fail_exit("luo_create_session for '%s' at index %d",
+ name, i);
+ }
+ }
+
+ ksft_print_msg("[STAGE 1] Successfully created %d sessions.\n",
+ NUM_SESSIONS);
+
+ close(luo_fd);
+ daemonize_and_wait();
+}
+
+/* Stage 2: Executed after the kexec reboot. */
+static void run_stage_2(int luo_fd, int state_session_fd)
+{
+ int i, stage;
+
+ ksft_print_msg("[STAGE 2] Starting post-kexec verification...\n");
+
+ restore_and_read_stage(state_session_fd, STATE_MEMFD_TOKEN, &stage);
+ if (stage != 2) {
+ fail_exit("Expected stage 2, but state file contains %d",
+ stage);
+ }
+
+ ksft_print_msg("[STAGE 2] Retrieving and finishing %d sessions...\n",
+ NUM_SESSIONS);
+
+ for (i = 0; i < NUM_SESSIONS; i++) {
+ char name[LIVEUPDATE_SESSION_NAME_LENGTH];
+ int s_fd;
+
+ snprintf(name, sizeof(name), "many-test-%d", i);
+ s_fd = luo_retrieve_session(luo_fd, name);
+ if (s_fd < 0) {
+ fail_exit("luo_retrieve_session for '%s' at index %d",
+ name, i);
+ }
+
+ if (luo_session_finish(s_fd) < 0) {
+ fail_exit("luo_session_finish for '%s' at index %d",
+ name, i);
+ }
+ close(s_fd);
+ }
+
+ ksft_print_msg("[STAGE 2] Finalizing state session...\n");
+ if (luo_session_finish(state_session_fd) < 0)
+ fail_exit("luo_session_finish for state session");
+ close(state_session_fd);
+
+ ksft_print_msg("\n--- MANY-SESSIONS KEXEC TEST PASSED (%d sessions) ---\n",
+ NUM_SESSIONS);
+}
+
+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/luo_test_utils.c b/tools/testing/selftests/liveupdate/luo_test_utils.c
index 3c8721c505df..333a3530051b 100644
--- a/tools/testing/selftests/liveupdate/luo_test_utils.c
+++ b/tools/testing/selftests/liveupdate/luo_test_utils.c
@@ -17,6 +17,7 @@
#include <sys/syscall.h>
#include <sys/mman.h>
#include <sys/types.h>
+#include <sys/resource.h>
#include <sys/stat.h>
#include <errno.h>
#include <stdarg.h>
@@ -28,6 +29,29 @@ int luo_open_device(void)
return open(LUO_DEVICE, O_RDWR);
}
+int luo_ensure_nofile_limit(long min_limit)
+{
+ struct rlimit hl;
+
+ /* Allow to extra files to be used by test itself */
+ min_limit += 32;
+
+ if (getrlimit(RLIMIT_NOFILE, &hl) < 0)
+ return -errno;
+
+ if (hl.rlim_cur >= min_limit)
+ return 0;
+
+ hl.rlim_cur = min_limit;
+ if (hl.rlim_cur > hl.rlim_max)
+ hl.rlim_max = hl.rlim_cur;
+
+ if (setrlimit(RLIMIT_NOFILE, &hl) < 0)
+ return -errno;
+
+ return 0;
+}
+
int luo_create_session(int luo_fd, const char *name)
{
struct liveupdate_ioctl_create_session arg = { .size = sizeof(arg) };
diff --git a/tools/testing/selftests/liveupdate/luo_test_utils.h b/tools/testing/selftests/liveupdate/luo_test_utils.h
index 90099bf49577..6a0d85386613 100644
--- a/tools/testing/selftests/liveupdate/luo_test_utils.h
+++ b/tools/testing/selftests/liveupdate/luo_test_utils.h
@@ -26,6 +26,8 @@ int luo_create_session(int luo_fd, const char *name);
int luo_retrieve_session(int luo_fd, const char *name);
int luo_session_finish(int session_fd);
+int luo_ensure_nofile_limit(long min_limit);
+
int create_and_preserve_memfd(int session_fd, int token, const char *data);
int restore_and_verify_memfd(int session_fd, int token, const char *expected_data);