From a2773b531457bb98b28b9f42b382679875f5fd2f Mon Sep 17 00:00:00 2001 From: "Liang, Zhou" Date: Fri, 18 Sep 2026 19:04:30 +0530 Subject: [PATCH] hypervisor: Allow configuring TDX measurement registers KVM_TDX_INIT_VM carries three tenant/owner measurement-configuration digests -- MRCONFIGID, MROWNER and MROWNERCONFIG -- that are baked into the TD's attestation report. Cloud Hypervisor previously hardcoded all three to zero, so a tenant could not bind non-owner configuration or owner identity into the measurement, unlike QEMU which exposes them. Add three optional `--platform` options (mrconfigid, mrowner, mrownerconfig) accepting 96-character hex SHA384 digests. Signed-off-by: Fan Du Signed-off-by: Liang, Zhou --- docs/intel_tdx.md | 24 +++++++++ hypervisor/src/kvm/mod.rs | 15 ++++-- hypervisor/src/kvm/tdx.rs | 10 ++-- hypervisor/src/vm.rs | 9 +++- vmm/Cargo.toml | 2 +- vmm/src/config.rs | 111 ++++++++++++++++++++++++++++++++++++++ vmm/src/vm.rs | 10 +++- vmm/src/vm_config.rs | 13 +++++ 8 files changed, 184 insertions(+), 10 deletions(-) diff --git a/docs/intel_tdx.md b/docs/intel_tdx.md index efa4007c2..0a82d73da 100644 --- a/docs/intel_tdx.md +++ b/docs/intel_tdx.md @@ -189,3 +189,27 @@ devices responsible for handling PCI hotplug (PCI hotplug controller, PCI Express Bus and Generic Event Device) will not be allowed, therefore the corresponding drivers will not be loaded and the PCI hotplug feature will not be supported. + +## Measurement configuration registers + +A TD carries three owner/configuration measurement registers that are baked +into its attestation report: `MRCONFIGID`, `MROWNER` and `MROWNERCONFIG`. Each +is a 384-bit (48-byte) SHA384 digest chosen by the tenant or platform owner. + +They can be supplied through the matching `--platform` options as hex strings +of exactly 96 characters (48 bytes): + +```bash +./cloud-hypervisor \ + --platform tdx=on,mrconfigid=<96-hex-chars>,mrowner=<96-hex-chars>,mrownerconfig=<96-hex-chars> \ + --firmware td-shim/target/release/final.bin \ + --kernel bzImage \ + --cmdline "root=/dev/vda3 console=hvc0 rw" \ + --cpus boot=1 \ + --memory size=1G \ + --disk path=tdx_guest_img +``` + +Any register left unset defaults to all zeros, preserving the previous +behavior. + diff --git a/hypervisor/src/kvm/mod.rs b/hypervisor/src/kvm/mod.rs index 26631190f..b7595f6bf 100644 --- a/hypervisor/src/kvm/mod.rs +++ b/hypervisor/src/kvm/mod.rs @@ -1031,7 +1031,14 @@ impl vm::Vm for KvmVm { /// Initialize TDX for this VM /// #[cfg(feature = "tdx")] - fn tdx_init(&self, cpuid: &[CpuIdEntry], max_vcpus: u32) -> vm::Result<()> { + fn tdx_init( + &self, + cpuid: &[CpuIdEntry], + max_vcpus: u32, + mrconfigid: &[u8; 48], + mrowner: &[u8; 48], + mrownerconfig: &[u8; 48], + ) -> vm::Result<()> { let tdx_capabilities = self.tdx_capabilities()?; // `KVM_TDX_INIT_VM` only accepts the configurable leaves reported by @@ -1063,9 +1070,9 @@ impl vm::Vm for KvmVm { let data = KvmTdxInitVm { attributes, xfam, - mrconfigid: [0; 6], - mrowner: [0; 6], - mrownerconfig: [0; 6], + mrconfigid: *mrconfigid, + mrowner: *mrowner, + mrownerconfig: *mrownerconfig, reserved: [0; 12], cpuid: kvm_cpuid2 { nent: filtered_cpuid.len() as u32, diff --git a/hypervisor/src/kvm/tdx.rs b/hypervisor/src/kvm/tdx.rs index 1fef9aea2..0c829ea01 100644 --- a/hypervisor/src/kvm/tdx.rs +++ b/hypervisor/src/kvm/tdx.rs @@ -88,9 +88,13 @@ pub(crate) struct KvmTdxCmd { pub(crate) struct KvmTdxInitVm { pub attributes: u64, pub xfam: u64, - pub mrconfigid: [u64; 6], - pub mrowner: [u64; 6], - pub mrownerconfig: [u64; 6], + // The kernel treats mrconfigid/mrowner/mrownerconfig as raw 48-byte SHA384 + // digests (declared `__u64[6]`, populated by a byte copy), so store them + // as byte arrays to forward the caller's values verbatim without any + // endianness conversion. + pub mrconfigid: [u8; 48], + pub mrowner: [u8; 48], + pub mrownerconfig: [u8; 48], pub reserved: [u64; 12], pub cpuid: kvm_cpuid2, } diff --git a/hypervisor/src/vm.rs b/hypervisor/src/vm.rs index 2d1cbb20d..2c1505e11 100644 --- a/hypervisor/src/vm.rs +++ b/hypervisor/src/vm.rs @@ -408,7 +408,14 @@ pub trait Vm: Send + Sync + Any { } #[cfg(feature = "tdx")] /// Initialize TDX on this VM - fn tdx_init(&self, _cpuid: &[CpuIdEntry], _max_vcpus: u32) -> Result<()> { + fn tdx_init( + &self, + _cpuid: &[CpuIdEntry], + _max_vcpus: u32, + _mrconfigid: &[u8; 48], + _mrowner: &[u8; 48], + _mrownerconfig: &[u8; 48], + ) -> Result<()> { unimplemented!() } #[cfg(feature = "tdx")] diff --git a/vmm/Cargo.toml b/vmm/Cargo.toml index 0f326d488..0e452d68c 100644 --- a/vmm/Cargo.toml +++ b/vmm/Cargo.toml @@ -31,7 +31,7 @@ mshv = [ ] pvmemcontrol = ["devices/pvmemcontrol"] sev_snp = ["arch/sev_snp", "hypervisor/sev_snp", "virtio-devices/sev_snp"] -tdx = ["arch/tdx", "hypervisor/tdx"] +tdx = ["arch/tdx", "hex", "hypervisor/tdx"] tracing = ["tracer/tracing"] [dependencies] diff --git a/vmm/src/config.rs b/vmm/src/config.rs index 0742100da..c6bbdd232 100644 --- a/vmm/src/config.rs +++ b/vmm/src/config.rs @@ -279,6 +279,14 @@ pub enum ValidationError { #[cfg(feature = "tdx")] #[error("No TDX firmware specified")] TdxFirmwareMissing, + /// Invalid TDX measurement-configuration digest + #[cfg(feature = "tdx")] + #[error("TDX '{0}' must be a 96-character (48-byte) hex SHA384 digest: {1}")] + TdxInvalidMeasurement(&'static str, String), + /// TDX measurement-configuration digest specified without enabling TDX + #[cfg(feature = "tdx")] + #[error("mrconfigid/mrowner/mrownerconfig require 'tdx=on'")] + TdxMeasurementWithoutTdx, /// Insufficient vCPUs for queues #[error("Queue count ({0}) must not exceed boot vCPUs ({1})")] TooManyQueues(usize /* queues */, usize /* vCPUs */), @@ -809,6 +817,12 @@ impl PlatformConfig { .add("oem_strings"); #[cfg(feature = "tdx")] parser.add("tdx"); + #[cfg(feature = "tdx")] + parser.add("mrconfigid"); + #[cfg(feature = "tdx")] + parser.add("mrowner"); + #[cfg(feature = "tdx")] + parser.add("mrownerconfig"); #[cfg(feature = "sev_snp")] parser.add("sev_snp"); parser.parse(platform).map_err(Error::ParsePlatform)?; @@ -839,6 +853,18 @@ impl PlatformConfig { .map_err(Error::ParsePlatform)? .unwrap_or(Toggle(false)) .0; + #[cfg(feature = "tdx")] + let tdx_mrconfigid = parser + .convert::("mrconfigid") + .map_err(Error::ParsePlatform)?; + #[cfg(feature = "tdx")] + let tdx_mrowner = parser + .convert::("mrowner") + .map_err(Error::ParsePlatform)?; + #[cfg(feature = "tdx")] + let tdx_mrownerconfig = parser + .convert::("mrownerconfig") + .map_err(Error::ParsePlatform)?; #[cfg(feature = "sev_snp")] let sev_snp = parser .convert::("sev_snp") @@ -854,11 +880,38 @@ impl PlatformConfig { oem_strings, #[cfg(feature = "tdx")] tdx, + #[cfg(feature = "tdx")] + tdx_mrconfigid, + #[cfg(feature = "tdx")] + tdx_mrowner, + #[cfg(feature = "tdx")] + tdx_mrownerconfig, #[cfg(feature = "sev_snp")] sev_snp, }) } + /// Decode the optional TDX SHA384 measurement-configuration digests + /// (`mrconfigid`, `mrowner`, `mrownerconfig`) from their hex string form + /// into 48-byte arrays. Any register left unset decodes to all zeros. + #[cfg(feature = "tdx")] + pub fn tdx_measurements(&self) -> ValidationResult<([u8; 48], [u8; 48], [u8; 48])> { + fn decode(name: &'static str, value: &Option) -> ValidationResult<[u8; 48]> { + let mut out = [0u8; 48]; + if let Some(s) = value { + hex::decode_to_slice(s, &mut out) + .map_err(|e| ValidationError::TdxInvalidMeasurement(name, e.to_string()))?; + } + Ok(out) + } + + Ok(( + decode("mrconfigid", &self.tdx_mrconfigid)?, + decode("mrowner", &self.tdx_mrowner)?, + decode("mrownerconfig", &self.tdx_mrownerconfig)?, + )) + } + pub fn validate(&self) -> ValidationResult<()> { if self.num_pci_segments == 0 || self.num_pci_segments > MAX_NUM_PCI_SEGMENTS { return Err(ValidationError::InvalidNumPciSegments( @@ -2873,6 +2926,15 @@ impl VmConfig { if tdx_enabled && (self.cpus.max_vcpus != self.cpus.boot_vcpus) { return Err(ValidationError::TdxNoCpuHotplug); } + let has_tdx_measurements = self.platform.as_ref().is_some_and(|p| { + p.tdx_mrconfigid.is_some() || p.tdx_mrowner.is_some() || p.tdx_mrownerconfig.is_some() + }); + if has_tdx_measurements && !tdx_enabled { + return Err(ValidationError::TdxMeasurementWithoutTdx); + } + if tdx_enabled { + self.platform.as_ref().unwrap().tdx_measurements()?; + } if tdx_enabled { // For TDX the guest physical address width (GPAW) is fixed at: // 48 - (GPAW-48, 4-level EPT) @@ -4415,6 +4477,38 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}" Ok(()) } + #[cfg(feature = "tdx")] + #[test] + fn test_platform_tdx_measurements() -> Result<()> { + // Unset registers decode to all zeros. + let p = PlatformConfig::parse("tdx=on")?; + assert_eq!(p.tdx_measurements(), Ok(([0u8; 48], [0u8; 48], [0u8; 48]))); + + // A valid 96-character hex digest decodes into the matching register. + let hexid = "01".repeat(48); + let p = PlatformConfig::parse(&format!("tdx=on,mrconfigid={hexid}"))?; + let (mrconfigid, mrowner, mrownerconfig) = p.tdx_measurements().unwrap(); + assert_eq!(mrconfigid, [1u8; 48]); + assert_eq!(mrowner, [0u8; 48]); + assert_eq!(mrownerconfig, [0u8; 48]); + + // Wrong length is rejected by validation. + let p = PlatformConfig::parse("tdx=on,mrowner=00")?; + assert!(matches!( + p.tdx_measurements(), + Err(ValidationError::TdxInvalidMeasurement("mrowner", _)) + )); + + // Non-hex characters are rejected too. + let p = PlatformConfig::parse(&format!("tdx=on,mrownerconfig={}", "zz".repeat(48)))?; + assert!(matches!( + p.tdx_measurements(), + Err(ValidationError::TdxInvalidMeasurement("mrownerconfig", _)) + )); + + Ok(()) + } + #[test] fn test_vsock_parsing() -> Result<()> { // socket and cid is required @@ -4808,6 +4902,12 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}" oem_strings: None, #[cfg(feature = "tdx")] tdx: false, + #[cfg(feature = "tdx")] + tdx_mrconfigid: None, + #[cfg(feature = "tdx")] + tdx_mrowner: None, + #[cfg(feature = "tdx")] + tdx_mrownerconfig: None, #[cfg(feature = "sev_snp")] sev_snp: false, } @@ -4926,6 +5026,17 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}" tdx_config.cpus.max_phys_bits = 52; tdx_config.validate().unwrap(); assert_eq!(tdx_config.cpus.max_phys_bits, 52); + + // mrconfigid/mrowner/mrownerconfig are rejected without tdx=on. + let mut no_tdx_config = valid_config.clone(); + no_tdx_config.platform = Some(PlatformConfig { + tdx_mrconfigid: Some("01".repeat(48)), + ..platform_fixture() + }); + assert_eq!( + no_tdx_config.validate(), + Err(ValidationError::TdxMeasurementWithoutTdx) + ); } let mut invalid_config = valid_config.clone(); diff --git a/vmm/src/vm.rs b/vmm/src/vm.rs index d7ead06d5..2c0b66617 100644 --- a/vmm/src/vm.rs +++ b/vmm/src/vm.rs @@ -984,7 +984,15 @@ impl Vm { if config.lock().unwrap().is_tdx_enabled() { let cpuid = cpu_manager.lock().unwrap().common_cpuid(); let max_vcpus = cpu_manager.lock().unwrap().max_vcpus(); - vm.tdx_init(&cpuid, max_vcpus) + let (mrconfigid, mrowner, mrownerconfig) = config + .lock() + .unwrap() + .platform + .as_ref() + .expect("TDX requires a platform configuration") + .tdx_measurements() + .map_err(Error::ConfigValidation)?; + vm.tdx_init(&cpuid, max_vcpus, &mrconfigid, &mrowner, &mrownerconfig) .map_err(Error::InitializeTdxVm)?; } Ok(()) diff --git a/vmm/src/vm_config.rs b/vmm/src/vm_config.rs index 06deffbff..ebf06358a 100644 --- a/vmm/src/vm_config.rs +++ b/vmm/src/vm_config.rs @@ -130,6 +130,19 @@ pub struct PlatformConfig { #[cfg(feature = "tdx")] #[serde(default)] pub tdx: bool, + // Optional SHA384 measurement-configuration digests, hex encoded (96 hex + // characters = 48 bytes each), forwarded verbatim to `KVM_TDX_INIT_VM` as + // the TD's `mrconfigid`, `mrowner` and `mrownerconfig` registers. When + // unset each register is left as all zeros. + #[cfg(feature = "tdx")] + #[serde(default)] + pub tdx_mrconfigid: Option, + #[cfg(feature = "tdx")] + #[serde(default)] + pub tdx_mrowner: Option, + #[cfg(feature = "tdx")] + #[serde(default)] + pub tdx_mrownerconfig: Option, #[cfg(feature = "sev_snp")] #[serde(default)] pub sev_snp: bool,