vmspawn: add Intel TDX confidential VM support (#42835)

This commit is contained in:
Lennart Poettering
2026-07-01 22:14:09 +02:00
committed by GitHub
6 changed files with 128 additions and 13 deletions
+23 -1
View File
@@ -346,7 +346,8 @@
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
@@ -367,6 +368,27 @@
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. Credentials passed via <option>--set-credential=</option> or
<option>--load-credential=</option> are delivered through SMBIOS Type 11 OEM strings, which the
firmware (TDVF) measures into RTMR 0. This channel is measured but not confidential with respect
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
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>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
</variablelist>
+8 -1
View File
@@ -622,7 +622,14 @@ static int import_credentials_smbios(ImportCredentialsContext *c) {
if (detect_container() > 0) /* don't access /sys/ in a container */
return 0;
if (detect_confidential_virtualization() > 0) /* don't trust firmware if confidential VMs */
/* SMBIOS OEM strings are host-controlled, so by default we don't trust them in a confidential VM.
* Exception: under Intel TDX the SMBIOS table (including Type 11) is measured into RTMR0 by the
* firmware (TDVF's SmbiosMeasurementDxe) on direct boot, and additionally by systemd-stub on UKI
* boot, so a remote verifier can detect host tampering with these credentials. As with the kernel
* command line (also host-influenced but measured), we can't and needn't locally verify the
* measurement actually happened; the verifier anchors the firmware's behaviour via MRTD. */
ConfidentialVirtualization cv = detect_confidential_virtualization();
if (cv > 0 && cv != CONFIDENTIAL_VIRTUALIZATION_TDX)
return 0;
for (unsigned i = 0;; i++) {
+8
View File
@@ -142,6 +142,14 @@ static int tsm_report_fill(
if (r < 0)
return log_debug_errno(r, "Failed to read 'provider' attribute: %m");
/* A zero-length outblob is never a valid attestation report, e.g. under Intel TDX the kernel
* yields an empty quote when the GetQuote round-trip to the host's Quote Generation Service
* produced nothing. */
if (!iovec_is_set(&report->outblob))
return log_debug_errno(SYNTHETIC_ERRNO(ENODATA),
"TSM provider '%s' returned an empty 'outblob'; no attestation report available.",
report->provider);
r = read_full_file_at(entry_fd, "auxblob",
(char**) &report->auxblob.iov_base, &report->auxblob.iov_len);
if (r < 0 && r != -ENOENT) /* auxblob is optional */
+1
View 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);
+1
View 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;
+87 -11
View File
@@ -105,6 +105,13 @@
#define DISK_SERIAL_MAX_LEN_NVME 20
#define DISK_SERIAL_MAX_LEN_VIRTIO_BLK 20
/* Well-known endpoints for the host's TDX Quote Generation Service (qgsd), auto-discovered so the
* guest can obtain TD Quotes. The Intel reference qgsd listens on a unix socket; the common
* deployment listens on vsock port 4050 (cid 2 = host). */
#define TDX_QGS_UNIX_SOCKET_PATH "/run/tdx-qgs/qgs.socket"
#define TDX_QGS_VSOCK_CID "2"
#define TDX_QGS_VSOCK_PORT "4050"
/* First and one-past-last pcie.0 device-numbers used for multifunction-packed
* pcie-root-ports. Sits above the auto-assigned virtio devices (0x01-0x03) and
* below 0x1f, which q35 reserves for ICH9 LPC at 0x1f.0 (single-function). */
@@ -634,7 +641,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 +2614,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 +2692,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 +2726,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 +2938,41 @@ 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;
/* The guest needs a connection to the TDX Quote Generation Service (QGS) running on
* the host to obtain TD Quotes (e.g. via the systemd-report-sign-tsm). There are two
* well-known interfaces for QGS: a well-known unix socket, or vsock port 4050. QEMU
* only connects on a GetQuote request, and pointing at an absent QGS is harmless,
* the request just fails and the guest gets no quote. */
r = is_socket(TDX_QGS_UNIX_SOCKET_PATH);
if (r < 0)
log_debug_errno(r, "Failed to check for QGS socket '%s', falling back to vsock: %m", TDX_QGS_UNIX_SOCKET_PATH);
if (r > 0) {
r = qemu_config_key(config_file, "quote-generation-socket.type", "unix");
if (r < 0)
return r;
r = qemu_config_key(config_file, "quote-generation-socket.path", TDX_QGS_UNIX_SOCKET_PATH);
if (r < 0)
return r;
log_debug("Using TDX Quote Generation Service at unix socket %s.", TDX_QGS_UNIX_SOCKET_PATH);
} else {
r = qemu_config_key(config_file, "quote-generation-socket.type", "vsock");
if (r < 0)
return r;
r = qemu_config_key(config_file, "quote-generation-socket.cid", TDX_QGS_VSOCK_CID);
if (r < 0)
return r;
r = qemu_config_key(config_file, "quote-generation-socket.port", TDX_QGS_VSOCK_PORT);
if (r < 0)
return r;
log_debug("No QGS unix socket found, pointing TDX quote generation at vsock %s:%s.",
TDX_QGS_VSOCK_CID, TDX_QGS_VSOCK_PORT);
}
}
unsigned child_cid = arg_vsock_cid;
@@ -2948,11 +2994,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
@@ -3531,7 +3579,7 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
* confidential VMs. Instead, package credentials into a cpio archive appended to the initrd
* (mirroring what systemd-stub does for ESP credentials) so they enter the launch measurement
* via QEMU's "kernel-hashes=on". The new initrd path requires a guest PID1 that knows about
* /.extra/system_credentials/, so we keep this scoped to SNP for now. Non-CoCo guests
* /.extra/system_credentials/, so we keep this scoped to SNP for now. Non-SNP guests
* continue to use the SMBIOS path below, which works with older systemd versions too.
* Must run after all credential-mutating calls above so the cpio captures the complete set. */
bool use_initrd_cpio = arg_confidential_computing == COCO_AMD_SEV_SNP &&
@@ -4128,7 +4176,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 +4188,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 +4201,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;
}