Add a new KVM selftest `guest_memfd_preservation_test` to verify that guest memory backed by guest_memfd is preserved properly.
Here, I have used the kvm selftests framework by creating a new vm and mapping two memory slots to it. One is the code that is executed inside the vm and other is the guest_memfd whose memory is being written by the guest code. In Stage 1: Once data is written the vm exits and wait for the user to trigger the kexec. In Stage 2: A new vm is created with retrieved kvm and again two memory slots are assigned. Once for guest code, and another is for retrieved guest_memfd where guest_memfd memory is verified by the executed guest code. If verification succeeds, The test passes. // Kernel is compiled with CONFIG_LIVEUPDATE_GUEST_MEMFD and booted // with kho=on liveupdate=on command line parameter. $ ./selftests/kvm/guest_memfd_preservation_test --stage 1 $ <kexec> $ ./selftests/kvm/guest_memfd_preservation_test --stage 2 Signed-off-by: Tarun Sahu <[email protected]> --- MAINTAINERS | 1 + tools/testing/selftests/kvm/Makefile.kvm | 6 +- .../kvm/guest_memfd_preservation_test.c | 236 ++++++++++++++++++ 3 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 tools/testing/selftests/kvm/guest_memfd_preservation_test.c diff --git a/MAINTAINERS b/MAINTAINERS index e27b677..d0033a9 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -14421,6 +14421,7 @@ L: [email protected] S: Maintained T: git git://git.kernel.org/pub/scm/linux/kernel/git/liveupdate/linux.git F: Documentation/liveupdate/vmm.rst +F: tools/testing/selftests/kvm/guest_memfd_preservation_test.c F: virt/kvm/guest_memfd_luo.c F: virt/kvm/kvm_luo.c diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm index d28a057..d5bc8be2 100644 --- a/tools/testing/selftests/kvm/Makefile.kvm +++ b/tools/testing/selftests/kvm/Makefile.kvm @@ -164,6 +164,8 @@ TEST_GEN_PROGS_x86 += pre_fault_memory_test # Compiled outputs used by test targets TEST_GEN_PROGS_EXTENDED_x86 += x86/nx_huge_pages_test +# Manual test that forks a persistent background daemon; skip auto CI run +TEST_GEN_PROGS_EXTENDED_x86 += guest_memfd_preservation_test TEST_GEN_PROGS_arm64 = $(TEST_GEN_PROGS_COMMON) TEST_GEN_PROGS_arm64 += arm64/aarch32_id_regs @@ -258,6 +260,7 @@ OVERRIDE_TARGETS = 1 # which causes the environment variable to override the makefile). include ../lib.mk include ../cgroup/lib/libcgroup.mk +include ../liveupdate/lib/libliveupdate.mk INSTALL_HDR_PATH = $(top_srcdir)/usr LINUX_HDR_PATH = $(INSTALL_HDR_PATH)/include/ @@ -312,7 +315,8 @@ LIBKVM_S := $(filter %.S,$(LIBKVM)) LIBKVM_C_OBJ := $(patsubst %.c, $(OUTPUT)/%.o, $(LIBKVM_C)) LIBKVM_S_OBJ := $(patsubst %.S, $(OUTPUT)/%.o, $(LIBKVM_S)) LIBKVM_STRING_OBJ := $(patsubst %.c, $(OUTPUT)/%.o, $(LIBKVM_STRING)) -LIBKVM_OBJS = $(LIBKVM_C_OBJ) $(LIBKVM_S_OBJ) $(LIBKVM_STRING_OBJ) $(LIBCGROUP_O) +LIBKVM_OBJS = $(LIBKVM_C_OBJ) $(LIBKVM_S_OBJ) $(LIBKVM_STRING_OBJ) \ + $(LIBCGROUP_O) $(LIBLIVEUPDATE_O) SPLIT_TEST_GEN_PROGS := $(patsubst %, $(OUTPUT)/%, $(SPLIT_TESTS)) SPLIT_TEST_GEN_OBJ := $(patsubst %, $(OUTPUT)/$(ARCH)/%.o, $(SPLIT_TESTS)) diff --git a/tools/testing/selftests/kvm/guest_memfd_preservation_test.c b/tools/testing/selftests/kvm/guest_memfd_preservation_test.c new file mode 100644 index 0000000..c0a20e7 --- /dev/null +++ b/tools/testing/selftests/kvm/guest_memfd_preservation_test.c @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2026, Google LLC. + * + * Author: Tarun Sahu <[email protected]> + * + * Test for VM and guest_memfd preservation across kexec (Live Update) via LUO. + * + * NOTE: This is a MANUAL test and is excluded from automated CI/testing + * frameworks because Stage 1 daemonizes into the background to pin resources + * and requires a human operator to manually trigger kexec before Stage 2 + * is executed. Running Stage 1 automatically would leak the background daemon + * and cause CI runners to falsely interpret it as a passed test. + * + * Usage: + * Stage 1: ./guest_memfd_preservation_test --stage 1 + * Stage 2: ./guest_memfd_preservation_test --stage 2 + */ +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <errno.h> +#include <stdio.h> +#include <fcntl.h> +#include <sys/mman.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/ioctl.h> +#include <linux/sizes.h> +#include <linux/falloc.h> + +#include "kvm_util.h" +#include "processor.h" +#include "test_util.h" +#include "ucall_common.h" +#include "../kselftest.h" +#include "../kselftest_harness.h" + +#include <libliveupdate.h> + +#define SESSION_NAME "gmem_vm_preservation_session" +#define VM_TOKEN 0x1001 +#define GMEM_TOKEN 0x1002 + +#define STATE_SESSION_NAME "gmem_preservation_state" +#define STATE_TOKEN 0x999 + +#define GMEM_SIZE (16ULL * 1024 * 1024) +#define DATA_SIZE (5ULL * 1024 * 1024) + +static size_t page_size; + +/* Deterministic byte pattern generation based on offset */ +static inline uint8_t get_pattern_byte(size_t offset) +{ + return (uint8_t)(offset ^ 0x5A); +} + +static void guest_code_phase1(uint64_t gpa, uint64_t size, uint64_t data_size) +{ + uint8_t *mem = (uint8_t *)gpa; + size_t i; + + for (i = 0; i < data_size; i++) + mem[i] = get_pattern_byte(i); + + GUEST_DONE(); +} + +static void guest_code_phase2(uint64_t gpa, uint64_t size, uint64_t data_size) +{ + uint8_t *mem = (uint8_t *)gpa; + size_t i; + + for (i = 0; i < data_size; i++) { + uint8_t val = get_pattern_byte(i); + + __GUEST_ASSERT(mem[i] == val, + "Data mismatch at offset %lu! Expected 0x%x, got 0x%x", + i, val, mem[i]); + } + + GUEST_DONE(); +} + +static void run_stage_1(int luo_fd) +{ + uint64_t flags = GUEST_MEMFD_FLAG_MMAP | GUEST_MEMFD_FLAG_INIT_SHARED; + int gmem_fd, session_fd, ret; + const uint64_t gpa = SZ_4G; + struct kvm_vcpu *vcpu; + const int slot = 1; + struct kvm_vm *vm; + + 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_TOKEN, 2); + + vm = __vm_create_shape_with_one_vcpu(VM_SHAPE_DEFAULT, &vcpu, 1, + guest_code_phase1); + gmem_fd = vm_create_guest_memfd(vm, GMEM_SIZE, flags); + vm_set_user_memory_region2(vm, slot, KVM_MEM_GUEST_MEMFD, gpa, GMEM_SIZE, NULL, + gmem_fd, 0); + + for (size_t i = 0; i < GMEM_SIZE; i += page_size) + virt_pg_map(vm, gpa + i, gpa + i); + + vcpu_args_set(vcpu, 3, gpa, GMEM_SIZE, DATA_SIZE); + + vcpu_run(vcpu); + TEST_ASSERT_EQ(get_ucall(vcpu, NULL), UCALL_DONE); + + ksft_print_msg("[STAGE 1] Creating session '%s' and preserving VM/guest_memfd...\n", + SESSION_NAME); + session_fd = luo_create_session(luo_fd, SESSION_NAME); + TEST_ASSERT(session_fd >= 0, "Failed to create LUO session"); + + ret = luo_session_preserve_fd(session_fd, vm->fd, VM_TOKEN); + TEST_ASSERT(ret == 0, "Failed to preserve VM file descriptor"); + + ret = luo_session_preserve_fd(session_fd, gmem_fd, GMEM_TOKEN); + TEST_ASSERT(ret == 0, "Failed to preserve guest_memfd file descriptor"); + + printf("\n============================================================\n"); + printf("Phase 1 Complete Successfully!\n"); + printf("VM file and guest_memfd file have been preserved via LUO.\n"); + printf("Tokens: VM_TOKEN=0x%x, GMEM_TOKEN=0x%x\n", VM_TOKEN, GMEM_TOKEN); + printf("Machine Size: %llu MB, Data Size: %llu MB\n", GMEM_SIZE / SZ_1M, + DATA_SIZE / SZ_1M); + printf("------------------------------------------------------------\n"); + + close(luo_fd); + daemonize_and_wait(); +} + +static struct kvm_vm *vm_create_from_fd(int resurrected_vm_fd, + struct vm_shape shape) +{ + struct kvm_vm *vm; + + vm = calloc(1, sizeof(*vm)); + TEST_ASSERT(vm != NULL, "Insufficient Memory"); + + vm_init_fields(vm, shape); + + vm->kvm_fd = open_path_or_exit(KVM_DEV_PATH, O_RDWR); + vm->fd = resurrected_vm_fd; + + if (kvm_has_cap(KVM_CAP_BINARY_STATS_FD)) + vm->stats.fd = vm_get_stats_fd(vm); + else + vm->stats.fd = -1; + + vm_init_memory_properties(vm); + + return vm; +} + +static void run_stage_2(int luo_fd, int state_session_fd) +{ + int retrieved_vm_fd, retrieved_gmem_fd, session_fd, stage; + struct vm_shape shape = VM_SHAPE_DEFAULT; + const uint64_t gpa = SZ_4G; + struct kvm_vcpu *vcpu; + const int slot = 1; + struct kvm_vm *vm; + + ksft_print_msg("[STAGE 2] Starting post-kexec verification...\n"); + + restore_and_read_stage(state_session_fd, STATE_TOKEN, &stage); + if (stage != 2) + fail_exit("Expected stage 2, but state file contains %d", stage); + + ksft_print_msg("[STAGE 2] Retrieving session '%s'...\n", SESSION_NAME); + session_fd = luo_retrieve_session(luo_fd, SESSION_NAME); + TEST_ASSERT(session_fd >= 0, "Failed to retrieve LUO session"); + + retrieved_vm_fd = luo_session_retrieve_fd(session_fd, VM_TOKEN); + TEST_ASSERT(retrieved_vm_fd >= 0, "Failed to retrieve VM file descriptor"); + + retrieved_gmem_fd = luo_session_retrieve_fd(session_fd, GMEM_TOKEN); + TEST_ASSERT(retrieved_gmem_fd >= 0, "Failed to retrieve guest_memfd file descriptor"); + + vm = vm_create_from_fd(retrieved_vm_fd, shape); + + u64 nr_pages = 2048; /* 8MB is plenty for slot0 pages */ + + vm_userspace_mem_region_add(vm, VM_MEM_SRC_ANONYMOUS, 0, 0, nr_pages, 0); + kvm_vm_elf_load(vm, program_invocation_name); + + for (int i = 0; i < NR_MEM_REGIONS; i++) + vm->memslots[i] = 0; + + struct userspace_mem_region *slot0 = memslot2region(vm, 0); + + ucall_init(vm, slot0->region.guest_phys_addr + slot0->region.memory_size); + + vm_set_user_memory_region2(vm, slot, KVM_MEM_GUEST_MEMFD, gpa, GMEM_SIZE, NULL, + retrieved_gmem_fd, 0); + + for (size_t i = 0; i < GMEM_SIZE; i += page_size) + virt_pg_map(vm, gpa + i, gpa + i); + + vcpu = vm_vcpu_add(vm, 0, guest_code_phase2); + kvm_arch_vm_finalize_vcpus(vm); + + vcpu_args_set(vcpu, 3, gpa, GMEM_SIZE, DATA_SIZE); + + printf("Resuming / Running VM in Phase 2...\n"); + vcpu_run(vcpu); + TEST_ASSERT_EQ(get_ucall(vcpu, NULL), UCALL_DONE); + + printf("\nSUCCESS: Phase 2 Complete! All 5MB complex data verified intact!\n"); + + luo_session_finish(session_fd); + 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); + + /* This will also close the vm_fd */ + kvm_vm_free(vm); + close(retrieved_gmem_fd); +} + +int main(int argc, char *argv[]) +{ + TEST_REQUIRE(kvm_has_cap(KVM_CAP_GUEST_MEMFD)); + page_size = getpagesize(); + + return luo_test(argc, argv, STATE_SESSION_NAME, + run_stage_1, run_stage_2); +} -- 2.55.0.rc0.786.g65d90a0328-goog

