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 <field>`.

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 <leander.kohler@cyberus-technology.de>
This commit is contained in:
Leander Kohler
2026-02-09 15:44:24 +01:00
committed by Rob Bradford
parent e097d7d495
commit 063caca4a8
6 changed files with 313 additions and 39 deletions

View File

@@ -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,

View File

@@ -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<T> = result::Result<T, Error>;
// 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<SmbiosSystem>,
pub chassis: Option<SmbiosChassisConfig>,
pub oem_strings: Box<[String]>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SmbiosSystem {
pub manufacturer: Option<String>,
pub product_name: Option<String>,
pub version: Option<String>,
pub serial_number: Option<String>,
pub uuid: Option<String>,
pub oem_strings: Box<[String]>,
pub sku_number: Option<String>,
pub family: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SmbiosChassisConfig {
pub asset_tag: Option<String>,
}
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<T: ByteValued>(
@@ -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<u8> {
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::<SmbiosSysInfo>() 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::<SmbiosChassis>() 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<u64> {
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::<Smbios30Entrypoint>() 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;

View File

@@ -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()

View File

@@ -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

View File

@@ -847,9 +847,11 @@ impl PlatformConfig {
static SYNTAX: LazyLock<String> = LazyLock::new(|| {
let mut syntax = "Platform configuration parameters \
\"num_pci_segments=<num_pci_segments>,iommu_segments=<list_of_segments>,\
iommu_address_width=<bits>,serial_number=<dmi_device_serial_number>,\
uuid=<dmi_device_uuid>,oem_strings=<list_of_strings>,iommufd=on|off,\
vfio_p2p_dma=on|off"
iommu_address_width=<bits>,iommufd=on|off,vfio_p2p_dma=on|off,system_manufacturer=<dmi_system_manufacturer>,\
system_product_name=<dmi_system_product_name>,system_version=<dmi_system_version>,\
system_serial_number=<dmi_system_serial_number>,system_uuid=<dmi_system_uuid>,\
system_sku_number=<dmi_system_sku_number>,system_family=<dmi_system_family>,\
oem_strings=<list_of_strings>,chassis_asset_tag=<dmi_chassis_asset_tag>"
.to_string();
if cfg!(feature = "tdx") {
@@ -869,6 +871,46 @@ impl PlatformConfig {
}
pub fn parse(platform: &str) -> Result<Self> {
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::<StringList>("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::<String>(field.key)
.map_err(Error::ParsePlatform)?
{
(field.apply)(&mut platform_config, value);
}
}
let legacy_serial_number = parser
.convert::<String>("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::<String>("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")]

View File

@@ -131,12 +131,24 @@ pub struct PlatformConfig {
pub iommu_segments: Option<Box<[u16]>>,
#[serde(default = "default_platformconfig_iommu_address_width_bits")]
pub iommu_address_width_bits: u8,
#[serde(default)]
pub serial_number: Option<String>,
#[serde(default)]
pub uuid: Option<String>,
#[serde(default, alias = "serial_number")]
pub system_serial_number: Option<String>,
#[serde(default, alias = "uuid")]
pub system_uuid: Option<String>,
#[serde(default)]
pub oem_strings: Option<Box<[String]>>,
#[serde(default)]
pub system_manufacturer: Option<String>,
#[serde(default)]
pub system_product_name: Option<String>,
#[serde(default)]
pub system_version: Option<String>,
#[serde(default)]
pub system_family: Option<String>,
#[serde(default)]
pub system_sku_number: Option<String>,
#[serde(default)]
pub chassis_asset_tag: Option<String>,
#[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<arch::x86_64::SmbiosConfig> {
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(),
};