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}
61
62impl HostCapabilitiesReport {
63 pub const SCHEMA: u64 = 1;
64
65 pub fn probe() -> Self {
66 Self {
67 schema: Self::SCHEMA,
68 host_capabilities: probe_host_capabilities(),
69 }
70 }
71
72 pub fn validate(&self) -> Result<(), String> {
73 if self.schema != Self::SCHEMA {
74 return Err(format!(
75 "host-capabilities schema must be {}, got {}",
76 Self::SCHEMA,
77 self.schema
78 ));
79 }
80 for capability in HostCapability::ALL {
81 let verdict = self
82 .host_capabilities
83 .get(&capability)
84 .ok_or_else(|| format!("host-capabilities missing {}", capability.value()))?;
85 if verdict.evidence.trim().is_empty() {
86 return Err(format!(
87 "host-capabilities {} evidence must be nonempty",
88 capability.value()
89 ));
90 }
91 }
92 if self.host_capabilities.len() != HostCapability::ALL.len() {
93 return Err("host-capabilities must contain the complete closed set".into());
94 }
95 Ok(())
96 }
97}
98
99pub const ASSUME_PRESENT_ENV: &str = "HERMIT_VALIDATE_HOST_CAPABILITY_PRESENT";
100
101pub fn probe_host_capability(capability: HostCapability) -> CapabilityVerdict {
102 let forced = std::env::var(ASSUME_PRESENT_ENV).unwrap_or_default();
103 if forced
104 .split(',')
105 .map(str::trim)
106 .any(|candidate| candidate == capability.value())
107 {
108 return CapabilityVerdict {
109 present: true,
110 evidence: format!(
111 "{ASSUME_PRESENT_ENV} names {}; assumed PRESENT without probing (this override can only ADD capabilities)",
112 capability.value()
113 ),
114 };
115 }
116 match capability {
117 HostCapability::CpuidFaulting => probe_cpuid_faulting(),
118 HostCapability::Kvm => probe_kvm(),
119 }
120}
121
122pub fn probe_host_capabilities() -> HostCapabilities {
123 HostCapability::ALL
124 .into_iter()
125 .map(|capability| (capability, probe_host_capability(capability)))
126 .collect()
127}
128
129pub fn cpuid_faulting_absent(syscall: Result<(), i32>, advertised: Option<bool>) -> bool {
130 syscall == Err(libc::ENODEV) && advertised == Some(false)
131}
132
133fn probe_cpuid_faulting() -> CapabilityVerdict {
134 let syscall = arch_prctl_set_cpuid_off();
135 let advertised = cpuinfo_advertises_cpuid_fault();
136 let syscall_text = match syscall {
137 Ok(()) => "arch_prctl(ARCH_SET_CPUID, 0) = 0".to_string(),
138 Err(0) => "arch_prctl(ARCH_SET_CPUID, 0) probe could not be completed".to_string(),
139 Err(errno) => format!("arch_prctl(ARCH_SET_CPUID, 0) = -1 errno={errno}"),
140 };
141 let cpuinfo_text = match advertised {
142 Some(true) => "/proc/cpuinfo advertises cpuid_fault",
143 Some(false) => "/proc/cpuinfo does not advertise cpuid_fault",
144 None => "/proc/cpuinfo could not be read",
145 };
146 CapabilityVerdict {
147 present: !cpuid_faulting_absent(syscall, advertised),
148 evidence: format!("{syscall_text}; {cpuinfo_text}"),
149 }
150}
151
152fn arch_prctl_set_cpuid_off() -> Result<(), i32> {
153 const ARCH_SET_CPUID: libc::c_int = 0x1012;
154 let child = unsafe { libc::fork() };
156 if child < 0 {
157 return Err(0);
158 }
159 if child == 0 {
160 let result = unsafe { libc::syscall(libc::SYS_arch_prctl, ARCH_SET_CPUID, 0) };
161 let code = if result == 0 {
162 0
163 } else {
164 let errno = unsafe { *libc::__errno_location() };
166 errno.clamp(1, 255)
167 };
168 unsafe { libc::_exit(code) };
170 }
171 let mut status = 0;
172 if unsafe { libc::waitpid(child, &mut status, 0) } != child || !libc::WIFEXITED(status) {
173 return Err(0);
174 }
175 match libc::WEXITSTATUS(status) {
176 0 => Ok(()),
177 errno => Err(errno),
178 }
179}
180
181fn cpuinfo_advertises_cpuid_fault() -> Option<bool> {
182 let text = std::fs::read_to_string("/proc/cpuinfo").ok()?;
183 Some(text.split_whitespace().any(|word| word == "cpuid_fault"))
184}
185
186pub fn kvm_absent(open: Result<(), i32>) -> bool {
192 open == Err(libc::ENOENT)
193}
194
195fn probe_kvm() -> CapabilityVerdict {
196 let open = open_dev_kvm();
197 let advertised = cpuinfo_advertises_virtualization();
198 let open_text = match open {
199 Ok(()) => "open(/dev/kvm, O_RDWR) = ok".to_string(),
200 Err(errno) => format!("open(/dev/kvm, O_RDWR) = -1 errno={errno}"),
201 };
202 let cpuinfo_text = match advertised {
203 Some(true) => "/proc/cpuinfo advertises vmx or svm",
204 Some(false) => "/proc/cpuinfo advertises neither vmx nor svm",
205 None => "/proc/cpuinfo could not be read",
206 };
207 CapabilityVerdict {
208 present: !kvm_absent(open),
209 evidence: format!("{open_text}; {cpuinfo_text}"),
210 }
211}
212
213fn open_dev_kvm() -> Result<(), i32> {
214 let path = std::ffi::CString::new("/dev/kvm").expect("static path has no NUL");
215 let fd = unsafe { libc::open(path.as_ptr(), libc::O_RDWR | libc::O_CLOEXEC) };
217 if fd >= 0 {
218 unsafe { libc::close(fd) };
220 return Ok(());
221 }
222 let errno = unsafe { *libc::__errno_location() };
224 Err(errno.clamp(1, 255))
225}
226
227fn cpuinfo_advertises_virtualization() -> Option<bool> {
228 let text = std::fs::read_to_string("/proc/cpuinfo").ok()?;
229 Some(
230 text.split_whitespace()
231 .any(|word| word == "vmx" || word == "svm"),
232 )
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn complete_probe_records_every_closed_capability_with_evidence() {
241 let report = HostCapabilitiesReport::probe();
242 report.validate().unwrap();
243 assert_eq!(report.host_capabilities.len(), HostCapability::ALL.len());
244 }
245
246 #[test]
249 fn a_missing_kvm_device_is_proof_of_absence() {
250 assert!(kvm_absent(Err(libc::ENOENT)));
251 for doubt in [
252 Ok(()),
253 Err(libc::EACCES),
254 Err(libc::EPERM),
255 Err(libc::EBUSY),
256 ] {
257 assert!(!kvm_absent(doubt), "{doubt:?} must not read as absent");
258 }
259 }
260
261 #[test]
262 fn incomplete_report_refuses_by_capability_name() {
263 let mut report = HostCapabilitiesReport::probe();
264 report
265 .host_capabilities
266 .remove(&HostCapability::CpuidFaulting);
267 assert_eq!(
268 report.validate().unwrap_err(),
269 "host-capabilities missing cpuid-faulting"
270 );
271 }
272}