From 063caca4a841d149dbe0a929433128a6e64a4bef Mon Sep 17 00:00:00 2001 From: Leander Kohler Date: Mon, 9 Feb 2026 15:44:24 +0100 Subject: [PATCH] vmm: platform: add structured SMBIOS config Extend SMBIOS System Information with manufacturer, product, version, family, sku, serial, and uuid fields, add a chassis asset tag, and pass a structured SMBIOS config from --platform into arch setup. Keep OEM strings and legacy serial_number/uuid options working for compatibility. The platform option naming follows `dmidecode -s `. Fields: - system_manufacturer - system_product_name - system_version - system_family - system_serial_number - system_uuid - chassis_asset_tag On-behalf-of: SAP leander.kohler@sap.com Signed-off-by: Leander Kohler --- arch/src/x86_64/mod.rs | 2 +- arch/src/x86_64/smbios.rs | 170 ++++++++++++++++-- .../tests/common/tests_wrappers.rs | 6 +- vmm/src/api/openapi/cloud-hypervisor.yaml | 16 ++ vmm/src/config.rs | 105 +++++++++-- vmm/src/vm_config.rs | 53 +++++- 6 files changed, 313 insertions(+), 39 deletions(-) diff --git a/arch/src/x86_64/mod.rs b/arch/src/x86_64/mod.rs index 6c983bcd2..d5ae87aed 100644 --- a/arch/src/x86_64/mod.rs +++ b/arch/src/x86_64/mod.rs @@ -31,7 +31,7 @@ use linux_loader::loader::elf::start_info::{ hvm_memmap_table_entry, hvm_modlist_entry, hvm_start_info, }; use log::{debug, error, info}; -pub use smbios::SmbiosConfig; +pub use smbios::{SmbiosChassisConfig, SmbiosConfig, SmbiosSystem}; use thiserror::Error; use vm_memory::{ Address, Bytes, GuestAddress, GuestAddressSpace, GuestMemory, GuestMemoryAtomic, diff --git a/arch/src/x86_64/smbios.rs b/arch/src/x86_64/smbios.rs index 4cef9b5e1..44382a0f2 100644 --- a/arch/src/x86_64/smbios.rs +++ b/arch/src/x86_64/smbios.rs @@ -35,16 +35,24 @@ pub enum Error { /// Failure to parse uuid, uuid format may be error #[error("Failure to parse uuid: {1}")] ParseUuid(#[source] uuid::Error, String), + /// SMBIOS string index overflow (u8 limit reached). + #[error("SMBIOS string index overflow (u8 limit reached: {})", u8::MAX)] + TooManyStrings, } pub type Result = result::Result; -// Constants sourced from SMBIOS Spec 3.2.0. +// Constants sourced from SMBIOS Spec 3.9.0. const SM3_MAGIC_IDENT: &[u8; 5usize] = b"_SM3_"; const BIOS_INFORMATION: u8 = 0; const SYSTEM_INFORMATION: u8 = 1; const OEM_STRINGS: u8 = 11; +const SYSTEM_ENCLOSURE: u8 = 3; const END_OF_TABLE: u8 = 127; +const SYSTEM_WAKE_UP_TYPE_UNKNOWN: u8 = 0x02; +const CHASSIS_TYPE_UNKNOWN: u8 = 0x02; +const CHASSIS_STATE_UNKNOWN: u8 = 0x02; +const CHASSIS_SECURITY_STATUS_NONE: u8 = 0x03; const PCI_SUPPORTED: u64 = 1 << 7; const IS_VIRTUAL_MACHINE: u8 = 1 << 4; pub const DEFAULT_SYSTEM_MANUFACTURER: &str = "Cloud Hypervisor"; @@ -52,9 +60,25 @@ pub const DEFAULT_SYSTEM_PRODUCT_NAME: &str = "cloud-hypervisor"; #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct SmbiosConfig { + pub system: Option, + pub chassis: Option, + pub oem_strings: Box<[String]>, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SmbiosSystem { + pub manufacturer: Option, + pub product_name: Option, + pub version: Option, pub serial_number: Option, pub uuid: Option, - pub oem_strings: Box<[String]>, + pub sku_number: Option, + pub family: Option, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SmbiosChassisConfig { + pub asset_tag: Option, } impl SmbiosConfig { @@ -130,6 +154,33 @@ struct SmbiosOemStrings { count: u8, } +/// SMBIOS Chassis Table (Type 3) as defined in DMTF SMBIOS 3.9.0: +/// https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.9.0.pdf +/// Note: trailing fields are omitted, so this structure is not complete. +#[repr(C, packed)] +#[derive(Default, Copy, Clone)] +struct SmbiosChassis { + r#type: u8, + length: u8, + handle: u16, + manufacturer: u8, + chassis_type: u8, + version: u8, + serial_number: u8, + asset_tag: u8, + bootup_state: u8, + power_supply_state: u8, + thermal_state: u8, + security_status: u8, + oem_defined: u32, + height: u8, + number_of_power_cords: u8, + contained_element_count: u8, + contained_element_record_length: u8, + // followed by contained element records (optional, variable-length) + // followed by sku_number: u8, rack_type: u8, rack_height: u8 +} + #[repr(C, packed)] #[derive(Default, Copy, Clone)] struct SmbiosEndOfTable { @@ -147,6 +198,8 @@ unsafe impl ByteValued for SmbiosSysInfo {} // SAFETY: data structure only contain a series of integers unsafe impl ByteValued for SmbiosOemStrings {} // SAFETY: data structure only contain a series of integers +unsafe impl ByteValued for SmbiosChassis {} +// SAFETY: data structure only contain a series of integers unsafe impl ByteValued for SmbiosEndOfTable {} fn write_and_incr( @@ -200,44 +253,125 @@ fn write_string_terminator( } } +/// Allocate the next string index for an SMBIOS string-set. +/// +/// Per SMBIOS DSP0134, index `0` means "no string", so valid indices run from +/// `1` to `255`. Returns `0` when `present` is `false`. Otherwise returns the +/// current value of `*next` and advances it by one. Fails with +/// [`Error::TooManyStrings`] once all 255 indices have been used: `next` +/// starts at `1`, so it can only be `0` here after wrapping past `255`. +fn alloc_index(next: &mut u8, present: bool) -> Result { + if !present { + return Ok(0); + } + + let idx = *next; + if idx == 0 { + return Err(Error::TooManyStrings); + } + + *next = next.wrapping_add(1); + Ok(idx) +} + fn write_type1_system( mem: &GuestMemoryMmap, curptr: &mut GuestAddress, handle: &mut u16, - serial_number: Option<&str>, - uuid: Option<&str>, + system: Option<&SmbiosSystem>, ) -> Result<()> { *handle += 1; + let manufacturer = system + .and_then(|s| s.manufacturer.as_deref()) + .unwrap_or(DEFAULT_SYSTEM_MANUFACTURER); + let product = system + .and_then(|s| s.product_name.as_deref()) + .unwrap_or(DEFAULT_SYSTEM_PRODUCT_NAME); + let version = system.and_then(|s| s.version.as_deref()); + let serial = system.and_then(|s| s.serial_number.as_deref()); + let uuid = system.and_then(|s| s.uuid.as_deref()); + let sku = system.and_then(|s| s.sku_number.as_deref()); + let family = system.and_then(|s| s.family.as_deref()); + let uuid_number = uuid .map(Uuid::parse_str) .transpose() .map_err(|e| Error::ParseUuid(e, uuid.unwrap().to_string()))? .unwrap_or(Uuid::nil()); - let serial_idx = serial_number.map(|_| 3).unwrap_or_default(); - let smbios_sysinfo = SmbiosSysInfo { + let mut next = 1u8; + let manufacturer_idx = alloc_index(&mut next, true)?; + let product_idx = alloc_index(&mut next, true)?; + let version_idx = alloc_index(&mut next, version.is_some())?; + let serial_idx = alloc_index(&mut next, serial.is_some())?; + let sku_idx = alloc_index(&mut next, sku.is_some())?; + let family_idx = alloc_index(&mut next, family.is_some())?; + + let sys = SmbiosSysInfo { r#type: SYSTEM_INFORMATION, length: mem::size_of::() as u8, handle: *handle, - manufacturer: 1, // First string written in this section - product_name: 2, // Second string written in this section + manufacturer: manufacturer_idx, + product_name: product_idx, + version: version_idx, serial_number: serial_idx, uuid: uuid_number.to_bytes_le(), - ..Default::default() + wake_up_type: SYSTEM_WAKE_UP_TYPE_UNKNOWN, + sku: sku_idx, + family: family_idx, }; - *curptr = write_and_incr(mem, smbios_sysinfo, *curptr)?; - *curptr = write_string(mem, DEFAULT_SYSTEM_MANUFACTURER, *curptr)?; - *curptr = write_string(mem, DEFAULT_SYSTEM_PRODUCT_NAME, *curptr)?; - *curptr = write_opt_string(mem, serial_number, *curptr)?; + *curptr = write_and_incr(mem, sys, *curptr)?; + *curptr = write_string(mem, manufacturer, *curptr)?; + *curptr = write_string(mem, product, *curptr)?; + *curptr = write_opt_string(mem, version, *curptr)?; + *curptr = write_opt_string(mem, serial, *curptr)?; + *curptr = write_opt_string(mem, sku, *curptr)?; + *curptr = write_opt_string(mem, family, *curptr)?; *curptr = write_and_incr(mem, 0u8, *curptr)?; Ok(()) } +fn write_type3_chassis( + mem: &GuestMemoryMmap, + curptr: &mut GuestAddress, + handle: &mut u16, + chassis: &SmbiosChassisConfig, +) -> Result<()> { + *handle += 1; + + let asset_tag = chassis.asset_tag.as_deref(); + let mut next = 1u8; + let asset_idx = alloc_index(&mut next, asset_tag.is_some())?; + + let ch = SmbiosChassis { + r#type: SYSTEM_ENCLOSURE, + length: mem::size_of::() as u8, + handle: *handle, + manufacturer: 0, + chassis_type: CHASSIS_TYPE_UNKNOWN, + version: 0, + serial_number: 0, + asset_tag: asset_idx, + bootup_state: CHASSIS_STATE_UNKNOWN, + power_supply_state: CHASSIS_STATE_UNKNOWN, + thermal_state: CHASSIS_STATE_UNKNOWN, + security_status: CHASSIS_SECURITY_STATUS_NONE, + contained_element_count: 0, + contained_element_record_length: 0, + ..Default::default() + }; + + *curptr = write_and_incr(mem, ch, *curptr)?; + *curptr = write_opt_string(mem, asset_tag, *curptr)?; + *curptr = write_string_terminator(mem, *curptr, asset_tag.is_some())?; + Ok(()) +} + pub fn setup_smbios(mem: &GuestMemoryMmap, smbios: Option<&SmbiosConfig>) -> Result { - let serial_number = smbios.and_then(|cfg| cfg.serial_number.as_deref()); - let uuid = smbios.and_then(|cfg| cfg.uuid.as_deref()); + let system = smbios.and_then(|cfg| cfg.system.as_ref()); + let chassis = smbios.and_then(|cfg| cfg.chassis.as_ref()); let oem_strings: &[String] = smbios.map_or(&[], |cfg| &cfg.oem_strings); let physptr = GuestAddress(SMBIOS_START) .checked_add(mem::size_of::() as u64) @@ -263,7 +397,11 @@ pub fn setup_smbios(mem: &GuestMemoryMmap, smbios: Option<&SmbiosConfig>) -> Res curptr = write_and_incr(mem, 0u8, curptr)?; } - write_type1_system(mem, &mut curptr, &mut handle, serial_number, uuid)?; + write_type1_system(mem, &mut curptr, &mut handle, system)?; + + if let Some(chassis) = chassis { + write_type3_chassis(mem, &mut curptr, &mut handle, chassis)?; + } if !oem_strings.is_empty() { handle += 1; diff --git a/cloud-hypervisor/tests/common/tests_wrappers.rs b/cloud-hypervisor/tests/common/tests_wrappers.rs index 693702b55..27e4c1f27 100644 --- a/cloud-hypervisor/tests/common/tests_wrappers.rs +++ b/cloud-hypervisor/tests/common/tests_wrappers.rs @@ -2219,7 +2219,7 @@ pub(crate) fn _test_dmi_serial_number(guest: &Guest) { let mut child = GuestCommand::new(guest) .default_cpus() .default_memory() - .default_kernel_cmdline_with_platform(Some("serial_number=a=b;c=d")) + .default_kernel_cmdline_with_platform(Some("system_serial_number=a=b;c=d")) .default_disks() .default_net() .capture_output() @@ -2248,7 +2248,9 @@ pub(crate) fn _test_dmi_uuid(guest: &Guest) { let mut child = GuestCommand::new(guest) .default_cpus() .default_memory() - .default_kernel_cmdline_with_platform(Some("uuid=1e8aa28a-435d-4027-87f4-40dceff1fa0a")) + .default_kernel_cmdline_with_platform(Some( + "system_uuid=1e8aa28a-435d-4027-87f4-40dceff1fa0a", + )) .default_disks() .default_net() .capture_output() diff --git a/vmm/src/api/openapi/cloud-hypervisor.yaml b/vmm/src/api/openapi/cloud-hypervisor.yaml index a5ad7a21e..c5e360f1a 100644 --- a/vmm/src/api/openapi/cloud-hypervisor.yaml +++ b/vmm/src/api/openapi/cloud-hypervisor.yaml @@ -786,14 +786,30 @@ components: iommu_address_width_bits: type: integer format: uint8 + system_serial_number: + type: string serial_number: type: string + system_uuid: + type: string uuid: type: string oem_strings: type: array items: type: string + system_manufacturer: + type: string + system_product_name: + type: string + system_version: + type: string + system_family: + type: string + system_sku_number: + type: string + chassis_asset_tag: + type: string tdx: type: boolean default: false diff --git a/vmm/src/config.rs b/vmm/src/config.rs index c9ba9c064..0627018c6 100644 --- a/vmm/src/config.rs +++ b/vmm/src/config.rs @@ -847,9 +847,11 @@ impl PlatformConfig { static SYNTAX: LazyLock = LazyLock::new(|| { let mut syntax = "Platform configuration parameters \ \"num_pci_segments=,iommu_segments=,\ - iommu_address_width=,serial_number=,\ - uuid=,oem_strings=,iommufd=on|off,\ - vfio_p2p_dma=on|off" + iommu_address_width=,iommufd=on|off,vfio_p2p_dma=on|off,system_manufacturer=,\ + system_product_name=,system_version=,\ + system_serial_number=,system_uuid=,\ + system_sku_number=,system_family=,\ + oem_strings=,chassis_asset_tag=" .to_string(); if cfg!(feature = "tdx") { @@ -869,6 +871,46 @@ impl PlatformConfig { } pub fn parse(platform: &str) -> Result { + struct StringField { + key: &'static str, + apply: fn(&mut PlatformConfig, String), + } + + const SMBIOS_STRING_FIELDS: &[StringField] = &[ + StringField { + key: "system_manufacturer", + apply: |config, value| config.system_manufacturer = Some(value), + }, + StringField { + key: "system_product_name", + apply: |config, value| config.system_product_name = Some(value), + }, + StringField { + key: "system_version", + apply: |config, value| config.system_version = Some(value), + }, + StringField { + key: "system_serial_number", + apply: |config, value| config.system_serial_number = Some(value), + }, + StringField { + key: "system_uuid", + apply: |config, value| config.system_uuid = Some(value), + }, + StringField { + key: "system_sku_number", + apply: |config, value| config.system_sku_number = Some(value), + }, + StringField { + key: "system_family", + apply: |config, value| config.system_family = Some(value), + }, + StringField { + key: "chassis_asset_tag", + apply: |config, value| config.chassis_asset_tag = Some(value), + }, + ]; + let mut parser = OptionParser::new(); parser .add("num_pci_segments") @@ -879,6 +921,9 @@ impl PlatformConfig { .add("oem_strings") .add("iommufd") .add("vfio_p2p_dma"); + for field in SMBIOS_STRING_FIELDS { + parser.add(field.key); + } #[cfg(feature = "tdx")] parser.add("tdx"); #[cfg(feature = "sev_snp")] @@ -897,10 +942,6 @@ impl PlatformConfig { .convert("iommu_address_width") .map_err(Error::ParsePlatform)? .unwrap_or(MAX_IOMMU_ADDRESS_WIDTH_BITS); - let serial_number = parser - .convert("serial_number") - .map_err(Error::ParsePlatform)?; - let uuid = parser.convert("uuid").map_err(Error::ParsePlatform)?; let oem_strings = parser .convert::("oem_strings") .map_err(Error::ParsePlatform)? @@ -927,20 +968,50 @@ impl PlatformConfig { .map_err(Error::ParsePlatform)? .unwrap_or(Toggle(false)) .0; - Ok(PlatformConfig { + + let mut platform_config = PlatformConfig { num_pci_segments, iommu_segments, iommu_address_width_bits, - serial_number, - uuid, + system_serial_number: None, + system_uuid: None, oem_strings, + system_manufacturer: None, + system_product_name: None, + system_version: None, + system_family: None, + system_sku_number: None, + chassis_asset_tag: None, iommufd, - vfio_p2p_dma, #[cfg(feature = "tdx")] tdx, #[cfg(feature = "sev_snp")] sev_snp, - }) + vfio_p2p_dma, + }; + + for field in SMBIOS_STRING_FIELDS { + if let Some(value) = parser + .convert::(field.key) + .map_err(Error::ParsePlatform)? + { + (field.apply)(&mut platform_config, value); + } + } + + let legacy_serial_number = parser + .convert::("serial_number") + .map_err(Error::ParsePlatform)?; + platform_config.system_serial_number = platform_config + .system_serial_number + .or(legacy_serial_number); + + let legacy_uuid = parser + .convert::("uuid") + .map_err(Error::ParsePlatform)?; + platform_config.system_uuid = platform_config.system_uuid.or(legacy_uuid); + + Ok(platform_config) } pub fn validate(&self) -> ValidationResult<()> { @@ -5027,11 +5098,17 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}" num_pci_segments: MAX_NUM_PCI_SEGMENTS, iommu_segments: None, iommu_address_width_bits: MAX_IOMMU_ADDRESS_WIDTH_BITS, - serial_number: None, - uuid: None, + system_serial_number: None, + system_uuid: None, oem_strings: None, iommufd: false, vfio_p2p_dma: default_platformconfig_vfio_p2p_dma(), + system_manufacturer: None, + system_product_name: None, + system_version: None, + system_family: None, + system_sku_number: None, + chassis_asset_tag: None, #[cfg(feature = "tdx")] tdx: false, #[cfg(feature = "sev_snp")] diff --git a/vmm/src/vm_config.rs b/vmm/src/vm_config.rs index aa2ac6bef..16351d3de 100644 --- a/vmm/src/vm_config.rs +++ b/vmm/src/vm_config.rs @@ -131,12 +131,24 @@ pub struct PlatformConfig { pub iommu_segments: Option>, #[serde(default = "default_platformconfig_iommu_address_width_bits")] pub iommu_address_width_bits: u8, - #[serde(default)] - pub serial_number: Option, - #[serde(default)] - pub uuid: Option, + #[serde(default, alias = "serial_number")] + pub system_serial_number: Option, + #[serde(default, alias = "uuid")] + pub system_uuid: Option, #[serde(default)] pub oem_strings: Option>, + #[serde(default)] + pub system_manufacturer: Option, + #[serde(default)] + pub system_product_name: Option, + #[serde(default)] + pub system_version: Option, + #[serde(default)] + pub system_family: Option, + #[serde(default)] + pub system_sku_number: Option, + #[serde(default)] + pub chassis_asset_tag: Option, #[cfg(feature = "tdx")] #[serde(default)] pub tdx: bool, @@ -154,9 +166,38 @@ impl PlatformConfig { /// Returns `None` if no SMBIOS-relevant platform fields are set, otherwise /// `Some` with a [`SmbiosConfig`] built from the populated fields. pub fn smbios_config(&self) -> Option { + let has_system = [ + &self.system_serial_number, + &self.system_uuid, + &self.system_manufacturer, + &self.system_product_name, + &self.system_version, + &self.system_family, + &self.system_sku_number, + ] + .iter() + .any(|v| v.is_some()); + + let system = has_system.then_some(arch::x86_64::SmbiosSystem { + manufacturer: self.system_manufacturer.clone(), + product_name: self.system_product_name.clone(), + version: self.system_version.clone(), + serial_number: self.system_serial_number.clone(), + uuid: self.system_uuid.clone(), + sku_number: self.system_sku_number.clone(), + family: self.system_family.clone(), + }); + + let chassis = + self.chassis_asset_tag + .clone() + .map(|asset_tag| arch::x86_64::SmbiosChassisConfig { + asset_tag: Some(asset_tag), + }); + let smbios = arch::x86_64::SmbiosConfig { - serial_number: self.serial_number.clone(), - uuid: self.uuid.clone(), + system, + chassis, oem_strings: self.oem_strings.clone().unwrap_or_default(), };