]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
vmspawn: select coco firmware via fw descriptors
authorPaul Meyer <katexochen0@gmail.com>
Thu, 2 Jul 2026 14:22:36 +0000 (16:22 +0200)
committerPaul Meyer <katexochen0@gmail.com>
Mon, 6 Jul 2026 09:02:04 +0000 (11:02 +0200)
Instead of requiring --firmware= to point at a raw .fd image for
--coco=sev-snp/tdx, pick a suitable firmware automatically from the
QEMU firmware descriptors, requiring the amd-sev-snp/intel-tdx feature.
CoCo firmware is stateless (it carries no NVRAM template), so teach
find_ovmf_config() to select stateless raw-format firmware via new
flags. --firmware= now uniformly takes a firmware descriptor path.

Signed-off-by: Paul Meyer <katexochen0@gmail.com>
man/systemd-vmspawn.xml
src/vmspawn/test-vmspawn-util.c
src/vmspawn/vmspawn-util.c
src/vmspawn/vmspawn-util.h
src/vmspawn/vmspawn.c

index d8afb9d0e157fe4a0597a3f302929c0ab0295173..4ef1b18f57628200c275fb978111370e3915d732 100644 (file)
           boot. Booting a UKI requires <literal>uefi</literal>. If the special string <literal>list</literal>
           is specified, all discovered firmware definition files are listed. If the special string
           <literal>describe</literal> is specified, the UEFI firmware that would be selected (taking
-          <option>--firmware-features=</option> into account) is printed and the program exits. If an empty
-          string is specified, the option is reset to its default.</para>
+          <option>--firmware-features=</option> and <option>--coco=</option> into account) is printed and
+          the program exits. If an empty string is specified, the option is reset to its default.</para>
 
           <xi:include href="version-info.xml" xpointer="v256"/></listitem>
         </varlistentry>
           <literal>no</literal>.</para>
 
           <para><literal>sev-snp</literal> enables AMD SEV-SNP. This requires KVM on an x86_64 host with
-          SNP-capable hardware and firmware. <option>--firmware=</option> must point to a raw SNP-built
-          OVMF <filename>.fd</filename> image; the standard pflash + NVRAM split is not supported under
-          SNP, so the firmware is loaded via QEMU's <option>-bios</option> and Secure Boot is
-          unavailable. Direct kernel boot via <option>--linux=</option> is required so that the kernel,
-          initrd and command line are hashed into the launch measurement
-          (<literal>kernel-hashes=on</literal>); booting the kernel off the disk image via the firmware
-          would leave it outside the measurement. Credentials passed via <option>--set-credential=</option>
-          or <option>--load-credential=</option> are bundled into a cpio archive appended to the initrd
-          (mirroring what <command>systemd-stub</command> does for ESP credentials), so they enter the
-          launch measurement via <literal>kernel-hashes=on</literal>; the SMBIOS and fw_cfg channels
-          normally used to deliver credentials are not used because they are unmeasured and would be
-          discarded by PID1 in confidential guests. This channel is measured but not confidential with
-          respect to the host or VMM: the initrd (and thus the credentials it carries) is supplied to QEMU
-          as plaintext and only its hash enters the launch measurement, which guarantees integrity but does
-          not keep the credentials secret from the host. This requires the guest to run a sufficiently
-          recent version of systemd (supporting <filename>/.extra/system_credentials/</filename>). A vTPM,
-          if attached via <option>--tpm=</option>, must be treated as untrusted by the guest.</para>
+          SNP-capable hardware and firmware. A suitable SNP-built OVMF firmware is picked automatically
+          from the installed QEMU firmware descriptors, by requiring the <literal>amd-sev-snp</literal>
+          firmware feature; use <option>--firmware=</option> with a path to a firmware descriptor file to
+          select a specific one. Secure Boot is unavailable. Direct kernel boot via
+          <option>--linux=</option> is required so that the kernel, initrd and command line are hashed
+          into the launch measurement (<literal>kernel-hashes=on</literal>); booting the kernel off the
+          disk image via the firmware would leave it outside the measurement. Credentials passed via
+          <option>--set-credential=</option> or <option>--load-credential=</option> are bundled into a
+          cpio archive appended to the initrd (mirroring what <command>systemd-stub</command> does for
+          ESP credentials), so they enter the launch measurement via <literal>kernel-hashes=on</literal>;
+          the SMBIOS and fw_cfg channels normally used to deliver credentials are not used because they are
+          unmeasured and would be discarded by PID1 in confidential guests. This channel is measured but
+          not confidential with respect to the host or VMM: the initrd (and thus the credentials it
+          carries) is supplied to QEMU as plaintext and only its hash enters the launch measurement,
+          which guarantees integrity but does not keep the credentials secret from the host. This requires
+          the guest to run a sufficiently recent version of systemd (supporting
+          <filename>/.extra/system_credentials/</filename>). A vTPM, if attached via
+          <option>--tpm=</option>, must be treated as untrusted by the guest.</para>
 
           <para><literal>tdx</literal> enables Intel TDX. This requires KVM on an x86_64 host with
