| /* | |
| * vmlaunchupdate device fwcfg test. | |
| * | |
| * Copyright (c) 2026 Red Hat, Inc. | |
| * | |
| * Author: | |
| * Ani Sinha <anisinha@redhat.com> | |
| * | |
| * SPDX-License-Identifier: GPL-2.0-or-later | |
| */ | |
| #include "qemu/osdep.h" | |
| #include "libqos/libqos-pc.h" | |
| #include "libqtest.h" | |
| #include "standard-headers/linux/qemu_fw_cfg.h" | |
| #include "libqos/fw_cfg.h" | |
| #include "qemu/bswap.h" | |
| #include "hw/misc/vmlaunchupdate.h" | |
| #define WAIT_SEC 10 | |
| static bool debug; | |
| static bool trace; | |
| static bool confidential; | |
| static void test_vm_launch_update_capability(void) | |
| { | |
| QFWCFG *fw_cfg; | |
| QTestState *s; | |
| VMLaunchUpdate launch_update; | |
| size_t filesize; | |
| uint64_t capabilities; | |
| if (!qtest_has_device("vm-launch-update")) { | |
| g_test_skip("Device vm-launch-update is not available"); | |
| return; | |
| } | |
| s = qtest_init("-device vm-launch-update"); | |
| fw_cfg = pc_fw_cfg_init(s); | |
| filesize = qfw_cfg_get_file(fw_cfg, FILE_VMLAUNCHUPDATE, | |
| &launch_update, sizeof(launch_update)); | |
| g_assert_cmpint(filesize, ==, sizeof(launch_update)); | |
| capabilities = le64_to_cpu(launch_update.capabilities); | |
| g_assert_cmpint(capabilities, ==, VM_LAUNCHUPDATE_FORMAT_IGVM); | |
| pc_fw_cfg_uninit(fw_cfg); | |
| qtest_quit(s); | |
| } | |
| static void test_vm_launch_update_disable(void) | |
| { | |
| QFWCFG *fw_cfg; | |
| QOSState *qs; | |
| VMLaunchUpdate launch_update; | |
| uint64_t control; | |
| size_t filesize; | |
| if (!qtest_has_device("vm-launch-update")) { | |
| g_test_skip("Device vm-launch-update is not available"); | |
| return; | |
| } | |
| /* use default accelerator */ | |
| qs = qtest_pc_boot("-device vm-launch-update"); | |
| fw_cfg = pc_fw_cfg_init(qs->qts); | |
| filesize = qfw_cfg_get_file(fw_cfg, FILE_VMLAUNCHUPDATE, | |
| &launch_update, sizeof(launch_update)); | |
| g_assert_cmpint(filesize, ==, sizeof(launch_update)); | |
| control = le64_to_cpu(launch_update.control); | |
| g_assert_cmpint(VM_LAUNCHUPDATE_CTL_DISABLE & control, ==, 0); | |
| /* disable the device */ | |
| memset(&launch_update, 0, sizeof(launch_update)); | |
| launch_update.control |= VM_LAUNCHUPDATE_CTL_DISABLE; | |
| filesize = qfw_cfg_write_file(fw_cfg, qs, FILE_VMLAUNCHUPDATE, | |
| &launch_update, sizeof(launch_update)); | |
| g_assert_cmpint(filesize, ==, sizeof(launch_update)); | |
| /* try to clear the dsable flag */ | |
| memset(&launch_update, 0, sizeof(launch_update)); | |
| filesize = qfw_cfg_write_file(fw_cfg, qs, FILE_VMLAUNCHUPDATE, | |
| &launch_update, sizeof(launch_update)); | |
| g_assert_cmpint(filesize, ==, sizeof(launch_update)); | |
| /* check if the device is still disabled */ | |
| filesize = qfw_cfg_get_file(fw_cfg, FILE_VMLAUNCHUPDATE, | |
| &launch_update, sizeof(launch_update)); | |
| g_assert_cmpint(filesize, ==, sizeof(launch_update)); | |
| control = le64_to_cpu(launch_update.control); | |
| g_assert_cmpint(VM_LAUNCHUPDATE_CTL_DISABLE & control, ==, 1); | |
| pc_fw_cfg_uninit(fw_cfg); | |
| qtest_shutdown(qs); | |
| } | |
| static void check_error(void) | |
| { | |
| QFWCFG *fw_cfg; | |
| QOSState *qs; | |
| VMLaunchUpdate launch_update; | |
| uint16_t status; | |
| size_t filesize; | |
| if (!qtest_has_device("vm-launch-update")) { | |
| g_test_skip("Device vm-launch-update is not available"); | |
| return; | |
| } | |
| /* guest not started with IGVM and with default accelerator */ | |
| qs = qtest_pc_boot("-device vm-launch-update"); | |
| fw_cfg = pc_fw_cfg_init(qs->qts); | |
| memset(&launch_update, 0, sizeof(launch_update)); | |
| launch_update.fw_image_size = 50; | |
| launch_update.fw_image_addr = cpu_to_le64(0xdeadbeef); | |
| launch_update.control |= VM_LAUNCHUPDATE_FORMAT_IGVM; | |
| filesize = qfw_cfg_write_file(fw_cfg, qs, FILE_VMLAUNCHUPDATE, | |
| &launch_update, sizeof(launch_update)); | |
| g_assert_cmpint(filesize, ==, sizeof(launch_update)); | |
| memset(&launch_update, 0, sizeof(launch_update)); | |
| filesize = qfw_cfg_get_file(fw_cfg, FILE_VMLAUNCHUPDATE, | |
| &launch_update, sizeof(launch_update)); | |
| g_assert_cmpint(filesize, ==, sizeof(launch_update)); | |
| status = le64_to_cpu(launch_update.status); | |
| /* should fail with NOT_IGVM_INIT */ | |
| g_assert_cmpint(status, ==, VM_LAUNCHUPDATE_NOT_IGVM_INIT); | |
| memset(&launch_update, 0, sizeof(launch_update)); | |
| launch_update.fw_image_size = 50; | |
| launch_update.fw_image_addr = cpu_to_le64(0xdeadbeef); | |
| /* control set to 0, not VM_LAUNCHUPDATE_FORMAT_IGVM */ | |
| filesize = qfw_cfg_write_file(fw_cfg, qs, FILE_VMLAUNCHUPDATE, | |
| &launch_update, sizeof(launch_update)); | |
| g_assert_cmpint(filesize, ==, sizeof(launch_update)); | |
| memset(&launch_update, 0, sizeof(launch_update)); | |
| filesize = qfw_cfg_get_file(fw_cfg, FILE_VMLAUNCHUPDATE, | |
| &launch_update, sizeof(launch_update)); | |
| g_assert_cmpint(filesize, ==, sizeof(launch_update)); | |
| status = le64_to_cpu(launch_update.status); | |
| /* should fail with LOAD_FAIL since it was not IGVM format */ | |
| g_assert_cmpint(status, ==, VM_LAUNCHUPDATE_LOAD_FAIL); | |
| } | |
| static int64_t get_image_size(const char *filename) | |
| { | |
| int fd; | |
| int64_t size; | |
| fd = open(filename, O_RDONLY | O_BINARY); | |
| g_assert_true(fd > 0); | |
| size = lseek(fd, 0, SEEK_END); | |
| close(fd); | |
| return size; | |
| } | |
| static ssize_t load_image(const char *igvm_f, void **addr, size_t *size) | |
| { | |
| ssize_t actsize = 0, l = 0; | |
| int f_igvm_f; | |
| size_t l_size; | |
| f_igvm_f = open(igvm_f, O_RDONLY | O_BINARY); | |
| g_assert_true(f_igvm_f); | |
| l_size = get_image_size(igvm_f); | |
| g_assert_true(l_size > 0); | |
| *addr = g_malloc0(l_size); | |
| g_assert_true(*addr); | |
| while (l < l_size) { | |
| actsize = read(f_igvm_f, *addr + l, 1); | |
| if (actsize < 0) { | |
| break; | |
| } | |
| l += actsize; | |
| } | |
| close(f_igvm_f); | |
| *size = l_size; | |
| return actsize < 0 ? -1 : l; | |
| } | |
| static guint32 match_string(char *serial_f, const char *exp_out) | |
| { | |
| GError *error = NULL; | |
| g_autofree gchar *f_contents = NULL; | |
| g_autofree GRegex *regex = NULL; | |
| g_autofree GMatchInfo *match_info = NULL; | |
| gsize len; | |
| guint32 count = 0; | |
| gboolean ret; | |
| ret = g_file_get_contents(serial_f, &f_contents, &len, &error); | |
| g_assert(ret); | |
| g_assert_no_error(error); | |
| regex = g_regex_new(exp_out, G_REGEX_CASELESS, 0, &error); | |
| g_assert_no_error(error); | |
| ret = g_regex_match_full(regex, f_contents, -1, 0, 0, &match_info, &error); | |
| g_assert_no_error(error); | |
| while (g_match_info_matches(match_info)) { | |
| gchar *word = g_match_info_fetch(match_info, 0); | |
| g_free(word); | |
| g_match_info_next(match_info, &error); | |
| count++; | |
| } | |
| g_regex_unref(regex); | |
| return count; | |
| } | |
| static int wait_for_match(char *serial_f, | |
| const char *exp_out, int64_t timeout_s, | |
| guint32 count) | |
| { | |
| time_t start, delta; | |
| int ret = -1; | |
| start = time(NULL); | |
| while (1) { | |
| if (match_string(serial_f, exp_out) == count) { | |
| ret = 0; | |
| break; | |
| } | |
| delta = time(NULL) - start; | |
| if (delta >= timeout_s) { | |
| fprintf(stderr, "timed out waiting to read serial output\n"); | |
| break; | |
| } | |
| /* wait 20 ms before trying again */ | |
| if (false) { | |
| fprintf(stderr, | |
| "sleeping 20 ms before checking serial output again.\n"); | |
| } | |
| g_usleep(20000); | |
| } | |
| return ret; | |
| } | |
| static void set_test_params(const char **igvm_f, const char **igvm_init, | |
| const char **snp, const char **cgs) | |
| { | |
| if (confidential) { | |
| *snp = "-object \'{\"qom-type\":\"sev-snp-guest\",\"id\":\"lsec0\"," | |
| "\"cbitpos\":51,\"reduced-phys-bits\":1,\"policy\":196608}\'"; | |
| *cgs = "confidential-guest-support=lsec0"; | |
| /* | |
| * The following two IGVM files can be built from the source | |
| * present in https://gitlab.com/anisinha/virt-firmware-rs . | |
| * Typing 'make' from the top of this repository will build the | |
| * IGVM files for both confidential and | |
| * non-confidential tests. The IGVM files for the non-coco | |
| * case has been checked-in into the QEMU repository for | |
| * convenience and easy CI pipeline testing. | |
| */ | |
| *igvm_f = "tests/data/igvm/snptest.igvm"; /* prints 'hello world' */ | |
| *igvm_init = "tests/data/igvm/snptest-nohello.igvm"; | |
| } else { | |
| *igvm_f = "tests/data/igvm/hello.igvm"; | |
| *igvm_init = "tests/data/igvm/qemuinit.igvm"; | |
| *snp = ""; | |
| *cgs = ""; | |
| } | |
| return; | |
| } | |
| static void set_expected_out(const char **exp_out, const char **exp_out2, | |
| const char **exp_out3) | |
| { | |
| *exp_out = "Hello world!"; | |
| *exp_out2 = "Test succeeded!"; | |
| *exp_out3 = "boot process complete with initial igvm"; | |
| return; | |
| } | |
| static QOSState *set_qemu_args(const char *cgs, const char *tp, char *serialf, | |
| const char *igvm_init, const char *snp) | |
| { | |
| QOSState *qs; | |
| if (tp) { | |
| qs = qtest_pc_boot("-machine q35,igvm-cfg=igvm0,%s -m 1G -accel kvm " | |
| "-device vm-launch-update %s " | |
| "-chardev file,id=serial0,path=%s " | |
| "-serial chardev:serial0 " | |
| "-object igvm-cfg,id=igvm0,file=%s %s", | |
| cgs, tp, serialf, igvm_init, snp); | |
| } else { | |
| qs = qtest_pc_boot("-machine q35,igvm-cfg=igvm0,%s -m 1G -accel kvm " | |
| "-device vm-launch-update " | |
| "-chardev file,id=serial0,path=%s " | |
| "-serial chardev:serial0 " | |
| "-object igvm-cfg,id=igvm0,file=%s %s", | |
| cgs, serialf, igvm_init, snp); | |
| } | |
| return qs; | |
| } | |
| static void test_load_igvm(void) | |
| { | |
| const char *igvm_f; | |
| const char *igvm_init; | |
| int ser_fd; | |
| g_autofree void *igvm_blob = NULL; | |
| g_autofree char *serialtmp = NULL; | |
| const char *exp_out; | |
| const char *exp_out2; | |
| const char *exp_out3; | |
| const char *tracepoints = "--trace memory_region_finalize " | |
| "--trace qigvm_cleanup_memory -D /tmp/qemu-debug.log "; | |
| const char *snp, *cgs; | |
| uint64_t gaddr; | |
| size_t igvm_sz; | |
| size_t filesize; | |
| QFWCFG *fw_cfg; | |
| QOSState *qs; | |
| VMLaunchUpdate launch_update; | |
| if (!trace) { | |
| tracepoints = ""; | |
| } | |
| if (!qtest_has_machine("q35")) { | |
| g_test_skip("q35 machine not available"); | |
| return; | |
| } | |
| if (!qtest_has_accel("kvm")) { | |
| g_test_skip("No KVM accelerator available"); | |
| return; | |
| } | |
| if (!qtest_has_device("vm-launch-update")) { | |
| g_test_skip("Device vm-launch-update is not available"); | |
| return; | |
| } | |
| set_test_params(&igvm_f, &igvm_init, &snp, &cgs); | |
| set_expected_out(&exp_out, &exp_out2, &exp_out3); | |
| if (!g_file_test(igvm_f, G_FILE_TEST_EXISTS) || | |
| !g_file_test(igvm_init, G_FILE_TEST_EXISTS)) { | |
| g_test_skip("igvm file bundle(s) does not exist!"); | |
| return; | |
| } | |
| ser_fd = g_file_open_tmp("launchupdate-qtest-serial-sXXXXXX", | |
| &serialtmp, NULL); | |
| g_assert_true(ser_fd != -1); | |
| if (debug) { | |
| fprintf(stderr, "serial console file is %s\n", serialtmp); | |
| } | |
| qs = set_qemu_args(cgs, tracepoints, serialtmp, igvm_init, snp); | |
| fw_cfg = pc_fw_cfg_init(qs->qts); | |
| if (debug) { | |
| fprintf(stderr, "target endianness: %s\n", | |
| qtest_big_endian(qs->qts) ? "big" : "little"); | |
| } | |
| /* exp_out3 should be printed once from initial boot */ | |
| g_assert_true(wait_for_match(serialtmp, exp_out3, WAIT_SEC, 1) == 0); | |
| if (debug) { | |
| fprintf(stderr, "initially booted with host igvm\n"); | |
| } | |
| g_assert_true(load_image(igvm_f, &igvm_blob, &igvm_sz) == igvm_sz); | |
| /* create a data buffer in guest memory */ | |
| gaddr = guest_alloc(&qs->alloc, igvm_sz); | |
| if (debug) { | |
| fprintf(stderr, "guest paddr: %"PRIx64 " igvm size: %lu\n", | |
| gaddr, igvm_sz); | |
| } | |
| if (debug) { | |
| fprintf(stderr, "writing igvm file into the guest memory\n"); | |
| } | |
| qtest_bufwrite(qs->qts, gaddr, igvm_blob, igvm_sz); | |
| if (debug) { | |
| fprintf(stderr, | |
| "tell hypervisor where igvm is loaded in guest memory\n"); | |
| } | |
| /* now tell hypervisor where we loaded the bios */ | |
| memset(&launch_update, 0, sizeof(launch_update)); | |
| launch_update.fw_image_size = cpu_to_le64(igvm_sz); | |
| launch_update.fw_image_addr = cpu_to_le64(gaddr); | |
| launch_update.control |= VM_LAUNCHUPDATE_FORMAT_IGVM; | |
| filesize = qfw_cfg_write_file(fw_cfg, qs, FILE_VMLAUNCHUPDATE, | |
| &launch_update, sizeof(launch_update)); | |
| g_assert_cmpint(filesize, ==, sizeof(launch_update)); | |
| if (debug) { | |
| fprintf(stderr, "resetting the virtual machine now\n"); | |
| } | |
| qtest_system_reset(qs->qts); | |
| /* expected string should be printed on the console */ | |
| g_assert_true(wait_for_match(serialtmp, exp_out, WAIT_SEC, 1) == 0); | |
| g_assert_true(wait_for_match(serialtmp, exp_out2, WAIT_SEC, 1) == 0); | |
| if (debug) { | |
| fprintf(stderr, "hello world found on console\n"); | |
| } | |
| /* check if VM_LAUNCHUPDATE_CTL_HOST_IGVM function works */ | |
| /* set only VM_LAUNCHUPDATE_CTL_HOST_IGVM control without IGVM bundle */ | |
| memset(&launch_update, 0, sizeof(launch_update)); | |
| launch_update.control |= VM_LAUNCHUPDATE_CTL_HOST_IGVM; | |
| filesize = qfw_cfg_write_file(fw_cfg, qs, FILE_VMLAUNCHUPDATE, | |
| &launch_update, sizeof(launch_update)); | |
| g_assert_cmpint(filesize, ==, sizeof(launch_update)); | |
| /* now reset the guest */ | |
| if (debug) { | |
| fprintf(stderr, | |
| "resetting again in order to restore host provided IGVM\n"); | |
| } | |
| qtest_system_reset(qs->qts); | |
| /* | |
| * exp_out3 should be printed twice, once from initial boot, | |
| * once from restoring host igvm. | |
| */ | |
| g_assert_true(wait_for_match(serialtmp, exp_out3, WAIT_SEC, 2) == 0); | |
| if (debug) { | |
| fprintf(stderr, "booted with host igvm again\n"); | |
| } | |
| close(ser_fd); | |
| guest_free(&qs->alloc, gaddr); | |
| pc_fw_cfg_uninit(fw_cfg); | |
| /* qtest_quit() kils QEMU, first by sending SIGTERM, then SIGKILL */ | |
| qtest_quit(qs->qts); | |
| } | |
| static void test_set_ctrl_once_and_reset_to_host_igvm(void) | |
| { | |
| const char *igvm_f; | |
| const char *igvm_init; | |
| int ser_fd; | |
| g_autofree void *igvm_blob = NULL; | |
| g_autofree char *serialtmp = NULL; | |
| const char *exp_out; | |
| const char *exp_out2; | |
| const char *exp_out3; | |
| const char *snp, *cgs; | |
| uint64_t gaddr; | |
| size_t igvm_sz; | |
| size_t filesize; | |
| QFWCFG *fw_cfg; | |
| QOSState *qs; | |
| VMLaunchUpdate launch_update; | |
| if (!qtest_has_machine("q35")) { | |
| g_test_skip("q35 machine not available"); | |
| return; | |
| } | |
| if (!qtest_has_accel("kvm")) { | |
| g_test_skip("No KVM accelerator available"); | |
| return; | |
| } | |
| if (!qtest_has_device("vm-launch-update")) { | |
| g_test_skip("Device vm-launch-update is not available"); | |
| return; | |
| } | |
| set_test_params(&igvm_f, &igvm_init, &snp, &cgs); | |
| set_expected_out(&exp_out, &exp_out2, &exp_out3); | |
| if (!g_file_test(igvm_f, G_FILE_TEST_EXISTS) || | |
| !g_file_test(igvm_init, G_FILE_TEST_EXISTS)) { | |
| g_test_skip("igvm file bundle(s) does not exist!"); | |
| return; | |
| } | |
| ser_fd = g_file_open_tmp("launchupdate-qtest-serial-sXXXXXX", | |
| &serialtmp, NULL); | |
| g_assert_true(ser_fd != -1); | |
| if (debug) { | |
| fprintf(stderr, "serial console file is %s\n", serialtmp); | |
| } | |
| qs = set_qemu_args(cgs, NULL, serialtmp, igvm_init, snp); | |
| fw_cfg = pc_fw_cfg_init(qs->qts); | |
| g_assert_true(wait_for_match(serialtmp, exp_out3, WAIT_SEC, 1) == 0); | |
| if (debug) { | |
| fprintf(stderr, "initially booted with host igvm\n"); | |
| } | |
| g_assert_true(load_image(igvm_f, &igvm_blob, &igvm_sz) == igvm_sz); | |
| /* create a data buffer in guest memory */ | |
| gaddr = guest_alloc(&qs->alloc, igvm_sz); | |
| if (debug) { | |
| fprintf(stderr, "guest paddr: %"PRIx64 " igvm size: %lu\n", | |
| gaddr, igvm_sz); | |
| } | |
| if (debug) { | |
| fprintf(stderr, "writing igvm file into the guest memory\n"); | |
| } | |
| qtest_bufwrite(qs->qts, gaddr, igvm_blob, igvm_sz); | |
| if (debug) { | |
| fprintf(stderr, | |
| "tell hypervisor where igvm is loaded in guest memory\n"); | |
| } | |
| /* now tell hypervisor where we loaded the bios */ | |
| memset(&launch_update, 0, sizeof(launch_update)); | |
| launch_update.fw_image_size = cpu_to_le64(igvm_sz); | |
| launch_update.fw_image_addr = cpu_to_le64(gaddr); | |
| /* set both host ctrl and format_igvm ctrl once */ | |
| launch_update.control |= VM_LAUNCHUPDATE_FORMAT_IGVM; | |
| launch_update.control |= VM_LAUNCHUPDATE_CTL_HOST_IGVM; | |
| filesize = qfw_cfg_write_file(fw_cfg, qs, FILE_VMLAUNCHUPDATE, | |
| &launch_update, sizeof(launch_update)); | |
| g_assert_cmpint(filesize, ==, sizeof(launch_update)); | |
| if (debug) { | |
| fprintf(stderr, "resetting the virtual machine. This should load " | |
| "user provided igvm.\n"); | |
| } | |
| qtest_system_reset(qs->qts); | |
| /* expected string should be printed on the console */ | |
| g_assert_true(wait_for_match(serialtmp, exp_out, WAIT_SEC, 1) == 0); | |
| g_assert_true(wait_for_match(serialtmp, exp_out2, WAIT_SEC, 1) == 0); | |
| if (debug) { | |
| fprintf(stderr, "hello world found on console\n"); | |
| fprintf(stderr, "Now resetting again in order to reset to host igvm\n"); | |
| } | |
| qtest_system_reset(qs->qts); | |
| /* | |
| * exp_out3 should be printed twice, once from initial boot, | |
| * once from restoring host igvm. | |
| */ | |
| g_assert_true(wait_for_match(serialtmp, exp_out3, WAIT_SEC, 2) == 0); | |
| if (debug) { | |
| fprintf(stderr, "booted with host igvm\n"); | |
| } | |
| close(ser_fd); | |
| guest_free(&qs->alloc, gaddr); | |
| pc_fw_cfg_uninit(fw_cfg); | |
| /* qtest_quit() kils QEMU, first by sending SIGTERM, then SIGKILL */ | |
| qtest_quit(qs->qts); | |
| } | |
| int main(int argc, char **argv) | |
| { | |
| const char *arch = qtest_get_arch(); | |
| g_test_init(&argc, &argv, NULL); | |
| if (strcmp(arch, "x86_64")) { | |
| g_test_skip("vmlaunchupdate tests are only available on x86_64\n"); | |
| return 0; | |
| } | |
| g_test_add_func("/vm-launch-update/cap", test_vm_launch_update_capability); | |
| g_test_add_func("/vm-launch-update/disabled", | |
| test_vm_launch_update_disable); | |
| g_test_add_func("/vm-launch-update/errorcheck", check_error); | |
| g_test_add_func("/vm-launch-update/load_igvm", | |
| test_load_igvm); | |
| g_test_add_func("/vm-launch-update/ctrl_set_once", | |
| test_set_ctrl_once_and_reset_to_host_igvm); | |
| if (getenv("LAUNCHUPDATE_DEBUG")) { | |
| debug = true; | |
| } | |
| if (getenv("LAUNCHUPDATE_TRACE")) { | |
| trace = true; | |
| } | |
| if (getenv("COCO")) { | |
| confidential = true; | |
| } | |
| return g_test_run(); | |
| } |