diff --git a/MAINTAINERS b/MAINTAINERS index ac8e73bb24..08017f4786 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -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 +M: Gerd Hoffman +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é S: Maintained @@ -4119,7 +4132,7 @@ F: tests/uefi-test-tools/ IGVM Firmware M: Gerd Hoffmann M: Stefano Garzarella -R: Ani Sinha +M: Ani Sinha S: Maintained F: backends/igvm*.c F: docs/system/igvm.rst diff --git a/backends/igvm-cfg.c b/backends/igvm-cfg.c index e1f09855f6..935ba54f54 100644 --- a/backends/igvm-cfg.c +++ b/backends/igvm-cfg.c @@ -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, diff --git a/backends/igvm.c b/backends/igvm.c index 80e87fe602..7b7bdc72b7 100644 --- a/backends/igvm.c +++ b/backends/igvm.c @@ -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(®ion[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(®ion[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(®ion[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); + } +} diff --git a/backends/trace-events b/backends/trace-events index 009a25b0be..9b7b9b7d68 100644 --- a/backends/trace-events +++ b/backends/trace-events @@ -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" diff --git a/docs/specs/index.rst b/docs/specs/index.rst index b7909a108a..3cdf242661 100644 --- a/docs/specs/index.rst +++ b/docs/specs/index.rst @@ -34,6 +34,7 @@ guest hardware that is specific to QEMU. virt-ctlr vmcoreinfo vmgenid + vmlaunchupdate rapl-msr rocker riscv-iommu diff --git a/docs/specs/vmlaunchupdate.rst b/docs/specs/vmlaunchupdate.rst new file mode 100644 index 0000000000..2f1e95afbf --- /dev/null +++ b/docs/specs/vmlaunchupdate.rst @@ -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 `__ +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. diff --git a/hw/misc/meson.build b/hw/misc/meson.build index e86d9ad6b3..858ca845a2 100644 --- a/hw/misc/meson.build +++ b/hw/misc/meson.build @@ -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')) diff --git a/hw/misc/trace-events b/hw/misc/trace-events index c9a868b3ef..2d6d2238c5 100644 --- a/hw/misc/trace-events +++ b/hw/misc/trace-events @@ -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) "" diff --git a/hw/misc/vmlaunchupdate.c b/hw/misc/vmlaunchupdate.c new file mode 100644 index 0000000000..afa2d278ae --- /dev/null +++ b/hw/misc/vmlaunchupdate.c @@ -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 + * + * 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) diff --git a/include/hw/misc/vmlaunchupdate.h b/include/hw/misc/vmlaunchupdate.h new file mode 100644 index 0000000000..02f96cebdc --- /dev/null +++ b/include/hw/misc/vmlaunchupdate.h @@ -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 + * + * 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 diff --git a/include/standard-headers/misc/vmlaunchupdate.h b/include/standard-headers/misc/vmlaunchupdate.h new file mode 100644 index 0000000000..7f8382a765 --- /dev/null +++ b/include/standard-headers/misc/vmlaunchupdate.h @@ -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 + * + * 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 diff --git a/include/system/igvm-internal.h b/include/system/igvm-internal.h index 7eb3792ed8..9e9fa1d9af 100644 --- a/include/system/igvm-internal.h +++ b/include/system/igvm-internal.h @@ -18,6 +18,11 @@ #include "system/confidential-guest-support.h" #include +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; diff --git a/include/system/igvm.h b/include/system/igvm.h index 64d3542311..e219f1a4ed 100644 --- a/include/system/igvm.h +++ b/include/system/igvm.h @@ -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, diff --git a/system/memory.c b/system/memory.c index da710bbade..9760721e45 100644 --- a/system/memory.c +++ b/system/memory.c @@ -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 diff --git a/system/trace-events b/system/trace-events index d483b31419..38ad0be8a9 100644 --- a/system/trace-events +++ b/system/trace-events @@ -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" diff --git a/tests/data/igvm/README b/tests/data/igvm/README new file mode 100644 index 0000000000..d4d26478ee --- /dev/null +++ b/tests/data/igvm/README @@ -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. diff --git a/tests/data/igvm/hello.igvm b/tests/data/igvm/hello.igvm new file mode 100644 index 0000000000..2d3ffda5b1 Binary files /dev/null and b/tests/data/igvm/hello.igvm differ diff --git a/tests/data/igvm/qemuinit.igvm b/tests/data/igvm/qemuinit.igvm new file mode 100644 index 0000000000..08e605ae0d Binary files /dev/null and b/tests/data/igvm/qemuinit.igvm differ diff --git a/tests/functional/aarch64/meson.build b/tests/functional/aarch64/meson.build index e81afd6c39..f0881bed16 100644 --- a/tests/functional/aarch64/meson.build +++ b/tests/functional/aarch64/meson.build @@ -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', diff --git a/tests/functional/aarch64/test_vm_launch_update_aarch.py b/tests/functional/aarch64/test_vm_launch_update_aarch.py new file mode 100755 index 0000000000..2b3d7cf4a3 --- /dev/null +++ b/tests/functional/aarch64/test_vm_launch_update_aarch.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# +# Check for vm-launch-update device. +# +# Copyright (c) 2026 Red Hat, Inc. +# +# Author: +# Ani Sinha +# +# 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() diff --git a/tests/functional/x86_64/meson.build b/tests/functional/x86_64/meson.build index 27b31f2e96..0353b2af8e 100644 --- a/tests/functional/x86_64/meson.build +++ b/tests/functional/x86_64/meson.build @@ -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', diff --git a/tests/functional/x86_64/test_vm_launch_update.py b/tests/functional/x86_64/test_vm_launch_update.py new file mode 100755 index 0000000000..aac7ef915f --- /dev/null +++ b/tests/functional/x86_64/test_vm_launch_update.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# +# Check for vm-launch-update device. +# +# Copyright (c) 2026 Red Hat, Inc. +# +# Author: +# Ani Sinha +# +# 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() diff --git a/tests/qtest/launchupdate-test.c b/tests/qtest/launchupdate-test.c new file mode 100644 index 0000000000..225c843df5 --- /dev/null +++ b/tests/qtest/launchupdate-test.c @@ -0,0 +1,625 @@ +/* + * vmlaunchupdate device fwcfg test. + * + * Copyright (c) 2026 Red Hat, Inc. + * + * Author: + * Ani Sinha + * + * 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(); +} diff --git a/tests/qtest/meson.build b/tests/qtest/meson.build index f7c7d06620..12ed368997 100644 --- a/tests/qtest/meson.build +++ b/tests/qtest/meson.build @@ -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'] : []) + \