igvm + vmlaunchupdate: allow guests supply their own igvm firmware images.

-----BEGIN PGP SIGNATURE-----
 
 iQIzBAABCgAdFiEEoDKM/7k6F6eZAf59TLbY7tPocTgFAmqIeGcACgkQTLbY7tPo
 cTi6eA/+LTgcwLrRIRlZBJ1ExwYBAVeeI2AwfoVvL7LrNrVTeW5O3HBkkd5iW3u1
 O5GGabV/8dQsrt3cTbQ6Tj1FuTaAFluNgykok4uCb9kavji/WQofkBgoek6PmDBi
 4CPwseO12eh9i/OV1yyCRdKqBuUjwnk1LbJIaBRDhz+AOhvTrPZYtxZMC1XZUY/4
 QPTHzo4lDyRphdq/puXe+Z4WPRMpt0tEUzSq1ev57W2Pwu3Rrf6BNM5X+uH8vTXQ
 3OwzmpNDsJusMKpo0/+gc1pGuLrNSSVWfbrEzsKot6jUg3hfkqmonTEFT+ArZNSn
 cqBCulWVlopJttRcnbUUSdjTaoBHIfS2yX2w10K2eVX7s2DbXy49GERhlfC23BHi
 zMrBQ0JGU4njdZXobFnR8HtCqwAlaTgFHiTHIjk4+IZHRPHUCAOVAIb0rkCeFSX3
 qgE1vHJhEBHLIsCv5F6Kb3aUbTiqWSzfFR67FRZctTzJp0nvJ06EiTlKURDk9CyG
 krInt79qeTHgNu8zNTDwoy8WbHlbDepv0aL/S+liHcE8Drvr5X0pWA0vMWE6mgvj
 Ibd8BEpw6VbLrGe/cz0eLRdu2/6tIDEKYmlAReuRX9ifP3kT41fT9aF/h9BOuJae
 zKWRSDgNfPB6MNf2r/6jvOHNCVaaG69jd0HJH/9cbzxHqinMe54=
 =9hRx
 -----END PGP SIGNATURE-----

Merge tag 'igvm-20260821-pull-request' of https://gitlab.com/kraxel/qemu into staging

igvm + vmlaunchupdate: allow guests supply their own igvm firmware images.

# -----BEGIN PGP SIGNATURE-----
#
# iQIzBAABCgAdFiEEoDKM/7k6F6eZAf59TLbY7tPocTgFAmqIeGcACgkQTLbY7tPo
# cTi6eA/+LTgcwLrRIRlZBJ1ExwYBAVeeI2AwfoVvL7LrNrVTeW5O3HBkkd5iW3u1
# O5GGabV/8dQsrt3cTbQ6Tj1FuTaAFluNgykok4uCb9kavji/WQofkBgoek6PmDBi
# 4CPwseO12eh9i/OV1yyCRdKqBuUjwnk1LbJIaBRDhz+AOhvTrPZYtxZMC1XZUY/4
# QPTHzo4lDyRphdq/puXe+Z4WPRMpt0tEUzSq1ev57W2Pwu3Rrf6BNM5X+uH8vTXQ
# 3OwzmpNDsJusMKpo0/+gc1pGuLrNSSVWfbrEzsKot6jUg3hfkqmonTEFT+ArZNSn
# cqBCulWVlopJttRcnbUUSdjTaoBHIfS2yX2w10K2eVX7s2DbXy49GERhlfC23BHi
# zMrBQ0JGU4njdZXobFnR8HtCqwAlaTgFHiTHIjk4+IZHRPHUCAOVAIb0rkCeFSX3
# qgE1vHJhEBHLIsCv5F6Kb3aUbTiqWSzfFR67FRZctTzJp0nvJ06EiTlKURDk9CyG
# krInt79qeTHgNu8zNTDwoy8WbHlbDepv0aL/S+liHcE8Drvr5X0pWA0vMWE6mgvj
# Ibd8BEpw6VbLrGe/cz0eLRdu2/6tIDEKYmlAReuRX9ifP3kT41fT9aF/h9BOuJae
# zKWRSDgNfPB6MNf2r/6jvOHNCVaaG69jd0HJH/9cbzxHqinMe54=
# =9hRx
# -----END PGP SIGNATURE-----
# gpg: Signature made Fri 21 Aug 2026 09:10:15 AM PDT
# gpg:                using RSA key A0328CFFB93A17A79901FE7D4CB6D8EED3E87138
# gpg: Good signature from "Gerd Hoffmann (work) <kraxel@redhat.com>" [unknown]
# gpg:                 aka "Gerd Hoffmann <gerd@kraxel.org>" [unknown]
# gpg:                 aka "Gerd Hoffmann (private) <kraxel@gmail.com>" [unknown]
# gpg: WARNING: This key is not certified with a trusted signature!
# gpg:          There is no indication that the signature belongs to the owner.
# Primary key fingerprint: A032 8CFF B93A 17A7 9901  FE7D 4CB6 D8EE D3E8 7138

* tag 'igvm-20260821-pull-request' of https://gitlab.com/kraxel/qemu:
  Update MAINTAINERS
  Add functional and unit tests for the vm-launch-update device
  tests/qtest: Add small igvm files for testing purpose
  docs/spec: Add a specification document for vm-launch-update device
  hw/misc/vmlaunchupdate: Introduce hypervisor fw-cfg interface support
  hw/misc/vmlaunchupdate: add api header
  backends/igvm: add a tracepoint for qigvm_cleanup_memory
  system/memory: add a tracepoint for memory_region_finalize
  igvm: cleanup memory regions
  igvm: track memory regions
  igvm: store IgvmCfg pointer in QIgvm
  MAINTAINERS: elevating myself to be a maintainer for igvm

Signed-off-by: Richard Henderson <richard.henderson@linaro.org>
This commit is contained in:
Richard Henderson 2026-08-21 10:39:31 -07:00
commit eea8fe61b8
24 changed files with 1535 additions and 34 deletions

View file

