Skip to main content

detcore_model/
cpu_profile.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! The virtual CPU a guest sees (<https://github.com/rrnewton/hermit/issues/4243>).
10
11use serde::Deserialize;
12use serde::Serialize;
13
14/// Which intercepted CPUID answers the guest receives.
15#[derive(
16    Debug,
17    Clone,
18    Copy,
19    Default,
20    PartialEq,
21    Eq,
22    Hash,
23    Serialize,
24    Deserialize,
25    clap::ValueEnum
26)]
27pub enum CpuProfile {
28    /// x86-64-v2, the default: the table every earlier Hermit has served.
29    #[default]
30    #[value(name = "x86-64-v2")]
31    #[serde(rename = "x86-64-v2")]
32    X86_64V2,
33    /// x86-64-v3: v2 plus AVX2, BMI1/2, FMA, F16C, LZCNT, MOVBE and XSAVE.
34    /// Offered only on the ptrace backend, on hosts that pass its startup
35    /// qualification.
36    #[value(name = "x86-64-v3")]
37    #[serde(rename = "x86-64-v3")]
38    X86_64V3,
39    /// Host CPUID with hardware-randomness and unvirtualized timing features
40    /// hidden. Requires CPUID faulting and virtual time on the ptrace backend.
41    #[value(name = "host-no-rdrand")]
42    #[serde(rename = "host-no-rdrand")]
43    HostNoRdrand,
44}
45
46impl CpuProfile {
47    /// Whether this is the default profile, which needs no qualification and
48    /// binds no host facts.
49    pub fn is_default(&self) -> bool {
50        *self == CpuProfile::X86_64V2
51    }
52
53    /// The name used on the command line.
54    pub fn name(&self) -> &'static str {
55        match self {
56            CpuProfile::X86_64V2 => "x86-64-v2",
57            CpuProfile::X86_64V3 => "x86-64-v3",
58            CpuProfile::HostNoRdrand => "host-no-rdrand",
59        }
60    }
61}
62
63impl std::fmt::Display for CpuProfile {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        f.write_str(self.name())
66    }
67}
68
69/// The host facts a v3 guest's CPUID and xstate answers are built from,
70/// captured once at startup on a host that passed the qualification, and bound
71/// into a recording so that replay refuses a host where any of them differs.
72///
73/// On the ptrace backend a guest's XGETBV and XSAVE run on the host, so leaf
74/// 0xD must describe the host's XCR0 and save layout; everything else in the v3
75/// table is fixed.
76#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
77pub struct HostXstate {
78    /// CPUID leaf 0 EBX, EDX, ECX: the vendor string.
79    pub vendor: [u32; 3],
80    /// CPUID leaf 1 EAX: family, model and stepping.
81    pub signature: u32,
82    /// XCR0, as XGETBV(0) reads it.
83    pub xcr0: u64,
84    /// The host's CPUID leaf 0xD rows the guest table is built from, as
85    /// `[subleaf, eax, ebx, ecx, edx]`: subleaf 0 and each enabled component
86    /// from 2 up, in order.
87    pub xsave_leaves: Vec<[u32; 5]>,
88    /// `arch_prctl` ARCH_GET_XCOMP_SUPP, ARCH_GET_XCOMP_PERM and
89    /// ARCH_GET_XCOMP_GUEST_PERM.
90    pub xcomp_supp: u64,
91    pub xcomp_perm: u64,
92    pub xcomp_guest_perm: u64,
93    /// The first component index ARCH_REQ_XCOMP_PERM answers EINVAL for
94    /// (Linux's XFEATURE_MAX); below it the answer is EOPNOTSUPP.
95    pub xfeature_max: u32,
96    /// The qualification this host passed: its case count and whether the
97    /// forced-exit arm ran.
98    pub probe_cases: u32,
99    pub probe_forced_exit: bool,
100}
101
102impl HostXstate {
103    /// The host's leaf 0xD row for `subleaf`, if it was captured.
104    pub fn xsave_leaf(&self, subleaf: u32) -> Option<[u32; 4]> {
105        self.xsave_leaves
106            .iter()
107            .find(|row| row[0] == subleaf)
108            .map(|row| [row[1], row[2], row[3], row[4]])
109    }
110}
111
112/// The residual as a literal, so that the command-line help can include it
113/// with `concat!` while [`V3_RESIDUAL`] stays the one source of its words.
114#[macro_export]
115macro_rules! v3_residual {
116    () => {
117        "With --cpu-profile x86-64-v3, Hermit checks at startup that this host does not \
118         change the XSTATE_BV bits a guest reads (with XSAVE, XSAVEC or XSAVEOPT, or in a \
119         signal frame) when the host saves and restores the guest's registers. The check \
120         samples a fixed set of cases and forces only CPUID exits, so a CPU or hypervisor \
121         that behaves differently only on other instruction paths, or on VM exits the guest \
122         cannot see, is not excluded. Firmware (SMM) entries cannot be checked at all. \
123         Default runs use x86-64-v2, which this does not affect."
124    };
125}
126
127/// What the host's XSAVE results can depend on, stated wherever v3 is offered:
128/// the `--cpu-profile` help, `--verify` output, the recording and the docs.
129pub const V3_RESIDUAL: &str = v3_residual!();
130
131/// The host-profile caveat, shared by help and admission diagnostics.
132#[macro_export]
133macro_rules! host_no_rdrand_residual {
134    () => {
135        "With --cpu-profile host-no-rdrand, CPUID remains intercepted, but its answers \
136         come from the host CPU with hardware-randomness and unvirtualized timing \
137         features hidden. Results are host-CPU-model specific: cross-host builds need \
138         the same CPU model and compatible kernel/XSTATE settings and CPU topology. \
139         The tracer and guest are pinned to the lowest originally allowed CPU, even \
140         with the timeslice disabled; repeat runs require the same allowed CPU set. Host XSAVE behavior \
141         is retained without the x86-64-v3 XSTATE qualification, so programs inspecting \
142         XSAVE or signal-register metadata have no determinism guarantee. Programs must honor \
143         the advertised feature bits; unconditional RDRAND or RDSEED is not trapped \
144         by this profile."
145    };
146}
147
148pub const HOST_NO_RDRAND_RESIDUAL: &str = host_no_rdrand_residual!();
149
150/// The long help of `--cpu-profile`, for `hermit run` and `hermit record start`.
151pub const CPU_PROFILE_LONG_HELP: &str = concat!(
152    "The CPU the guest sees: x86-64-v2 (the default), x86-64-v3, or host-no-rdrand. \
153     host-no-rdrand is available for `hermit run` on the ptrace backend with CPUID \
154     faulting and virtual time; it refuses hosts without CPUID faulting instead of \
155     allowing native CPUID. RDRAND and RDSEED are hidden. Check availability with \
156     `hermit host-capabilities --cpu-profile host-no-rdrand`. Recording and replay \
157     with host-no-rdrand are not supported.\n\n",
158    host_no_rdrand_residual!(),
159    "\n\n",
160    "v3 is offered only on \
161     the ptrace backend, on hosts that pass a startup qualification \
162     (`hermit host-capabilities --cpu-profile x86-64-v3` runs it alone). A recording binds \
163     the host facts its guest's answers come from, and replay refuses a host where any of \
164     them differs (<https://github.com/rrnewton/hermit/issues/4243>).\n\n",
165    v3_residual!()
166);
167
168#[cfg(test)]
169mod tests {
170    use clap::ValueEnum;
171
172    use super::*;
173
174    #[test]
175    fn names_round_trip_on_the_command_line_and_in_serde() {
176        for profile in CpuProfile::value_variants() {
177            assert_eq!(CpuProfile::from_str(profile.name(), false), Ok(*profile));
178            let json = serde_json::to_string(profile).unwrap();
179            assert_eq!(json, format!("\"{}\"", profile.name()));
180            assert_eq!(serde_json::from_str::<CpuProfile>(&json).unwrap(), *profile);
181        }
182        assert_eq!(CpuProfile::default(), CpuProfile::X86_64V2);
183        assert!(CpuProfile::X86_64V2.is_default());
184        assert!(!CpuProfile::X86_64V3.is_default());
185        assert!(!CpuProfile::HostNoRdrand.is_default());
186    }
187}