| // SPDX-License-Identifier: GPL-2.0-only | |
| /* | |
| * Copyright (c) 2025, Google LLC. | |
| * Pasha Tatashin <pasha.tatashin@soleen.com> | |
| */ | |
| #define _GNU_SOURCE | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <getopt.h> | |
| #include <fcntl.h> | |
| #include <unistd.h> | |
| #include <sys/ioctl.h> | |
| #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> | |
| #include "luo_test_utils.h" | |
| 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) }; | |
| 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; | |
| 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_retrieve_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_RETRIEVE_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! Expected '%s', Got '%s'\n", | |
| expected_data, (char *)map); | |
| ret = -EINVAL; | |
| goto out_munmap; | |
| } | |
| ret = mfd; | |
| 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_session_finish(int session_fd) | |
| { | |
| struct liveupdate_session_finish arg = { .size = sizeof(arg) }; | |
| if (ioctl(session_fd, LIVEUPDATE_SESSION_FINISH, &arg) < 0) | |
| return -errno; | |
| return 0; | |
| } | |
| void create_state_file(int luo_fd, const char *session_name, int token, | |
| int next_stage) | |
| { | |
| char buf[32]; | |
| int state_session_fd; | |
| state_session_fd = luo_create_session(luo_fd, session_name); | |
| if (state_session_fd < 0) | |
| fail_exit("luo_create_session for state tracking"); | |
| snprintf(buf, sizeof(buf), "%d", next_stage); | |
| if (create_and_preserve_memfd(state_session_fd, token, buf) < 0) | |
| fail_exit("create_and_preserve_memfd for state tracking"); | |
| /* | |
| * DO NOT close session FD, otherwise it is going to be unpreserved | |
| */ | |
| } | |
| void restore_and_read_stage(int state_session_fd, int token, int *stage) | |
| { | |
| char buf[32] = {0}; | |
| int mfd; | |
| mfd = restore_and_verify_memfd(state_session_fd, 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); | |
| } | |
| void daemonize_and_wait(void) | |
| { | |
| pid_t pid; | |
| ksft_print_msg("[STAGE 1] Forking persistent child to hold sessions...\n"); | |
| pid = fork(); | |
| if (pid < 0) | |
| fail_exit("fork failed"); | |
| if (pid > 0) { | |
| ksft_print_msg("[STAGE 1] Child PID: %d. Resources are pinned.\n", pid); | |
| ksft_print_msg("[STAGE 1] You may now perform kexec reboot.\n"); | |
| exit(EXIT_SUCCESS); | |
| } | |
| /* Detach from terminal so closing the window doesn't kill us */ | |
| if (setsid() < 0) | |
| fail_exit("setsid failed"); | |
| close(STDIN_FILENO); | |
| close(STDOUT_FILENO); | |
| close(STDERR_FILENO); | |
| /* Change dir to root to avoid locking filesystems */ | |
| if (chdir("/") < 0) | |
| exit(EXIT_FAILURE); | |
| while (1) | |
| sleep(60); | |
| } | |
| static int parse_stage_args(int argc, char *argv[]) | |
| { | |
| static struct option long_options[] = { | |
| {"stage", required_argument, 0, 's'}, | |
| {0, 0, 0, 0} | |
| }; | |
| int option_index = 0; | |
| int stage = 1; | |
| int opt; | |
| optind = 1; | |
| while ((opt = getopt_long(argc, argv, "s:", long_options, &option_index)) != -1) { | |
| switch (opt) { | |
| case 's': | |
| stage = atoi(optarg); | |
| if (stage != 1 && stage != 2) | |
| fail_exit("Invalid stage argument"); | |
| break; | |
| default: | |
| fail_exit("Unknown argument"); | |
| } | |
| } | |
| return stage; | |
| } | |
| int luo_test(int argc, char *argv[], | |
| const char *state_session_name, | |
| luo_test_stage1_fn stage1, | |
| luo_test_stage2_fn stage2) | |
| { | |
| int target_stage = parse_stage_args(argc, argv); | |
| int luo_fd = luo_open_device(); | |
| int state_session_fd; | |
| int detected_stage; | |
| if (luo_fd < 0) { | |
| ksft_exit_skip("Failed to open %s. Is the luo module loaded?\n", | |
| LUO_DEVICE); | |
| } | |
| state_session_fd = luo_retrieve_session(luo_fd, state_session_name); | |
| if (state_session_fd == -ENOENT) | |
| detected_stage = 1; | |
| else if (state_session_fd >= 0) | |
| detected_stage = 2; | |
| else | |
| fail_exit("Failed to check for state session"); | |
| if (target_stage != detected_stage) { | |
| ksft_exit_fail_msg("Stage mismatch Requested --stage %d, but system is in stage %d.\n" | |
| "(State session %s: %s)\n", | |
| target_stage, detected_stage, state_session_name, | |
| (detected_stage == 2) ? "EXISTS" : "MISSING"); | |
| } | |
| if (target_stage == 1) | |
| stage1(luo_fd); | |
| else | |
| stage2(luo_fd, state_session_fd); | |
| return 0; | |
| } |