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}