[PATCH v5 6/6] selftests: liveupdate: Add tests for LIVEUPDATE_SESSION_FREEZE

From: Pasha Tatashin

Date: Thu Nov 05 2026 - 00:12:56 EST


Add a luo_session_freeze(session_fd, flags) helper and selftests for the
LIVEUPDATE_SESSION_FREEZE ioctl:

- liveupdate.c: verify unsupported flags return -EOPNOTSUPP, repeated
freeze and post-freeze preserve return -EBUSY, closing a frozen
uncommitted session unfreezes and returns resources, and closing a
committed session (LIVEUPDATE_SESSION_FREEZE_COMMIT) retains LUO
ownership of the session and its preserved files.
- luo_kexec_simple.c: verify end-to-end kexec preservation of committed
sessions after closing all session FDs and exiting without
daemonize_and_wait(), and verify freezing an incoming session returns
-EINVAL.
- luo_multi_session.c: verify auto-frozen, kept-open frozen, closed
uncommitted frozen, and closed committed sessions together across a
kexec reboot.

Signed-off-by: Pasha Tatashin <pasha.tatashin@xxxxxxxxxx>
---
.../liveupdate/lib/include/libliveupdate.h | 5 +-
.../selftests/liveupdate/lib/lu_utils.c | 21 +++-
.../testing/selftests/liveupdate/liveupdate.c | 107 ++++++++++++++++++
.../selftests/liveupdate/luo_kexec_simple.c | 22 +++-
.../selftests/liveupdate/luo_multi_session.c | 33 +++++-
5 files changed, 179 insertions(+), 9 deletions(-)

diff --git a/tools/testing/selftests/liveupdate/lib/include/libliveupdate.h b/tools/testing/selftests/liveupdate/lib/include/libliveupdate.h
index fa07fed08364..a35871cefb82 100644
--- a/tools/testing/selftests/liveupdate/lib/include/libliveupdate.h
+++ b/tools/testing/selftests/liveupdate/lib/include/libliveupdate.h
@@ -26,6 +26,7 @@ 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_get_session_name(int session_fd, char *name, size_t name_len);
+int luo_session_freeze(int session_fd, __u32 flags);

int luo_ensure_nofile_limit(long min_limit);
int luo_session_preserve_fd(int session_fd, int fd, __u64 token);
@@ -34,8 +35,8 @@ int luo_session_retrieve_fd(int session_fd, __u64 token);
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);

-void create_state_file(int luo_fd, const char *session_name, int token,
- int next_stage);
+int create_state_file(int luo_fd, const char *session_name, int token,
+ int next_stage);
void restore_and_read_stage(int state_session_fd, int token, int *stage);

void daemonize_and_wait(void);
diff --git a/tools/testing/selftests/liveupdate/lib/lu_utils.c b/tools/testing/selftests/liveupdate/lib/lu_utils.c
index 74d41115c281..e71e7e90da27 100644
--- a/tools/testing/selftests/liveupdate/lib/lu_utils.c
+++ b/tools/testing/selftests/liveupdate/lib/lu_utils.c
@@ -198,8 +198,21 @@ int luo_session_finish(int session_fd)
return 0;
}

-void create_state_file(int luo_fd, const char *session_name, int token,
- int next_stage)
+int luo_session_freeze(int session_fd, __u32 flags)
+{
+ struct liveupdate_session_freeze arg = {
+ .size = sizeof(arg),
+ .flags = flags,
+ };
+
+ if (ioctl(session_fd, LIVEUPDATE_SESSION_FREEZE, &arg) < 0)
+ return -errno;
+
+ return 0;
+}
+
+int create_state_file(int luo_fd, const char *session_name, int token,
+ int next_stage)
{
char buf[32];
int state_session_fd;
@@ -213,8 +226,10 @@ void create_state_file(int luo_fd, const char *session_name, int token,
fail_exit("create_and_preserve_memfd for state tracking");

/*
- * DO NOT close session FD, otherwise it is going to be unpreserved
+ * DO NOT close session FD unless committed, otherwise it is going to
+ * be unpreserved.
*/
+ return state_session_fd;
}

void restore_and_read_stage(int state_session_fd, int token, int *stage)
diff --git a/tools/testing/selftests/liveupdate/liveupdate.c b/tools/testing/selftests/liveupdate/liveupdate.c
index 2dedd5fc2534..16b210da37a9 100644
--- a/tools/testing/selftests/liveupdate/liveupdate.c
+++ b/tools/testing/selftests/liveupdate/liveupdate.c
@@ -530,4 +530,111 @@ TEST_F(liveupdate_device, preserve_many_files)
ASSERT_EQ(close(session_fd), 0);
}

