qemu/tests/qtest/launchupdate-test.c
Ani Sinha 0b70c6ce7b Add functional and unit tests for the vm-launch-update device
This patchset adds functional and unit tests that exercize various functions
and behaviors of vm-launch-update device. It uses the IGVM files that were
introduced in the previous patch for exercizing the hypervisor interface.

CC: Alex Graf <graf@amazon.com>
CC: Gerd Hoffman <kraxel@redhat.com>
Reviewed-by: Alexander Graf <graf@amazon.com>
Signed-off-by: Ani Sinha <anisinha@redhat.com>
Message-ID: <20260817142010.80693-11-anisinha@redhat.com>
Signed-off-by: Gerd Hoffmann <kraxel@redhat.com>
2026-08-20 16:40:33 +02:00

625 lines
19 KiB
C

/*
* 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();
}