vmspawn: select coco firmware via fw descriptors
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>
This commit is contained in:
+28
-26
@@ -317,8 +317,8 @@
|
||||
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>
|
||||
@@ -350,31 +350,33 @@
|
||||
<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
|
||||
@@ -384,7 +386,7 @@
|
||||
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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
+56
-27
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user