-          TDX-capable hardware and a TDX-enabled host kernel. As with <literal>sev-snp</literal>,
-          <option>--firmware=</option> must point to a raw TDX-built OVMF (TDVF) <filename>.fd</filename>
-          image, which is loaded via QEMU's <option>-bios</option> (pflash + NVRAM split is not
-          supported), and the CPU model is fixed to <literal>host</literal>. Firmware is measured into
-          MRTD when the TD is built. Secure Boot cannot be enrolled at runtime (there is no writable
-          NVRAM); its state is fixed by the supplied TDVF image and is part of the measured firmware.
+          TDX-capable hardware and a TDX-enabled host kernel. As with <literal>sev-snp</literal>, a
+          TDX-built OVMF (TDVF) firmware is picked automatically from the installed QEMU firmware
+          descriptors, by requiring the <literal>intel-tdx</literal> firmware feature; use
+          <option>--firmware=</option> with a path to a firmware descriptor file to select a specific
+          one. The CPU model is fixed to <literal>host</literal>. Firmware is measured into MRTD when the
+          TD is built. Secure Boot cannot be enrolled at runtime (there is no writable NVRAM); its state
+          is fixed by the selected TDVF image and is part of the measured firmware.
           When booting a UKI, the whole UKI PE is measured into RTMR 1, and the loaded sections are
           measured individually by <command>systemd-stub</command> into RTMR 2. For direct linux boot,
           firmware measures the kernel PE into RTMR 1, and the Linux EFI stub measures initrd and
           to the host or VMM, since the host assembles the SMBIOS table. A vTPM, if attached via
           <option>--tpm=</option>, must be treated as untrusted by the guest. To obtain TD Quotes for
           remote attestation, the guest is wired to the host's local TDX Quote Generation Service
-          automatically: the unix socket up <filename>/run/tdx-qgs/qgs.socket</filename> is used if it
+          automatically: the unix socket <filename>/run/tdx-qgs/qgs.socket</filename> is used if it
           exists, otherwise vsock port 4050 on the host (cid 2). If the QGS is listening on neither
           channel, the guest's quote requests will fail.
           <xi:include href="version-info.xml" xpointer="v262"/></para>
index d487cbcacb781c90eaa9136194cdb1e66dd45093..d5108a73d7d7f058811ddfea1b6aed9a5097ceb1 100644 (file)
@@ -1,8 +1,17 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 
+#include <stdlib.h>
+
+#include "sd-json.h"
+
 #include "alloc-util.h"
+#include "fileio.h"
+#include "path-util.h"
+#include "rm-rf.h"
+#include "set.h"
 #include "string-util.h"
 #include "tests.h"
+#include "tmpfile-util.h"
 #include "vmspawn-util.h"
 
 #define _ESCAPE_QEMU_VALUE_CHECK(str, correct, varname) \
@@ -23,4 +32,182 @@ TEST(escape_qemu_value) {
         ESCAPE_QEMU_VALUE_CHECK("", "");
 }
 
