Skip to main content

petri/
requirements.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Test requirements framework for runtime test filtering.
5
6use petri_artifacts_common::capabilities;
7use std::collections::BTreeSet;
8
9/// Execution environments where tests can run.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ExecutionEnvironment {
12    /// Bare metal execution (not nested virtualization).
13    Baremetal,
14    /// Nested virtualization environment.
15    Nested,
16}
17
18/// CPU vendors.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Vendor {
21    /// AMD processors.
22    Amd,
23    /// Intel processors.
24    Intel,
25    /// ARM processors.
26    Arm,
27}
28
29impl Vendor {
30    /// Detect the vendor of the host CPU the test is running on.
31    pub fn host() -> Self {
32        // xtask-fmt allow-target-arch cpu-intrinsic
33        #[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        // xtask-fmt allow-target-arch cpu-intrinsic
47        #[cfg(not(target_arch = "x86_64"))]
48        {
49            Vendor::Arm
50        }
51    }
52}
53
54/// Types of isolation supported.
55#[derive(Clone, Copy, Debug, PartialEq)]
56pub enum IsolationType {
57    /// Virtualization-based Security (VBS)
58    Vbs,
59    /// Secure Nested Paging (SNP)
60    Snp,
61    /// Trusted Domain Extensions (TDX)
62    Tdx,
63}
64
65/// VMM implementation types.
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum VmmType {
68    /// OpenVMM.
69    OpenVmm,
70    /// Microsoft Hyper-V.
71    HyperV,
72    /// QEMU.
73    Qemu,
74}
75
76/// Hypervisor backends that OpenVMM can use.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum OpenVmmHypervisor {
79    /// Linux Microsoft Hypervisor backend.
80    Mshv,
81    /// Linux KVM backend.
82    Kvm,
83    /// Windows Hypervisor Platform backend.
84    Whp,
85    /// macOS Hypervisor Framework backend.
86    Hvf,
87}
88
89/// Information about the VM host. On Windows, petri reads it with the
90/// `Get-VMHost` PowerShell cmdlet. On Linux, petri probes MSHV for SNP support.
91#[derive(Debug, Clone)]
92pub struct VmHostInfo {
93    /// VBS support status
94    pub vbs_supported: bool,
95    /// SNP support status
96    pub snp_status: bool,
97    /// TDX support status
98    pub tdx_status: bool,
99}
100
101/// Platform-specific host context extending the base HostContext
102#[derive(Debug, Clone)]
103pub struct HostContext {
104    /// VM host information, if petri can get it on this host
105    pub vm_host_info: Option<VmHostInfo>,
106    /// CPU vendor
107    pub vendor: Vendor,
108    /// Execution environment
109    pub execution_environment: ExecutionEnvironment,
110    /// Whether the host hypervisor supports software VPCI device emulation
111    pub vpci_supported: bool,
112    /// Hypervisor backend that OpenVMM will select on this host.
113    pub openvmm_hypervisor: Option<OpenVmmHypervisor>,
114}
115
116impl HostContext {
117    /// Create a new host context by querying host information
118    pub async fn new() -> Self {
119        let is_nested = {
120            // xtask-fmt allow-target-arch cpu-intrinsic
121            #[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            // xtask-fmt allow-target-arch cpu-intrinsic
136            #[cfg(not(target_arch = "x86_64"))]
137            {
138                false
139            }
140        };
141
142        let vendor = Vendor::host();
143
144        // VPCI support: only Windows (virt_whp and Hyper-V) supports it for now.
145        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                // MSHV has no SNP capability query, so probe by creating a
185                // temporary SNP partition. Only AMD hosts can support SNP.
186                (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
210/// A single requirement for a test to run.
211pub enum TestRequirement {
212    /// Execution environment requirement.
213    ExecutionEnvironment(ExecutionEnvironment),
214    /// Vendor requirement.
215    Vendor(Vendor),
216    /// Hypervisor backend that OpenVMM must select on the host.
217    OpenVmmHypervisor(OpenVmmHypervisor),
218    /// A host capability probe evaluated only during runtime host filtering.
219    ///
220    /// Combine this with a backend requirement using [`Self::and`] to avoid
221    /// probing backends that are not available on the host.
222    ///
223    /// The probe runs synchronously before test execution, including when
224    /// listing tests for nextest. Artifact enumeration does not evaluate it.
225    /// Callers should cache expensive probes and report probe errors from the
226    /// test body rather than treating errors as unsupported capabilities.
227    HostCapability(fn() -> bool),
228    /// Isolation requirement.
229    Isolation(IsolationType),
230    /// Requires a named capability advertised by the execution environment or
231    /// detected by petri.
232    ///
233    /// Capabilities are how a test says "I need a specific resource to be
234    /// provisioned for me" without naming who provides it or how. The
235    /// execution environment can advertise capabilities via the
236    /// comma-separated `PETRI_CAPABILITIES` environment variable, and petri can
237    /// add capabilities that it detects itself. A test requiring a capability
238    /// that is not available is skipped, so such tests automatically
239    /// self-exclude on any host that cannot satisfy them.
240    RequiresCapability {
241        /// Capability name.
242        name: &'static str,
243        /// VMM used by the test, which may affect capability availability.
244        vmm: VmmType,
245    },
246    /// Logical AND of two requirements.
247    And(Box<TestRequirement>, Box<TestRequirement>),
248    /// Logical OR of two requirements.
249    Or(Box<TestRequirement>, Box<TestRequirement>),
250    /// Logical NOT of a requirement.
251    Not(Box<TestRequirement>),
252    /// Requirement satisfied by any host context.
253    Any,
254}
255
256impl TestRequirement {
257    /// Combine this requirement with another requirement using logical AND.
258    pub fn and(self, other: TestRequirement) -> TestRequirement {
259        TestRequirement::And(Box::new(self), Box::new(other))
260    }
261
262    /// Combine this requirement with another requirement using logical OR.
263    pub fn or(self, other: TestRequirement) -> TestRequirement {
264        TestRequirement::Or(Box::new(self), Box::new(other))
265    }
266
267    /// Negate this requirement.
268    #[expect(clippy::should_implement_trait)]
269    pub fn not(self) -> TestRequirement {
270        TestRequirement::Not(Box::new(self))
271    }
272
273    /// Evaluate if this requirement is satisfied with the given host context
274    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
308/// Returns the canonical runtime name for a known capability.
309pub fn known_capability(name: &str) -> Option<&'static str> {
310    capabilities::known(name)
311}
312
313/// Returns whether `name` is a known capability.
314pub 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    // virt_mshv cannot currently reset partitions running Windows.
326    // This is due to two issues:
327    // * A hypervisor issue that prevents locked hv#1 MSRs from being set by the host VMM.
328    // * Missing support for the HvScrubPartition hypercall.
329    // Once either of these are fixed, we can remove this check and feature.
330    if !matches!(
331        (vmm, context.openvmm_hypervisor),
332        (VmmType::OpenVmm, Some(OpenVmmHypervisor::Mshv))
333    ) {
334        capabilities.insert(capabilities::WINDOWS_PARTITION_RESET);
335    }
336
337    // Nested virtualization is supported by the x86 OpenVMM backends and QEMU.
338    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
368/// Container for test requirements that can be evaluated
369pub struct TestCaseRequirements {
370    requirements: TestRequirement,
371}
372
373impl TestCaseRequirements {
374    /// Create a new TestCaseRequirements from a TestRequirement
375    pub fn new(requirements: TestRequirement) -> Self {
376        Self { requirements }
377    }
378}
379
380/// Evaluates if a test case can be run in the current execution environment with context.
381pub 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}