vmspawn: initial support for SEV-SNP guests (#42193)

This commit is contained in:
Lennart Poettering
2026-05-20 17:42:26 +02:00
committed by GitHub
4 changed files with 143 additions and 15 deletions
+24
View File
@@ -385,6 +385,30 @@
<xi:include href="version-info.xml" xpointer="v255"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--coco=</option></term>
<listitem><para>Caveat: This feature is experimental, and is likely to be changed (or removed in
its current form) in a future version of systemd.</para>
<para>Configures whether to run the guest as a confidential VM. Takes one of
<literal>no</literal> or <literal>sev-snp</literal>. Defaults to <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. SMBIOS credentials passed via <option>--set-credential=</option> or
<option>--load-credential=</option> are rejected because they are outside the SNP launch
measurement. 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. A vTPM, if attached via
<option>--tpm=</option>, must be treated as untrusted by the guest.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--grow-image=<replaceable>BYTES</replaceable></option></term>
<term><option>-G <replaceable>BYTES</replaceable></option></term>
+7
View File
@@ -36,3 +36,10 @@ static const char *const firmware_table[_FIRMWARE_MAX] = {
};
DEFINE_STRING_TABLE_LOOKUP(firmware, Firmware);
static const char *const confidential_computing_table[_COCO_MAX] = {
[COCO_NO] = "no",
[COCO_AMD_SEV_SNP] = "sev-snp",
};
DEFINE_STRING_TABLE_LOOKUP(confidential_computing, ConfidentialComputing);
+8
View File
@@ -42,6 +42,13 @@ typedef enum Firmware {
_FIRMWARE_INVALID = -EINVAL,
} Firmware;
typedef enum ConfidentialComputing {
COCO_NO,
COCO_AMD_SEV_SNP,
_COCO_MAX,
_COCO_INVALID = -EINVAL,
} ConfidentialComputing;
typedef enum SettingsMask {
SETTING_START_MODE = UINT64_C(1) << 0,
SETTING_MACHINE_ID = UINT64_C(1) << 6,
@@ -55,3 +62,4 @@ typedef enum SettingsMask {
DECLARE_STRING_TABLE_LOOKUP(console_mode, ConsoleMode);
DECLARE_STRING_TABLE_LOOKUP(console_transport, ConsoleTransport);
DECLARE_STRING_TABLE_LOOKUP(firmware, Firmware);
DECLARE_STRING_TABLE_LOOKUP(confidential_computing, ConfidentialComputing);
+104 -15
View File
@@ -162,6 +162,7 @@ static Firmware arg_firmware_type = _FIRMWARE_INVALID;
static bool arg_firmware_describe = false;
static Set *arg_firmware_features_include = NULL;
static Set *arg_firmware_features_exclude = NULL;
static ConfidentialComputing arg_confidential_computing = COCO_NO;
static char *arg_forward_journal = NULL;
static uint64_t arg_forward_journal_max_use = UINT64_MAX;
static uint64_t arg_forward_journal_keep_free = UINT64_MAX;
@@ -637,6 +638,14 @@ static int parse_argv(int argc, char *argv[]) {
break;
}
OPTION_LONG("coco", "no|sev-snp", "Run the guest as a confidential VM"): {
ConfidentialComputing cc = confidential_computing_from_string(opts.arg);
if (cc < 0)
return log_error_errno(cc, "Unknown --coco= value: %s", opts.arg);
arg_confidential_computing = cc;
break;
}
OPTION_LONG("discard-disk", "BOOL", "Control processing of discard requests"):
r = parse_boolean_argument("--discard-disk=", opts.arg, &arg_discard_disk);
if (r < 0)
@@ -2589,7 +2598,11 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
use_kvm = r;
}
if (arg_firmware_type == FIRMWARE_UEFI) {
if (arg_confidential_computing == COCO_AMD_SEV_SNP && !use_kvm)
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
"--coco=sev-snp requires KVM, but KVM is not available.");
if (arg_firmware_type == FIRMWARE_UEFI && arg_confidential_computing != COCO_AMD_SEV_SNP) {
if (arg_firmware)
r = load_ovmf_config(arg_firmware, &ovmf_config);
else
@@ -2663,13 +2676,19 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
if (r < 0)
return r;
if (arg_confidential_computing == COCO_AMD_SEV_SNP) {
r = qemu_config_key(config_file, "kernel-irqchip", "split");
if (r < 0)
return r;
}
if (ovmf_config && ARCHITECTURE_SUPPORTS_SMM) {
r = qemu_config_key(config_file, "smm", on_off(ovmf_config->supports_sb));
if (r < 0)
return r;
}
if (ARCHITECTURE_SUPPORTS_CXL) {
if (ARCHITECTURE_SUPPORTS_CXL && arg_confidential_computing == COCO_NO) {
r = qemu_config_key(config_file, "cxl", "on");
if (r < 0)
return r;
@@ -2687,6 +2706,12 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
return r;
}
if (arg_confidential_computing == COCO_AMD_SEV_SNP) {
r = qemu_config_key(config_file, "confidential-guest-support", "snp0");
if (r < 0)
return r;
}
r = qemu_config_section(config_file, "smp-opts", /* id= */ NULL,
"cpus", arg_cpus ?: "1");
if (r < 0)
@@ -2719,11 +2744,13 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
if (r < 0)
return r;
r = qemu_config_section(config_file, "device", "balloon0",
"driver", "virtio-balloon",
"free-page-reporting", "on");
if (r < 0)
return r;
if (arg_confidential_computing == COCO_NO) {
r = qemu_config_section(config_file, "device", "balloon0",
"driver", "virtio-balloon",
"free-page-reporting", "on");
if (r < 0)
return r;
}
if (ARCHITECTURE_SUPPORTS_VMGENID) {
sd_id128_t vmgenid;
@@ -2877,6 +2904,22 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
return r;
}
if (arg_confidential_computing == COCO_AMD_SEV_SNP) {
/* SNP marks encrypted guest pages via the "C-bit" in the page table entry. On all
* SNP-capable processors (Milan and later) the C-bit lives at bit 51, which reduces
* the usable guest physical address space by one bit.
* Embed the hashes of kernel, initrd and cmdline into the firmware
* so they are covered by the launch measurement and the guest's
* boot chain starts from a measured state. */
r = qemu_config_section(config_file, "object", "snp0",
"qom-type", "sev-snp-guest",
"cbitpos", "51",
"reduced-phys-bits", "1",
"kernel-hashes", "on");
if (r < 0)
return r;
}
unsigned child_cid = arg_vsock_cid;
if (use_vsock) {
config.vsock.fd = TAKE_FD(vhost_device_fd);
@@ -2894,14 +2937,17 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
config.vsock.cid = child_cid;
}
/* -cpu stays on cmdline since not all flags are supported in config */
r = strv_extend_many(&cmdline, "-cpu",
/* -cpu stays on cmdline since not all flags are supported in config. SNP needs a stable,
* named CPU model so the launch measurement is reproducible across hosts; EPYC-v4 is the
* baseline that covers all SNP-capable processors (Milan and later). */
const char *cpu_model =
#ifdef __x86_64__
"max,hv_relaxed,hv-vapic,hv-time"
arg_confidential_computing == COCO_AMD_SEV_SNP ? "EPYC-v4"
: "max,hv_relaxed,hv-vapic,hv-time";
#else
"max"
"max";
#endif
);
r = strv_extend_many(&cmdline, "-cpu", cpu_model);
if (r < 0)
return log_oom();
@@ -3032,9 +3078,15 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
}
_cleanup_(unlink_and_freep) char *ovmf_vars = NULL;
r = cmdline_add_ovmf(config_file, ovmf_config, &ovmf_vars);
if (r < 0)
return r;
if (arg_confidential_computing != COCO_NO) {
r = strv_extend_many(&cmdline, "-bios", arg_firmware);
if (r < 0)
return r;
} else {
r = cmdline_add_ovmf(config_file, ovmf_config, &ovmf_vars);
if (r < 0)
return r;
}
if (arg_linux) {
r = strv_extend_many(&cmdline, "-kernel", arg_linux);
@@ -4009,6 +4061,43 @@ 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_AMD_SEV_SNP) {
if (native_architecture() != ARCHITECTURE_X86_64)
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
"--coco=sev-snp is only supported on x86_64.");
if (arg_kvm == 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"--coco=sev-snp requires KVM, remove --kvm=no.");
if (arg_firmware_type != FIRMWARE_UEFI)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"--coco 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),
"--secure-boot=yes cannot be combined with --coco.");
if (arg_credentials.n_credentials != 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"SMBIOS credentials aren't trusted by the confidential computing guest and will be rejected.");
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.
* Without --linux= the kernel and initrd come off disk via OVMF and aren't part
* of the launch measurement, leaving the guest unattestable in any meaningful
* way. Require direct kernel boot so the boot chain starts from a measured state. */
if (!arg_linux)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"--coco=sev-snp requires --linux= "
"so kernel, initrd and cmdline are covered by the launch measurement.");
}
return 0;
}