]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
vmspawn: initial support for SEV-SNP guests
authorPaul Meyer <katexochen0@gmail.com>
Mon, 18 May 2026 05:50:34 +0000 (07:50 +0200)
committerPaul Meyer <katexochen0@gmail.com>
Wed, 20 May 2026 11:25:34 +0000 (13:25 +0200)
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 <noreply@anthropic.com>
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
man/systemd-vmspawn.xml
src/vmspawn/vmspawn-settings.c
src/vmspawn/vmspawn-settings.h
src/vmspawn/vmspawn.c

index d75993846f754d87b178857a5576ed999e7d220c..20be0c099b16d69aae7c7bc23278e8d097c79d61 100644 (file)
           <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>
index 502189e7bea630b6e4a78470c2d7ff8451a7f0ae..d05c4e1a9e1add0a3ac16b524c278d2da82c0c72 100644 (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);
index 596a66cecddb8a8a055a1e9c49fa3326be96ad33..9be3afdef4c42c927b230a54db0755c461f2ee92 100644 (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);
index 7d520bbad5ca2631b2e16e8b292e9396b7a1869d..e52e9d9852b8218bc37ac614e6a7b2df3d30bac9 100644 (file)
@@ -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;
 }