diff --git a/Cargo.lock b/Cargo.lock index 2914f37b5..bd807e8f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4535,6 +4535,7 @@ dependencies = [ "anyhow", "bhyve_api 0.0.0", "clap", + "cpuid_utils", "libc", "propolis", "propolis_api_types", diff --git a/bin/propolis-server/src/lib/initializer.rs b/bin/propolis-server/src/lib/initializer.rs index 094e7cc3c..bdf1032af 100644 --- a/bin/propolis-server/src/lib/initializer.rs +++ b/bin/propolis-server/src/lib/initializer.rs @@ -994,7 +994,7 @@ impl MachineInitializer<'_> { ) -> Option { let leaf = CpuidIdent::leaf(leaf); let Some(cpuid) = &spec.cpuid else { - return Some(cpuid_utils::host_query(leaf)); + return Some(cpuid_utils::host::query(leaf)); }; cpuid.get(leaf).copied() diff --git a/bin/propolis-standalone/src/main.rs b/bin/propolis-standalone/src/main.rs index 4010bb096..5b2b8f48d 100644 --- a/bin/propolis-standalone/src/main.rs +++ b/bin/propolis-standalone/src/main.rs @@ -867,10 +867,10 @@ fn generate_smbios(params: SmbiosParams) -> anyhow::Result { ..Default::default() }; - let cpuid_vendor = cpuid_utils::host_query(CpuidIdent::leaf(0)); + let cpuid_vendor = cpuid_utils::host::query(CpuidIdent::leaf(0)); let cpuid_ident = params .cpuid_ident - .unwrap_or_else(|| cpuid_utils::host_query(CpuidIdent::leaf(1))); + .unwrap_or_else(|| cpuid_utils::host::query(CpuidIdent::leaf(1))); let family = match cpuid_ident.eax & 0xf00 { // If family ID is 0xf, extended family is added to it 0xf00 => (cpuid_ident.eax >> 20 & 0xff) + 0xf, @@ -894,13 +894,13 @@ fn generate_smbios(params: SmbiosParams) -> anyhow::Result { }; let proc_id = u64::from(cpuid_ident.eax) | u64::from(cpuid_ident.edx) << 32; let procname_entries = params.cpuid_procname.or_else(|| { - if cpuid_utils::host_query(CpuidIdent::leaf(0x8000_0000)).eax + if cpuid_utils::host::query(CpuidIdent::leaf(0x8000_0000)).eax >= 0x8000_0004 { Some([ - cpuid_utils::host_query(CpuidIdent::leaf(0x8000_0002)), - cpuid_utils::host_query(CpuidIdent::leaf(0x8000_0003)), - cpuid_utils::host_query(CpuidIdent::leaf(0x8000_0004)), + cpuid_utils::host::query(CpuidIdent::leaf(0x8000_0002)), + cpuid_utils::host::query(CpuidIdent::leaf(0x8000_0003)), + cpuid_utils::host::query(CpuidIdent::leaf(0x8000_0004)), ]) } else { None diff --git a/bin/propolis-utils/Cargo.toml b/bin/propolis-utils/Cargo.toml index 23cebab1e..85a7e3f74 100644 --- a/bin/propolis-utils/Cargo.toml +++ b/bin/propolis-utils/Cargo.toml @@ -17,6 +17,7 @@ doctest = false [dependencies] anyhow.workspace = true clap = { workspace = true, features = ["derive"] } +cpuid_utils = { workspace = true, features = ["instance-spec"] } serde = { workspace = true, features = ["derive"] } propolis = { workspace = true, default-features = false } propolis_api_types.workspace = true diff --git a/bin/propolis-utils/src/bin/cpuid-gen.rs b/bin/propolis-utils/src/bin/cpuid-gen.rs index 078780c6e..478c24464 100644 --- a/bin/propolis-utils/src/bin/cpuid-gen.rs +++ b/bin/propolis-utils/src/bin/cpuid-gen.rs @@ -2,243 +2,19 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use std::cmp::{Ord, Ordering}; -use std::collections::BTreeMap; use std::str::FromStr; -use bhyve_api::{VmmCtlFd, VmmFd}; use clap::{Parser, ValueEnum}; +use cpuid_utils::CpuidSet; -fn create_vm() -> anyhow::Result { - let name = format!("cpuid-gen-{}", std::process::id()); - let mut req = - bhyve_api::vm_create_req::new(name.as_bytes()).expect("valid VM name"); - - let ctl = VmmCtlFd::open()?; - let _ = unsafe { ctl.ioctl(bhyve_api::VMM_CREATE_VM, &mut req) }?; - - let vm = match VmmFd::open(&name) { - Ok(vm) => vm, - Err(e) => { - // Attempt to manually destroy the VM if we cannot open it - let _ = ctl.vm_destroy(name.as_bytes()); - return Err(e.into()); - } - }; - - match vm.ioctl_usize(bhyve_api::ioctls::VM_SET_AUTODESTRUCT, 1) { - Ok(_res) => {} - Err(e) => { - // Destroy instance if auto-destruct cannot be set - let _ = vm.ioctl_usize(bhyve_api::VM_DESTROY_SELF, 0); - return Err(e.into()); - } - }; - - Ok(vm) -} - -#[derive(Clone, Copy, Default, Debug)] -struct Cpuid { - eax: u32, - ebx: u32, - ecx: u32, - edx: u32, -} -impl Cpuid { - #[allow(unused)] - const fn is_authentic_amd(&self) -> bool { - self.ebx == 0x68747541 - && self.ecx == 0x444d4163 - && self.edx == 0x69746e65 - } - const fn all_zeros(&self) -> bool { - self.eax == 0 && self.ebx == 0 && self.ecx == 0 && self.edx == 0 - } -} -impl From<&bhyve_api::vm_legacy_cpuid> for Cpuid { - fn from(value: &bhyve_api::vm_legacy_cpuid) -> Self { - Self { - eax: value.vlc_eax, - ebx: value.vlc_ebx, - ecx: value.vlc_ecx, - edx: value.vlc_edx, - } - } -} - -#[derive(Copy, Clone, Eq, PartialEq)] -enum CpuidKey { - Leaf(u32), - SubLeaf(u32, u32), -} -impl CpuidKey { - const fn eax(&self) -> u32 { - match self { - CpuidKey::Leaf(eax) => *eax, - CpuidKey::SubLeaf(eax, _) => *eax, - } - } -} -impl Ord for CpuidKey { - fn cmp(&self, other: &Self) -> Ordering { - if self.eq(other) { - return Ordering::Equal; - } - match self.eax().cmp(&other.eax()) { - Ordering::Equal => match (self, other) { - (CpuidKey::Leaf(_), _) => Ordering::Less, - (_, CpuidKey::Leaf(_)) => Ordering::Greater, - (CpuidKey::SubLeaf(_, ecx), CpuidKey::SubLeaf(_, oecx)) => { - ecx.cmp(oecx) - } - }, - o => o, - } - } -} -impl PartialOrd for CpuidKey { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -/// Query `cpuid` through bhyve-defined masks -fn query_cpuid(vm: &VmmFd, eax: u32, ecx: u32) -> anyhow::Result { - let mut data = bhyve_api::vm_legacy_cpuid { - vlc_eax: eax, - vlc_ecx: ecx, - ..Default::default() - }; - unsafe { vm.ioctl(bhyve_api::VM_LEGACY_CPUID, &mut data) }?; - Ok(Cpuid::from(&data)) -} - -/// Query `cpuid` directly from host CPU -fn query_raw_cpuid(eax: u32, ecx: u32) -> Cpuid { - let mut res = Cpuid::default(); - - unsafe { - std::arch::asm!( - "push rbx", - "cpuid", - "mov {0:e}, ebx", - "pop rbx", - out(reg) res.ebx, - // select cpuid 0, also specify eax as clobbered - inout("eax") eax => res.eax, - inout("ecx") ecx => res.ecx, - out("edx") res.edx, - ); - } - res -} - -const STD_EAX_BASE: u32 = 0x0; -const EXTD_EAX_BASE: u32 = 0x80000000; - -const CPU_FEAT_ECX_XSAVE: u32 = 1 << 26; - -fn collect_cpuid( - query_cpuid: &impl Fn(u32, u32) -> anyhow::Result, - zero_elide: bool, -) -> anyhow::Result> { - let std = query_cpuid(STD_EAX_BASE, 0)?; - let extd = query_cpuid(EXTD_EAX_BASE, 0)?; - - let mut results: BTreeMap = BTreeMap::new(); - - let mut xsave_supported = false; - - for eax in 0..=std.eax { - let data = query_cpuid(eax, 0)?; - - if zero_elide && data.all_zeros() { - continue; - } - - match eax { - 0x1 => { - if data.ecx & CPU_FEAT_ECX_XSAVE != 0 { - xsave_supported = true; - } - results.insert(CpuidKey::Leaf(eax), data); - } - 0x7 => { - results.insert(CpuidKey::SubLeaf(eax, 0), data); - - // TODO: handle more sub-leaf entries? - - // Default entry for invalid sub-leaf is all-zeroes - results.insert(CpuidKey::Leaf(eax), Cpuid::default()); - } - 0xb => { - // Extended topo - results.insert(CpuidKey::SubLeaf(eax, 0), data); - results.insert(CpuidKey::SubLeaf(eax, 1), query_cpuid(eax, 1)?); - } - 0xd if xsave_supported => { - // XSAVE - let xcr0_bits = u64::from(data.eax) | u64::from(data.edx); - results.insert(CpuidKey::SubLeaf(eax, 0), data); - let data = query_cpuid(eax, 1)?; - let xss_bits = u64::from(data.ecx) | u64::from(data.edx); - results.insert(CpuidKey::SubLeaf(eax, 1), data); - - // Fetch all the 2:63 sub-leaf entries which are valid - for ecx in 2..63 { - if (1 << ecx) & (xcr0_bits | xss_bits) == 0 { - continue; - } - let data = query_cpuid(eax, ecx)?; - results.insert(CpuidKey::SubLeaf(eax, ecx), data); - } - // Default entry for invalid sub-leaf is all-zeroes - results.insert(CpuidKey::Leaf(eax), Cpuid::default()); - } - _ => { - results.insert(CpuidKey::Leaf(eax), data); - } - } - } - - for eax in EXTD_EAX_BASE..extd.eax { - let data = query_cpuid(eax, 0)?; - - if zero_elide && data.all_zeros() { - continue; - } - match eax { - 0x8000001d => { - // AMD cache topo - for ecx in 0..u32::MAX { - let data = query_cpuid(eax, ecx)?; - // cache type of none indicates no more entries - if data.eax & 0b11111 == 0 { - break; - } - results.insert(CpuidKey::SubLeaf(eax, ecx), data); - } - // Default entry for invalid sub-leaf is all-zeroes - results.insert(CpuidKey::Leaf(eax), Cpuid::default()); - } - _ => { - results.insert(CpuidKey::Leaf(eax), data); - } - } - } - - Ok(results) -} - -fn print_text(results: &BTreeMap) { +fn print_text(results: &CpuidSet) { for (key, value) in results.iter() { - let header = match key { - CpuidKey::Leaf(eax) => { - format!("eax:{:x}\t\t", eax) + let header = match key.subleaf { + None => { + format!("eax:{:x}\t\t", key.leaf) } - CpuidKey::SubLeaf(eax, ecx) => { - format!("eax:{:x} ecx:{:x}", eax, ecx) + Some(subleaf) => { + format!("eax:{:x} ecx:{:x}", key.leaf, subleaf) } }; @@ -248,12 +24,12 @@ fn print_text(results: &BTreeMap) { ); } } -fn print_toml(results: &BTreeMap) { +fn print_toml(results: &CpuidSet) { println!("[cpuid]"); for (key, value) in results.iter() { - let key_name = match key { - CpuidKey::Leaf(eax) => format!("{:x}", eax), - CpuidKey::SubLeaf(eax, ecx) => format!("{:x}-{:x}", eax, ecx), + let key_name = match key.subleaf { + None => format!("{:x}", key.leaf), + Some(subleaf) => format!("{:x}-{:x}", key.leaf, subleaf), }; println!( "\"{}\" = [0x{:x}, 0x{:x}, 0x{:x}, 0x{:x}]", @@ -262,71 +38,8 @@ fn print_toml(results: &BTreeMap) { } } -fn print_json(results: &BTreeMap) { - use propolis_api_types::instance_spec::components::board::{ - Cpuid, CpuidEntry, - }; - - let vendor = { - use propolis_api_types::instance_spec::{CpuidValues, CpuidVendor}; - match results.get(&CpuidKey::Leaf(0)) { - None => { - eprintln!("no result for leaf 0, setting vendor to AMD"); - CpuidVendor::Amd - } - Some(values) => { - let values = CpuidValues { - eax: values.eax, - ebx: values.ebx, - ecx: values.ecx, - edx: values.edx, - }; - - match CpuidVendor::try_from(values) { - Err(_) => { - eprintln!( - "vendor in leaf 0 values ({values:?}) not \ - recognized, setting vendor to AMD" - ); - CpuidVendor::Amd - } - Ok(v) => v, - } - } - } - }; - - // propolis-server will reject CPUID entry lists that contain a no-subleaf - // and a subleaf-bearing entry for the same leaf. Filter these out by - // dropping Leaf entries whose immediate successor is a SubLeaf with the - // same leaf number. - let entries = results - .iter() - .zip(results.keys().skip(1)) - .filter_map(|((current_key, value), next_key)| { - let (leaf, subleaf) = match (current_key, next_key) { - (CpuidKey::Leaf(l1), CpuidKey::SubLeaf(l2, _)) if l1 == l2 => { - return None; - } - (CpuidKey::Leaf(leaf), _) => (*leaf, None), - (CpuidKey::SubLeaf(leaf, subleaf), _) => { - (*leaf, Some(*subleaf)) - } - }; - - Some(CpuidEntry { - leaf, - subleaf, - eax: value.eax, - ebx: value.ebx, - ecx: value.ecx, - edx: value.edx, - }) - }) - .collect(); - - let cpuid = Cpuid { entries, vendor }; - +fn print_json(results: CpuidSet) { + let cpuid = results.into_instance_spec_cpuid(); println!("{}", serde_json::to_string_pretty(&cpuid).unwrap()); } @@ -378,20 +91,21 @@ struct Opts { fn main() -> anyhow::Result<()> { let opts = Opts::parse(); - let queryf: Box anyhow::Result> = - if opts.raw_query { - Box::new(|eax, ecx| Ok(query_raw_cpuid(eax, ecx))) - } else { - let vm = create_vm()?; - Box::new(move |eax, ecx| query_cpuid(&vm, eax, ecx)) - }; + let source = if opts.raw_query { + cpuid_utils::host::CpuidSource::HostCpu + } else { + cpuid_utils::host::CpuidSource::BhyveDefault + }; - let results = collect_cpuid(&queryf, opts.zero_elide)?; + let mut results = cpuid_utils::host::query_complete(source)?; + if opts.zero_elide { + results.retain(|_id, val| !val.all_zero()); + } match opts.format { OutputFormat::Text => print_text(&results), OutputFormat::Toml => print_toml(&results), - OutputFormat::Json => print_json(&results), + OutputFormat::Json => print_json(results), } Ok(()) diff --git a/crates/cpuid-utils/src/bits.rs b/crates/cpuid-utils/src/bits.rs index f78d2df0b..7b5c45f9e 100644 --- a/crates/cpuid-utils/src/bits.rs +++ b/crates/cpuid-utils/src/bits.rs @@ -8,6 +8,9 @@ //! Definitions here are taken from the AMD Architecture Programmer's Manual, //! volume 3, appendix E (Publication 24594, revision 3.36, March 2024). +pub const STANDARD_BASE_LEAF: u32 = 0; +pub const EXTENDED_BASE_LEAF: u32 = 0x8000_0000; + bitflags::bitflags! { /// Leaf 1 ecx: instruction feature identifiers. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -193,4 +196,57 @@ bitflags::bitflags! { Self::LONG_MODE.bits() | Self::THREED_NOW_EXT.bits() | Self::THREED_NOW.bits(); } + + /// Leaf 0x8000_001D eax: Cache topology information. + /// + /// NOTE: These definitions are AMD-specific. + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub struct AmdExtLeaf1DEax: u32 { + const NUM_SHARING_CACHE_MASK = (0xFFF << 14); + const FULLY_ASSOCIATIVE = 1 << 9; + const SELF_INITIALIZATION = 1 << 8; + const CACHE_LEVEL_MASK = (0x7 << 5); + const CACHE_TYPE_MASK = 0x1F; + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AmdExtLeaf1DCacheType { + Null, + Data, + Instruction, + Unified, + Reserved, +} + +impl AmdExtLeaf1DCacheType { + pub fn is_null(&self) -> bool { + matches!(self, Self::Null) + } +} + +impl TryFrom for AmdExtLeaf1DCacheType { + type Error = (); + + /// Returns the leaf 0x8000001D cache type corresponding to the supplied + /// value, or an error if the supplied value cannot be represented in 5 bits + /// (the width of the cache type field in leaf 0x8000001D eax). + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(Self::Null), + 1 => Ok(Self::Data), + 2 => Ok(Self::Instruction), + 3 => Ok(Self::Unified), + 4..=0x1F => Ok(Self::Reserved), + _ => Err(()), + } + } +} + +impl AmdExtLeaf1DEax { + pub fn cache_type(&self) -> AmdExtLeaf1DCacheType { + let bits = (*self & Self::CACHE_TYPE_MASK).bits(); + AmdExtLeaf1DCacheType::try_from(bits) + .expect("invalid bits were already masked") + } } diff --git a/crates/cpuid-utils/src/host.rs b/crates/cpuid-utils/src/host.rs new file mode 100644 index 000000000..97d8b9b37 --- /dev/null +++ b/crates/cpuid-utils/src/host.rs @@ -0,0 +1,225 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use bhyve_api::{VmmCtlFd, VmmFd}; +use propolis_types::{CpuidIdent, CpuidValues, CpuidVendor}; +use thiserror::Error; + +use crate::{ + bits::{ + AmdExtLeaf1DCacheType, AmdExtLeaf1DEax, Leaf1Ecx, EXTENDED_BASE_LEAF, + STANDARD_BASE_LEAF, + }, + CpuidSet, SubleafInsertConflict, +}; + +#[derive(Debug, Error)] +pub enum GetHostCpuidError { + #[error("failed to insert into the CPUID map")] + CpuidInsertFailed(#[from] SubleafInsertConflict), + + #[error("CPUID vendor not recognized: {0}")] + VendorNotRecognized(&'static str), + + #[error("I/O error from bhyve API")] + BhyveError(#[from] std::io::Error), +} + +/// A wrapper around a handle to a bhyve VM that can be used to query bhyve's +/// default CPUID values. +struct Vm(bhyve_api::VmmFd); + +impl Vm { + fn new() -> Result { + let name = format!("cpuid-gen-{}", std::process::id()); + let mut req = bhyve_api::vm_create_req::new(name.as_bytes()) + .expect("valid VM name"); + + let ctl = VmmCtlFd::open()?; + let _ = unsafe { ctl.ioctl(bhyve_api::VMM_CREATE_VM, &mut req) }?; + + let vm = match VmmFd::open(&name) { + Ok(vm) => vm, + Err(e) => { + // Attempt to manually destroy the VM if we cannot open it + let _ = ctl.vm_destroy(name.as_bytes()); + return Err(e.into()); + } + }; + + Ok(Self(vm)) + } + + fn query( + &self, + vlc_eax: u32, + vlc_ecx: u32, + ) -> Result { + let mut data = bhyve_api::vm_legacy_cpuid { + vlc_eax, + vlc_ecx, + ..Default::default() + }; + unsafe { self.0.ioctl(bhyve_api::VM_LEGACY_CPUID, &mut data) }?; + Ok(CpuidValues { + eax: data.vlc_eax, + ebx: data.vlc_ebx, + ecx: data.vlc_ecx, + edx: data.vlc_edx, + }) + } +} + +impl Drop for Vm { + fn drop(&mut self) { + let _ = self.0.ioctl_usize(bhyve_api::VM_DESTROY_SELF, 0); + } +} + +/// Queries the supplied CPUID leaf on the caller's machine. +#[cfg(target_arch = "x86_64")] +pub fn query(leaf: CpuidIdent) -> CpuidValues { + unsafe { + core::arch::x86_64::__cpuid_count(leaf.leaf, leaf.subleaf.unwrap_or(0)) + } + .into() +} + +#[cfg(not(target_arch = "x86_64"))] +pub fn query(leaf: CpuidIdent) -> CpuidValues { + panic!("host CPUID queries only work on x86-64 hosts") +} + +fn collect_cpuid( + query: &impl Fn(u32, u32) -> Result, +) -> Result { + let mut set = CpuidSet::default(); + + // Enumerate standard leaves and copy their values into the output set. + // + // Note that enumeration order matters here: leaf D is only treated as + // having subleaves if leaf 1 indicates support for XSAVE. + let std = query(STANDARD_BASE_LEAF, 0)?; + set.vendor = CpuidVendor::try_from(std) + .map_err(GetHostCpuidError::VendorNotRecognized)?; + let mut xsave_supported = false; + for leaf in 0..=std.eax { + match leaf { + 0x1 => { + let data = query(leaf, 0)?; + xsave_supported = (Leaf1Ecx::from_bits_retain(data.ecx) + & Leaf1Ecx::XSAVE) + .bits() + != 0; + set.insert(CpuidIdent::leaf(leaf), data)?; + } + // Leaf 7 subleaf 0 eax indicates the total number of leaf-7 + // subleaves. + 0x7 => { + let data = query(leaf, 0)?; + set.insert(CpuidIdent::subleaf(leaf, 0), data)?; + for subleaf in 1..=data.eax { + let sub_data = query(leaf, subleaf)?; + set.insert(CpuidIdent::subleaf(leaf, subleaf), sub_data)?; + } + } + // Leaf B contains CPU topology information. Although this leaf can + // theoretically support many levels of information, bhyve supports + // only subleaves 0 and 1, so just query those without trying to + // reason about exactly how many topology nodes the host exposes. + 0xB => { + set.insert(CpuidIdent::subleaf(leaf, 0), query(leaf, 0)?)?; + set.insert(CpuidIdent::subleaf(leaf, 1), query(leaf, 1)?)?; + } + // Leaf D contains information about extended processor state. + 0xD if xsave_supported => { + let data = query(leaf, 0)?; + set.insert(CpuidIdent::subleaf(leaf, 0), data)?; + + // Subleaf 0 edx:eax contains a 64-bit mask indicating what + // features requiring extended state can be enabled in xcr0. + let xcr0_bits = + u64::from(data.eax) | (u64::from(data.edx) << 32); + + let data = query(leaf, 1)?; + set.insert(CpuidIdent::subleaf(leaf, 1), data)?; + + // Subleaf 1 edx:ecx contains a 64-bit mask indicating what + // features requiring extended state can be enabled in the + // IA32_XSS MSR. + let xss_bits = + u64::from(data.ecx) | (u64::from(data.edx) << 32); + + // Subleaves 2 through 63 are valid if the corresponding mask + // bit is set either in the xcr0 mask returned by subleaf 0 or + // the XSS mask returned by subleaf 1. + for ecx in 2..64 { + if (1 << ecx) & (xcr0_bits | xss_bits) == 0 { + continue; + } + + set.insert( + CpuidIdent::subleaf(leaf, ecx), + query(leaf, ecx)?, + )?; + } + } + _ => { + set.insert(CpuidIdent::leaf(leaf), query(leaf, 0)?)?; + } + } + } + + let extended = query(EXTENDED_BASE_LEAF, 0)?; + for leaf in EXTENDED_BASE_LEAF..=extended.eax { + match leaf { + 0x8000_001D => { + for subleaf in 0..=u32::MAX { + let data = query(leaf, subleaf)?; + let eax = AmdExtLeaf1DEax::from_bits_retain(data.eax); + if eax.cache_type() == AmdExtLeaf1DCacheType::Null { + break; + } + + set.insert(CpuidIdent::subleaf(leaf, subleaf), data)?; + } + } + _ => { + set.insert(CpuidIdent::leaf(leaf), query(leaf, 0)?)?; + } + } + } + + Ok(set) +} + +/// A possible source of CPUID information. +#[derive(Clone, Copy)] +pub enum CpuidSource { + /// Create a temporary VM and ask bhyve what values it would return if one + /// of its CPUs executed CPUID. + BhyveDefault, + + /// Execute the CPUID instruction on the host. + HostCpu, +} + +/// Queries the supplied `source` for a "complete" set of CPUID values, i.e., a +/// full set of leaves and subleaves describing the CPU platform the selected +/// source exposes. +pub fn query_complete( + source: CpuidSource, +) -> Result { + let query: Box Result<_, _>> = match source { + CpuidSource::BhyveDefault => { + let vm = Vm::new()?; + Box::new(move |eax, ecx| vm.query(eax, ecx)) + } + CpuidSource::HostCpu => { + Box::new(|eax, ecx| Ok(query(CpuidIdent::subleaf(eax, ecx)))) + } + }; + + collect_cpuid(&query) +} diff --git a/crates/cpuid-utils/src/lib.rs b/crates/cpuid-utils/src/lib.rs index 8faaf1d36..a49dfd2b1 100644 --- a/crates/cpuid-utils/src/lib.rs +++ b/crates/cpuid-utils/src/lib.rs @@ -59,6 +59,7 @@ pub use propolis_types::{CpuidIdent, CpuidValues, CpuidVendor}; use thiserror::Error; pub mod bits; +pub mod host; #[cfg(feature = "instance-spec")] mod instance_spec; @@ -265,28 +266,20 @@ impl CpuidMap { self.0.remove(&leaf); } - /// Passes each leaf number in the map to `f` and removes any leaf entries - /// for which `f` returns `false`. If a removed leaf has subleaves, all - /// their entries are removed. - // - // This function can be made to consider subleaves by changing `f` to take a - // `CpuidIdent` and then writing the call to `retain` with a `match`: - // - // - If the leaf has `Subleaves::Absent`, pass the leaf ID through to `f` - // directly and return the result to `retain`. - // - If the leaf has `Subleaves::Present`, call `retain` on the subleaf map, - // passing each subleaf to `f`, then return `!map.is_empty()` to the outer - // call to `retain` (i.e. keep the leaf entry if the subleaf map still has - // entries in it). - // - // The cost of doing this is that the function now needs to visit every - // subleaf entry in the map, even if the caller doesn't care about subleaf - // IDs. So it may be better to break this out into a separate function. - pub fn retain_by_leaf(&mut self, mut f: F) + /// Retains only the entries in this map for which `f` returns `true`. + pub fn retain(&mut self, mut f: F) where - F: FnMut(u32) -> bool, + F: FnMut(CpuidIdent, CpuidValues) -> bool, { - self.0.retain(|leaf, _| f(*leaf)); + self.0.retain(|leaf, subleaves| match subleaves { + Subleaves::Absent(v) => f(CpuidIdent::leaf(*leaf), *v), + Subleaves::Present(sl_map) => { + sl_map.retain(|subleaf, v| { + f(CpuidIdent::subleaf(*leaf, *subleaf), *v) + }); + !sl_map.is_empty() + } + }) } /// Clears the entire map. @@ -437,7 +430,7 @@ impl CpuidSet { /// Panics if the host is not an Intel or AMD CPU (leaf 0 ebx/ecx/edx /// contain something other than "GenuineIntel" or "AuthenticAMD"). pub fn new_host() -> Self { - let vendor = CpuidVendor::try_from(host_query(CpuidIdent::leaf(0))) + let vendor = CpuidVendor::try_from(host::query(CpuidIdent::leaf(0))) .expect("host CPU should be from recognized vendor"); Self::new(vendor) } @@ -466,6 +459,14 @@ impl CpuidSet { self.map.remove_leaf(leaf); } + /// See [`CpuidMap::retain`]. + pub fn retain(&mut self, f: F) + where + F: FnMut(CpuidIdent, CpuidValues) -> bool, + { + self.map.retain(f); + } + /// See [`CpuidMap::is_empty`]. pub fn is_empty(&self) -> bool { self.map.is_empty() @@ -565,50 +566,6 @@ pub const STANDARD_LEAVES: RangeInclusive = 0..=0xFFFF; /// vendor-specific. pub const EXTENDED_LEAVES: RangeInclusive = 0x8000_0000..=0x8000_FFFF; -/// Queries the supplied CPUID leaf on the caller's machine. -#[cfg(target_arch = "x86_64")] -pub fn host_query(leaf: CpuidIdent) -> CpuidValues { - unsafe { - core::arch::x86_64::__cpuid_count(leaf.leaf, leaf.subleaf.unwrap_or(0)) - } - .into() -} - -#[cfg(not(target_arch = "x86_64"))] -pub fn host_query(leaf: CpuidIdent) -> CpuidValues { - panic!("host CPUID queries only work on x86-64 hosts") -} - -/// Queries subleaf 0 of all of the valid CPUID leaves on the host and returns -/// the results in a [`CpuidMap`]. -/// -/// # Panics -/// -/// Panics if the target architecture is not x86-64. -pub fn host_query_all() -> CpuidMap { - let mut map = CpuidMap::default(); - let leaf_0 = CpuidIdent::leaf(*STANDARD_LEAVES.start()); - let leaf_0_values = host_query(leaf_0); - map.insert(leaf_0, leaf_0_values).unwrap(); - - for l in (STANDARD_LEAVES.start() + 1)..=leaf_0_values.eax { - let leaf = CpuidIdent::leaf(l); - map.insert(leaf, host_query(leaf)).unwrap(); - } - - // This needs to be done by hand because the `__get_cpuid_max` intrinsic - // only returns the maximum standard leaf. - let ext_leaf_0 = CpuidIdent::leaf(*EXTENDED_LEAVES.start()); - let ext_leaf_0_values = host_query(ext_leaf_0); - map.insert(ext_leaf_0, ext_leaf_0_values).unwrap(); - for l in (EXTENDED_LEAVES.start() + 1)..=ext_leaf_0_values.eax { - let leaf = CpuidIdent::leaf(l); - map.insert(leaf, host_query(leaf)).unwrap(); - } - - map -} - #[cfg(test)] mod test { use proptest::prelude::*; @@ -729,8 +686,8 @@ mod test { len -= 3; assert_eq!(map.len(), len); - // Remove leaf 4 via `retain_by_leaf`. - map.retain_by_leaf(|leaf| leaf != 4); + // Remove leaf 4 via `retain`. + map.retain(|id, _val| id.leaf != 4); len -= 3; assert_eq!(map.len(), len); diff --git a/crates/propolis-types/src/lib.rs b/crates/propolis-types/src/lib.rs index bb9c5f260..0d5d28bf6 100644 --- a/crates/propolis-types/src/lib.rs +++ b/crates/propolis-types/src/lib.rs @@ -289,6 +289,11 @@ impl CpuidValues { pub fn iter_mut(&mut self) -> impl Iterator { [&mut self.eax, &mut self.ebx, &mut self.ecx, &mut self.edx].into_iter() } + + /// Returns `true` if eax, ebx, ecx, and edx are all zero. + pub fn all_zero(&self) -> bool { + self.eax == 0 && self.ebx == 0 && self.ecx == 0 && self.edx == 0 + } } #[cfg(target_arch = "x86_64")] diff --git a/lib/propolis/src/vcpu.rs b/lib/propolis/src/vcpu.rs index 579094839..c9af9bb34 100644 --- a/lib/propolis/src/vcpu.rs +++ b/lib/propolis/src/vcpu.rs @@ -252,7 +252,7 @@ impl Vcpu { // (by nature of doing the cpuid queries against the host CPU) it // ignores the INTEL_FALLBACK flag. We must determine the vendor // kind by querying it. - let vendor = CpuidVendor::try_from(cpuid_utils::host_query( + let vendor = CpuidVendor::try_from(cpuid_utils::host::query( CpuidIdent::leaf(0), )) .map_err(GetCpuidError::UnsupportedVendor)?; diff --git a/phd-tests/framework/src/test_vm/config.rs b/phd-tests/framework/src/test_vm/config.rs index cb3e7569a..dbc20eb15 100644 --- a/phd-tests/framework/src/test_vm/config.rs +++ b/phd-tests/framework/src/test_vm/config.rs @@ -268,7 +268,7 @@ impl<'dr> VmConfig<'dr> { ); } - let host_leaf_0 = cpuid_utils::host_query(CpuidIdent::leaf(0)); + let host_leaf_0 = cpuid_utils::host::query(CpuidIdent::leaf(0)); let host_vendor = cpuid_utils::CpuidVendor::try_from(host_leaf_0) .map_err(|_| { anyhow::anyhow!( diff --git a/phd-tests/tests/src/cpuid.rs b/phd-tests/tests/src/cpuid.rs index 9d1a4e7c0..60f8727db 100644 --- a/phd-tests/tests/src/cpuid.rs +++ b/phd-tests/tests/src/cpuid.rs @@ -63,77 +63,34 @@ async fn cpuid_boot_test(ctx: &Framework) { // host's CPUID leaves, then filter to just a handful of leaves that // advertise features to the guest (and that Linux guests will check for // during boot). - let mut host_cpuid = cpuid_utils::host_query_all(); - info!(?host_cpuid, "read host CPUID"); - let leaf_0_values = *host_cpuid - .get(CpuidIdent::leaf(0)) - .expect("host CPUID leaf 0 should always be present"); + let mut host_cpuid = cpuid_utils::host::query_complete( + cpuid_utils::host::CpuidSource::BhyveDefault, + )?; + + info!(?host_cpuid, "read bhyve default CPUID"); // Linux guests expect to see at least a couple of leaves in the extended // CPUID range. These have vendor-specific meanings. This test only encodes // AMD's definitions, so skip the test if leaf 0 reports any other vendor. - let Ok(CpuidVendor::Amd) = CpuidVendor::try_from(leaf_0_values) else { + if host_cpuid.vendor() != CpuidVendor::Amd { phd_skip!("cpuid_boot_test can only run on AMD hosts"); - }; - - // Report up through leaf 7 to get the extended feature flags that are - // defined there. - if leaf_0_values.eax < 7 { - phd_skip!( - "cpuid_boot_test requires the host to report at least leaf 7" - ); } - // Keep only standard leaves up to 7 and the first two extended leaves. - // Extended leaves 2 through 4 will be overwritten with a fake brand string - // (see below). - host_cpuid.retain_by_leaf(|leaf| { - leaf <= 0x7 || (0x8000_0000..=0x8000_0001).contains(&leaf) - }); + // This test works by injecting a fake brand string into extended leaves + // 0x8000_0002-0x8000_0004 and seeing if the guest observes that string. For + // this to work those leaves need to be present in the host's extended CPUID + // leaves. + let Some(ext_leaf_0) = host_cpuid.get(CpuidIdent::leaf(EXTENDED_BASE_LEAF)) + else { + phd_skip!("cpuid_boot_test requires extended CPUID leaves"); + }; - // Report that leaf 7 is the last available standard leaf. - host_cpuid.get_mut(CpuidIdent::leaf(0)).unwrap().eax = 7; + if ext_leaf_0.eax < 0x8000_0004 { + phd_skip!("cpuid_boot_test requires at least leaf 0x8000_0004"); + } - // Mask off bits that a minimum-viable Oxide platform doesn't support or - // can't (or won't) expose to guests. See RFD 314 for further discussion of - // how these masks were chosen. - // - // These are masks (and not assignments) so that, if the host processor - // doesn't support a feature contained in an `ALL_FLAGS` value, the - // feature will not be enabled in the guest. - let leaf_1 = host_cpuid.get_mut(CpuidIdent::leaf(1)).unwrap(); - leaf_1.ecx &= - (Leaf1Ecx::ALL_FLAGS & !(Leaf1Ecx::OSXSAVE | Leaf1Ecx::MONITOR)).bits(); - leaf_1.edx &= Leaf1Edx::ALL_FLAGS.bits(); - let leaf_7 = host_cpuid.get_mut(CpuidIdent::leaf(7)).unwrap(); - leaf_7.eax = 0; - leaf_7.ebx &= (Leaf7Sub0Ebx::ALL_FLAGS - & !(Leaf7Sub0Ebx::PQM - | Leaf7Sub0Ebx::PQE - | Leaf7Sub0Ebx::RDSEED - | Leaf7Sub0Ebx::INVPCID)) - .bits(); - leaf_7.ecx = 0; - leaf_7.edx = 0; - - // Report that leaf 0x8000_0004 is the last extended leaf and clean up - // feature bits in leaf 0x8000_0001. - host_cpuid.get_mut(CpuidIdent::leaf(0x8000_0000)).unwrap().eax = - 0x8000_0004; - let ext_leaf_1 = host_cpuid.get_mut(CpuidIdent::leaf(0x8000_0001)).unwrap(); - ext_leaf_1.ecx &= (AmdExtLeaf1Ecx::ALT_MOV_CR8 - | AmdExtLeaf1Ecx::ABM - | AmdExtLeaf1Ecx::SSE4A - | AmdExtLeaf1Ecx::MISALIGN_SSE - | AmdExtLeaf1Ecx::THREED_NOW_PREFETCH - | AmdExtLeaf1Ecx::DATA_ACCESS_BP - | AmdExtLeaf1Ecx::DATA_BP_ADDR_MASK_EXT) - .bits(); - ext_leaf_1.edx &= - (AmdExtLeaf1Edx::ALL_FLAGS & !AmdExtLeaf1Edx::RDTSCP).bits(); - - // Test the plumbing by pumping a fake processor brand string into extended - // leaves 2-4 and seeing if the guest recognizes it. + // Reprogram the brand string leaves and see if the new string shows up in + // the guest. const BRAND_STRING: &[u8; 48] = b"Oxide Cloud Computer Company Cloud Computer\0\0\0\0\0";