@ -2852,6 +2852,19 @@ F: include/hw/acpi/vmgenid.h
F: docs/specs/vmgenid.rst
F: tests/qtest/vmgenid-test.c
VM Launch Update
M: Ani Sinha <anisinha@redhat.com>
M: Gerd Hoffman <kraxel@redhat.com>
S: Maintained
F: hw/misc/vmlaunchupdate.c
F: include/hw/misc/vmlaunchupdate.h
F: include/standard-headers/misc/vmlaunchupdate.h
F: docs/specs/vmlaunchupdate.rst
F: tests/functional/aarch64/test_vm_launch_update_aarch.py
F: tests/functional/x86_64/test_vm_launch_update.py
F: tests/qtest/launchupdate-test.c
F: tests/data/igvm/*
LED
M: Philippe Mathieu-Daudé <philmd@mailo.com>
S: Maintained
@ -4119,7 +4132,7 @@ F: tests/uefi-test-tools/
IGVM Firmware
M: Gerd Hoffmann <kraxel@redhat.com>
M: Stefano Garzarella <sgarzare@redhat.com>
R: Ani Sinha <anisinha@redhat.com>
M: Ani Sinha <anisinha@redhat.com>
S: Maintained
F: backends/igvm*.c
F: docs/system/igvm.rst

View file

@ -52,6 +52,8 @@ static void igvm_reset_hold(Object *obj, ResetType type)
trace_igvm_reset_hold(type);
/* cleanup existing memory regions first */
qigvm_cleanup_memory(igvm);
qigvm_process_file(igvm, ms, false, &error_fatal);
}
@ -65,6 +67,7 @@ static void igvm_complete(UserCreatable *uc, Error **errp)
IgvmCfg *igvm = IGVM_CFG(uc);
igvm->file = qigvm_file_init(igvm->filename, errp);
QTAILQ_INIT(&igvm->memory_regions);
}
OBJECT_DEFINE_TYPE_WITH_INTERFACES(IgvmCfg, igvm_cfg, IGVM_CFG, OBJECT,

View file

@ -14,6 +14,7 @@
#include "qapi/error.h"
#include "qemu/error-report.h"
#include "qemu/target-info-qapi.h"
#include "migration/vmstate.h"
#include "system/igvm.h"
#include "system/igvm-cfg.h"
#include "system/igvm-internal.h"
@ -178,7 +179,8 @@ static int qigvm_handler(QIgvm *ctx, IgvmVariableHeaderType raw_type,
if (handlers[handler].type != type) {
continue;
}
header_handle = igvm_get_header(ctx->file, handlers[handler].section,
header_handle = igvm_get_header(ctx->cfg->file,
handlers[handler].section,
ctx->current_header_index);
if (header_handle < 0) {
error_setg(
@ -187,7 +189,7 @@ static int qigvm_handler(QIgvm *ctx, IgvmVariableHeaderType raw_type,
(int)header_handle);
return -1;
}
header_data = igvm_get_buffer(ctx->file, header_handle);
header_data = igvm_get_buffer(ctx->cfg->file, header_handle);
if (header_data != NULL) {
header_data += sizeof(IGVM_VHS_VARIABLE_HEADER);
result = handlers[handler].handler(ctx, header_data, errp);
@ -198,7 +200,7 @@ static int qigvm_handler(QIgvm *ctx, IgvmVariableHeaderType raw_type,
header_handle, type);
result = -1;
}
igvm_free_buffer(ctx->file, header_handle);
igvm_free_buffer(ctx->cfg->file, header_handle);
return result;
}
@ -219,7 +221,7 @@ static void *qigvm_prepare_memory(QIgvm *ctx, uint64_t addr, uint64_t size,
int region_identifier, Error **errp)
{
ERRP_GUARD();
MemoryRegion *igvm_pages = NULL;
IgvmMemoryRegion *imr = NULL;
Int128 gpa_region_size;
MemoryRegionSection mrs =
memory_region_find(get_system_memory(), addr, size);
@ -253,23 +255,27 @@ static void *qigvm_prepare_memory(QIgvm *ctx, uint64_t addr, uint64_t size,
*/
g_autofree char *region_name =
g_strdup_printf("igvm.%X", region_identifier);
igvm_pages = g_new0(MemoryRegion, 1);
imr = g_new0(IgvmMemoryRegion, 1);
imr->mr = g_new0(MemoryRegion, 1);
if (ctx->machine_state->cgs &&
ctx->machine_state->cgs->require_guest_memfd) {
if (!memory_region_init_ram_guest_memfd(igvm_pages, NULL,
if (!memory_region_init_ram_guest_memfd(imr->mr, NULL,
region_name, size, errp)) {
g_free(igvm_pages);
g_free(imr->mr);
g_free(imr);
return NULL;
}
} else {
if (!memory_region_init_ram(igvm_pages, NULL, region_name, size,
if (!memory_region_init_ram(imr->mr, NULL, region_name, size,
errp)) {
g_free(igvm_pages);
g_free(imr->mr);
g_free(imr);
return NULL;
}
}
memory_region_add_subregion(get_system_memory(), addr, igvm_pages);
return memory_region_get_ram_ptr(igvm_pages);
memory_region_add_subregion(get_system_memory(), addr, imr->mr);
QTAILQ_INSERT_TAIL(&ctx->cfg->memory_regions, imr, next);
return memory_region_get_ram_ptr(imr->mr);
}
}
@ -344,7 +350,8 @@ static int qigvm_process_mem_region(QIgvm *ctx, unsigned start_index,
for (page_index = 0; page_index < page_count; page_index++) {
data_handle = igvm_get_header_data(
ctx->file, IGVM_HEADER_SECTION_DIRECTIVE, page_index + start_index);
ctx->cfg->file, IGVM_HEADER_SECTION_DIRECTIVE,
page_index + start_index);
if (data_handle == IGVMAPI_NO_DATA) {
/* No data indicates a zero page */
memset(&region[page_index * page_size], 0, page_size);
@ -357,7 +364,7 @@ static int qigvm_process_mem_region(QIgvm *ctx, unsigned start_index,
return -1;
} else {
zero = false;
data_size = igvm_get_buffer_size(ctx->file, data_handle);
data_size = igvm_get_buffer_size(ctx->cfg->file, data_handle);
if (data_size < page_size) {
memset(&region[page_index * page_size], 0, page_size);
} else if (data_size > page_size) {
@ -367,14 +374,14 @@ static int qigvm_process_mem_region(QIgvm *ctx, unsigned start_index,
page_index + start_index);
return -1;
}
data = igvm_get_buffer(ctx->file, data_handle);
data = igvm_get_buffer(ctx->cfg->file, data_handle);
if (data == NULL) {
error_setg(errp, "IGVM: No buffer for handle %d", data_handle);
igvm_free_buffer(ctx->file, data_handle);
igvm_free_buffer(ctx->cfg->file, data_handle);
return -1;
}
memcpy(&region[page_index * page_size], data, data_size);
igvm_free_buffer(ctx->file, data_handle);
igvm_free_buffer(ctx->cfg->file, data_handle);
}
}
@ -411,7 +418,8 @@ static int qigvm_process_mem_page(QIgvm *ctx,
ctx->region_start = page_data->gpa;
ctx->region_start_index = ctx->current_header_index;
} else {
if (!qigvm_page_attrs_equal(ctx->file, ctx->current_header_index,
if (!qigvm_page_attrs_equal(ctx->cfg->file,
ctx->current_header_index,
page_data,
&ctx->region_prev_page_data) ||
((ctx->region_prev_page_data.gpa +
@ -474,7 +482,8 @@ static int qigvm_directive_vp_context(QIgvm *ctx, const uint8_t *header_data,
return 0;
}
data_handle = igvm_get_header_data(ctx->file, IGVM_HEADER_SECTION_DIRECTIVE,
data_handle = igvm_get_header_data(ctx->cfg->file,
IGVM_HEADER_SECTION_DIRECTIVE,
ctx->current_header_index);
if (data_handle < 0) {
error_setg(errp, "Invalid VP context in IGVM file. Error code: %X",
@ -482,7 +491,7 @@ static int qigvm_directive_vp_context(QIgvm *ctx, const uint8_t *header_data,
return -1;
}
data = (uint8_t *)igvm_get_buffer(ctx->file, data_handle);
data = (uint8_t *)igvm_get_buffer(ctx->cfg->file, data_handle);
if (data == NULL) {
error_setg(errp, "IGVM: No buffer for handle %d", data_handle);
result = -1;
@ -491,7 +500,8 @@ static int qigvm_directive_vp_context(QIgvm *ctx, const uint8_t *header_data,
if (ctx->machine_state->cgs) {
result = ctx->cgsc->set_guest_state(
vp_context->gpa, data, igvm_get_buffer_size(ctx->file, data_handle),
vp_context->gpa, data,
igvm_get_buffer_size(ctx->cfg->file, data_handle),
CGS_PAGE_TYPE_VMSA, vp_context->vp_index, errp);
} else if (target_arch() == SYS_EMU_TARGET_X86_64) {
result = qigvm_x86_set_vp_context(data, vp_context->vp_index, errp);
@ -504,7 +514,7 @@ static int qigvm_directive_vp_context(QIgvm *ctx, const uint8_t *header_data,
}
exit:
igvm_free_buffer(ctx->file, data_handle);
igvm_free_buffer(ctx->cfg->file, data_handle);
if (result < 0) {
return result;
}
@ -863,7 +873,8 @@ static int qigvm_supported_platform_compat_mask(QIgvm *ctx, Error **errp)
uint32_t compatibility_mask_sev_snp = 0;
uint32_t compatibility_mask = 0;
header_count = igvm_header_count(ctx->file, IGVM_HEADER_SECTION_PLATFORM);
header_count = igvm_header_count(ctx->cfg->file,
IGVM_HEADER_SECTION_PLATFORM);
if (header_count < 0) {
error_setg(errp,
"Invalid platform header count in IGVM file. Error code: %X",
@ -874,11 +885,11 @@ static int qigvm_supported_platform_compat_mask(QIgvm *ctx, Error **errp)
for (header_index = 0; header_index < (unsigned)header_count;
header_index++) {
IgvmVariableHeaderType typ = igvm_get_header_type(
ctx->file, IGVM_HEADER_SECTION_PLATFORM, header_index);
ctx->cfg->file, IGVM_HEADER_SECTION_PLATFORM, header_index);
typ = igvm_vht_type(typ);
if (typ == IGVM_VHT_SUPPORTED_PLATFORM) {
header_handle = igvm_get_header(
ctx->file, IGVM_HEADER_SECTION_PLATFORM, header_index);
ctx->cfg->file, IGVM_HEADER_SECTION_PLATFORM, header_index);
if (header_handle < 0) {
error_setg(errp,
"Invalid platform header in IGVM file. "
@ -887,11 +898,11 @@ static int qigvm_supported_platform_compat_mask(QIgvm *ctx, Error **errp)
return -1;
}
platform =
(IGVM_VHS_SUPPORTED_PLATFORM *)(igvm_get_buffer(ctx->file,
(IGVM_VHS_SUPPORTED_PLATFORM *)(igvm_get_buffer(ctx->cfg->file,
header_handle));
if (platform == NULL) {
error_setg(errp, "IGVM: No buffer for handle %d", header_handle);
igvm_free_buffer(ctx->file, header_handle);
igvm_free_buffer(ctx->cfg->file, header_handle);
return -1;
}
@ -922,7 +933,7 @@ static int qigvm_supported_platform_compat_mask(QIgvm *ctx, Error **errp)
} else if (platform->platform_type == IGVM_PLATFORM_TYPE_NATIVE) {
compatibility_mask = platform->compatibility_mask;
}
igvm_free_buffer(ctx->file, header_handle);
igvm_free_buffer(ctx->cfg->file, header_handle);
}
}
/* Choose the strongest supported isolation technology */
@ -999,7 +1010,7 @@ int qigvm_process_file(IgvmCfg *cfg, MachineState *machine_state,
error_setg(errp, "No IGVM file loaded.");
return -1;
}
ctx.file = cfg->file;
ctx.cfg = cfg;
trace_igvm_process_file(cfg->file, onlyVpContext);
ctx.machine_state = machine_state;
@ -1021,7 +1032,8 @@ int qigvm_process_file(IgvmCfg *cfg, MachineState *machine_state,
goto cleanup;
}
header_count = igvm_header_count(ctx.file, IGVM_HEADER_SECTION_DIRECTIVE);
header_count = igvm_header_count(ctx.cfg->file,
IGVM_HEADER_SECTION_DIRECTIVE);
if (header_count <= 0) {
error_setg(
errp, "Invalid directive header count in IGVM file. Error code: %X",
@ -1035,7 +1047,8 @@ int qigvm_process_file(IgvmCfg *cfg, MachineState *machine_state,
ctx.current_header_index < (unsigned)header_count;
ctx.current_header_index++) {
IgvmVariableHeaderType raw_type = igvm_get_header_type(
ctx.file, IGVM_HEADER_SECTION_DIRECTIVE, ctx.current_header_index);
ctx.cfg->file, IGVM_HEADER_SECTION_DIRECTIVE,
ctx.current_header_index);
if (!onlyVpContext || igvm_vht_type(raw_type) == IGVM_VHT_VP_CONTEXT) {
if (qigvm_handler(&ctx, raw_type, errp) < 0) {
goto cleanup_parameters;
@ -1053,7 +1066,7 @@ int qigvm_process_file(IgvmCfg *cfg, MachineState *machine_state,
}
header_count =
igvm_header_count(ctx.file, IGVM_HEADER_SECTION_INITIALIZATION);
igvm_header_count(ctx.cfg->file, IGVM_HEADER_SECTION_INITIALIZATION);
if (header_count < 0) {
error_setg(
errp,
@ -1066,7 +1079,8 @@ int qigvm_process_file(IgvmCfg *cfg, MachineState *machine_state,
ctx.current_header_index < (unsigned)header_count;
ctx.current_header_index++) {
IgvmVariableHeaderType type =
igvm_get_header_type(ctx.file, IGVM_HEADER_SECTION_INITIALIZATION,
igvm_get_header_type(ctx.cfg->file,
IGVM_HEADER_SECTION_INITIALIZATION,
ctx.current_header_index);
if (qigvm_handler(&ctx, type, errp) < 0) {
goto cleanup_parameters;
@ -1096,3 +1110,22 @@ cleanup_parameters:
cleanup:
return retval;
}
/*
* cleanup any memory regions created by qigvm_prepare_memory()
*/
void qigvm_cleanup_memory(IgvmCfg *cfg)
{
IgvmMemoryRegion *imr, *tmp;
QTAILQ_FOREACH_SAFE(imr, &cfg->memory_regions, next, tmp)
{
trace_qigvm_cleanup_memory(imr->mr->name);
memory_region_del_subregion(get_system_memory(), imr->mr);
vmstate_unregister_ram(imr->mr, NULL);
QTAILQ_REMOVE(&cfg->memory_regions, imr, next);
/* this triggers MemoryRegion cleanup */
object_unparent(OBJECT(imr->mr));
g_free(imr);
}
}

View file

@ -33,3 +33,4 @@ igvm_reset_hold(int type) "type=%u"
igvm_reset_exit(int type) "type=%u"
igvm_file_loaded(const char *fn, int32_t handle) "fn=%s, handle=0x%x"
igvm_process_file(int32_t handle, bool context_only) "handle=0x%x context-only=%d"
qigvm_cleanup_memory(const char* name) "freeing mr %s"

View file

@ -34,6 +34,7 @@ guest hardware that is specific to QEMU.
virt-ctlr
vmcoreinfo
vmgenid
vmlaunchupdate
rapl-msr
rocker
riscv-iommu

View file

@ -0,0 +1,198 @@
.. SPDX-License-Identifier: GPL-2.0-or-later
VMLAUNCHUPDATE Interface Specification
######################################
Introduction
************
``VmLaunchUpdate`` is an extension to ``fw-cfg`` that allows guests to replace
boot state in their virtual machine using IGVM file container. Through a combination
of this ``fw-cfg`` hypervisor interface, an IGVM file containing specific directives
and with hypervisor stack knowledge, guests can deterministically replace the launch
state for guests. This is useful for environments like SEV-SNP where the
launch payload becomes the launch digest. Guests can use vm-launch-update device to
provide a measured, full guest payload (BIOS image, kernel, initramfs, kernel
command line) to the virtual machine which enables them to easily reason about
integrity of the resulting system.
It is also to be noted that this mechanism currently works only when the guest was
already started with an IGVM file defining its initial launch state. Subsequent
guest resets will use the launch state as defined in the guest provided IGVM file,
not the file with which the guest was initially started. If the guest was not started
with IGVM, writing a new bundle through the ``fw-cfg`` interface has no effect.
For more information, please see the `KVM Forum 2024 presentation <KVMFORUM_>`__
about this work.
.. _KVMFORUM: https://www.youtube.com/watch?v=VCMBxU6tAto
Base Requirements
*****************
#. **fw-cfg**:
The target system must provide a ``fw-cfg`` interface. For x86 based
environments, this ``fw-cfg`` interface must be accessible through PIO ports
0x510 and 0x511. The ``fw-cfg`` interface does not need to be announced as part
of system device tables such as DSDT. The ``fw-cfg`` interface must support the
DMA interface. It may only support the DMA interface for write operations.
#. **IGVM support**:
The hypervisor must provide support for parsing and executing the IGVM file bundle.
#. **Confidential guests**:
For confidential guests, the hypervisor must support guest reset. Otherwise, the new
boot state provided through IGVM will not be applied.
The Fw-cfg File
***************
Guests drive vmlaunchupdate through special ``fw-cfg`` files that control its flow
followed by a standard system reset operation. When the ``vm-launch-update`` device
is available, it provides the following ``fw-cfg`` file:
* ``etc/vmlaunchupdate`` - It exposes a structure of the following type, all in
little-endian format:
.. code-block:: c
:linenos:
typedef struct {
uint16_t version;
uint16_t status;
uint32_t _padding;
uint64_t capabilities;
uint64_t control;
uint64_t fw_image_addr;
uint64_t fw_image_size;
uint64_t opaque_addr;
uint64_t opaque_size;
} VMLaunchUpdate;
Currently, the ``version`` number (line 2 above) is initialized to the value ``1``.
Only IGVM files are supported at present. The ``capabilities`` (line 7) and ``control`` (line 8) both support
the following single value:
* ``VM_LAUNCHUPDATE_FORMAT_IGVM``
This value is used by the hypervisor to indicate that only IGVM container files are supported.
This is set as a part of ``capabilities`` parameter (line 7) in the above structure. This same value
is passed by the guest to the hypervisor in the ``control`` parameter (line 8) in the above structure
to indicate that the guest passed IGVM file in memory to the hypervisor. The starting guest physical
address of the IGVM file in memory is specified in ``fw_image_addr`` and it's length is specified in
``fw_image_size`` by the guest. If any other value is passed by the guest in the ``control`` parameter,
the write is ignored by the hypervisor.
Following ``control`` parameters are supported:
* ``VM_LAUNCHUPDATE_CTL_DISABLE``
This value is set in the ``control`` parameter by the guest in order to disable this ``fw-cfg``
hypervisor interface from further updating the guest launch state with a new IGVM file.
* ``VM_LAUNCHUPDATE_CTL_HOST_IGVM``
This value is set in the ``control`` parameter by the guest in order to send request to the
hypervisor to initialize the guest using the original host provided IGVM file.
It is useful if the guest wanted to update the UKIs present in the ESP and upon
reset, use one of the updated UKIs present there. If the guest passed addresses in memory
where its own IGVM file is loaded (see below) while also setting this control value, the next
reset will load the guest provided IGVM file and a subsequent second reset will restore the original
host IGVM. If the guest did not provide any addresses of its own IGVM (the address values are
cleared) while setting this control parameter, the immediate next guest reset will load the
original host provided IGVM file.
The combination of the above two ctl interfaces work as
follows:
A) ``CTL_HOST_IGVM`` = off ``CTL_DISABLE`` = off
Supplied IGVM file replaces the firmware permanently. Updating the
firmware again is possible.
B) ``CTL_HOST_IGVM`` = off ``CTL_DISABLE`` = on
Supplied IGVM file replaces the firmware permanently. Updating the
firmware again is not possible.
C) ``CTL_HOST_IGVM`` = on ``CTL_DISABLE`` = off
Supplied IGVM file replaces the firmware for one reset. Resetting
again will switch back to the original firmware. Updating the
firmware again is possible.
D) ``CTL_HOST_IGVM`` = on ``CTL_DISABLE`` = on
Supplied IGVM file replaces the firmware for one reset. Resetting
again will switch back to the original firmware. Updating the
firmware again is NOT possible.
``fw_image_addr`` (line 10) is the base guest physical address of the guest memory where the IGVM file of size
``fw_image_size`` (line 11) is loaded. ``opaque_addr`` (line 13) and ``opaque_size`` (line 14) are used by
the guest for passing data across resets. The contents of this guest memory are preserved across the
reset. For confidential guests, this memory region must come from guest shared unencrypted memory.
``status`` (line 3) is written by the hypervisor and it indicates the result of the IGVM loading operation.
A success indicates status code 0. Otherwise a non-zero status code indicates failure. The nature of the
failure is indicated by the value of the code.
Triggering the Launch State Update using IGVM
*********************************************
To initiate the launch update process, the guest issues a standard system reset
operation through any of the means implemented by the machine model.
On a write to the ``etc/vmlaunchupdate`` interface, the hypervisor evaluates whether this
hypervisor interface is disabled. If it is, it ignores any writes to this ``fw-cfg`` file
by the guest. No updates to initial launch state is performed.
If the hypervisor interface is enabled, upon write to the ``etc/vmlaunchupdate`` interface,
the hypervisor parses the IGVM file bundle passed to it in memory, with starting guest physical
address at ``fw_image_addr`` and length ``fw_image_size``. If parsing is successful, it creates
a context handle to the IGVM file. If parsing and context loading is successful and there are no
errors, ``fw_image_addr`` and ``fw_image_size`` are cleared. The guest can check this in order
to determine if the IGVM was successfully parsed and the new context was loaded. If not, the
guest can throw error and abort rebooting to new IGVM boot state. Alternatively, the guest can
also check the ``status`` code from the ``fw-cfg`` file. A status code of 0 indicates success
of the operation. Non-zero status code indicates failure. Exact nature of the failure is
indicated by the value of the code. Currently, only two error values are supported:
* ``VM_LAUNCHUPDATE_LOAD_FAIL`` - defined as value 1 and is set when loading of the IGVM file failed.
* ``VM_LAUNCHUPDATE_NOT_IGVM_INIT`` - defined as value 2 and is set when the guest was not started with
IGVM file.
Upon guest reset, the hypervisor executes the IGVM bundle using
the context handle, setting the initial launch state of the guest accordingly.
If an invalid IGVM file is passed, parsing the file fails and the hypervisor ignores it
when ``fw-cfg`` files are written. In this case, the initial launch state
is not modified. If invalid addresses are passed, the hypervisor ignores them as well and no
new launch state is set.
The launch state update mechanism works both for confidential and non-confidential
guests. In confidential guests, as a part of the reset operation, all existing
guest shared memory (shared with the hypervisor) as well as the guest memory region
starting with ``opaque_addr`` and length ``opaque_size`` are preserved.
The reset causes recreation of the VM context which triggers a fresh
measurement of the replaced BIOS region and reset CPU state.
For non-confidential guests, there is no concept of guest private memory and all the existing
guest memory is preserved (this is the default behaviour today - QEMU does not reset/clear
guest memory upon reset).
In both confidential and non-confidential cases, CPU and device state are reset to
the reset states specified in IGVM. In confidential environments, the guest
always resumes operation in the highest privileged mode available to it (VMPL0 in SEV-SNP).
Closing Remarks
***************
The exact content of the memory region specified by starting address ``opaque_addr``
and length ``opaque_size`` is guest specific and is hypervisor agnostic. The hypervisor does
not care about the contents of this memory region. Therefore, it is not included in this
specification. As of writing this document, TDX guests on QEMU does not support IGVM.
Therefore, this mechanism cannot be used to change launch state of TDX guests.

View file

@ -164,6 +164,9 @@ specific_ss.add(when: 'CONFIG_MIPS_ITU', if_true: files('mips_itu.c'))
specific_ss.add(when: 'CONFIG_RISCV_MIPS_CMGCR', if_true: files('riscv_cmgcr.c'))
specific_ss.add(when: 'CONFIG_RISCV_MIPS_CPC', if_true: files('riscv_cpc.c'))
if igvm.found()
specific_ss.add(when: 'CONFIG_FW_CFG_DMA', if_true: files('vmlaunchupdate.c'))
endif
system_ss.add(when: 'CONFIG_SBSA_REF', if_true: files('sbsa_ec.c'))

View file

@ -442,3 +442,9 @@ iommu_testdev_dma_read(uint64_t gva, uint32_t len) "gva=0x%" PRIx64 " len=%u"
iommu_testdev_dma_verify(uint32_t expected, uint32_t actual) "expected=0x%x actual=0x%x"
iommu_testdev_dma_result(uint32_t result) "DMA completed result=0x%x"
iommu_testdev_dma_armed(bool armed) "armed=%d"
# vmlaunchupdate.c
launch_update_write(void) ""
vmlaunch_reset_enter(void) ""
vm_launchupdate_finalize(void) ""
restore_host_x86_igvm(void) ""

333
hw/misc/vmlaunchupdate.c Normal file
View file

@ -0,0 +1,333 @@
/*
* Guest driven VM launch component update (using IGVM) device
* For details and specification, please look at docs/specs/vmlaunchupdate.rst.
*
* Copyright (C) 2026 Red Hat, Inc.
*
* Authors: Ani Sinha <anisinha@redhat.com>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "qemu/osdep.h"
#include "qapi/error.h"
#include "qemu/module.h"
#include "system/physmem.h"
#include "system/reset.h"
#include "qemu/target-info-qapi.h"
#include "hw/nvram/fw_cfg.h"
#include "hw/core/qdev-properties.h"
#include "hw/i386/pc.h"
#include "exec/cpu-common.h"
#include "hw/misc/vmlaunchupdate.h"
#include "system/igvm.h"
#include "system/igvm-internal.h"
#include "qemu/error-report.h"
#include "trace.h"
/* returns NULL unless there is exactly one device */
static VMLaunchUpdateState *vm_launchupdate_find(void)
{
Object *o = object_resolve_path_type("", TYPE_VMLAUNCHUPDATE, NULL);
return o ? VMLAUNCHUPDATE(o) : NULL;
}
static bool vmlaunchupdate_supported(void)
{
return target_arch() == SYS_EMU_TARGET_X86_64;
}
static void init_vm_launch_update(VMLaunchUpdateState *s)
{
s->launch_update.capabilities = VM_LAUNCHUPDATE_FORMAT_IGVM;
s->launch_update.control = 0;
if (s->disabled) {
s->launch_update.control |= VM_LAUNCHUPDATE_CTL_DISABLE;
}
s->launch_update.version = VM_LAUNCHUPDATE_VERSION;
return;
}
static void clear_init_vm_launch_update(VMLaunchUpdateState *s)
{
memset(&s->launch_update, 0, sizeof(s->launch_update));
init_vm_launch_update(s);
}
static bool no_igvmcfg(X86MachineState *x86m)
{
IgvmCfg *igvmc;
if (!x86m) {
return true;
}
igvmc = x86m->igvm;
if (!igvmc) {
/* The VM was not started with an IGVM, bail */
info_report("guest was not initially started with IGVM, "
"not changing launch state.");
return true;
}
return false;
}
static int process_x86_igvm(VMLaunchUpdateState *s,
uint64_t fw_image_addr, uint64_t fw_image_size)
{
X86MachineState *x86machine = X86_MACHINE(qdev_get_machine());
IgvmCfg *igvmc = x86machine->igvm;
IgvmHandle igvm;
void *image_addr_ptr;
hwaddr len;
if (no_igvmcfg(x86machine)) {
return -2;
}
if (!fw_image_addr || !fw_image_size) {
return -1;
}
len = (hwaddr) fw_image_size;
image_addr_ptr = physical_memory_map((hwaddr) fw_image_addr,
(hwaddr *) &len, 0);
if (!image_addr_ptr || (len < fw_image_size)) {
warn_report("vmlaunchupdate: Invalid guest addresses.");
goto err;
}
igvm = igvm_new_from_binary(image_addr_ptr, fw_image_size);
if (igvm < 0) {
warn_report("vmlaunchupdate: Unable to parse IGVM file %"
PRIx64 ": %" PRIx64, fw_image_addr, fw_image_size);
goto err;
}
/* free previous file context */
if (igvmc->file >= 0) {
igvm_free(igvmc->file);
}
/* set new context */
igvmc->file = igvm;
physical_memory_unmap(image_addr_ptr, len, 0, 0);
info_report("vmlaunchupdate: new IGVM context set.");
return 0;
err:
if (image_addr_ptr) {
physical_memory_unmap(image_addr_ptr, len, 0, 0);
}
return -1;
}
static void restore_host_x86_igvm(void)
{
X86MachineState *x86machine = X86_MACHINE(qdev_get_machine());
IgvmCfg *igvmc = x86machine->igvm;
Error *errp = NULL;
if (no_igvmcfg(x86machine)) {
return;
}
/* free previous file context */
if (igvmc->file >= 0) {
igvm_free(igvmc->file);
}
info_report("restoring original host IGVM: %s", igvmc->filename);
igvmc->file = qigvm_file_init(igvmc->filename, &errp);
assert(!errp);
info_report("vmlaunchupdate: host IGVM context set.");
trace_restore_host_x86_igvm();
return;
}
static bool fw_address_cleared(VMLaunchUpdateState *s)
{
return !s->launch_update.fw_image_addr &&
!s->launch_update.fw_image_size;
}
static void launch_update_write(void *dev, off_t offset, size_t len)
{
VMLaunchUpdateState *s = VMLAUNCHUPDATE(dev);
uint64_t addr;
uint64_t size;
int rc;
s->launch_update.status = VM_LAUNCHUPDATE_SUCCESS;
if (s->disabled) {
goto end;
}
if (s->launch_update.control & VM_LAUNCHUPDATE_CTL_DISABLE) {
s->disabled = true;
goto end;
}
if (fw_address_cleared(s) &&
(s->launch_update.control & VM_LAUNCHUPDATE_CTL_HOST_IGVM)) {
/* restore host IGVM on immediate next reset */
s->host_igvm_on_reset = true;
goto end;
}
if (!(s->launch_update.control & VM_LAUNCHUPDATE_FORMAT_IGVM) &&
!fw_address_cleared(s)) {
/* at least one address provided but the format is not IGVM */
s->launch_update.status = VM_LAUNCHUPDATE_LOAD_FAIL;
goto end;
}
/* process guest provided IGVM image */
if (s->launch_update.control & VM_LAUNCHUPDATE_FORMAT_IGVM) {
if (target_arch() == SYS_EMU_TARGET_X86_64) {
addr = le64_to_cpu(s->launch_update.fw_image_addr);
size = le64_to_cpu(s->launch_update.fw_image_size);
rc = process_x86_igvm(s, addr, size);
if (rc < 0) {
switch (rc) {
case -2:
s->launch_update.status = VM_LAUNCHUPDATE_NOT_IGVM_INIT;
break;
default:
s->launch_update.status = VM_LAUNCHUPDATE_LOAD_FAIL;
}
goto end;
}
}
/* process other machines here when support is added */
}
/* clear the addresses */
s->launch_update.fw_image_addr = 0x0;
s->launch_update.fw_image_size = 0x0;
end:
trace_launch_update_write();
return;
}
static void launch_update_select(void *dev)
{
VMLaunchUpdateState *s = VMLAUNCHUPDATE(dev);
init_vm_launch_update(s);
}
static void vmlaunch_reset_enter(Object *obj, ResetType type)
{
VMLaunchUpdateState *s = VMLAUNCHUPDATE(obj);
if (target_arch() != SYS_EMU_TARGET_X86_64) {
return;
}
if (s->host_igvm_on_reset) {
restore_host_x86_igvm();
s->host_igvm_on_reset = false;
/* restoring host igvm enables the interface again */
s->disabled = false;
/* clear the host IGVM ctrl bit */
s->launch_update.control &= ~VM_LAUNCHUPDATE_CTL_HOST_IGVM;
}
if ((s->launch_update.control & VM_LAUNCHUPDATE_CTL_HOST_IGVM) &&
(s->launch_update.status == VM_LAUNCHUPDATE_SUCCESS)) {
info_report("vmlaunchupdate: next reset will use host igvm");
s->host_igvm_on_reset = true;
}
trace_vmlaunch_reset_enter();
}
static ResettableState *vmlaunch_reset_state(Object *obj)
{
VMLaunchUpdateState *s = VMLAUNCHUPDATE(obj);
return &s->reset_state;
}
static void vm_launchupdate_realize(DeviceState *dev, Error **errp)
{
VMLaunchUpdateState *s = VMLAUNCHUPDATE(dev);
FWCfgState *fw_cfg = fw_cfg_find();
/* multiple devices are not supported */
if (!vm_launchupdate_find()) {
error_setg(errp, "at most one %s device is permitted",
TYPE_VMLAUNCHUPDATE);
return;
}
/* if current machine is not supported, do not initialize */
if (!vmlaunchupdate_supported()) {
error_setg(errp,
"This machine does not support vm-launch-update device");
return;
}
/* fw_cfg with DMA support is necessary to support this device */
if (!fw_cfg || !fw_cfg_dma_enabled(fw_cfg)) {
error_setg(errp, "%s device requires fw_cfg",
TYPE_VMLAUNCHUPDATE);
return;
}
fw_cfg_add_file_callback(fw_cfg, FILE_VMLAUNCHUPDATE,
launch_update_select, launch_update_write, s,
&s->launch_update,
sizeof(s->launch_update),
false);
clear_init_vm_launch_update(s);
/*
* This device requires to register a global reset because it is
* not plugged to a bus (which, as its QOM parent, would reset it).
*/
qemu_register_resettable(OBJECT(s));
}
static void vm_launchupdate_finalize(Object *obj)
{
qemu_unregister_resettable(obj);
trace_vm_launchupdate_finalize();
}
static void vmlaunchupdate_device_class_init(ObjectClass *klass,
const void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass);
ResettableClass *rc = RESETTABLE_CLASS(klass);
/* we are not interested in migration - so no need to populate dc->vmsd */
dc->desc = "VM launch state update device";
dc->realize = vm_launchupdate_realize;
dc->hotpluggable = false;
set_bit(DEVICE_CATEGORY_MISC, dc->categories);
rc->phases.enter = vmlaunch_reset_enter;
rc->get_state = vmlaunch_reset_state;
}
static const TypeInfo vmlaunchupdate_device_types[] = {
{
.name = TYPE_VMLAUNCHUPDATE,
.parent = TYPE_DEVICE,
.instance_size = sizeof(VMLaunchUpdateState),
.class_init = vmlaunchupdate_device_class_init,
.instance_finalize = vm_launchupdate_finalize,
},
};
DEFINE_TYPES(vmlaunchupdate_device_types)

View file

@ -0,0 +1,38 @@
/*
* Guest driven VM launch state update device via IGVM.
* For details and specification, please look at docs/specs/vmlaunchupdate.rst.
*
* Copyright (C) 2026 Red Hat, Inc.
*
* Authors: Ani Sinha <anisinha@redhat.com>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*
*/
#ifndef VMLAUNCHUPDATE_H
#define VMLAUNCHUPDATE_H
#include "hw/core/qdev.h"
#include "qom/object.h"
#include "qemu/units.h"
#include "system/igvm-cfg.h"
#include "standard-headers/misc/vmlaunchupdate.h"
#define TYPE_VMLAUNCHUPDATE "vm-launch-update"
typedef struct VMLaunchUpdateState {
DeviceState parent_obj;
VMLaunchUpdate launch_update;
bool disabled;
bool host_igvm_on_reset;
ResettableState reset_state;
} VMLaunchUpdateState;
typedef struct VMLaunchUpdateStateClass {
ObjectClass parent_class;
} VMLaunchUpdateStateClass;
OBJECT_DECLARE_SIMPLE_TYPE(VMLaunchUpdateState, VMLAUNCHUPDATE);
#endif

View file

@ -0,0 +1,102 @@
/*
* Guest driven VM launch state update device via IGVM.
* The definitions in this header defines the API for the hypervisor interface.
* For details and specification, please look at docs/specs/vmlaunchupdate.rst.
*
* Copyright (C) 2026 Red Hat, Inc.
*
* Authors: Ani Sinha <anisinha@redhat.com>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*
*/
#ifndef VMLAUNCHUPDATE_API_H
#define VMLAUNCHUPDATE_API_H
/* fw-cfg file definition */
#define FILE_VMLAUNCHUPDATE "etc/vmlaunchupdate"
/* version */
#define VM_LAUNCHUPDATE_VERSION 0x01
/* format bits, used by both 'capabilities' and 'control' */
/* igvm */
#define VM_LAUNCHUPDATE_FORMAT_IGVM (1ULL << 32)
/* 'control' field bits */
/* disable vmlaunchupdate interface */
#define VM_LAUNCHUPDATE_CTL_DISABLE (1 << 0)
/* revert to the original host provided igvm */
#define VM_LAUNCHUPDATE_CTL_HOST_IGVM (1 << 1)
/* The combination of the above two ctl interfaces work as
* follows:
*
* A) CTL_HOST_IGVM=off CTL_DISABLE=off
*
* Supplied IGVM file replaces the firmware permanently. Updating the
* firmware again is possible.
*
* B) CTL_HOST_IGVM=off CTL_DISABLE=on
*
* Supplied IGVM file replaces the firmware permanently. Updating the
* firmware again is not possible.
*
* C) CTL_HOST_IGVM=on CTL_DISABLE=off
*
* Supplied IGVM file replaces the firmware for one reset. Resetting
* again will switch back to the original firmware. Updating the
* firmware again is possible.
*
* D) CTL_HOST_IGVM=on CTL_DISABLE=on
*
* Supplied IGVM file replaces the firmware for one reset. Resetting
* again will switch back to the original firmware. Updating the
* firmware again is NOT possible.
*
*/
/* status code */
enum VMLaunchUpdateStatus {
VM_LAUNCHUPDATE_SUCCESS = 0,
VM_LAUNCHUPDATE_LOAD_FAIL = 1,
VM_LAUNCHUPDATE_NOT_IGVM_INIT = 2,
};
typedef struct QEMU_PACKED {
/* api version */
uint16_t version;
/*
* The guest can read this in order to determine if loading new IGVM
* succeeded.
*/
uint16_t status;
uint32_t _padding;
/* VMM capabilities, read-only. */
uint64_t capabilities;
/* control bits, see VMFWUPDATE_CTL_* */
uint64_t control;
/*
* address and size of the IGVM image. Will be cleared when
* the write completes successfully and IGVM file is correctly parsed.
*/
uint64_t fw_image_addr;
uint64_t fw_image_size;
/*
* address + size of opaque blob. The guest can use this to pass on
* information, for example which memory region the linux kernel has been
* loaded to. writable, will be kept intact on firmware update.
*/
uint64_t opaque_addr;
uint64_t opaque_size;
} VMLaunchUpdate;
#endif

View file

@ -18,6 +18,11 @@
#include "system/confidential-guest-support.h"
#include <igvm/igvm.h>
typedef struct IgvmMemoryRegion {
QTAILQ_ENTRY(IgvmMemoryRegion) next;
MemoryRegion *mr;
} IgvmMemoryRegion;
struct IgvmCfg {
Object parent_obj;
@ -29,6 +34,7 @@ struct IgvmCfg {
char *filename;
IgvmHandle file;
ResettableState reset_state;
QTAILQ_HEAD(, IgvmMemoryRegion) memory_regions;
};
typedef struct QIgvmParameterData {
@ -43,7 +49,7 @@ typedef struct QIgvmParameterData {
* file.
*/
struct QIgvm {
IgvmHandle file;
IgvmCfg *cfg;
MachineState *machine_state;
ConfidentialGuestSupportClass *cgsc;
uint32_t compatibility_mask;

View file

@ -20,6 +20,7 @@ typedef struct QIgvm QIgvm;
int qigvm_process_file(IgvmCfg *igvm, MachineState *machine_state,
bool onlyVpContext, Error **errp);
void qigvm_cleanup_memory(IgvmCfg *igvm);
/* x86 native */
int qigvm_x86_get_mem_map_entry(int index,

View file

@ -1691,6 +1691,7 @@ static void memory_region_finalize(Object *obj)
{
MemoryRegion *mr = MEMORY_REGION(obj);
trace_memory_region_finalize(mr->name);
/*
* Each memory region (that can be freed) must have an owner, and it
* always has the same lifecycle of its owner. It means when reaching

View file

@ -25,6 +25,7 @@ flatview_new(void *view, void *root) "%p (root %p)"
flatview_destroy(void *view, void *root) "%p (root %p)"
flatview_destroy_rcu(void *view, void *root) "%p (root %p)"
global_dirty_changed(unsigned int bitmask) "bitmask 0x%"PRIx32
memory_region_finalize(const char* name) "mr %s"
# physmem.c
address_space_map(void *as, uint64_t addr, uint64_t len, bool is_write, uint32_t attrs) "as:%p addr 0x%"PRIx64":%"PRIx64" write:%d attrs:0x%x"

45
tests/data/igvm/README Normal file
View file

@ -0,0 +1,45 @@
=== This is the readme file for all IGVM file bundles provided ===
=== All IGVM files are covered by GPL v2 or later ===
SPDX-License-Identifier: GPL-2.0-or-later
Small IGVM file bundles required for tesing vm-launch-update device
===================================================================
This directory contains IGVM files required for launchupdate-test.c.
These IGVM bundles can be built from the following repository:
https://gitlab.com/anisinha/virt-firmware-rs
Just type "make" in the top of the repository. The built IGVM files
can be found in the target/x86_64-unknown-none/debug directory.
$ ls -l ./target/x86_64-unknown-none/debug/*.igvm
-rw-r--r--. 1 anisinha anisinha 153624 Jun 8 10:05 ./target/x86_64-unknown-none/debug/hello.igvm
-rw-r--r--. 1 anisinha anisinha 157848 Jun 8 10:05 ./target/x86_64-unknown-none/debug/igvmtest.igvm
-rw-r--r--. 1 anisinha anisinha 987384 Jun 8 10:05 ./target/x86_64-unknown-none/debug/mefisto.igvm
-rw-r--r--. 1 anisinha anisinha 368312 Jun 8 10:05 ./target/x86_64-unknown-none/debug/qemuinit.igvm
-rw-r--r--. 1 anisinha anisinha 223944 Jun 8 10:05 ./target/x86_64-unknown-none/debug/snptest.igvm
The IGVM bundles for sev-snp testing are also kept in a seperate directory /snp_igvm_bundles in the
above repository for use without requiring to build from source:
$ virtfirmware-rs/snp_igvm_bundles$ ls -l
total 388
-rw-r--r-- 1 anisinha anisinha 199176 Aug 7 00:58 snptest.igvm
-rw-r--r-- 1 anisinha anisinha 195048 Aug 6 06:10 snptest-nohello.igvm
Following files are used by the tests/qtest/launchupdate-test.c for non-coco case:
hello.igvm
qemuinit.igvm
For confidential case (that is when COCO=1 is passed in the environment), the following
IGVM bundles are used:
snptest-nohello.igvm
snptest.igvm
The IGVM bundles used by the confidential case are not checked into the QEMU
repository. Users who wish to test the confidential case can use the IGVM
bundles from https://gitlab.com/anisinha/virt-firmware-rs as mentioned above.

BIN
tests/data/igvm/hello.igvm Normal file

Binary file not shown.

Binary file not shown.

View file

@ -25,6 +25,10 @@ tests_aarch64_system_quick = [
'vmstate',
]
if igvm.found()
tests_aarch64_system_quick += [ 'vm_launch_update_aarch' ]
endif
tests_aarch64_system_thorough = [
'aspeed_ast2700a1',
'aspeed_ast2700a2',

View file

@ -0,0 +1,33 @@
#!/usr/bin/env python3
#
# Check for vm-launch-update device.
#
# Copyright (c) 2026 Red Hat, Inc.
#
# Author:
# Ani Sinha <anisinha@redhat.com>
#
# SPDX-License-Identifier: GPL-2.0-or-later
from qemu_test import QemuSystemTest
class VmLaunchUpdateDeviceCheck(QemuSystemTest):
def aarch64_fail_test(self):
"""
Currently the device is only supported for pc platforms.
"""
self.vm.add_args('-machine', 'virt', '-device',
'vm-launch-update,id=fwupd1')
self.vm.set_qmp_monitor(enabled=False)
self.vm.launch()
self.vm.wait()
self.assertEqual(self.vm.exitcode(), 1, "QEMU exit code should be 1")
self.assertRegex(self.vm.get_log(),
r'This machine does not support vm-launch-update device')
def test_vm_launch_update(self):
self.aarch64_fail_test()
if __name__ == '__main__':
QemuSystemTest.main()

View file

@ -28,6 +28,10 @@ if not get_option('asan')
tests_x86_64_system_quick += [ 'memlock' ]
endif
if igvm.found()
tests_x86_64_system_quick += [ 'vm_launch_update' ]
endif
tests_x86_64_system_thorough = [
'acpi_bits',
'hotplug_blk',

View file

@ -0,0 +1,48 @@
#!/usr/bin/env python3
#
# Check for vm-launch-update device.
#
# Copyright (c) 2026 Red Hat, Inc.
#
# Author:
# Ani Sinha <anisinha@redhat.com>
#
# SPDX-License-Identifier: GPL-2.0-or-later
from qemu_test import QemuSystemTest
import time
class VmLaunchUpdateDeviceCheck(QemuSystemTest):
DELAY_BOOT_SEQUENCE = 1
def vm_launch_update_pass(self):
"""
Basic test to make sure vm-launch-update device can be instantiated.
"""
self.vm.add_args('-device', 'vm-launch-update,id=fwupd1')
self.vm.set_qmp_monitor(enabled=False)
self.vm.launch()
time.sleep(self.DELAY_BOOT_SEQUENCE)
self.vm.shutdown()
self.assertEqual(self.vm.exitcode(), 0, "QEMU exit code should be 0")
def multiple_device_fail(self):
"""
Only one vm-launch-update device can be instantiated. Ensure failure if
user tries to create more than one device.
"""
self.vm.add_args('-device', 'vm-launch-update,id=fw1',
'-device', 'vm-launch-update,id=fw2')
self.vm.set_qmp_monitor(enabled=False)
self.vm.launch()
self.vm.wait()
self.assertEqual(self.vm.exitcode(), 1, "QEMU exit code should be 1")
self.assertRegex(self.vm.get_log(),
r'at most one vm-launch-update device is permitted')
def test_vm_launch_update(self):
self.vm_launch_update_pass()
self.multiple_device_fail()
if __name__ == '__main__':
QemuSystemTest.main()

View file

@ -0,0 +1,625 @@
/*
* 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();
}

View file

@ -61,6 +61,8 @@ qtests_i386 = \
(config_all_devices.has_key('CONFIG_Q35') ? ['e820-test'] : []) + \
(config_all_devices.has_key('CONFIG_FW_CFG_DMA') ? ['vmcoreinfo-test'] : []) + \
(config_all_devices.has_key('CONFIG_Q35') ? ['dump-test'] : []) + \
(igvm.found() and
config_all_devices.has_key('CONFIG_FW_CFG_DMA') ? ['launchupdate-test'] : []) + \
(config_all_devices.has_key('CONFIG_I440FX') ? ['i440fx-test'] : []) + \
(config_all_devices.has_key('CONFIG_I440FX') ? ['ide-test'] : []) + \
(config_all_devices.has_key('CONFIG_I440FX') ? ['numa-test'] : []) + \