Introduce multi-stage selftest, luo_multi_kexec, to validate the end-to-end lifecycle of Live Update Orchestrator sessions across multiple kexec reboots. The test operates in three stages, using a preserved memfd within a dedicated "state_session" to track its progress across reboots. This avoids reliance on filesystem flags and tests the core preservation mechanism itself. The test validates the following critical LUO functionalities: 1. Initial Preservation (Stage 1 -> 2): - Creates multiple sessions (session-A, session-B, session-C) and populates them with memfd files containing unique data. - Triggers a global LIVEUPDATE_PREPARE event and executes the first kexec. 2. Intermediate State Management (Stage 2 -> 3): - After the first reboot, it verifies that all sessions were correctly preserved. - It then tests divergent session lifecycles: - Session A: Is retrieved and explicitly finalized with a per-session LIVEUPDATE_FINISH event. This validates that a finished session is not carried over to the next kexec. - Session B: Is retrieved but left open. This validates that an active, retrieved session is correctly re-preserved during the next global PREPARE. - Session C: Is deliberately not retrieved. This validates that the global LIVEUPDATE_FINISH event correctly identifies and cleans up stale, unreclaimed sessions. - The state-tracking memfd is updated by un-preserving and re-preserving it, testing in-place modification of a session's contents. - A global FINISH followed by a global PREPARE is triggered before the second kexec. 3. Final Verification (Stage 3): - After the second reboot, it confirms the final state: - Asserts that session-B (the re-preserved session) and the updated state session have survived. - Asserts that session-A (explicitly finished) and session-C (unreclaimed) were correctly cleaned up and no longer exist. Example output: root@debian-vm:~/liveupdate$ ./luo_multi_kexec LUO state is NORMAL. Starting Stage 1. [STAGE 1] Creating state file for next stage (2)... [STAGE 1] Setting up Sessions A, B, C for first kexec... - Session 'session-A' created. - Session 'session-B' created. - Session 'session-C' created. [STAGE 1] Triggering global PREPARE... [STAGE 1] Executing kexec... <---- cut reboot messages ----> Debian GNU/Linux 12 debian-vm ttyS0 debian-vm login: root (automatic login) root@debian-vm:~$ cd liveupdate/ root@debian-vm:~/liveupdate$ ./luo_multi_kexec LUO state is UPDATED. Restoring state to determine stage... State file indicates we are entering Stage 2. [STAGE 2] Partially reclaiming and preparing for second kexec... - Verifying session 'session-A'... Success. All files verified. - Verifying session 'session-B'... Success. All files verified. - Finishing state session to allow modification... - Updating state file for next stage (3)... - Session A verified. Sending per-session FINISH. - Session B verified. Keeping FD open for next kexec. - NOT retrieving Session C to test global finish cleanup. [STAGE 2] Triggering global FINISH... [STAGE 2] Triggering global PREPARE for next kexec... [STAGE 2] Executing second kexec... <---- cut reboot messages ----> Debian GNU/Linux 12 debian-vm ttyS0 debian-vm login: root (automatic login) root@debian-vm:~$ cd liveupdate/ root@debian-vm:~/liveupdate$ ./luo_multi_kexec LUO state is UPDATED. Restoring state to determine stage... State file indicates we are entering Stage 3. [STAGE 3] Final verification... [STAGE 3] Verifying surviving sessions... - Verifying session 'session-B'... Success. All files verified. [STAGE 3] Verifying Session A was cleaned up... Success. Session A not found as expected. [STAGE 3] Verifying Session C was cleaned up... Success. Session C not found as expected. [STAGE 3] Triggering final global FINISH... --- TEST PASSED --- Signed-off-by: Pasha Tatashin --- tools/testing/selftests/liveupdate/.gitignore | 1 + tools/testing/selftests/liveupdate/Makefile | 31 +++ .../testing/selftests/liveupdate/do_kexec.sh | 6 + .../selftests/liveupdate/luo_multi_kexec.c | 182 +++++++++++++ .../selftests/liveupdate/luo_test_utils.c | 241 ++++++++++++++++++ .../selftests/liveupdate/luo_test_utils.h | 51 ++++ 6 files changed, 512 insertions(+) create mode 100755 tools/testing/selftests/liveupdate/do_kexec.sh create mode 100644 tools/testing/selftests/liveupdate/luo_multi_kexec.c create mode 100644 tools/testing/selftests/liveupdate/luo_test_utils.c create mode 100644 tools/testing/selftests/liveupdate/luo_test_utils.h diff --git a/tools/testing/selftests/liveupdate/.gitignore b/tools/testing/selftests/liveupdate/.gitignore index af6e773cf98f..de7ca45d3892 100644 --- a/tools/testing/selftests/liveupdate/.gitignore +++ b/tools/testing/selftests/liveupdate/.gitignore @@ -1 +1,2 @@ /liveupdate +/luo_multi_kexec diff --git a/tools/testing/selftests/liveupdate/Makefile b/tools/testing/selftests/liveupdate/Makefile index 2a573c36016e..1cbc816ed5c5 100644 --- a/tools/testing/selftests/liveupdate/Makefile +++ b/tools/testing/selftests/liveupdate/Makefile @@ -1,7 +1,38 @@ # SPDX-License-Identifier: GPL-2.0-only + +KHDR_INCLUDES ?= -I../../../usr/include CFLAGS += -Wall -O2 -Wno-unused-function CFLAGS += $(KHDR_INCLUDES) +LDFLAGS += -static + +# --- Test Configuration (Edit this section when adding new tests) --- +LUO_SHARED_SRCS := luo_test_utils.c +LUO_SHARED_HDRS += luo_test_utils.h + +LUO_MANUAL_TESTS += luo_multi_kexec + +TEST_FILES += do_kexec.sh TEST_GEN_PROGS += liveupdate +# --- Automatic Rule Generation (Do not edit below) --- + +TEST_GEN_PROGS_EXTENDED += $(LUO_MANUAL_TESTS) + +# Define the full list of sources for each manual test. +$(foreach test,$(LUO_MANUAL_TESTS), \ + $(eval $(test)_SOURCES := $(test).c $(LUO_SHARED_SRCS))) + +# This loop automatically generates an explicit build rule for each manual test. +# It includes dependencies on the shared headers and makes the output +# executable. +# Note the use of '$$' to escape automatic variables for the 'eval' command. +$(foreach test,$(LUO_MANUAL_TESTS), \ + $(eval $(OUTPUT)/$(test): $($(test)_SOURCES) $(LUO_SHARED_HDRS) \ + $(call msg,LINK,,$$@) ; \ + $(Q)$(LINK.c) $$^ $(LDLIBS) -o $$@ ; \ + $(Q)chmod +x $$@ \ + ) \ +) + include ../lib.mk diff --git a/tools/testing/selftests/liveupdate/do_kexec.sh b/tools/testing/selftests/liveupdate/do_kexec.sh new file mode 100755 index 000000000000..bb396a92c3b8 --- /dev/null +++ b/tools/testing/selftests/liveupdate/do_kexec.sh @@ -0,0 +1,6 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +set -e + +kexec -l -s --reuse-cmdline /boot/bzImage +kexec -e diff --git a/tools/testing/selftests/liveupdate/luo_multi_kexec.c b/tools/testing/selftests/liveupdate/luo_multi_kexec.c new file mode 100644 index 000000000000..1f350990ee67 --- /dev/null +++ b/tools/testing/selftests/liveupdate/luo_multi_kexec.c @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: GPL-2.0-only + +/* + * Copyright (c) 2025, Google LLC. + * Pasha Tatashin + */ + +#include "luo_test_utils.h" + +#define KEXEC_SCRIPT "./do_kexec.sh" + +#define NUM_SESSIONS 3 + +/* Helper to set up one session and all its files */ +static void setup_session(int luo_fd, struct session_info *s, int session_idx) +{ + int i; + + snprintf(s->name, sizeof(s->name), "session-%c", 'A' + session_idx); + + s->fd = luo_create_session(luo_fd, s->name); + if (s->fd < 0) + fail_exit("luo_create_session for %s", s->name); + + for (i = 0; i < 2; i++) { + s->file_tokens[i] = (session_idx * 100) + i; + snprintf(s->file_data[i], sizeof(s->file_data[i]), + "Data for %.*s-File%d", + (int)sizeof(s->name), s->name, i); + + if (create_and_preserve_memfd(s->fd, s->file_tokens[i], + s->file_data[i]) < 0) + fail_exit("create_and_preserve_memfd for token %d", + s->file_tokens[i]); + } +} + +/* Run before the first kexec */ +static void run_stage_1(int luo_fd) +{ + struct session_info sessions[NUM_SESSIONS] = {0}; + int i; + + ksft_print_msg("[STAGE 1] Creating state file for next stage (2)...\n"); + create_state_file(luo_fd, 2); + + ksft_print_msg("[STAGE 1] Setting up Sessions A, B, C for first kexec...\n"); + for (i = 0; i < NUM_SESSIONS; i++) { + setup_session(luo_fd, &sessions[i], i); + ksft_print_msg(" - Session '%s' created.\n", sessions[i].name); + } + + ksft_print_msg("[STAGE 1] Triggering global PREPARE...\n"); + if (luo_set_global_event(luo_fd, LIVEUPDATE_PREPARE) < 0) + fail_exit("luo_set_global_event(PREPARE)"); + + ksft_print_msg("[STAGE 1] Executing kexec...\n"); + if (system(KEXEC_SCRIPT) != 0) + fail_exit("kexec script failed"); + + /* Should not be reached */ + sleep(10); + exit(EXIT_FAILURE); +} + +/* Run after first kexec, before second kexec */ +static void run_stage_2(int luo_fd, int state_session_fd) +{ + struct session_info sessions[NUM_SESSIONS] = {0}; + int session_fd_A; + + ksft_print_msg("[STAGE 2] Partially reclaiming and preparing for second kexec...\n"); + + reinit_all_sessions(sessions, NUM_SESSIONS); + + session_fd_A = verify_session_and_get_fd(luo_fd, &sessions[0]); + verify_session_and_get_fd(luo_fd, &sessions[1]); + + ksft_print_msg(" - Finishing state session to allow modification...\n"); + if (luo_set_session_event(state_session_fd, LIVEUPDATE_FINISH) < 0) + fail_exit("luo_set_session_event(FINISH) for state_session"); + + ksft_print_msg(" - Updating state file for next stage (3)...\n"); + update_state_file(state_session_fd, 3); + + ksft_print_msg(" - Session A verified. Sending per-session FINISH.\n"); + if (luo_set_session_event(session_fd_A, LIVEUPDATE_FINISH) < 0) + fail_exit("luo_set_session_event(FINISH) for Session A"); + close(session_fd_A); + + ksft_print_msg(" - Session B verified. Its FD will be auto-closed for next kexec.\n"); + ksft_print_msg(" - NOT retrieving Session C to test global finish cleanup.\n"); + + ksft_print_msg("[STAGE 2] Triggering global FINISH...\n"); + if (luo_set_global_event(luo_fd, LIVEUPDATE_FINISH) < 0) + fail_exit("luo_set_global_event(FINISH)"); + + ksft_print_msg("[STAGE 2] Triggering global PREPARE for next kexec...\n"); + if (luo_set_global_event(luo_fd, LIVEUPDATE_PREPARE) < 0) + fail_exit("luo_set_global_event(PREPARE)"); + + ksft_print_msg("[STAGE 2] Executing second kexec...\n"); + if (system(KEXEC_SCRIPT) != 0) + fail_exit("kexec script failed"); + + sleep(10); + exit(EXIT_FAILURE); +} + +/* Run after second kexec */ +static void run_stage_3(int luo_fd) +{ + struct session_info sessions[NUM_SESSIONS] = {0}; + int ret; + + ksft_print_msg("[STAGE 3] Final verification...\n"); + + reinit_all_sessions(sessions, NUM_SESSIONS); + + ksft_print_msg("[STAGE 3] Verifying surviving sessions...\n"); + /* Session B */ + verify_session_and_get_fd(luo_fd, &sessions[1]); + + ksft_print_msg("[STAGE 3] Verifying Session A was cleaned up...\n"); + ret = luo_retrieve_session(luo_fd, sessions[0].name); + if (ret != -ENOENT) + fail_exit("Expected ENOENT for Session A, but got %d", ret); + ksft_print_msg(" Success. Session A not found as expected.\n"); + + ksft_print_msg("[STAGE 3] Verifying Session C was cleaned up...\n"); + ret = luo_retrieve_session(luo_fd, sessions[2].name); + if (ret != -ENOENT) + fail_exit("Expected ENOENT for Session C, but got %d", ret); + ksft_print_msg(" Success. Session C not found as expected.\n"); + + ksft_print_msg("[STAGE 3] Triggering final global FINISH...\n"); + if (luo_set_global_event(luo_fd, LIVEUPDATE_FINISH) < 0) + fail_exit("luo_set_global_event(FINISH)"); + + ksft_print_msg("\n--- MULTI-KEXEC TEST PASSED ---\n"); +} + +int main(int argc, char *argv[]) +{ + enum liveupdate_state state; + int luo_fd, stage = 0; + + luo_fd = luo_open_device(); + if (luo_fd < 0) { + ksft_exit_skip("Failed to open %s. Is the luo module loaded?\n", + LUO_DEVICE); + } + + if (luo_get_global_state(luo_fd, &state) < 0) + fail_exit("luo_get_global_state"); + + if (state == LIVEUPDATE_STATE_NORMAL) { + ksft_print_msg("LUO state is NORMAL. Starting Stage 1.\n"); + run_stage_1(luo_fd); + } else if (state == LIVEUPDATE_STATE_UPDATED) { + int state_session_fd; + + ksft_print_msg("LUO state is UPDATED. Restoring state to determine stage...\n"); + state_session_fd = restore_and_read_state(luo_fd, &stage); + if (state_session_fd < 0) + fail_exit("Could not restore test state"); + + if (stage == 2) { + ksft_print_msg("State file indicates we are entering Stage 2.\n"); + run_stage_2(luo_fd, state_session_fd); + } else if (stage == 3) { + ksft_print_msg("State file indicates we are entering Stage 3.\n"); + run_stage_3(luo_fd); + } else { + fail_exit("Invalid stage found in state file: %d", + stage); + } + } + + close(luo_fd); + ksft_exit_pass(); +} diff --git a/tools/testing/selftests/liveupdate/luo_test_utils.c b/tools/testing/selftests/liveupdate/luo_test_utils.c new file mode 100644 index 000000000000..c0840e6e66fd --- /dev/null +++ b/tools/testing/selftests/liveupdate/luo_test_utils.c @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: GPL-2.0-only + +/* + * Copyright (c) 2025, Google LLC. + * Pasha Tatashin + */ + +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "luo_test_utils.h" +#include "../kselftest.h" + +/* The fail_exit function is now a macro in the header. */ + +int luo_open_device(void) +{ + return open(LUO_DEVICE, O_RDWR); +} + +int luo_create_session(int luo_fd, const char *name) +{ + struct liveupdate_ioctl_create_session arg = { .size = sizeof(arg) }; + + snprintf((char *)arg.name, LIVEUPDATE_SESSION_NAME_LENGTH, "%.*s", + LIVEUPDATE_SESSION_NAME_LENGTH - 1, name); + if (ioctl(luo_fd, LIVEUPDATE_IOCTL_CREATE_SESSION, &arg) < 0) + return -errno; + return arg.fd; +} + +int luo_retrieve_session(int luo_fd, const char *name) +{ + struct liveupdate_ioctl_retrieve_session arg = { .size = sizeof(arg) }; + + snprintf((char *)arg.name, LIVEUPDATE_SESSION_NAME_LENGTH, "%.*s", + LIVEUPDATE_SESSION_NAME_LENGTH - 1, name); + if (ioctl(luo_fd, LIVEUPDATE_IOCTL_RETRIEVE_SESSION, &arg) < 0) + return -errno; + return arg.fd; +} + +int create_and_preserve_memfd(int session_fd, int token, const char *data) +{ + struct liveupdate_session_preserve_fd arg = { .size = sizeof(arg) }; + long page_size = sysconf(_SC_PAGE_SIZE); + void *map = MAP_FAILED; + int mfd = -1, ret = -1; + + mfd = memfd_create("test_mfd", 0); + if (mfd < 0) + return -errno; + + if (ftruncate(mfd, page_size) != 0) + goto out; + + map = mmap(NULL, page_size, PROT_WRITE, MAP_SHARED, mfd, 0); + if (map == MAP_FAILED) + goto out; + + snprintf(map, page_size, "%s", data); + munmap(map, page_size); + + arg.fd = mfd; + arg.token = token; + if (ioctl(session_fd, LIVEUPDATE_SESSION_PRESERVE_FD, &arg) < 0) + goto out; + + ret = 0; /* Success */ +out: + if (ret != 0 && errno != 0) + ret = -errno; + if (mfd >= 0) + close(mfd); + return ret; +} + +int restore_and_verify_memfd(int session_fd, int token, + const char *expected_data) +{ + struct liveupdate_session_restore_fd arg = { .size = sizeof(arg) }; + long page_size = sysconf(_SC_PAGE_SIZE); + void *map = MAP_FAILED; + int mfd = -1, ret = -1; + + arg.token = token; + if (ioctl(session_fd, LIVEUPDATE_SESSION_RESTORE_FD, &arg) < 0) + return -errno; + mfd = arg.fd; + + map = mmap(NULL, page_size, PROT_READ, MAP_SHARED, mfd, 0); + if (map == MAP_FAILED) + goto out; + + if (expected_data && strcmp(expected_data, map) != 0) { + ksft_print_msg("Data mismatch for token %d!\n", token); + ret = -EINVAL; + goto out_munmap; + } + + ret = mfd; /* Success, return the new fd */ +out_munmap: + munmap(map, page_size); +out: + if (ret < 0 && errno != 0) + ret = -errno; + if (ret < 0 && mfd >= 0) + close(mfd); + return ret; +} + +int luo_set_session_event(int session_fd, enum liveupdate_event event) +{ + struct liveupdate_session_set_event arg = { .size = sizeof(arg) }; + + arg.event = event; + return ioctl(session_fd, LIVEUPDATE_SESSION_SET_EVENT, &arg); +} + +int luo_set_global_event(int luo_fd, enum liveupdate_event event) +{ + struct liveupdate_ioctl_set_event arg = { .size = sizeof(arg) }; + + arg.event = event; + return ioctl(luo_fd, LIVEUPDATE_IOCTL_SET_EVENT, &arg); +} + +int luo_get_global_state(int luo_fd, enum liveupdate_state *state) +{ + struct liveupdate_ioctl_get_state arg = { .size = sizeof(arg) }; + + if (ioctl(luo_fd, LIVEUPDATE_IOCTL_GET_STATE, &arg) < 0) + return -errno; + *state = arg.state; + return 0; +} + +void create_state_file(int luo_fd, int next_stage) +{ + char buf[32]; + int state_session_fd; + + state_session_fd = luo_create_session(luo_fd, STATE_SESSION_NAME); + if (state_session_fd < 0) + fail_exit("luo_create_session failed"); + + snprintf(buf, sizeof(buf), "%d", next_stage); + if (create_and_preserve_memfd(state_session_fd, + STATE_MEMFD_TOKEN, buf) < 0) { + fail_exit("create_and_preserve_memfd failed"); + } +} + +int restore_and_read_state(int luo_fd, int *stage) +{ + char buf[32] = {0}; + int state_session_fd, mfd; + + state_session_fd = luo_retrieve_session(luo_fd, STATE_SESSION_NAME); + if (state_session_fd < 0) + return state_session_fd; + + mfd = restore_and_verify_memfd(state_session_fd, STATE_MEMFD_TOKEN, + NULL); + if (mfd < 0) + fail_exit("failed to restore state memfd"); + + if (read(mfd, buf, sizeof(buf) - 1) < 0) + fail_exit("failed to read state mfd"); + + *stage = atoi(buf); + + close(mfd); + return state_session_fd; +} + +void update_state_file(int session_fd, int next_stage) +{ + char buf[32]; + struct liveupdate_session_unpreserve_fd arg = { .size = sizeof(arg) }; + + arg.token = STATE_MEMFD_TOKEN; + if (ioctl(session_fd, LIVEUPDATE_SESSION_UNPRESERVE_FD, &arg) < 0) + fail_exit("unpreserve failed"); + + snprintf(buf, sizeof(buf), "%d", next_stage); + if (create_and_preserve_memfd(session_fd, STATE_MEMFD_TOKEN, buf) < 0) + fail_exit("create_and_preserve failed"); +} + +void reinit_all_sessions(struct session_info *sessions, int num) +{ + int i, j; + + for (i = 0; i < num; i++) { + snprintf(sessions[i].name, sizeof(sessions[i].name), + "session-%c", 'A' + i); + for (j = 0; j < 2; j++) { + sessions[i].file_tokens[j] = (i * 100) + j; + snprintf(sessions[i].file_data[j], + sizeof(sessions[i].file_data[j]), + "Data for %.*s-File%d", + LIVEUPDATE_SESSION_NAME_LENGTH, + sessions[i].name, j); + } + } +} + +int verify_session_and_get_fd(int luo_fd, struct session_info *s) +{ + int i, session_fd; + + ksft_print_msg(" - Verifying session '%s'...\n", s->name); + + session_fd = luo_retrieve_session(luo_fd, s->name); + if (session_fd < 0) + fail_exit("luo_retrieve_session for %s", s->name); + + for (i = 0; i < 2; i++) { + int mfd = restore_and_verify_memfd(session_fd, + s->file_tokens[i], + s->file_data[i]); + if (mfd < 0) { + fail_exit("restore_and_verify_memfd for token %d", + s->file_tokens[i]); + } + close(mfd); + } + ksft_print_msg(" Success. All files verified.\n"); + return session_fd; +} diff --git a/tools/testing/selftests/liveupdate/luo_test_utils.h b/tools/testing/selftests/liveupdate/luo_test_utils.h new file mode 100644 index 000000000000..e30cfcb0a596 --- /dev/null +++ b/tools/testing/selftests/liveupdate/luo_test_utils.h @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: GPL-2.0 */ + +/* + * Copyright (c) 2025, Google LLC. + * Pasha Tatashin + */ + +#ifndef LUO_TEST_UTILS_H +#define LUO_TEST_UTILS_H + +#include +#include +#include +#include "../kselftest.h" + +#define LUO_DEVICE "/dev/liveupdate" +#define STATE_SESSION_NAME "state_session" +#define STATE_MEMFD_TOKEN 999 + +#define MAX_FILES_PER_SESSION 5 + +struct session_info { + char name[LIVEUPDATE_SESSION_NAME_LENGTH]; + int fd; + int file_tokens[MAX_FILES_PER_SESSION]; + char file_data[MAX_FILES_PER_SESSION][128]; +}; + +#define fail_exit(fmt, ...) \ + ksft_exit_fail_msg("[%s] " fmt " (errno: %s)\n", \ + __func__, ##__VA_ARGS__, strerror(errno)) + +int luo_open_device(void); + +int luo_create_session(int luo_fd, const char *name); +int luo_retrieve_session(int luo_fd, const char *name); + +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); +int verify_session_and_get_fd(int luo_fd, struct session_info *s); + +int luo_set_session_event(int session_fd, enum liveupdate_event event); +int luo_set_global_event(int luo_fd, enum liveupdate_event event); +int luo_get_global_state(int luo_fd, enum liveupdate_state *state); + +void create_state_file(int luo_fd, int next_stage); +int restore_and_read_state(int luo_fd, int *stage); +void update_state_file(int session_fd, int next_stage); +void reinit_all_sessions(struct session_info *sessions, int num); + +#endif /* LUO_TEST_UTILS_H */ -- 2.51.0.536.g15c5d4f767-goog