+typedef enum TestMapping {
+        MAPPING_FLASH_SPLIT,     /* pflash executable + NVRAM template */
+        MAPPING_FLASH_STATELESS, /* read-only pflash, no NVRAM */
+        MAPPING_FLASH_COMBINED,  /* read-write pflash with the variable store inside the executable */
+        MAPPING_MEMORY,          /* mapped into memory, loaded via -bios */
+        MAPPING_KERNEL,          /* loaded like a Linux kernel */
+} TestMapping;
+
+static void write_descriptor(
+                const char *dir,
+                const char *name,
+                const char *executable,
+                const char *format,
+                const char *interface_type,
+                const char *arch,
+                const char *machine,
+                TestMapping mapping,
+                char **features) {
+
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+        _cleanup_free_ char *j = NULL, *p = NULL;
+
+        if (!arch)
+                ASSERT_OK(native_arch_as_qemu(&arch));
+
+        bool flash = IN_SET(mapping, MAPPING_FLASH_SPLIT, MAPPING_FLASH_STATELESS, MAPPING_FLASH_COMBINED);
+
+        ASSERT_OK(sd_json_buildo(&v,
+                        SD_JSON_BUILD_PAIR_STRING("description", name),
+                        SD_JSON_BUILD_PAIR_STRV("interface-types", STRV_MAKE(interface_type ?: "uefi")),
+                        SD_JSON_BUILD_PAIR("mapping", SD_JSON_BUILD_OBJECT(
+                                SD_JSON_BUILD_PAIR_STRING("device", flash ? "flash" : mapping == MAPPING_MEMORY ? "memory" : "kernel"),
+                                SD_JSON_BUILD_PAIR_CONDITION(!flash, "filename", SD_JSON_BUILD_STRING(executable)),
+                                SD_JSON_BUILD_PAIR_CONDITION(mapping == MAPPING_FLASH_STATELESS, "mode", SD_JSON_BUILD_STRING("stateless")),
+                                SD_JSON_BUILD_PAIR_CONDITION(mapping == MAPPING_FLASH_COMBINED, "mode", SD_JSON_BUILD_STRING("combined")),
+                                SD_JSON_BUILD_PAIR_CONDITION(mapping == MAPPING_FLASH_SPLIT, "nvram-template", SD_JSON_BUILD_OBJECT(
+                                        SD_JSON_BUILD_PAIR_STRING("filename", "/test/vars.fd"),
+                                        SD_JSON_BUILD_PAIR_STRING("format", "raw"))),
+                                SD_JSON_BUILD_PAIR_CONDITION(flash, "executable", SD_JSON_BUILD_OBJECT(
+                                        SD_JSON_BUILD_PAIR_STRING("filename", executable),
+                                        SD_JSON_BUILD_PAIR_STRING("format", format))))),
+                        SD_JSON_BUILD_PAIR("targets", SD_JSON_BUILD_ARRAY(SD_JSON_BUILD_OBJECT(
+                                SD_JSON_BUILD_PAIR_STRING("architecture", arch),
+                                SD_JSON_BUILD_PAIR_STRV("machines", STRV_MAKE(machine ?: QEMU_MACHINE_TYPE))))),
+                        SD_JSON_BUILD_PAIR_STRV("features", features),
+                        SD_JSON_BUILD_PAIR_EMPTY_ARRAY("tags")));
+
+        ASSERT_OK(sd_json_variant_format(v, /* flags= */ 0, &j));
+        ASSERT_NOT_NULL(p = path_join(dir, name));
+        ASSERT_OK(write_string_file(p, j, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MKDIR_0755));
+}
+
+/* Searches with the given feature sets and flags, and asserts that the firmware with the expected
+ * executable path is selected, or -ENOENT if expect_path is NULL. Optionally returns the selected config
+ * for further assertions. */
+static void check_find(char **include, char **exclude, FindOvmfConfigFlags flags, const char *expect_path, OvmfConfig **ret) {
+        _cleanup_set_free_ Set *inc = NULL, *exc = NULL;
+        _cleanup_(ovmf_config_freep) OvmfConfig *config = NULL;
+        int r;
+
+        ASSERT_OK(set_put_strdupv(&inc, include));
+        ASSERT_OK(set_put_strdupv(&exc, exclude));
+
+        r = find_ovmf_config(inc, exc, flags, &config, /* ret_firmware_json= */ NULL);
+        if (!expect_path)
+                ASSERT_ERROR(r, ENOENT);
+        else {
+                ASSERT_OK(r);
+                ASSERT_STREQ(config->path, expect_path);
+        }
+
+        if (ret)
+                *ret = TAKE_PTR(config);
+}
+
+TEST(find_ovmf_config) {
+        _cleanup_(rm_rf_physical_and_freep) char *tmp = NULL;
+        _cleanup_(ovmf_config_freep) OvmfConfig *config = NULL;
+        _cleanup_free_ char *dir = NULL;
+
+        if (native_arch_as_qemu(/* ret= */ NULL) < 0)
+                return (void) log_tests_skipped("native architecture not supported by qemu");
+
+        ASSERT_OK(mkdtemp_malloc("/tmp/test-vmspawn-firmware-XXXXXX", &tmp));
+        ASSERT_OK_ERRNO(setenv("XDG_CONFIG_HOME", tmp, /* overwrite= */ true));
+        ASSERT_NOT_NULL(dir = path_join(tmp, "qemu/firmware"));
+
+        /* All fixtures declare made-up features and every search below requires one of them, so
+         * descriptors installed on the host can never match and the test stays hermetic. */
+
+        /* Stateful vs. stateless selection, in both sort orders. */
+        write_descriptor(dir, "00-a-stateless.json", "/test/a-stateless.fd", "raw", NULL, NULL, NULL, MAPPING_FLASH_STATELESS, STRV_MAKE("vmspawn-test-a"));
+        write_descriptor(dir, "10-a-stateful.json", "/test/a-stateful.fd", "raw", NULL, NULL, NULL, MAPPING_FLASH_SPLIT, STRV_MAKE("vmspawn-test-a"));
+        write_descriptor(dir, "00-b-stateful.json", "/test/b-stateful.fd", "raw", NULL, NULL, NULL, MAPPING_FLASH_SPLIT, STRV_MAKE("vmspawn-test-b"));
+        write_descriptor(dir, "10-b-stateless.json", "/test/b-stateless.fd", "raw", NULL, NULL, NULL, MAPPING_FLASH_STATELESS, STRV_MAKE("vmspawn-test-b"));
+
+        /* By default only firmware with an NVRAM template is considered. */
+        check_find(STRV_MAKE("vmspawn-test-a"), /* exclude= */ NULL, /* flags= */ 0, "/test/a-stateful.fd", &config);
+        ASSERT_STREQ(config->vars, "/test/vars.fd");
+        ASSERT_STREQ(config->format, "raw");
+        ASSERT_FALSE(ovmf_config_is_stateless(config));
+        ASSERT_FALSE(config->supports_sb);
+        config = ovmf_config_free(config);
+
+        /* With FIND_OVMF_STATELESS only firmware in stateless flash mode is considered. */
+        check_find(STRV_MAKE("vmspawn-test-a"), /* exclude= */ NULL, FIND_OVMF_STATELESS, "/test/a-stateless.fd", &config);
+        ASSERT_NULL(config->vars);
+        ASSERT_TRUE(ovmf_config_is_stateless(config));
+        config = ovmf_config_free(config);
+
+        check_find(STRV_MAKE("vmspawn-test-b"), /* exclude= */ NULL, /* flags= */ 0, "/test/b-stateful.fd", /* ret= */ NULL);
+        check_find(STRV_MAKE("vmspawn-test-b"), /* exclude= */ NULL, FIND_OVMF_STATELESS, "/test/b-stateless.fd", /* ret= */ NULL);
+
+        /* FIND_OVMF_REQUIRE_RAW skips firmware in other formats, which is accepted otherwise. */
+        write_descriptor(dir, "00-c-qcow2.json", "/test/c.qcow2", "qcow2", NULL, NULL, NULL, MAPPING_FLASH_STATELESS, STRV_MAKE("vmspawn-test-c"));
+        write_descriptor(dir, "10-c-raw.json", "/test/c.fd", "raw", NULL, NULL, NULL, MAPPING_FLASH_STATELESS, STRV_MAKE("vmspawn-test-c"));
+
+        check_find(STRV_MAKE("vmspawn-test-c"), /* exclude= */ NULL, FIND_OVMF_STATELESS, "/test/c.qcow2", &config);
+        ASSERT_STREQ(config->format, "qcow2");
+        config = ovmf_config_free(config);
+
+        check_find(STRV_MAKE("vmspawn-test-c"), /* exclude= */ NULL, FIND_OVMF_STATELESS|FIND_OVMF_REQUIRE_RAW, "/test/c.fd", /* ret= */ NULL);
+
+        /* Memory-mapped firmware (loaded via -bios) is stateless and raw by definition. */
+        write_descriptor(dir, "00-m-memory.json", "/test/m-memory.fd", "raw", NULL, NULL, NULL, MAPPING_MEMORY, STRV_MAKE("vmspawn-test-m"));
+
+        check_find(STRV_MAKE("vmspawn-test-m"), /* exclude= */ NULL, /* flags= */ 0, /* expect_path= */ NULL, /* ret= */ NULL);
+        check_find(STRV_MAKE("vmspawn-test-m"), /* exclude= */ NULL, FIND_OVMF_STATELESS|FIND_OVMF_REQUIRE_RAW, "/test/m-memory.fd", &config);
+        ASSERT_NULL(config->vars);
+        ASSERT_NULL(config->format);
+        ASSERT_STREQ(ovmf_config_format(config), "raw");
+        ASSERT_TRUE(ovmf_config_is_stateless(config));
+        config = ovmf_config_free(config);
+
+        /* Combined-mode flash firmware carries a writable variable store inside the executable, it must not be treated as stateless. */
+        write_descriptor(dir, "00-f-combined.json", "/test/f-combined.fd", "raw", NULL, NULL, NULL, MAPPING_FLASH_COMBINED, STRV_MAKE("vmspawn-test-f"));
+
+        check_find(STRV_MAKE("vmspawn-test-f"), /* exclude= */ NULL, /* flags= */ 0, /* expect_path= */ NULL, /* ret= */ NULL);
+        check_find(STRV_MAKE("vmspawn-test-f"), /* exclude= */ NULL, FIND_OVMF_STATELESS, /* expect_path= */ NULL, /* ret= */ NULL);
+
+        /* Firmware that is not UEFI, uses an unsupported mapping device, or doesn't match the native
+         * architecture or machine type, is skipped. */
+        write_descriptor(dir, "00-d-bios.json", "/test/d-bios.fd", "raw", "bios", NULL, NULL, MAPPING_FLASH_SPLIT, STRV_MAKE("vmspawn-test-d"));
+        write_descriptor(dir, "05-d-kernel.json", "/test/d-kernel.fd", "raw", NULL, NULL, NULL, MAPPING_KERNEL, STRV_MAKE("vmspawn-test-d"));
+        write_descriptor(dir, "10-d-arch.json", "/test/d-arch.fd", "raw", NULL, "vmspawn-test-arch", NULL, MAPPING_FLASH_SPLIT, STRV_MAKE("vmspawn-test-d"));
+        write_descriptor(dir, "20-d-machine.json", "/test/d-machine.fd", "raw", NULL, NULL, "vmspawn-test-mach", MAPPING_FLASH_SPLIT, STRV_MAKE("vmspawn-test-d"));
+        write_descriptor(dir, "30-d-good.json", "/test/d-good.fd", "raw", NULL, NULL, NULL, MAPPING_FLASH_SPLIT, STRV_MAKE("vmspawn-test-d"));
+
+        check_find(STRV_MAKE("vmspawn-test-d"), /* exclude= */ NULL, /* flags= */ 0, "/test/d-good.fd", /* ret= */ NULL);
+
+        /* Feature include/exclude handling. */
+        write_descriptor(dir, "00-e-both.json", "/test/e-both.fd", "raw", NULL, NULL, NULL, MAPPING_FLASH_SPLIT, STRV_MAKE("vmspawn-test-e1", "vmspawn-test-e2", "secure-boot"));
+        write_descriptor(dir, "10-e-one.json", "/test/e-one.fd", "raw", NULL, NULL, NULL, MAPPING_FLASH_SPLIT, STRV_MAKE("vmspawn-test-e1"));
+
+        /* The first matching descriptor in sort order wins. */
+        check_find(STRV_MAKE("vmspawn-test-e1"), /* exclude= */ NULL, /* flags= */ 0, "/test/e-both.fd", &config);
+        ASSERT_TRUE(config->supports_sb);
+        config = ovmf_config_free(config);
+
+        /* Descriptors with an excluded feature are skipped. */
+        check_find(STRV_MAKE("vmspawn-test-e1"), STRV_MAKE("vmspawn-test-e2"), /* flags= */ 0, "/test/e-one.fd", /* ret= */ NULL);
+
+        /* Inclusion wins over exclusion. */
+        check_find(STRV_MAKE("vmspawn-test-e1", "vmspawn-test-e2"), STRV_MAKE("vmspawn-test-e2"), /* flags= */ 0, "/test/e-both.fd", /* ret= */ NULL);
+
+        /* All included features must be present. */
+        check_find(STRV_MAKE("vmspawn-test-e1", "vmspawn-test-nonexistent"), /* exclude= */ NULL, /* flags= */ 0, /* expect_path= */ NULL, /* ret= */ NULL);
+
+        /* The firmware description JSON is returned on request. */
+        _cleanup_set_free_ Set *inc = NULL;
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
+        ASSERT_OK(set_put_strdup(&inc, "vmspawn-test-a"));
+        ASSERT_OK(find_ovmf_config(inc, /* features_exclude= */ NULL, /* flags= */ 0, &config, &json));
+        ASSERT_TRUE(sd_json_variant_is_object(json));
+
+        ASSERT_OK_ERRNO(unsetenv("XDG_CONFIG_HOME"));
+}
+
 DEFINE_TEST_MAIN(LOG_INFO);
