]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
vmspawn: add Intel TDX confidential VM support
authorPaul Meyer <katexochen0@gmail.com>
Fri, 26 Jun 2026 09:57:29 +0000 (11:57 +0200)
committerPaul Meyer <katexochen0@gmail.com>
Wed, 1 Jul 2026 12:35:25 +0000 (14:35 +0200)
Wire up --coco=tdx alongside the existing SEV-SNP path. TDX requires KVM
on x86_64, a raw TDVF firmware loaded via -bios (no pflash/NVRAM split),
kernel-irqchip=split, and the "host" CPU model since QEMU rejects named
models. Sets up the tdx-guest object and confidential-guest-support=tdx0.

TDX measurement is different from QEMU's kernel-hashes injection: TDX
provides runtime measurements via RTMRs, so the initial measurement only
covers the firmware, which then measures the rest of the boot chain into
those RTMRs (done by OVMF today). Therefore a restriction to direct
kernel boot isn't required either.

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 fd73e0077075ef4f73010118369def2f8cd3af94..66c49352a9a396c00bceeb87eb4a4005296ca80e 100644 (file)
           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>
+          <literal>no</literal>, <literal>sev-snp</literal> or <literal>tdx</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
           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.
+          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
+          command line into RTMR 2. A vTPM, if attached via <option>--tpm=</option>, must be treated
+          as untrusted by the guest.
+          <xi:include href="version-info.xml" xpointer="v262"/></para>
+
           <xi:include href="version-info.xml" xpointer="v261"/></listitem>
         </varlistentry>
       </variablelist>
index d05c4e1a9e1add0a3ac16b524c278d2da82c0c72..043920c36ca5b06d18059012daa37ab09f57dc30 100644 (file)
@@ -40,6 +40,7 @@ DEFINE_STRING_TABLE_LOOKUP(firmware, Firmware);
 static const char *const confidential_computing_table[_COCO_MAX] = {
         [COCO_NO]          = "no",
         [COCO_AMD_SEV_SNP] = "sev-snp",
+        [COCO_INTEL_TDX]   = "tdx",
 };
 
 DEFINE_STRING_TABLE_LOOKUP(confidential_computing, ConfidentialComputing);
index 9be3afdef4c42c927b230a54db0755c461f2ee92..5f488337f93792157dc091ea5c8397f78eda6cee 100644 (file)
@@ -45,6 +45,7 @@ typedef enum Firmware {
 typedef enum ConfidentialComputing {
         COCO_NO,
         COCO_AMD_SEV_SNP,
+        COCO_INTEL_TDX,
         _COCO_MAX,
         _COCO_INVALID = -EINVAL,
 } ConfidentialComputing;
index 6e2329f7f9c3507c1d33eb167952a050f266d47c..41cf611aa93bbf962a0f0b23b87fa63251cb5544 100644 (file)
@@ -634,7 +634,7 @@ static int parse_argv(int argc, char *argv[]) {
                         break;
                 }
 
-                OPTION_LONG("coco", "no|sev-snp", "Run the guest as a confidential VM"): {
+                OPTION_LONG("coco", "no|sev-snp|tdx", "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);
@@ -2607,11 +2607,11 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
                 use_kvm = r;
         }
 
-        if (arg_confidential_computing == COCO_AMD_SEV_SNP && !use_kvm)
+        if (arg_confidential_computing != COCO_NO && !use_kvm)
                 return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
-                                       "--coco=sev-snp requires KVM, but KVM is not available.");
+                                       "--coco= requires KVM, but KVM is not available.");
 
-        if (arg_firmware_type == FIRMWARE_UEFI && arg_confidential_computing != COCO_AMD_SEV_SNP) {
+        if (arg_firmware_type == FIRMWARE_UEFI && arg_confidential_computing == COCO_NO) {
                 if (arg_firmware)
                         r = load_ovmf_config(arg_firmware, &ovmf_config);
                 else
@@ -2685,7 +2685,7 @@ 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) {
+        if (arg_confidential_computing != COCO_NO) {
                 r = qemu_config_key(config_file, "kernel-irqchip", "split");
                 if (r < 0)
                         return r;
@@ -2719,6 +2719,10 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
                 r = qemu_config_key(config_file, "confidential-guest-support", "snp0");
                 if (r < 0)
                         return r;
+        } else if (arg_confidential_computing == COCO_INTEL_TDX) {
+                r = qemu_config_key(config_file, "confidential-guest-support", "tdx0");
+                if (r < 0)
+                        return r;
         }
 
         r = qemu_config_section(config_file, "smp-opts", /* id= */ NULL,
@@ -2927,6 +2931,11 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
                                         "kernel-hashes", "on");
                 if (r < 0)
                         return r;
+        } else if (arg_confidential_computing == COCO_INTEL_TDX) {
+                r = qemu_config_section(config_file, "object", "tdx0",
+                                        "qom-type", "tdx-guest");
+                if (r < 0)
+                        return r;
         }
 
         unsigned child_cid = arg_vsock_cid;
@@ -2948,11 +2957,13 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
 
         /* -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). */
+         * baseline that covers all SNP-capable processors (Milan and later). TDX requires host
+         * CPU model. */
         const char *cpu_model =
 #ifdef __x86_64__
-                arg_confidential_computing == COCO_AMD_SEV_SNP ? "EPYC-v4"
-                                                             : "max,hv_relaxed,hv-vapic,hv-time";
+                arg_confidential_computing == COCO_AMD_SEV_SNP ? "EPYC-v4" :
+                arg_confidential_computing == COCO_INTEL_TDX   ? "host"    :
+                                                                 "max,hv_relaxed,hv-vapic,hv-time";
 #else
                 "max";
 #endif
@@ -4128,7 +4139,7 @@ static int verify_arguments(void) {
                                                "--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",
+                                               "--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
@@ -4140,7 +4151,7 @@ static int verify_arguments(void) {
                 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.");
+                                               "--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.
@@ -4153,6 +4164,34 @@ static int verify_arguments(void) {
                                                "so kernel, initrd and cmdline are covered by the launch measurement.");
         }
 
+        if (arg_confidential_computing == COCO_INTEL_TDX) {
+                if (native_architecture() != ARCHITECTURE_X86_64)
+                        return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+                                               "--coco=tdx is only supported on x86_64.");
+                if (arg_kvm == 0)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                               "--coco=tdx requires KVM, remove --kvm=no.");
+                if (arg_firmware_type != FIRMWARE_UEFI)
+                        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");
+        }
+
         return 0;
 }