Skip to main content

detcore/
machine.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 machine a guest runs on, as Hermit describes it: one description that
10//! every guest-visible source of machine facts reads, so that they tell the
11//! same story on every backend and every host
12//! (<https://github.com/rrnewton/hermit/issues/4266>).
13//!
14//! Today `uname`, `sysinfo` and the fixed procfs files (`procfs::fixed`)
15//! read it, and the CPU count: `/proc/stat`, `/proc/cpuinfo`, the CPU files
16//! under `/sys` and the per-CPU tables. The rest of the virtual CPU (CPUID's
17//! table, `sched_getaffinity`) still has its own sources, which this
18//! description is to absorb; the design lists every source that must agree.
19//!
20//! The values are fixed. They may come only from Hermit's configuration
21//! (today: the epoch, the memory size and the CPU count), never from the
22//! host.
23
24use chrono::DateTime;
25use chrono::Utc;
26
27/// The machine a guest sees.
28#[derive(Clone, Debug, Eq, PartialEq)]
29pub struct MachineDescription {
30    /// The running kernel.
31    pub kernel: Kernel,
32    /// The kernel's fixed limits and settings.
33    pub limits: KernelLimits,
34    /// The number of CPUs: `--virtual-cpus`, or 1 without it. Detcore runs a
35    /// guest's threads one at a time whatever the count; it is what the
36    /// guest is told. `/proc/stat`, `/proc/cpuinfo`, the CPU files under
37    /// `/sys` and the tables that have a column or block per CPU
38    /// (`/proc/interrupts`, `/proc/softirqs`, `/proc/schedstat`,
39    /// `/proc/zoneinfo`) render this many.
40    pub cpus: u32,
41    /// The memory size in bytes, which `/proc/meminfo` (in kB, rounded
42    /// down) and `sysinfo` report: the configured `--memory` (1 GB by
43    /// default). Linux counts memory in whole pages, so its two reports
44    /// agree exactly; here they differ by the size modulo 1024 (512 bytes
45    /// for the default), as they did before this description.
46    pub memory: u64,
47}
48
49/// `--memory`'s default, 1 GB (`bytesize` reads "1GB" as 10^9 bytes).
50const DEFAULT_MEMORY: u64 = 1_000_000_000;
51
52impl Default for MachineDescription {
53    fn default() -> Self {
54        Self {
55            kernel: Kernel::default(),
56            limits: KernelLimits::default(),
57            cpus: 1,
58            memory: DEFAULT_MEMORY,
59        }
60    }
61}
62
63impl MachineDescription {
64    /// The machine of a run whose configured epoch is `epoch`.
65    pub fn new(epoch: DateTime<Utc>) -> Self {
66        Self {
67            kernel: Kernel {
68                built: epoch,
69                ..Kernel::default()
70            },
71            ..Self::default()
72        }
73    }
74
75    /// The contents of the fixed procfs file at `path` on this machine, if
76    /// `path` names one (`procfs::fixed`).
77    pub fn procfs_file(&self, path: &str) -> Option<Vec<u8>> {
78        crate::procfs::FixedProcfsFile::from_path(path).map(|file| file.contents(self))
79    }
80
81    /// The machine of a run with configuration `config`.
82    pub fn of(config: &crate::config::Config) -> Self {
83        Self {
84            memory: config.memory,
85            cpus: config.virtual_cpu_count(),
86            ..Self::new(config.epoch)
87        }
88    }
89}
90
91/// The running kernel.
92#[derive(Clone, Debug, Eq, PartialEq)]
93pub struct Kernel {
94    /// `uname().sysname` and `kernel/ostype`.
95    pub sysname: String,
96    /// `uname().release` and `kernel/osrelease`: 5.2.0. It must pass glibc's
97    /// ABI-tag check for the libraries a guest loads; whether to advertise a
98    /// newer release is an open question in the design.
99    pub release: String,
100    /// When the kernel was built, which dates its version: the epoch.
101    pub built: DateTime<Utc>,
102    /// The options after the boot image on `/proc/cmdline`: `quiet`.
103    pub boot_options: String,
104    /// `kernel/random/boot_id`: one UUID for every run, version 4 in form;
105    /// its first bytes spell `hermit` in hex.
106    pub boot_id: String,
107}
108
109impl Default for Kernel {
110    fn default() -> Self {
111        Self {
112            sysname: "Linux".to_owned(),
113            release: "5.2.0".to_owned(),
114            built: DateTime::<Utc>::default(),
115            boot_options: "quiet".to_owned(),
116            boot_id: "6865726d-6974-4000-8000-000000000000".to_owned(),
117        }
118    }
119}
120
121impl Kernel {
122    /// `uname().version` and `kernel/version`: a build dated when it was
123    /// built. Formatted in UTC: `Local` initializes libc TLS, which is
124    /// unavailable while a DynamoRIO application thread is executing a
125    /// client callback.
126    pub fn version(&self) -> String {
127        format!("#1 SMP {}", self.built.format("%a %b %d %T %Z %Y"))
128    }
129
130    /// `/proc/version`: sysname, release and version, in the form
131    /// `Linux version <release> <version>` that gVisor also uses. Linux adds
132    /// the builder and compiler, which describe no kernel a guest can see.
133    pub fn banner(&self) -> String {
134        format!(
135            "{} version {} {}",
136            self.sysname,
137            self.release,
138            self.version()
139        )
140    }
141
142    /// `/proc/cmdline`: a boot image named after the release.
143    pub fn command_line(&self) -> String {
144        format!("BOOT_IMAGE=/vmlinuz-{} {}", self.release, self.boot_options)
145    }
146}
147
148/// The kernel's fixed limits and settings.
149#[derive(Clone, Debug, Eq, PartialEq)]
150pub struct KernelLimits {
151    /// `kernel/random/entropy_avail`: 256, which Linux 5.18 and later always
152    /// report.
153    pub entropy_avail: u32,
154    /// `kernel/overflowuid`: 65534, Linux's default, the ID an unmapped user
155    /// shows as.
156    pub overflow_uid: u32,
157    /// `kernel/overflowgid`: 65534, as `overflow_uid`, for groups.
158    pub overflow_gid: u32,
159    /// `kernel/pid_max`: 4194304 (2^22, `PID_MAX_LIMIT`), the value systemd
160    /// sets on 64-bit machines. Detcore's virtual PIDs stay far below it.
161    pub pid_max: u32,
162    /// `kernel/cap_last_cap`: 40, `CAP_CHECKPOINT_RESTORE`, the last
163    /// capability of the kernels Hermit runs on. `capget` and
164    /// `PR_CAPBSET_READ` reach the host kernel, so this agrees with the
165    /// capability sets the guest is shown.
166    pub cap_last_cap: u32,
167    /// `vm/overcommit_memory`: 0, Linux's heuristic default.
168    pub overcommit_memory: u32,
169    /// `vm/max_map_count`: 65530, Linux's default (`DEFAULT_MAX_MAP_COUNT`).
170    pub max_map_count: u32,
171    /// `vm/mmap_min_addr`: 65536, the default of the common distributions.
172    pub mmap_min_addr: u64,
173    /// `fs/file-max`: `LONG_MAX`, what Linux 5.0 and later default to.
174    pub file_max: u64,
175    /// `fs/aio-max-nr`: 65536, Linux's default. (Before, the guest read 0,
176    /// which says that no asynchronous I/O context can be set up.)
177    pub aio_max_nr: u64,
178    /// `fs/pipe-max-size`: the capacity Detcore pins every pipe at
179    /// (`deterministic_pipe_capacity_bytes`, two pages), which is also the most that
180    /// `F_SETPIPE_SZ` allows.
181    pub pipe_max_size: u32,
182}
183
184impl Default for KernelLimits {
185    fn default() -> Self {
186        Self {
187            entropy_avail: 256,
188            overflow_uid: 65534,
189            overflow_gid: 65534,
190            pid_max: 1 << 22,
191            cap_last_cap: 40,
192            overcommit_memory: 0,
193            max_map_count: 65530,
194            mmap_min_addr: 65536,
195            file_max: i64::MAX as u64,
196            aio_max_nr: 65536,
197            pipe_max_size: crate::syscalls::deterministic_pipe_capacity_bytes() as u32,
198        }
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use chrono::TimeZone;
205
206    use super::*;
207
208    /// The kernel's strings agree with each other as Linux's do.
209    #[test]
210    fn the_kernel_describes_itself_consistently() {
211        let machine = MachineDescription::new(Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap());
212        let kernel = &machine.kernel;
213        assert_eq!(kernel.version(), "#1 SMP Thu Jan 01 00:00:00 UTC 2026");
214        assert_eq!(
215            kernel.banner(),
216            format!("Linux version 5.2.0 {}", kernel.version())
217        );
218        assert_eq!(kernel.command_line(), "BOOT_IMAGE=/vmlinuz-5.2.0 quiet");
219    }
220}