//! Linux host-identity reads. Hostname, DMI product name, CPU model. //! //! Everything here is metadata that goes into a `BoxReportPayload`'s //! `host` field. None of these strings are inputs to scoring; they are //! recorded so a future reader can identify which physical machine //! produced the report. use std::fs; use everycycle_hal::HostIdentity; const UNKNOWN: &str = "unknown"; #[must_use] pub fn host_identity() -> HostIdentity { HostIdentity { hostname: read_hostname(), dmi_string: read_dmi_product_name(), cpu_model: read_cpu_model(), } } fn read_hostname() -> String { fs::read_to_string("/etc/hostname") .map(|s| s.trim().to_string()) .ok() .filter(|s| !s.is_empty()) .unwrap_or_else(|| UNKNOWN.to_string()) } fn read_dmi_product_name() -> Option { fs::read_to_string("/sys/class/dmi/id/product_name") .ok() .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) } /// Read a representative CPU model line from `/proc/cpuinfo`. fn read_cpu_model() -> String { fs::read_to_string("/proc/cpuinfo") .map_or_else(|_| UNKNOWN.to_string(), |c| parse_cpu_model(&c)) } /// Derive a human-readable CPU model from the contents of `/proc/cpuinfo`. /// /// x86_64 supplies `model name`. Aarch64 does not: the kernel exposes /// the MIDR fields (`CPU implementer`, `CPU part`) and leaves naming to /// userspace, though board-level kernels often add `Model` or /// `Hardware`. Prose keys are preferred in order; the MIDR decode is /// the fallback. /// /// The raw implementer and part are always kept in the decoded string. /// This value is identity metadata in a signed report, so a reader has /// to be able to check the decode rather than take its word for it. fn parse_cpu_model(cpuinfo: &str) -> String { const PROSE_KEYS: &[&str] = &["model name", "Model", "Hardware"]; for key in PROSE_KEYS { if let Some(value) = field(cpuinfo, key) { return value; } } decode_midr(cpuinfo).unwrap_or_else(|| UNKNOWN.to_string()) } /// First non-empty `key : value` line in `/proc/cpuinfo`. fn field(cpuinfo: &str, key: &str) -> Option { cpuinfo .lines() .filter_map(|line| { let (name, value) = line.split_once(':')?; (name.trim() == key).then(|| value.trim().to_string()) }) .find(|value| !value.is_empty()) } /// ARM implementer byte to vendor name. Only entries we are confident /// of; an unrecognized implementer reports its raw byte rather than a /// guess. fn implementer_name(implementer: u32) -> Option<&'static str> { Some(match implementer { 0x41 => "ARM", 0x42 => "Broadcom", 0x43 => "Cavium", 0x4e => "NVIDIA", 0x50 => "Applied Micro", 0x51 => "Qualcomm", 0x53 => "Samsung", 0x61 => "Apple", 0xc0 => "Ampere", _ => return None, }) } /// ARM-designed core names by part number. Deliberately partial: a /// wrong core name in a signed report is worse than an honest /// "unknown core". fn core_name(implementer: u32, part: u32) -> Option<&'static str> { if implementer != 0x41 { return None; } Some(match part { 0xd03 => "Cortex-A53", 0xd05 => "Cortex-A55", 0xd07 => "Cortex-A57", 0xd08 => "Cortex-A72", 0xd09 => "Cortex-A73", 0xd0b => "Cortex-A76", 0xd0c => "Neoverse-N1", 0xd40 => "Neoverse-V1", 0xd49 => "Neoverse-N2", 0xd4f => "Neoverse-V2", _ => return None, }) } /// Decode the aarch64 MIDR fields into a readable name, keeping the /// raw ids for verification. fn decode_midr(cpuinfo: &str) -> Option { let implementer_raw = field(cpuinfo, "CPU implementer")?; let part_raw = field(cpuinfo, "CPU part")?; let implementer = parse_hex(&implementer_raw)?; let part = parse_hex(&part_raw)?; let ids = format!("{implementer_raw}:{part_raw}"); Some( match (implementer_name(implementer), core_name(implementer, part)) { (Some(vendor), Some(core)) => format!("{vendor} {core} ({ids})"), (Some(vendor), None) => format!("{vendor} unknown core ({ids})"), (None, _) => format!("unknown implementer ({ids})"), }, ) } fn parse_hex(value: &str) -> Option { let digits = value .trim() .strip_prefix("0x") .or_else(|| value.trim().strip_prefix("0X")) .unwrap_or(value.trim()); u32::from_str_radix(digits, 16).ok() } #[cfg(test)] mod tests { use super::*; /// Trimmed from astra (Ampere Altra, Thelio Astra). No `model name` /// anywhere in the file, which is what made the raw part number leak /// into reports. const AARCH64_ALTRA: &str = "\ processor\t: 0 BogoMIPS\t: 50.00 Features\t: fp asimd evtstrm aes pmull sha1 sha2 crc32 CPU implementer\t: 0x41 CPU architecture: 8 CPU variant\t: 0x3 CPU part\t: 0xd0c CPU revision\t: 1 "; const X86_64: &str = "\ processor\t: 0 vendor_id\t: GenuineIntel model\t\t: 186 model name\t: 13th Gen Intel(R) Core(TM) i7-1370P stepping\t: 2 "; const RASPBERRY_PI: &str = "\ processor\t: 0 BogoMIPS\t: 108.00 CPU implementer\t: 0x41 CPU part\t: 0xd08 Hardware\t: BCM2835 Model\t\t: Raspberry Pi 4 Model B Rev 1.4 "; #[test] fn aarch64_midr_decodes_to_a_core_name() { assert_eq!( parse_cpu_model(AARCH64_ALTRA), "ARM Neoverse-N1 (0x41:0xd0c)" ); } #[test] fn x86_prefers_the_model_name_line() { assert_eq!( parse_cpu_model(X86_64), "13th Gen Intel(R) Core(TM) i7-1370P" ); } #[test] fn prose_keys_win_over_the_midr_decode() { // `Model` outranks `Hardware`, and both outrank the MIDR, even // though the MIDR lines come first in the file. The old // line-major scan returned whichever key appeared earliest. assert_eq!( parse_cpu_model(RASPBERRY_PI), "Raspberry Pi 4 Model B Rev 1.4" ); } #[test] fn unknown_part_stays_honest() { let unknown_core = AARCH64_ALTRA.replace("0xd0c", "0xd8e"); assert_eq!( parse_cpu_model(&unknown_core), "ARM unknown core (0x41:0xd8e)" ); let unknown_vendor = AARCH64_ALTRA.replace("0x41", "0x69"); assert_eq!( parse_cpu_model(&unknown_vendor), "unknown implementer (0x69:0xd0c)" ); } #[test] fn nothing_recognizable_reports_unknown() { assert_eq!(parse_cpu_model(""), UNKNOWN); assert_eq!(parse_cpu_model("processor\t: 0\n"), UNKNOWN); } #[test] fn empty_values_do_not_win() { let blank = "model name\t:\nModel\t\t: Thelio Astra\n"; assert_eq!(parse_cpu_model(blank), "Thelio Astra"); } #[test] fn values_containing_colons_survive() { let clocked = "model name\t: Some CPU @ 3.00GHz: turbo\n"; assert_eq!(parse_cpu_model(clocked), "Some CPU @ 3.00GHz: turbo"); } }