1use petri_artifacts_common::capabilities;
7use std::collections::BTreeSet;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ExecutionEnvironment {
12 Baremetal,
14 Nested,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Vendor {
21 Amd,
23 Intel,
25 Arm,
27}
28
29impl Vendor {
30 pub fn host() -> Self {
32 #[cfg(target_arch = "x86_64")]
34 {
35 let result =
36 safe_intrinsics::cpuid(x86defs::cpuid::CpuidFunction::VendorAndMaxFunction.0, 0);
37 let vendor =
38 x86defs::cpuid::Vendor::from_ebx_ecx_edx(result.ebx, result.ecx, result.edx);
39 if vendor.is_amd_compatible() {
40 Vendor::Amd
41 } else {
42 assert!(vendor.is_intel_compatible());
43 Vendor::Intel
44 }
45 }
46 #[cfg(not(target_arch = "x86_64"))]
48 {
49 Vendor::Arm
50 }
51 }
52}
53
54#[derive(Clone, Copy, Debug, PartialEq)]
56pub enum IsolationType {
57 Vbs,
59 Snp,
61 Tdx,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum VmmType {
68 OpenVmm,
70 HyperV,
72 Qemu,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum OpenVmmHypervisor {
79 Mshv,
81 Kvm,
83 Whp,
85 Hvf,
87}
88
89#[derive(Debug, Clone)]
92pub struct VmHostInfo {
93 pub vbs_supported: bool,
95 pub snp_status: bool,
97 pub tdx_status: bool,
99}
100
101#[derive(Debug, Clone)]
103pub struct HostContext {
104 pub vm_host_info: Option<VmHostInfo>,
106 pub vendor: Vendor,
108 pub execution_environment: ExecutionEnvironment,
110 pub vpci_supported: bool,
112 pub openvmm_hypervisor: Option<OpenVmmHypervisor>,
114}
115
116impl HostContext {
117 pub async fn new() -> Self {
119 let is_nested = {
120 #[cfg(target_arch = "x86_64")]
122 {
123 let result = safe_intrinsics::cpuid(
124 hvdef::HV_CPUID_FUNCTION_MS_HV_ENLIGHTENMENT_INFORMATION,
125 0,
126 );
127 hvdef::HvEnlightenmentInformation::from(
128 result.eax as u128
129 | (result.ebx as u128) << 32
130 | (result.ecx as u128) << 64
131 | (result.edx as u128) << 96,
132 )
133 .nested()
134 }
135 #[cfg(not(target_arch = "x86_64"))]
137 {
138 false
139 }
140 };
141
142 let vendor = Vendor::host();
143
144 let vpci_supported = cfg!(windows);
146
147 let openvmm_hypervisor = if cfg!(target_os = "linux") {
148 if fs_err::File::open("/dev/mshv").is_ok() {
149 Some(OpenVmmHypervisor::Mshv)
150 } else if fs_err::File::options()
151 .read(true)
152 .write(true)
153 .open("/dev/kvm")
154 .is_ok()
155 {
156 Some(OpenVmmHypervisor::Kvm)
157 } else {
158 None
159 }
160 } else if cfg!(windows) {
161 Some(OpenVmmHypervisor::Whp)
162 } else if cfg!(target_os = "macos") {
163 Some(OpenVmmHypervisor::Hvf)
164 } else {
165 None
166 };
167
168 let vm_host_info = {
169 #[cfg(windows)]
170 {
171 crate::vm::hyperv::powershell::run_get_vm_host()
172 .await
173 .ok()
174 .map(|info| VmHostInfo {
175 vbs_supported: info.guest_isolation_types.contains(
176 &crate::vm::hyperv::powershell::HyperVGuestStateIsolationType::Vbs,
177 ),
178 snp_status: info.snp_status,
179 tdx_status: info.tdx_status,
180 })
181 }
182 #[cfg(not(windows))]
183 {
184 (vendor == Vendor::Amd && openvmm_hypervisor == Some(OpenVmmHypervisor::Mshv)).then(
187 || VmHostInfo {
188 vbs_supported: false,
189 snp_status: openvmm_hypervisors::mshv_snp_available(),
190 tdx_status: false,
191 },
192 )
193 }
194 };
195
196 Self {
197 vm_host_info,
198 vendor,
199 execution_environment: if is_nested {
200 ExecutionEnvironment::Nested
201 } else {
202 ExecutionEnvironment::Baremetal
203 },
204 vpci_supported,
205 openvmm_hypervisor,
206 }
207 }
208}
209
210pub enum TestRequirement {
212 ExecutionEnvironment(ExecutionEnvironment),
214 Vendor(Vendor),
216 OpenVmmHypervisor(OpenVmmHypervisor),
218 HostCapability(fn() -> bool),
228 Isolation(IsolationType),
230 RequiresCapability {
241 name: &'static str,
243 vmm: VmmType,
245 },
246 And(Box<TestRequirement>, Box<TestRequirement>),
248 Or(Box<TestRequirement>, Box<TestRequirement>),
250 Not(Box<TestRequirement>),
252 Any,
254}
255
256impl TestRequirement {
257 pub fn and(self, other: TestRequirement) -> TestRequirement {
259 TestRequirement::And(Box::new(self), Box::new(other))
260 }
261
262 pub fn or(self, other: TestRequirement) -> TestRequirement {
264 TestRequirement::Or(Box::new(self), Box::new(other))
265 }
266
267 #[expect(clippy::should_implement_trait)]
269 pub fn not(self) -> TestRequirement {
270 TestRequirement::Not(Box::new(self))
271 }
272
273 pub fn is_satisfied(&self, context: &HostContext) -> bool {
275 match self {
276 TestRequirement::ExecutionEnvironment(env) => context.execution_environment == *env,
277 TestRequirement::Vendor(vendor) => context.vendor == *vendor,
278 TestRequirement::OpenVmmHypervisor(hypervisor) => {
279 context.openvmm_hypervisor == Some(*hypervisor)
280 }
281 TestRequirement::HostCapability(probe) => probe(),
282 TestRequirement::Isolation(isolation_type) => {
283 if let Some(vm_host_info) = &context.vm_host_info {
284 match isolation_type {
285 IsolationType::Vbs => vm_host_info.vbs_supported,
286 IsolationType::Snp => vm_host_info.snp_status,
287 IsolationType::Tdx => vm_host_info.tdx_status,
288 }
289 } else {
290 false
291 }
292 }
293 TestRequirement::RequiresCapability { name, vmm } => {
294 available_capabilities(context, *vmm).contains(name)
295 }
296 TestRequirement::And(req1, req2) => {
297 req1.is_satisfied(context) && req2.is_satisfied(context)
298 }
299 TestRequirement::Or(req1, req2) => {
300 req1.is_satisfied(context) || req2.is_satisfied(context)
301 }
302 TestRequirement::Not(req) => !req.is_satisfied(context),
303 TestRequirement::Any => true,
304 }
305 }
306}
307
308pub fn known_capability(name: &str) -> Option<&'static str> {
310 capabilities::known(name)
311}
312
313pub fn is_known_capability(name: &str) -> bool {
315 known_capability(name).is_some()
316}
317
318fn available_capabilities(context: &HostContext, vmm: VmmType) -> BTreeSet<&'static str> {
319 let mut capabilities = BTreeSet::new();
320
321 if context.vpci_supported {
322 capabilities.insert(capabilities::VPCI);
323 }
324
325 if !matches!(
331 (vmm, context.openvmm_hypervisor),
332 (VmmType::OpenVmm, Some(OpenVmmHypervisor::Mshv))
333 ) {
334 capabilities.insert(capabilities::WINDOWS_PARTITION_RESET);
335 }
336
337 if matches!(
339 (vmm, context.openvmm_hypervisor),
340 (
341 VmmType::OpenVmm,
342 Some(OpenVmmHypervisor::Kvm | OpenVmmHypervisor::Whp | OpenVmmHypervisor::Mshv)
343 ) | (VmmType::Qemu, _)
344 ) {
345 capabilities.insert(capabilities::NESTED_VIRT);
346 }
347
348 match std::env::var("PETRI_CAPABILITIES") {
349 Ok(env_capabilities) => {
350 for capability in env_capabilities.split(',').map(str::trim) {
351 if capability.is_empty() {
352 continue;
353 }
354 let capability = known_capability(capability)
355 .unwrap_or_else(|| panic!("unknown PETRI_CAPABILITIES entry: {capability}"));
356 capabilities.insert(capability);
357 }
358 }
359 Err(std::env::VarError::NotPresent) => {}
360 Err(std::env::VarError::NotUnicode(_)) => {
361 panic!("PETRI_CAPABILITIES is not valid UTF-8")
362 }
363 }
364
365 capabilities
366}
367
368pub struct TestCaseRequirements {
370 requirements: TestRequirement,
371}
372
373impl TestCaseRequirements {
374 pub fn new(requirements: TestRequirement) -> Self {
376 Self { requirements }
377 }
378}
379
380pub fn can_run_test_with_context(
382 config: Option<&TestCaseRequirements>,
383 context: &HostContext,
384) -> bool {
385 if let Some(config) = config {
386 config.requirements.is_satisfied(context)
387 } else {
388 true
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395
396 fn host_context(openvmm_hypervisor: OpenVmmHypervisor) -> HostContext {
397 HostContext {
398 vm_host_info: None,
399 vendor: Vendor::Intel,
400 execution_environment: ExecutionEnvironment::Baremetal,
401 vpci_supported: false,
402 openvmm_hypervisor: Some(openvmm_hypervisor),
403 }
404 }
405
406 #[test]
407 fn capabilities_are_evaluated_for_the_selected_vmm() {
408 let requirement = |vmm| TestRequirement::RequiresCapability {
409 name: capabilities::WINDOWS_PARTITION_RESET,
410 vmm,
411 };
412 let mshv = host_context(OpenVmmHypervisor::Mshv);
413
414 assert!(!requirement(VmmType::OpenVmm).is_satisfied(&mshv));
415 assert!(requirement(VmmType::HyperV).is_satisfied(&mshv));
416 assert!(requirement(VmmType::OpenVmm).is_satisfied(&host_context(OpenVmmHypervisor::Kvm)));
417 }
418
419 #[test]
420 fn nested_virt_capability_matches_the_selected_backend() {
421 let requirement = TestRequirement::RequiresCapability {
422 name: capabilities::NESTED_VIRT,
423 vmm: VmmType::OpenVmm,
424 };
425
426 assert!(requirement.is_satisfied(&host_context(OpenVmmHypervisor::Mshv)));
427 assert!(!requirement.is_satisfied(&host_context(OpenVmmHypervisor::Hvf)));
428 }
429
430 #[test]
431 fn openvmm_hypervisor_requirement_matches_the_selected_backend() {
432 let requirement = TestRequirement::OpenVmmHypervisor(OpenVmmHypervisor::Mshv);
433 assert!(requirement.is_satisfied(&host_context(OpenVmmHypervisor::Mshv)));
434 assert!(!requirement.is_satisfied(&host_context(OpenVmmHypervisor::Kvm)));
435 assert!(!requirement.is_satisfied(&host_context(OpenVmmHypervisor::Whp)));
436 let mut without_hypervisor = host_context(OpenVmmHypervisor::Mshv);
437 without_hypervisor.openvmm_hypervisor = None;
438 assert!(!requirement.is_satisfied(&without_hypervisor));
439 }
440
441 #[test]
442 fn capability_probes_are_short_circuited_by_backend_requirements() {
443 let requirement = TestRequirement::OpenVmmHypervisor(OpenVmmHypervisor::Kvm).and(
444 TestRequirement::HostCapability(|| panic!("unavailable backend must not be probed")),
445 );
446 assert!(!requirement.is_satisfied(&host_context(OpenVmmHypervisor::Mshv)));
447 assert!(
448 !TestRequirement::HostCapability(|| false)
449 .is_satisfied(&host_context(OpenVmmHypervisor::Kvm))
450 );
451 assert!(
452 TestRequirement::HostCapability(|| true)
453 .is_satisfied(&host_context(OpenVmmHypervisor::Kvm))
454 );
455 }
456}