Skip to main content

max / everycycle

7.0 KB · 238 lines History Blame Raw
1 //! Linux host-identity reads. Hostname, DMI product name, CPU model.
2 //!
3 //! Everything here is metadata that goes into a `BoxReportPayload`'s
4 //! `host` field. None of these strings are inputs to scoring; they are
5 //! recorded so a future reader can identify which physical machine
6 //! produced the report.
7
8 use std::fs;
9
10 use everycycle_hal::HostIdentity;
11
12 const UNKNOWN: &str = "unknown";
13
14 #[must_use]
15 pub fn host_identity() -> HostIdentity {
16 HostIdentity {
17 hostname: read_hostname(),
18 dmi_string: read_dmi_product_name(),
19 cpu_model: read_cpu_model(),
20 }
21 }
22
23 fn read_hostname() -> String {
24 fs::read_to_string("/etc/hostname")
25 .map(|s| s.trim().to_string())
26 .ok()
27 .filter(|s| !s.is_empty())
28 .unwrap_or_else(|| UNKNOWN.to_string())
29 }
30
31 fn read_dmi_product_name() -> Option<String> {
32 fs::read_to_string("/sys/class/dmi/id/product_name")
33 .ok()
34 .map(|s| s.trim().to_string())
35 .filter(|s| !s.is_empty())
36 }
37
38 /// Read a representative CPU model line from `/proc/cpuinfo`.
39 fn read_cpu_model() -> String {
40 fs::read_to_string("/proc/cpuinfo")
41 .map_or_else(|_| UNKNOWN.to_string(), |c| parse_cpu_model(&c))
42 }
43
44 /// Derive a human-readable CPU model from the contents of `/proc/cpuinfo`.
45 ///
46 /// x86_64 supplies `model name`. Aarch64 does not: the kernel exposes
47 /// the MIDR fields (`CPU implementer`, `CPU part`) and leaves naming to
48 /// userspace, though board-level kernels often add `Model` or
49 /// `Hardware`. Prose keys are preferred in order; the MIDR decode is
50 /// the fallback.
51 ///
52 /// The raw implementer and part are always kept in the decoded string.
53 /// This value is identity metadata in a signed report, so a reader has
54 /// to be able to check the decode rather than take its word for it.
55 fn parse_cpu_model(cpuinfo: &str) -> String {
56 const PROSE_KEYS: &[&str] = &["model name", "Model", "Hardware"];
57 for key in PROSE_KEYS {
58 if let Some(value) = field(cpuinfo, key) {
59 return value;
60 }
61 }
62 decode_midr(cpuinfo).unwrap_or_else(|| UNKNOWN.to_string())
63 }
64
65 /// First non-empty `key : value` line in `/proc/cpuinfo`.
66 fn field(cpuinfo: &str, key: &str) -> Option<String> {
67 cpuinfo
68 .lines()
69 .filter_map(|line| {
70 let (name, value) = line.split_once(':')?;
71 (name.trim() == key).then(|| value.trim().to_string())
72 })
73 .find(|value| !value.is_empty())
74 }
75
76 /// ARM implementer byte to vendor name. Only entries we are confident
77 /// of; an unrecognized implementer reports its raw byte rather than a
78 /// guess.
79 fn implementer_name(implementer: u32) -> Option<&'static str> {
80 Some(match implementer {
81 0x41 => "ARM",
82 0x42 => "Broadcom",
83 0x43 => "Cavium",
84 0x4e => "NVIDIA",
85 0x50 => "Applied Micro",
86 0x51 => "Qualcomm",
87 0x53 => "Samsung",
88 0x61 => "Apple",
89 0xc0 => "Ampere",
90 _ => return None,
91 })
92 }
93
94 /// ARM-designed core names by part number. Deliberately partial: a
95 /// wrong core name in a signed report is worse than an honest
96 /// "unknown core".
97 fn core_name(implementer: u32, part: u32) -> Option<&'static str> {
98 if implementer != 0x41 {
99 return None;
100 }
101 Some(match part {
102 0xd03 => "Cortex-A53",
103 0xd05 => "Cortex-A55",
104 0xd07 => "Cortex-A57",
105 0xd08 => "Cortex-A72",
106 0xd09 => "Cortex-A73",
107 0xd0b => "Cortex-A76",
108 0xd0c => "Neoverse-N1",
109 0xd40 => "Neoverse-V1",
110 0xd49 => "Neoverse-N2",
111 0xd4f => "Neoverse-V2",
112 _ => return None,
113 })
114 }
115
116 /// Decode the aarch64 MIDR fields into a readable name, keeping the
117 /// raw ids for verification.
118 fn decode_midr(cpuinfo: &str) -> Option<String> {
119 let implementer_raw = field(cpuinfo, "CPU implementer")?;
120 let part_raw = field(cpuinfo, "CPU part")?;
121 let implementer = parse_hex(&implementer_raw)?;
122 let part = parse_hex(&part_raw)?;
123
124 let ids = format!("{implementer_raw}:{part_raw}");
125 Some(
126 match (implementer_name(implementer), core_name(implementer, part)) {
127 (Some(vendor), Some(core)) => format!("{vendor} {core} ({ids})"),
128 (Some(vendor), None) => format!("{vendor} unknown core ({ids})"),
129 (None, _) => format!("unknown implementer ({ids})"),
130 },
131 )
132 }
133
134 fn parse_hex(value: &str) -> Option<u32> {
135 let digits = value
136 .trim()
137 .strip_prefix("0x")
138 .or_else(|| value.trim().strip_prefix("0X"))
139 .unwrap_or(value.trim());
140 u32::from_str_radix(digits, 16).ok()
141 }
142
143 #[cfg(test)]
144 mod tests {
145 use super::*;
146
147 /// Trimmed from astra (Ampere Altra, Thelio Astra). No `model name`
148 /// anywhere in the file, which is what made the raw part number leak
149 /// into reports.
150 const AARCH64_ALTRA: &str = "\
151 processor\t: 0
152 BogoMIPS\t: 50.00
153 Features\t: fp asimd evtstrm aes pmull sha1 sha2 crc32
154 CPU implementer\t: 0x41
155 CPU architecture: 8
156 CPU variant\t: 0x3
157 CPU part\t: 0xd0c
158 CPU revision\t: 1
159 ";
160
161 const X86_64: &str = "\
162 processor\t: 0
163 vendor_id\t: GenuineIntel
164 model\t\t: 186
165 model name\t: 13th Gen Intel(R) Core(TM) i7-1370P
166 stepping\t: 2
167 ";
168
169 const RASPBERRY_PI: &str = "\
170 processor\t: 0
171 BogoMIPS\t: 108.00
172 CPU implementer\t: 0x41
173 CPU part\t: 0xd08
174 Hardware\t: BCM2835
175 Model\t\t: Raspberry Pi 4 Model B Rev 1.4
176 ";
177
178 #[test]
179 fn aarch64_midr_decodes_to_a_core_name() {
180 assert_eq!(
181 parse_cpu_model(AARCH64_ALTRA),
182 "ARM Neoverse-N1 (0x41:0xd0c)"
183 );
184 }
185
186 #[test]
187 fn x86_prefers_the_model_name_line() {
188 assert_eq!(
189 parse_cpu_model(X86_64),
190 "13th Gen Intel(R) Core(TM) i7-1370P"
191 );
192 }
193
194 #[test]
195 fn prose_keys_win_over_the_midr_decode() {
196 // `Model` outranks `Hardware`, and both outrank the MIDR, even
197 // though the MIDR lines come first in the file. The old
198 // line-major scan returned whichever key appeared earliest.
199 assert_eq!(
200 parse_cpu_model(RASPBERRY_PI),
201 "Raspberry Pi 4 Model B Rev 1.4"
202 );
203 }
204
205 #[test]
206 fn unknown_part_stays_honest() {
207 let unknown_core = AARCH64_ALTRA.replace("0xd0c", "0xd8e");
208 assert_eq!(
209 parse_cpu_model(&unknown_core),
210 "ARM unknown core (0x41:0xd8e)"
211 );
212
213 let unknown_vendor = AARCH64_ALTRA.replace("0x41", "0x69");
214 assert_eq!(
215 parse_cpu_model(&unknown_vendor),
216 "unknown implementer (0x69:0xd0c)"
217 );
218 }
219
220 #[test]
221 fn nothing_recognizable_reports_unknown() {
222 assert_eq!(parse_cpu_model(""), UNKNOWN);
223 assert_eq!(parse_cpu_model("processor\t: 0\n"), UNKNOWN);
224 }
225
226 #[test]
227 fn empty_values_do_not_win() {
228 let blank = "model name\t:\nModel\t\t: Thelio Astra\n";
229 assert_eq!(parse_cpu_model(blank), "Thelio Astra");
230 }
231
232 #[test]
233 fn values_containing_colons_survive() {
234 let clocked = "model name\t: Some CPU @ 3.00GHz: turbo\n";
235 assert_eq!(parse_cpu_model(clocked), "Some CPU @ 3.00GHz: turbo");
236 }
237 }
238