1use std::collections::BTreeMap;
4
5use serde::Deserialize;
6use serde::Serialize;
7
8#[derive(
9 Clone,
10 Copy,
11 Debug,
12 Deserialize,
13 Eq,
14 Ord,
15 PartialEq,
16 PartialOrd,
17 Serialize
18)]
19pub enum HostCapability {
20 #[serde(rename = "cpuid-faulting")]
21 CpuidFaulting,
22 #[serde(rename = "kvm")]
23 Kvm,
24}
25
26impl HostCapability {
27 pub const ALL: [Self; 2] = [Self::CpuidFaulting, Self::Kvm];
28
29 pub fn value(self) -> &'static str {
30 match self {
31 Self::CpuidFaulting => "cpuid-faulting",
32 Self::Kvm => "kvm",
33 }
34 }
35
36 pub fn from_value(text: &str) -> Option<Self> {
37 match text {
38 "cpuid-faulting" => Some(Self::CpuidFaulting),
39 "kvm" => Some(Self::Kvm),
40 _ => None,
41 }
42 }
43}
44
45#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
46#[serde(deny_unknown_fields)]
47pub struct CapabilityVerdict {
48 pub present: bool,
49 pub evidence: String,
50}
51
52pub type HostCapabilities = BTreeMap<HostCapability, CapabilityVerdict>;
53
54#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
56#[serde(deny_unknown_fields)]
57pub struct HostCapabilitiesReport {
58 pub schema: u64,
59 pub host_capabilities: HostCapabilities,
60 pub exact_branch_counter: CapabilityVerdict,
72}
73
74impl HostCapabilitiesReport {
75 pub const SCHEMA: u64 = 2;
76
77 pub fn probe(exact_branch_counter: CapabilityVerdict) -> Self {
80 Self {
81 schema: Self::SCHEMA,
82 host_capabilities: probe_host_capabilities(),
83 exact_branch_counter,
84 }
85 }
86
87 pub fn validate(&self) -> Result<(), String> {
88 if self.schema != Self::SCHEMA {
89 return Err(format!(
90 "host-capabilities schema must be {}, got {}",
91 Self::SCHEMA,
92 self.schema
93 ));
94 }
95 for capability in HostCapability::ALL {
96 let verdict = self
97 .host_capabilities
98 .get(&capability)
99 .ok_or_else(|| format!("host-capabilities missing {}", capability.value()))?;
100 if verdict.evidence.trim().is_empty() {
101 return Err(format!(
102 "host-capabilities {} evidence must be nonempty",
103 capability.value()
104 ));
105 }
106 }
107 if self.host_capabilities.len() != HostCapability::ALL.len() {
108 return Err("host-capabilities must contain the complete closed set".into());
109 }
110 if self.exact_branch_counter.evidence.trim().is_empty() {
111 return Err("host-capabilities exact_branch_counter evidence must be nonempty".into());
112 }
113 Ok(())
114 }
115}
116
117pub const ASSUME_PRESENT_ENV: &str = "HERMIT_VALIDATE_HOST_CAPABILITY_PRESENT";
118
119pub fn probe_host_capability(capability: HostCapability) -> CapabilityVerdict {
120 let forced = std::env::var(ASSUME_PRESENT_ENV).unwrap_or_default();
121 if forced
122 .split(',')
123 .map(str::trim)
124 .any(|candidate| candidate == capability.value())
125 {
126 return CapabilityVerdict {
127 present: true,
128 evidence: format!(
129 "{ASSUME_PRESENT_ENV} names {}; assumed PRESENT without probing (this override can only ADD capabilities)",
130 capability.value()
131 ),
132 };
133 }
134 match capability {
135 HostCapability::CpuidFaulting => probe_cpuid_faulting(),
136 HostCapability::Kvm => probe_kvm(),
137 }
138}
139
140pub fn probe_host_capabilities() -> HostCapabilities {
141 HostCapability::ALL
142 .into_iter()
143 .map(|capability| (capability, probe_host_capability(capability)))
144 .collect()
145}
146
147pub fn cpuid_faulting_absent(syscall: Result<(), i32>, advertised: Option<bool>) -> bool {
148 syscall == Err(libc::ENODEV) && advertised == Some(false)
149}
150
151#[cfg(not(target_arch = "x86_64"))]
154fn probe_cpuid_faulting() -> CapabilityVerdict {
155 CapabilityVerdict {
156 present: false,
157 evidence: format!(
158 "{} has no CPUID instruction to fault",
159 std::env::consts::ARCH
160 ),
161 }
162}
163
164#[cfg(target_arch = "x86_64")]
165fn probe_cpuid_faulting() -> CapabilityVerdict {
166 let syscall = arch_prctl_set_cpuid_off();
167 let advertised = cpuinfo_advertises_cpuid_fault();
168 let syscall_text = match syscall {
169 Ok(()) => "arch_prctl(ARCH_SET_CPUID, 0) = 0".to_string(),
170 Err(0) => "arch_prctl(ARCH_SET_CPUID, 0) probe could not be completed".to_string(),
171 Err(errno) => format!("arch_prctl(ARCH_SET_CPUID, 0) = -1 errno={errno}"),
172 };
173 let cpuinfo_text = match advertised {
174 Some(true) => "/proc/cpuinfo advertises cpuid_fault",
175 Some(false) => "/proc/cpuinfo does not advertise cpuid_fault",
176 None => "/proc/cpuinfo could not be read",
177 };
178 CapabilityVerdict {
179 present: !cpuid_faulting_absent(syscall, advertised),
180 evidence: format!("{syscall_text}; {cpuinfo_text}"),
181 }
182}
183
184#[cfg(target_arch = "x86_64")]
185fn arch_prctl_set_cpuid_off() -> Result<(), i32> {
186 const ARCH_SET_CPUID: libc::c_int = 0x1012;
187 let child = unsafe { libc::fork() };
189 if child < 0 {
190 return Err(0);
191 }
192 if child == 0 {
193 let result = unsafe { libc::syscall(libc::SYS_arch_prctl, ARCH_SET_CPUID, 0) };
194 let code = if result == 0 {
195 0
196 } else {
197 let errno = unsafe { *libc::__errno_location() };
199 errno.clamp(1, 255)
200 };
201 unsafe { libc::_exit(code) };
203 }
204 let mut status = 0;
205 if unsafe { libc::waitpid(child, &mut status, 0) } != child || !libc::WIFEXITED(status) {
206 return Err(0);
207 }
208 match libc::WEXITSTATUS(status) {
209 0 => Ok(()),
210 errno => Err(errno),
211 }
212}
213
214#[cfg(target_arch = "x86_64")]
215fn cpuinfo_advertises_cpuid_fault() -> Option<bool> {
216 let text = std::fs::read_to_string("/proc/cpuinfo").ok()?;
217 Some(text.split_whitespace().any(|word| word == "cpuid_fault"))
218}
219
220pub fn kvm_absent(open: Result<(), i32>) -> bool {
226 open == Err(libc::ENOENT)
227}
228
229fn probe_kvm() -> CapabilityVerdict {
230 let open = open_dev_kvm();
231 let advertised = cpuinfo_advertises_virtualization();
232 let open_text = match open {
233 Ok(()) => "open(/dev/kvm, O_RDWR) = ok".to_string(),
234 Err(errno) => format!("open(/dev/kvm, O_RDWR) = -1 errno={errno}"),
235 };
236 let cpuinfo_text = match advertised {
237 Some(true) => "/proc/cpuinfo advertises vmx or svm",
238 Some(false) => "/proc/cpuinfo advertises neither vmx nor svm",
239 None => "/proc/cpuinfo could not be read",
240 };
241 CapabilityVerdict {
242 present: !kvm_absent(open),
243 evidence: format!("{open_text}; {cpuinfo_text}"),
244 }
245}
246
247fn open_dev_kvm() -> Result<(), i32> {
248 let path = std::ffi::CString::new("/dev/kvm").expect("static path has no NUL");
249 let fd = unsafe { libc::open(path.as_ptr(), libc::O_RDWR | libc::O_CLOEXEC) };
251 if fd >= 0 {
252 unsafe { libc::close(fd) };
254 return Ok(());
255 }
256 let errno = unsafe { *libc::__errno_location() };
258 Err(errno.clamp(1, 255))
259}
260
261fn cpuinfo_advertises_virtualization() -> Option<bool> {
262 let text = std::fs::read_to_string("/proc/cpuinfo").ok()?;
263 Some(
264 text.split_whitespace()
265 .any(|word| word == "vmx" || word == "svm"),
266 )
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 fn exact_branch_counter(evidence: &str) -> CapabilityVerdict {
274 CapabilityVerdict {
275 present: true,
276 evidence: evidence.into(),
277 }
278 }
279
280 #[test]
281 fn complete_probe_records_every_closed_capability_with_evidence() {
282 let report = HostCapabilitiesReport::probe(exact_branch_counter("fixture"));
283 report.validate().unwrap();
284 assert_eq!(report.host_capabilities.len(), HostCapability::ALL.len());
285 }
286
287 #[test]
288 fn exact_branch_counter_needs_evidence() {
289 let report = HostCapabilitiesReport::probe(exact_branch_counter(" "));
290 assert_eq!(
291 report.validate().unwrap_err(),
292 "host-capabilities exact_branch_counter evidence must be nonempty"
293 );
294 }
295
296 #[test]
299 fn a_missing_kvm_device_is_proof_of_absence() {
300 assert!(kvm_absent(Err(libc::ENOENT)));
301 for doubt in [
302 Ok(()),
303 Err(libc::EACCES),
304 Err(libc::EPERM),
305 Err(libc::EBUSY),
306 ] {
307 assert!(!kvm_absent(doubt), "{doubt:?} must not read as absent");
308 }
309 }
310
311 #[test]
312 fn incomplete_report_refuses_by_capability_name() {
313 let mut report = HostCapabilitiesReport::probe(exact_branch_counter("fixture"));
314 report
315 .host_capabilities
316 .remove(&HostCapability::CpuidFaulting);
317 assert_eq!(
318 report.validate().unwrap_err(),
319 "host-capabilities missing cpuid-faulting"
320 );
321 }
322}