diff --git a/man/systemd-vmspawn.xml b/man/systemd-vmspawn.xml index d8afb9d0e1..2475a0464f 100644 --- a/man/systemd-vmspawn.xml +++ b/man/systemd-vmspawn.xml @@ -269,7 +269,8 @@ Takes an absolute path, or a relative path beginning with ./. Specifies the path to an EFI NVRAM template file to copy and use as the initial EFI variable NVRAM state. If not specified, the default NVRAM template from the firmware - definition is copied and used. + definition is copied and used. Cannot be used together with , as + confidential computing firmware is stateless. @@ -283,7 +284,8 @@ automatically derived from the VM image path or directory path, with the .efinvramstate suffix appended. If set to the special string off the EFI variable NVRAM state is only maintained transiently and flushed out - when the VM shuts down. Defaults to auto. + when the VM shuts down. Defaults to auto. An explicit path cannot be used + together with , as confidential computing firmware is stateless. If is specified, auto behaves like off. @@ -300,6 +302,13 @@ . Setting this to auto removes secure-boot from both the included and excluded feature lists. + With , setting this to yes additionally + requires the enrolled-keys firmware feature, overriding its default + exclusion: confidential computing firmware is stateless, so keys cannot be enrolled at runtime + and Secure Boot is only operative with keys baked into the image at build time, which in turn + makes the firmware refuse to boot unsigned images from the very first boot. The default + exclusion of enrolled-keys hence keeps unsigned images bootable. + @@ -317,8 +326,8 @@ boot. Booting a UKI requires uefi. If the special string list is specified, all discovered firmware definition files are listed. If the special string describe is specified, the UEFI firmware that would be selected (taking - into account) is printed and the program exits. If an empty - string is specified, the option is reset to its default. + and into account) is printed and + the program exits. If an empty string is specified, the option is reset to its default. @@ -350,31 +359,35 @@ no. sev-snp enables AMD SEV-SNP. This requires KVM on an x86_64 host with - SNP-capable hardware and firmware. must point to a raw SNP-built - OVMF .fd image; the standard pflash + NVRAM split is not supported under - SNP, so the firmware is loaded via QEMU's and Secure Boot is - unavailable. Direct kernel boot via is required so that the kernel, - initrd and command line are hashed into the launch measurement - (kernel-hashes=on); booting the kernel off the disk image via the firmware - would leave it outside the measurement. Credentials passed via - or are bundled into a cpio archive appended to the initrd - (mirroring what systemd-stub does for ESP credentials), so they enter the - launch measurement via kernel-hashes=on; 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 /.extra/system_credentials/). A vTPM, - if attached via , must be treated as untrusted by the guest. + SNP-capable hardware and firmware. A suitable SNP-built OVMF firmware is picked automatically + from the installed QEMU firmware descriptors, by requiring the amd-sev-snp + firmware feature; use with a path to a firmware descriptor file to + select a specific one. Secure Boot is effectively off by default (see + ). Direct kernel boot via + is required so that the kernel, initrd and command line are hashed + into the launch measurement (kernel-hashes=on); booting the kernel off the + disk image via the firmware would leave it outside the measurement. Credentials passed via + or are bundled into a + cpio archive appended to the initrd (mirroring what systemd-stub does for + ESP credentials), so they enter the launch measurement via kernel-hashes=on; + 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 + /.extra/system_credentials/). A vTPM, if attached via + , must be treated as untrusted by the guest. tdx enables Intel TDX. This requires KVM on an x86_64 host with - TDX-capable hardware and a TDX-enabled host kernel. As with sev-snp, - must point to a raw TDX-built OVMF (TDVF) .fd - image, which is loaded via QEMU's (pflash + NVRAM split is not - supported), and the CPU model is fixed to host. 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 sev-snp, a + TDX-built OVMF (TDVF) firmware is picked automatically from the installed QEMU firmware + descriptors, by requiring the intel-tdx firmware feature; use + with a path to a firmware descriptor file to select a specific + one. The CPU model is fixed to host. 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 (see + ). When booting a UKI, the whole UKI PE is measured into RTMR 1, and the loaded sections are measured individually by systemd-stub into RTMR 2. For direct linux boot, firmware measures the kernel PE into RTMR 1, and the Linux EFI stub measures initrd and @@ -384,7 +397,7 @@ to the host or VMM, since the host assembles the SMBIOS table. A vTPM, if attached via , 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 /run/tdx-qgs/qgs.socket is used if it + automatically: the unix socket /run/tdx-qgs/qgs.socket 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. diff --git a/src/vmspawn/test-vmspawn-util.c b/src/vmspawn/test-vmspawn-util.c index d487cbcacb..21d22e9161 100644 --- a/src/vmspawn/test-vmspawn-util.c +++ b/src/vmspawn/test-vmspawn-util.c @@ -1,8 +1,17 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include + +#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,188 @@ 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(ovmf_config_has_feature(config, "secure-boot")); + 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(ovmf_config_has_feature(config, "secure-boot")); + 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", &config); + ASSERT_TRUE(ovmf_config_has_feature(config, "vmspawn-test-e1")); + ASSERT_FALSE(ovmf_config_has_feature(config, "vmspawn-test-e2")); + config = ovmf_config_free(config); + + /* 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", &config); + ASSERT_TRUE(ovmf_config_has_feature(config, "vmspawn-test-e1")); + ASSERT_TRUE(ovmf_config_has_feature(config, "vmspawn-test-e2")); + config = ovmf_config_free(config); + + /* 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); diff --git a/src/vmspawn/vmspawn-util.c b/src/vmspawn/vmspawn-util.c index 8ecd266000..03fef0f1b8 100644 --- a/src/vmspawn/vmspawn-util.c +++ b/src/vmspawn/vmspawn-util.c @@ -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,36 @@ OvmfConfig* ovmf_config_free(OvmfConfig *config) { free(config->format); free(config->vars); free(config->vars_format); + free(config->device); + free(config->mode); + strv_free(config->features); return mfree(config); } +bool ovmf_config_has_feature(const OvmfConfig *config, const char *feature) { + assert(config); + assert(feature); + + return strv_contains(config->features, feature); +} + +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 +167,8 @@ typedef struct FirmwareData { char *firmware_format; char *vars; char *vars_format; + char *device; + char *mode; FirmwareTarget **targets; size_t n_targets; } FirmwareData; @@ -164,12 +193,6 @@ static bool firmware_data_matches_machine(const FirmwareData *fwd, const char *a return false; } -static bool firmware_data_supports_sb(const FirmwareData *fwd) { - assert(fwd); - - return strv_contains(fwd->features, "secure-boot"); -} - static FirmwareData* firmware_data_free(FirmwareData *fwd) { if (!fwd) return NULL; @@ -180,6 +203,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 +232,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,7 +447,9 @@ 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), - .supports_sb = firmware_data_supports_sb(fwd), + .device = TAKE_PTR(fwd->device), + .mode = TAKE_PTR(fwd->mode), + .features = TAKE_PTR(fwd->features), }; *ret = TAKE_PTR(config); @@ -430,6 +473,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 +509,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 diff --git a/src/vmspawn/vmspawn-util.h b/src/vmspawn/vmspawn-util.h index 38bb331dfc..8d1911663c 100644 --- a/src/vmspawn/vmspawn-util.h +++ b/src/vmspawn/vmspawn-util.h @@ -99,7 +99,9 @@ typedef struct OvmfConfig { char *format; char *vars; char *vars_format; - bool supports_sb; + char *device; + char *mode; + char **features; } OvmfConfig; static inline const char* ovmf_config_format(const OvmfConfig *c) { @@ -110,6 +112,9 @@ 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); +bool ovmf_config_has_feature(const OvmfConfig *config, const char *feature); + OvmfConfig* ovmf_config_free(OvmfConfig *ovmf_config); DEFINE_TRIVIAL_CLEANUP_FUNC(OvmfConfig*, ovmf_config_free); @@ -134,7 +139,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); diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index f116499450..2a3ee83512 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -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" @@ -2542,6 +2543,54 @@ 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(); + + /* CoCo firmware is stateless, so Secure Boot keys cannot be enrolled at runtime + * but must have baked-in, enrolled keys, which we avoid in other cases. */ + if (set_contains(arg_firmware_features_include, "secure-boot")) { + r = set_put_strdup(&arg_firmware_features_include, "enrolled-keys"); + 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, select a firmware descriptor with --firmware=, or " + "opt in with --secure-boot=yes if the installed firmware enforces Secure Boot.", + 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; @@ -2608,19 +2657,53 @@ 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) + /* Flash mode "combined" places the variable store in the (writable) executable, + * which would have to be cloned for each guest. */ + if (streq_ptr(ovmf_config->mode, "combined")) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "Firmware descriptor '%s' declares flash mode 'combined', which is not supported.", + arg_firmware ?: ovmf_config->path); + + if (arg_confidential_computing != COCO_NO) { + const char *coco_feature = coco_firmware_feature_to_string(arg_confidential_computing); + + if (!ovmf_config_has_feature(ovmf_config, coco_feature)) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), + "Firmware descriptor '%s' does not declare the '%s' feature, but " + "--coco=%s requires firmware built specifically for it.", + arg_firmware ?: ovmf_config->path, coco_feature, + confidential_computing_to_string(arg_confidential_computing)); + if (!ovmf_config_is_stateless(ovmf_config)) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), + "Firmware descriptor '%s' does not describe stateless firmware, " + "but --coco=%s requires stateless firmware.", + arg_firmware ?: ovmf_config->path, + confidential_computing_to_string(arg_confidential_computing)); + if (!streq(ovmf_config_format(ovmf_config), "raw")) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), + "Firmware image '%s' is in %s format, " + "but --coco=%s requires a raw image.", + ovmf_config->path, ovmf_config_format(ovmf_config), + confidential_computing_to_string(arg_confidential_computing)); + } + + bool sb = ovmf_config_has_feature(ovmf_config, "secure-boot"); + if (set_contains(arg_firmware_features_include, "secure-boot") && !sb) return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Secure Boot requested, but selected OVMF firmware doesn't support it."); - log_debug("Using OVMF firmware %s Secure Boot support.", ovmf_config->supports_sb ? "with" : "without"); + log_debug("Using OVMF firmware %s Secure Boot support.", sb ? "with" : "without"); } _cleanup_(machine_bind_user_context_freep) MachineBindUserContext *bind_user_context = NULL; @@ -2683,8 +2766,9 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { return r; } - if (ovmf_config && ARCHITECTURE_SUPPORTS_SMM) { - r = qemu_config_key(config_file, "smm", on_off(ovmf_config->supports_sb)); + if (ovmf_config && ARCHITECTURE_SUPPORTS_SMM && arg_confidential_computing == COCO_NO) { + bool sb = ovmf_config_has_feature(ovmf_config, "secure-boot"); + r = qemu_config_key(config_file, "smm", on_off(sb)); if (r < 0) return r; } @@ -3126,9 +3210,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 { @@ -4142,6 +4228,19 @@ static int verify_arguments(void) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--grow-image is not supported for qcow2 images, use 'qemu-img resize FILE SIZE'."); + if (arg_confidential_computing != COCO_NO) { + /* Confidential computing firmware is stateless, there is no NVRAM to instantiate from a + * template or to persist. */ + if (arg_efi_nvram_template) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "--efi-nvram-template= cannot be used with --coco=, " + "confidential computing firmware is stateless."); + if (arg_efi_nvram_state_mode == STATE_PATH) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "An explicit --efi-nvram-state= path cannot be used with --coco=, " + "confidential computing firmware is stateless. Use 'off' or 'auto'."); + } + if (arg_confidential_computing == COCO_AMD_SEV_SNP) { if (native_architecture() != ARCHITECTURE_X86_64) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), @@ -4153,17 +4252,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."); if (arg_tpm > 0) log_warning("TPM can't be trusted by the confidential computing guest"); /* kernel-hashes=on only covers what QEMU itself loads via -kernel/-initrd/-append. @@ -4187,19 +4275,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")) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "--coco=tdx cannot be combined with --secure-boot=yes."); if (arg_tpm > 0) log_warning("TPM can't be trusted by the confidential computing guest"); } @@ -4227,9 +4302,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)