From 4f8215add24ac9018fb68399f0b957cf0eb3b0c6 Mon Sep 17 00:00:00 2001 From: Paul Meyer Date: Mon, 18 May 2026 07:50:34 +0200 Subject: [PATCH 1/2] vmspawn: initial support for SEV-SNP guests Add --confidential-computing=sev-snp to run the guest as an AMD SEV-SNP confidential VM. Loads a raw OVMF firmware blob via -bios (SNP doesn't support the pflash + NVRAM split), attaches a sev-snp-guest object, and hashes the kernel, initrd and cmdline into the launch measurement when direct kernel boot is used. Incompatible features (Secure Boot, CXL, virtio-balloon, SMBIOS credentials) are rejected or disabled; an attached vTPM must be treated as untrusted by the guest. The feature is marked experimental in the man page. Co-developed-by: Claude Opus 4.7 Signed-off-by: Paul Meyer --- man/systemd-vmspawn.xml | 24 ++++++++ src/vmspawn/vmspawn-settings.c | 7 +++ src/vmspawn/vmspawn-settings.h | 8 +++ src/vmspawn/vmspawn.c | 106 +++++++++++++++++++++++++++++---- 4 files changed, 135 insertions(+), 10 deletions(-) diff --git a/man/systemd-vmspawn.xml b/man/systemd-vmspawn.xml index d75993846f..20be0c099b 100644 --- a/man/systemd-vmspawn.xml +++ b/man/systemd-vmspawn.xml @@ -385,6 +385,30 @@ + + + + Caveat: This feature is experimental, and is likely to be changed (or removed in + its current form) in a future version of systemd. + + Configures whether to run the guest as a confidential VM. Takes one of + no or sev-snp. Defaults to 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. SMBIOS credentials passed via or + are rejected because they are outside the SNP launch + measurement. 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. A vTPM, if attached via + , must be treated as untrusted by the guest. + + + + diff --git a/src/vmspawn/vmspawn-settings.c b/src/vmspawn/vmspawn-settings.c index 502189e7be..d05c4e1a9e 100644 --- a/src/vmspawn/vmspawn-settings.c +++ b/src/vmspawn/vmspawn-settings.c @@ -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); diff --git a/src/vmspawn/vmspawn-settings.h b/src/vmspawn/vmspawn-settings.h index 596a66cecd..9be3afdef4 100644 --- a/src/vmspawn/vmspawn-settings.h +++ b/src/vmspawn/vmspawn-settings.h @@ -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); diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index 7d520bbad5..e52e9d9852 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -161,6 +161,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; @@ -644,6 +645,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) @@ -2596,7 +2605,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 @@ -2670,13 +2683,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; @@ -2694,6 +2713,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) @@ -2726,11 +2751,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; @@ -2884,6 +2911,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); @@ -3039,9 +3082,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); @@ -4016,6 +4065,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; } From 1ccda1533d6b7880495d194f41112a9eb1c8ad42 Mon Sep 17 00:00:00 2001 From: Paul Meyer Date: Tue, 19 May 2026 13:56:46 +0200 Subject: [PATCH 2/2] vmspawn: use EPYC-v4 cpu for SNP SNP requires a named, stable CPU model so the launch measurement is reproducible across hosts. EPYC-v4 is the baseline that covers all SNP-capable processors (Milan and later). Signed-off-by: Paul Meyer --- src/vmspawn/vmspawn.c | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index e52e9d9852..73f2a19eef 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -2944,14 +2944,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();