+/*
+ * Test Case: Session Freeze
+ *
+ * Verifies LIVEUPDATE_SESSION_FREEZE with flags == 0: unsupported flags return
+ * -EOPNOTSUPP, repeated freeze and post-freeze preserve return -EBUSY, and
+ * closing the session FD unfreezes the session and returns all resources.
+ */
+TEST_F(liveupdate_device, session_freeze)
+{
+ int session_fd, mem_fd1, mem_fd2;
+
+ 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 = luo_create_session(self->fd1, "session-freeze");
+ ASSERT_GE(session_fd, 0);
+
+ mem_fd1 = memfd_create("freeze-memfd-1", 0);
+ ASSERT_GE(mem_fd1, 0);
+ ASSERT_EQ(luo_session_preserve_fd(session_fd, mem_fd1, 0x1111), 0);
+
+ /* Unsupported flag bits must fail with -EOPNOTSUPP */
+ EXPECT_EQ(luo_session_freeze(session_fd, 1U << 1), -EOPNOTSUPP);
+
+ /* Freeze the session */
+ ASSERT_EQ(luo_session_freeze(session_fd, 0), 0);
+
+ /* Subsequent freeze attempts must fail with -EBUSY */
+ EXPECT_EQ(luo_session_freeze(session_fd, 0), -EBUSY);
+ EXPECT_EQ(luo_session_freeze(session_fd,
+ LIVEUPDATE_SESSION_FREEZE_COMMIT),
+ -EBUSY);
+
+ /* Preserving a file after freeze must fail with -EBUSY */
+ mem_fd2 = memfd_create("freeze-memfd-2", 0);
+ ASSERT_GE(mem_fd2, 0);
+ EXPECT_EQ(luo_session_preserve_fd(session_fd, mem_fd2, 0x2222), -EBUSY);
+
+ /* Closing the session FD must unfreeze and unpreserve resources */
+ ASSERT_EQ(close(session_fd), 0);
+
+ session_fd = luo_create_session(self->fd1, "session-freeze");
+ ASSERT_GE(session_fd, 0);
+ ASSERT_EQ(luo_session_preserve_fd(session_fd, mem_fd1, 0x1111), 0);
+
+ ASSERT_EQ(close(session_fd), 0);
+ ASSERT_EQ(close(mem_fd1), 0);
+ ASSERT_EQ(close(mem_fd2), 0);
+}
+
+/*
+ * Test Case: Session Freeze and Commit
+ *
+ * Verifies LIVEUPDATE_SESSION_FREEZE with LIVEUPDATE_SESSION_FREEZE_COMMIT:
+ * once committed, the frozen session is inserted into KHO and survives closing
+ * the session FD with ownership retained by LUO until reboot.
+ */
+TEST_F(liveupdate_device, session_freeze_commit)
+{
+ char name[LIVEUPDATE_SESSION_NAME_LENGTH];
+ int session_fd, session_fd2, mem_fd1, mem_fd2;
+
+ 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);
+
+ snprintf(name, sizeof(name), "freeze-commit-%d", getpid());
+ session_fd = luo_create_session(self->fd1, name);
+ ASSERT_GE(session_fd, 0);
+
+ mem_fd1 = memfd_create("freeze-commit-memfd-1", 0);
+ ASSERT_GE(mem_fd1, 0);
+ ASSERT_EQ(luo_session_preserve_fd(session_fd, mem_fd1, 0x1111), 0);
+
+ /* Freeze and commit the session */
+ ASSERT_EQ(luo_session_freeze(session_fd,
+ LIVEUPDATE_SESSION_FREEZE_COMMIT), 0);
+
+ /* Subsequent freeze attempts must fail with -EBUSY */
+ EXPECT_EQ(luo_session_freeze(session_fd, 0), -EBUSY);
+ EXPECT_EQ(luo_session_freeze(session_fd,
+ LIVEUPDATE_SESSION_FREEZE_COMMIT),
+ -EBUSY);
+
+ /* Preserving a file after freeze must fail with -EBUSY */
+ mem_fd2 = memfd_create("freeze-commit-memfd-2", 0);
+ ASSERT_GE(mem_fd2, 0);
+ EXPECT_EQ(luo_session_preserve_fd(session_fd, mem_fd2, 0x2222), -EBUSY);
+
+ /* Closing the session FD must retain LUO ownership of session & files */
+ ASSERT_EQ(close(session_fd), 0);
+
+ EXPECT_EQ(luo_create_session(self->fd1, name), -EEXIST);
+
+ session_fd2 = luo_create_session(self->fd1, "freeze-commit-check");
+ ASSERT_GE(session_fd2, 0);
+ EXPECT_EQ(luo_session_preserve_fd(session_fd2, mem_fd1, 0x3333),
+ -EBUSY);
+
+ ASSERT_EQ(close(session_fd2), 0);
+ ASSERT_EQ(close(mem_fd1), 0);
+ ASSERT_EQ(close(mem_fd2), 0);
+}
+
TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/liveupdate/luo_kexec_simple.c b/tools/testing/selftests/liveupdate/luo_kexec_simple.c
index 786ac93b9ae3..f065418a047f 100644
--- a/tools/testing/selftests/liveupdate/luo_kexec_simple.c
+++ b/tools/testing/selftests/liveupdate/luo_kexec_simple.c
@@ -21,12 +21,18 @@
/* Stage 1: Executed before the kexec reboot. */
static void run_stage_1(int luo_fd)
{
- int session_fd;
+ int session_fd, state_session_fd;

ksft_print_msg("[STAGE 1] Starting pre-kexec setup...\n");

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);
+ state_session_fd = create_state_file(luo_fd, STATE_SESSION_NAME,
+ STATE_MEMFD_TOKEN, 2);
+ if (luo_session_freeze(state_session_fd,
+ LIVEUPDATE_SESSION_FREEZE_COMMIT) < 0) {
+ fail_exit("luo_session_freeze for '%s'", STATE_SESSION_NAME);
+ }
+ close(state_session_fd);

