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    /// Isolation requirement.
219    Isolation(IsolationType),
220    /// Requires a named capability advertised by the execution environment or
221    /// detected by petri.
222    ///
223    /// Capabilities are how a test says "I need a specific resource to be
224    /// provisioned for me" without naming who provides it or how. The
225    /// execution environment can advertise capabilities via the
226    /// comma-separated `PETRI_CAPABILITIES` environment variable, and petri can
227    /// add capabilities that it detects itself. A test requiring a capability
228    /// that is not available is skipped, so such tests automatically
229    /// self-exclude on any host that cannot satisfy them.
230    RequiresCapability {
231        /// Capability name.
232        name: &'static str,
233        /// VMM used by the test, which may affect capability availability.
234        vmm: VmmType,
235    },
236    /// Logical AND of two requirements.
237    And(Box<TestRequirement>, Box<TestRequirement>),
238    /// Logical OR of two requirements.
239    Or(Box<TestRequirement>, Box<TestRequirement>),
240    /// Logical NOT of a requirement.
241    Not(Box<TestRequirement>),
242    /// Requirement satisfied by any host context.
243    Any,
244}
245
246impl TestRequirement {
247    /// Combine this requirement with another requirement using logical AND.
248    pub fn and(self, other: TestRequirement) -> TestRequirement {
249        TestRequirement::And(Box::new(self), Box::new(other))
250    }
251
252    /// Combine this requirement with another requirement using logical OR.
253    pub fn or(self, other: TestRequirement) -> TestRequirement {
254        TestRequirement::Or(Box::new(self), Box::new(other))
255    }
256
257    /// Negate this requirement.
258    #[expect(clippy::should_implement_trait)]
259    pub fn not(self) -> TestRequirement {
260        TestRequirement::Not(Box::new(self))
261    }
262
263    /// Evaluate if this requirement is satisfied with the given host context
264    pub fn is_satisfied(&self, context: &HostContext) -> bool {
265        match self {
266            TestRequirement::ExecutionEnvironment(env) => context.execution_environment == *env,
267            TestRequirement::Vendor(vendor) => context.vendor == *vendor,
268            TestRequirement::OpenVmmHypervisor(hypervisor) => {
269                context.openvmm_hypervisor == Some(*hypervisor)
270            }
271            TestRequirement::Isolation(isolation_type) => {
272                if let Some(vm_host_info) = &context.vm_host_info {
273                    match isolation_type {
274                        IsolationType::Vbs => vm_host_info.vbs_supported,
275                        IsolationType::Snp => vm_host_info.snp_status,
276                        IsolationType::Tdx => vm_host_info.tdx_status,
277                    }
278                } else {
279                    false
280                }
281            }
282            TestRequirement::RequiresCapability { name, vmm } => {
283                available_capabilities(context, *vmm).contains(name)
284            }
285            TestRequirement::And(req1, req2) => {
286                req1.is_satisfied(context) && req2.is_satisfied(context)
287            }
288            TestRequirement::Or(req1, req2) => {
289                req1.is_satisfied(context) || req2.is_satisfied(context)
290            }
291            TestRequirement::Not(req) => !req.is_satisfied(context),
292            TestRequirement::Any => true,
293        }
294    }
295}
296
297/// Returns the canonical runtime name for a known capability.
298pub fn known_capability(name: &str) -> Option<&'static str> {
299    capabilities::known(name)
300}
301
302/// Returns whether `name` is a known capability.
303pub fn is_known_capability(name: &str) -> bool {
304    known_capability(name).is_some()
305}
306
307fn available_capabilities(context: &HostContext, vmm: VmmType) -> BTreeSet<&'static str> {
308    let mut capabilities = BTreeSet::new();
309
310    if context.vpci_supported {
311        capabilities.insert(capabilities::VPCI);
312    }
313
314    // virt_mshv cannot currently reset partitions running Windows.
315    // This is due to two issues:
316    // * A hypervisor issue that prevents locked hv#1 MSRs from being set by the host VMM.
317    // * Missing support for the HvScrubPartition hypercall.
318    // Once either of these are fixed, we can remove this check and feature.
319    if !matches!(
320        (vmm, context.openvmm_hypervisor),
321        (VmmType::OpenVmm, Some(OpenVmmHypervisor::Mshv))
322    ) {
323        capabilities.insert(capabilities::WINDOWS_PARTITION_RESET);
324    }
325
326    // MSHV doesn't support nested virtualization yet
327    if matches!(
328        (vmm, context.openvmm_hypervisor),
329        (
330            VmmType::OpenVmm,
331            Some(OpenVmmHypervisor::Kvm | OpenVmmHypervisor::Whp)
332        ) | (VmmType::Qemu, _)
333    ) {
334        capabilities.insert(capabilities::NESTED_VIRT);
335    }
336
337    match std::env::var("PETRI_CAPABILITIES") {
338        Ok(env_capabilities) => {
339            for capability in env_capabilities.split(',').map(str::trim) {
340                if capability.is_empty() {
341                    continue;
342                }
343                let capability = known_capability(capability)
344                    .unwrap_or_else(|| panic!("unknown PETRI_CAPABILITIES entry: {capability}"));
345                capabilities.insert(capability);
346            }
347        }
348        Err(std::env::VarError::NotPresent) => {}
349        Err(std::env::VarError::NotUnicode(_)) => {
350            panic!("PETRI_CAPABILITIES is not valid UTF-8")
351        }
352    }
353
354    capabilities
355}
356
357/// Container for test requirements that can be evaluated
358pub struct TestCaseRequirements {
359    requirements: TestRequirement,
360}
361
362impl TestCaseRequirements {
363    /// Create a new TestCaseRequirements from a TestRequirement
364    pub fn new(requirements: TestRequirement) -> Self {
365        Self { requirements }
366    }
367}
368
369/// Evaluates if a test case can be run in the current execution environment with context.
370pub fn can_run_test_with_context(
371    config: Option<&TestCaseRequirements>,
372    context: &HostContext,
373) -> bool {
374    if let Some(config) = config {
375        config.requirements.is_satisfied(context)
376    } else {
377        true
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384
385    fn host_context(openvmm_hypervisor: OpenVmmHypervisor) -> HostContext {
386        HostContext {
387            vm_host_info: None,
388            vendor: Vendor::Intel,
389            execution_environment: ExecutionEnvironment::Baremetal,
390            vpci_supported: false,
391            openvmm_hypervisor: Some(openvmm_hypervisor),
392        }
393    }
394
395    #[test]
396    fn capabilities_are_evaluated_for_the_selected_vmm() {
397        let requirement = |vmm| TestRequirement::RequiresCapability {
398            name: capabilities::WINDOWS_PARTITION_RESET,
399            vmm,
400        };
401        let mshv = host_context(OpenVmmHypervisor::Mshv);
402
403        assert!(!requirement(VmmType::OpenVmm).is_satisfied(&mshv));
404        assert!(requirement(VmmType::HyperV).is_satisfied(&mshv));
405        assert!(requirement(VmmType::OpenVmm).is_satisfied(&host_context(OpenVmmHypervisor::Kvm)));
406    }
407
408    #[test]
409    fn openvmm_hypervisor_requirement_matches_the_selected_backend() {
410        let requirement = TestRequirement::OpenVmmHypervisor(OpenVmmHypervisor::Mshv);
411        assert!(requirement.is_satisfied(&host_context(OpenVmmHypervisor::Mshv)));
412        assert!(!requirement.is_satisfied(&host_context(OpenVmmHypervisor::Kvm)));
413        let mut without_hypervisor = host_context(OpenVmmHypervisor::Mshv);
414        without_hypervisor.openvmm_hypervisor = None;
415        assert!(!requirement.is_satisfied(&without_hypervisor));
416    }
417}