index 8ecd26600064e789a5f59ab4465a0f5a963267db..5b0ced8e01e0414887fff30eeaa20c9de6c1d23a 100644 (file)
@@ -43,7 +43,7 @@ static const char* const architecture_to_qemu_table[_ARCHITECTURE_MAX] = {
         [ARCHITECTURE_S390X]       = "s390x",
 };
 
-static int native_arch_as_qemu(const char **ret) {
+int native_arch_as_qemu(const char **ret) {
         const char *s = architecture_to_qemu_table[native_architecture()];
         if (!s)
                 return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Architecture %s not supported by qemu", architecture_to_string(native_architecture()));
@@ -62,9 +62,28 @@ OvmfConfig* ovmf_config_free(OvmfConfig *config) {
         free(config->format);
         free(config->vars);
         free(config->vars_format);
+        free(config->device);
+        free(config->mode);
         return mfree(config);
 }
 
+static bool firmware_is_stateless(const char *device, const char *mode) {
+        /* Memory-mapped firmware (loaded via -bios) carries no NVRAM. For flash, only mode "stateless"
+         * is stateless: an absent mode means "split" (executable plus NVRAM template), and "combined"
+         * images carry the variable store inside the (writable) executable. */
+
+        if (streq_ptr(device, "memory"))
+                return true;
+
+        return streq_ptr(device, "flash") && streq_ptr(mode, "stateless");
+}
+
+bool ovmf_config_is_stateless(const OvmfConfig *config) {
+        assert(config);
+
+        return firmware_is_stateless(config->device, config->mode);
+}
+
 DEFINE_STRING_TABLE_LOOKUP(network_stack, NetworkStack);
 
 int qemu_check_kvm_support(void) {
@@ -140,6 +159,8 @@ typedef struct FirmwareData {
         char *firmware_format;
         char *vars;
         char *vars_format;
+        char *device;
+        char *mode;
         FirmwareTarget **targets;
         size_t n_targets;
 } FirmwareData;
@@ -180,6 +201,8 @@ static FirmwareData* firmware_data_free(FirmwareData *fwd) {
         free(fwd->firmware_format);
         free(fwd->vars);
         free(fwd->vars_format);
+        free(fwd->device);
+        free(fwd->mode);
         firmware_target_free_many(fwd->targets, fwd->n_targets);
 
         return mfree(fwd);
@@ -207,14 +230,30 @@ static int firmware_nvram_template(const char *name, sd_json_variant *v, sd_json
 }
 
 static int firmware_mapping(const char *name, sd_json_variant *v, sd_json_dispatch_flags_t flags, void *userdata) {
-        static const sd_json_dispatch_field table[] = {
-                { "device",         SD_JSON_VARIANT_STRING, NULL,                    0, SD_JSON_MANDATORY },
-                { "executable",     SD_JSON_VARIANT_OBJECT, firmware_executable,     0, SD_JSON_MANDATORY },
-                { "nvram-template", SD_JSON_VARIANT_OBJECT, firmware_nvram_template, 0, 0                 },
+        static const sd_json_dispatch_field table_flash[] = {
+                { "device",         SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(FirmwareData, device), SD_JSON_MANDATORY },
+                { "mode",           SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(FirmwareData, mode),   0                 },
+                { "executable",     SD_JSON_VARIANT_OBJECT, firmware_executable,     0,                              SD_JSON_MANDATORY },
+                { "nvram-template", SD_JSON_VARIANT_OBJECT, firmware_nvram_template, 0,                              0                 },
+                {}
+        };
+        static const sd_json_dispatch_field table_memory[] = {
+                { "device",   SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(FirmwareData, device),   SD_JSON_MANDATORY },
+                { "filename", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(FirmwareData, firmware), SD_JSON_MANDATORY },
                 {}
         };
 
-        return sd_json_dispatch(v, table, flags, userdata);
+        sd_json_variant *d = sd_json_variant_by_key(v, "device");
+        if (!d || !sd_json_variant_is_string(d))
+                return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Firmware mapping lacks a device type.");
+
+        const char *device = sd_json_variant_string(d);
+        if (streq(device, "flash"))
+                return sd_json_dispatch(v, table_flash, flags, userdata);
+        if (streq(device, "memory"))
+                return sd_json_dispatch(v, table_memory, flags, userdata);
+
+        return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Unsupported firmware mapping device type '%s'.", device);
 }
 
 static int dispatch_targets(const char *name, sd_json_variant *v, sd_json_dispatch_flags_t flags, void *userdata) {
@@ -406,6 +445,8 @@ static int ovmf_config_make(FirmwareData *fwd, OvmfConfig **ret) {
                 .format = TAKE_PTR(fwd->firmware_format),
                 .vars = TAKE_PTR(fwd->vars),
                 .vars_format = TAKE_PTR(fwd->vars_format),
+                .device = TAKE_PTR(fwd->device),
+                .mode = TAKE_PTR(fwd->mode),
                 .supports_sb = firmware_data_supports_sb(fwd),
         };
 
@@ -430,6 +471,7 @@ int load_ovmf_config(const char *path, OvmfConfig **ret) {
 int find_ovmf_config(
                 Set *features_include,
                 Set *features_exclude,
+                FindOvmfConfigFlags flags,
                 OvmfConfig **ret,
                 sd_json_variant **ret_firmware_json) {
         _cleanup_(ovmf_config_freep) OvmfConfig *config = NULL;
@@ -465,11 +507,22 @@ int find_ovmf_config(
                         continue;
                 }
 
-                if (!fwd->vars) {
+                if (FLAGS_SET(flags, FIND_OVMF_STATELESS)) {
+                        if (!firmware_is_stateless(fwd->device, fwd->mode)) {
+                                log_debug("Skipping %s, firmware is not stateless.", *file);
+                                continue;
+                        }
+                } else if (!fwd->vars) {
                         log_debug("Skipping %s, firmware does not have an NVRAM template.", *file);
                         continue;
                 }
 
+                /* Memory-mapped firmware carries no format field and is raw by definition. */
+                if (FLAGS_SET(flags, FIND_OVMF_REQUIRE_RAW) && !streq(fwd->firmware_format ?: "raw", "raw")) {
+                        log_debug("Skipping %s, firmware image is not in raw format.", *file);
+                        continue;
+                }
+
                 /* Check if any target matches our architecture and machine type. Machine
                  * patterns in firmware descriptions use globs like "pc-q35-*", so we do a
                  * substring check to see if our machine type (e.g. "q35") appears in any of
index 38bb331dfc340b7fa6a7aca0f53b86b12d5c7386..cc7cd147aa67773bbcdeefacb2e1be17c4fd6f14 100644 (file)
@@ -99,6 +99,8 @@ typedef struct OvmfConfig {
         char *format;
         char *vars;
         char *vars_format;
+        char *device;
+        char *mode;
         bool supports_sb;
 } OvmfConfig;
 
@@ -109,6 +111,7 @@ static inline const char* ovmf_config_format(const OvmfConfig *c) {
 static inline const char* ovmf_config_vars_format(const OvmfConfig *c) {
         return ASSERT_PTR(c)->vars_format ?: "raw";
 }
+bool ovmf_config_is_stateless(const OvmfConfig *config);
 
 OvmfConfig* ovmf_config_free(OvmfConfig *ovmf_config);
 DEFINE_TRIVIAL_CLEANUP_FUNC(OvmfConfig*, ovmf_config_free);
@@ -134,7 +137,16 @@ int qemu_check_vsock_support(void);
 int list_ovmf_config(char ***ret);
 int list_ovmf_firmware_features(char ***ret);
 int load_ovmf_config(const char *path, OvmfConfig **ret);
-int find_ovmf_config(Set *features_include, Set *features_exclude, OvmfConfig **ret, sd_json_variant **ret_firmware_json);
+
+typedef enum FindOvmfConfigFlags {
+        FIND_OVMF_STATELESS   = 1 << 0, /* select stateless firmware (flash mode "stateless" or memory-mapped,
+                                         * bootable via -bios); default selects pflash firmware with an NVRAM
+                                         * template */
+        FIND_OVMF_REQUIRE_RAW = 1 << 1, /* skip non-raw firmware executables (-bios can't load qcow2) */
+} FindOvmfConfigFlags;
+
+int find_ovmf_config(Set *features_include, Set *features_exclude, FindOvmfConfigFlags flags, OvmfConfig **ret, sd_json_variant **ret_firmware_json);
+int native_arch_as_qemu(const char **ret);
 int find_qemu_binary(char **ret_qemu_binary);
 int vsock_fix_child_cid(int vhost_device_fd, unsigned *machine_cid, const char *machine);
 
index 00df10874ca026d929cab5cd1da94504bee05a64..e5fb02a2348253ee6aaf4d7d28fea62c33d72ade 100644 (file)
@@ -79,6 +79,7 @@
 #include "socket-util.h"
 #include "stat-util.h"
 #include "stdio-util.h"
+#include "string-table.h"
 #include "string-util.h"
 #include "strv.h"
 #include "swtpm-util.h"
@@ -2552,6 +2553,45 @@ static int prepare_device_info(const char *runtime_dir, MachineConfig *c) {
         return assign_pcie_ports(c);
 }
 
+/* Maps a confidential computing mode to the firmware descriptor feature that firmware must declare
+ * to support it. Note that the feature names are defined by the QEMU firmware interop spec and
+ * deviate slightly from our own names for the same modes (cf. confidential_computing_to_string()):
+ * "sev-snp" vs. "amd-sev-snp", and "tdx" vs. "intel-tdx". */
+static const char* const coco_firmware_feature_table[_COCO_MAX] = {
+        [COCO_AMD_SEV_SNP] = "amd-sev-snp",
+        [COCO_INTEL_TDX]   = "intel-tdx",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(coco_firmware_feature, ConfidentialComputing);
+
+static int discover_ovmf_config(OvmfConfig **ret, sd_json_variant **ret_firmware_json) {
+        int r;
+
+        assert(ret);
+
+        const char *coco_feature = coco_firmware_feature_to_string(arg_confidential_computing);
+        if (coco_feature) {
+                r = set_put_strdup(&arg_firmware_features_include, coco_feature);
+                if (r < 0)
+                        return log_oom();
+        }
+
+        FindOvmfConfigFlags flags =
+                arg_confidential_computing != COCO_NO ? FIND_OVMF_STATELESS|FIND_OVMF_REQUIRE_RAW :
+                                                        0;
+
+        r = find_ovmf_config(arg_firmware_features_include, arg_firmware_features_exclude, flags, ret, ret_firmware_json);
+        if (r == -ENOENT && coco_feature)
+                return log_error_errno(r, "No suitable firmware descriptor found for --coco=%s "
+                                       "(requires stateless firmware in raw format with the '%s' firmware feature). "
+                                       "Install a suitable firmware or select a firmware descriptor with --firmware=.",
+                                       confidential_computing_to_string(arg_confidential_computing), coco_feature);
+        if (r < 0)
+                return log_error_errno(r, "Failed to find OVMF config: %m");
+
+        return 0;
+}
+
 static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
         _cleanup_(ovmf_config_freep) OvmfConfig *ovmf_config = NULL;
         _cleanup_free_ char *qemu_binary = NULL, *mem = NULL;
@@ -2618,13 +2658,16 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
                 return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
                                        "--coco= requires KVM, but KVM is not available.");
 
-        if (arg_firmware_type == FIRMWARE_UEFI && arg_confidential_computing == COCO_NO) {
-                if (arg_firmware)
+        if (arg_firmware_type == FIRMWARE_UEFI) {
+                if (arg_firmware) {
                         r = load_ovmf_config(arg_firmware, &ovmf_config);
-                else
-                        r = find_ovmf_config(arg_firmware_features_include, arg_firmware_features_exclude, &ovmf_config, /* ret_firmware_json= */ NULL);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to find OVMF config: %m");
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to load firmware descriptor '%s': %m", arg_firmware);
+                } else {
+                        r = discover_ovmf_config(&ovmf_config, /* ret_firmware_json= */ NULL);
+                        if (r < 0)
+                                return r;
+                }
 
                 if (set_contains(arg_firmware_features_include, "secure-boot") && !ovmf_config->supports_sb)
                         return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE),
@@ -2698,7 +2741,7 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
                         return r;
         }
 
-        if (ovmf_config && ARCHITECTURE_SUPPORTS_SMM) {
+        if (ovmf_config && ARCHITECTURE_SUPPORTS_SMM && arg_confidential_computing == COCO_NO) {
                 r = qemu_config_key(config_file, "smm", on_off(ovmf_config->supports_sb));
                 if (r < 0)
                         return r;
@@ -3141,9 +3184,11 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
                 }
         }
 
+        /* Memory-mapped firmware asks for -bios loading by definition. Under
+         * confidential computing -bios is used even for (stateless) flash firmware. */
         _cleanup_(unlink_and_freep) char *ovmf_vars = NULL;
-        if (arg_confidential_computing != COCO_NO) {
-                r = strv_extend_many(&cmdline, "-bios", arg_firmware);
+        if (ovmf_config && (arg_confidential_computing != COCO_NO || streq_ptr(ovmf_config->device, "memory"))) {
+                r = strv_extend_many(&cmdline, "-bios", ovmf_config->path);
                 if (r < 0)
                         return r;
         } else {
@@ -4178,14 +4223,6 @@ static int verify_arguments(void) {
                         return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
                                                "--coco=sev-snp can't be used with %s firmware",
                                                firmware_to_string(arg_firmware_type));
-                /* SNP can't use pflash + NVRAM split, so the firmware-descriptor
-                 * machinery doesn't apply. Require an explicit raw .fd path and
-                 * use it verbatim with -bios later. */
-                if (!arg_firmware)
-                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
-                                               "--coco=sev-snp requires --firmware=PATH "
-                                               "pointing at a raw SNP-built OVMF .fd binary.");
-                log_debug("Using raw SNP firmware at %s (no NVRAM, no Secure Boot).", arg_firmware);
                 if (set_contains(arg_firmware_features_include, "secure-boot"))
                         return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
                                                "--coco=sev-snp cannot be combined with --secure-boot=yes.");
@@ -4212,14 +4249,6 @@ static int verify_arguments(void) {
                         return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
                                                "--coco=tdx can't be used with %s firmware",
                                                firmware_to_string(arg_firmware_type));
-                /* TDX can't use pflash + NVRAM split, so the firmware-descriptor
-                 * machinery doesn't apply. Require an explicit raw .fd path and
-                 * use it verbatim with -bios later. */
-                if (!arg_firmware)
-                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
-                                               "--coco=tdx requires --firmware=PATH "
-                                               "pointing at a raw TDX-built OVMF (TDVF) .fd binary.");
-                log_debug("Using raw TDX firmware at %s (no NVRAM, no Secure Boot).", arg_firmware);
                 /* Secure Boot state is baked into the supplied TDVF image and can't be enrolled at
                  * runtime (no writable NVRAM), so --secure-boot=yes would silently have no effect. */
                 if (set_contains(arg_firmware_features_include, "secure-boot"))
@@ -4252,9 +4281,9 @@ static int run(int argc, char *argv[]) {
                 _cleanup_(ovmf_config_freep) OvmfConfig *ovmf_config = NULL;
                 _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
 
-                r = find_ovmf_config(arg_firmware_features_include, arg_firmware_features_exclude, &ovmf_config, &json);
+                r = discover_ovmf_config(&ovmf_config, &json);
                 if (r < 0)
-                        return log_error_errno(r, "Failed to find OVMF config: %m");
+                        return r;
 
                 r = sd_json_variant_dump(json, SD_JSON_FORMAT_PRETTY|SD_JSON_FORMAT_COLOR_AUTO, stdout, /* prefix= */ NULL);
                 if (r < 0)