ksft_print_msg("[STAGE 1] Creating session '%s' and preserving memfd...\n",
TEST_SESSION_NAME);
@@ -40,8 +46,15 @@ static void run_stage_1(int luo_fd)
TEST_MEMFD_TOKEN);
}

+ ksft_print_msg("[STAGE 1] Freezing and committing session '%s' and closing FD...\n",
+ TEST_SESSION_NAME);
+ if (luo_session_freeze(session_fd,
+ LIVEUPDATE_SESSION_FREEZE_COMMIT) < 0) {
+ fail_exit("luo_session_freeze for '%s'", TEST_SESSION_NAME);
+ }
+
+ close(session_fd);
close(luo_fd);
- daemonize_and_wait();
}

/* Stage 2: Executed after the kexec reboot. */
@@ -60,6 +73,9 @@ static void run_stage_2(int luo_fd, int state_session_fd)
if (session_fd < 0)
fail_exit("luo_retrieve_session for '%s'", TEST_SESSION_NAME);

+ if (luo_session_freeze(session_fd, 0) != -EINVAL)
+ fail_exit("luo_session_freeze on incoming session did not return -EINVAL");
+
ksft_print_msg("[STAGE 2] Restoring and verifying memfd (token %#x)...\n",
TEST_MEMFD_TOKEN);
mfd = restore_and_verify_memfd(session_fd, TEST_MEMFD_TOKEN,
diff --git a/tools/testing/selftests/liveupdate/luo_multi_session.c b/tools/testing/selftests/liveupdate/luo_multi_session.c
index aac24a5f5ce3..6b532ec6b82f 100644
--- a/tools/testing/selftests/liveupdate/luo_multi_session.c
+++ b/tools/testing/selftests/liveupdate/luo_multi_session.c
@@ -15,14 +15,17 @@
#define SESSION_EMPTY_2 "multi-test-empty-2"
#define SESSION_FILES_1 "multi-test-files-1"
#define SESSION_FILES_2 "multi-test-files-2"
+#define SESSION_CLOSED "multi-test-closed"

#define MFD1_TOKEN 0x1001
#define MFD2_TOKEN 0x2002
#define MFD3_TOKEN 0x3003
+#define MFD_CLOSED_TOKEN 0x4004

#define MFD1_DATA "Data for session files 1"
#define MFD2_DATA "First file for session files 2"
#define MFD3_DATA "Second file for session files 2"
+#define MFD_CLOSED_DATA "Closed session data"

#define STATE_SESSION_NAME "kexec_multi_state"
#define STATE_MEMFD_TOKEN 998
@@ -30,7 +33,7 @@
/* Stage 1: Executed before the kexec reboot. */
static void run_stage_1(int luo_fd)
{
- int s_empty1_fd, s_empty2_fd, s_files1_fd, s_files2_fd;
+ int s_empty1_fd, s_empty2_fd, s_files1_fd, s_files2_fd, s_closed_fd;

ksft_print_msg("[STAGE 1] Starting pre-kexec setup for multi-session test...\n");

@@ -57,6 +60,8 @@ static void run_stage_1(int luo_fd)
fail_exit("create_and_preserve_memfd for token %#x",
MFD1_TOKEN);
}
+ if (luo_session_freeze(s_files1_fd, 0) < 0)
+ fail_exit("luo_session_freeze for '%s'", SESSION_FILES_1);

ksft_print_msg("[STAGE 1] Creating session '%s' with two memfds...\n",
SESSION_FILES_2);
@@ -72,6 +77,25 @@ static void run_stage_1(int luo_fd)
fail_exit("create_and_preserve_memfd for token %#x",
MFD3_TOKEN);
}
+ if (luo_session_freeze(s_files2_fd,
+ LIVEUPDATE_SESSION_FREEZE_COMMIT) < 0) {
+ fail_exit("luo_session_freeze for '%s'", SESSION_FILES_2);
+ }
+ close(s_files2_fd);
+
+ ksft_print_msg("[STAGE 1] Creating frozen uncommitted session '%s' and closing FD...\n",
+ SESSION_CLOSED);
+ s_closed_fd = luo_create_session(luo_fd, SESSION_CLOSED);
+ if (s_closed_fd < 0)
+ fail_exit("luo_create_session for '%s'", SESSION_CLOSED);
+ if (create_and_preserve_memfd(s_closed_fd, MFD_CLOSED_TOKEN,
+ MFD_CLOSED_DATA) < 0) {
+ fail_exit("create_and_preserve_memfd for token %#x",
+ MFD_CLOSED_TOKEN);
+ }
+ if (luo_session_freeze(s_closed_fd, 0) < 0)
+ fail_exit("luo_session_freeze for '%s'", SESSION_CLOSED);
+ close(s_closed_fd);

close(luo_fd);
daemonize_and_wait();
@@ -91,6 +115,13 @@ static void run_stage_2(int luo_fd, int state_session_fd)
stage);
}

+ ksft_print_msg("[STAGE 2] Verifying closed uncommitted session '%s' was not preserved...\n",
+ SESSION_CLOSED);
+ if (luo_retrieve_session(luo_fd, SESSION_CLOSED) != -ENOENT) {
+ fail_exit("Expected -ENOENT for closed uncommitted session '%s'",
+ SESSION_CLOSED);
+ }
+
ksft_print_msg("[STAGE 2] Retrieving all sessions...\n");
s_empty1_fd = luo_retrieve_session(luo_fd, SESSION_EMPTY_1);
if (s_empty1_fd < 0)
--
2.56.0.398.g882cbad275-goog