Skip to main content

running_process_platform_internal/platform/
host.rs

1//! Host facts, directories, user identity, resources, and autostart primitives.
2//!
3//! Callers ask what is true of this host and this process -- who am I, am I
4//! elevated -- and decide for themselves what that means. Whether the answer
5//! came from a uid comparison or a token query is not something a caller
6//! should have to know, or be able to tell.
7
8use std::io;
9
10pub use crate::{
11    host_boot_id as boot_id, host_current_process_privilege as current_process_privilege,
12    host_environment_keys_are_case_insensitive as environment_keys_are_case_insensitive,
13    host_filesystem_device_id as filesystem_device_id, host_hostname as hostname,
14    host_login_environment as login_environment, host_machine_id as machine_id,
15    host_namespace_id as namespace_id, host_process_cgroup as process_cgroup,
16    host_user_machine_identity as user_machine_identity,
17    HostPrivilegedIdentity as PrivilegedIdentity,
18};
19
20pub use crate::host_login_environment_block as login_environment_block;
21
22/// Resolve a machine identity from the first readable of `machine_id_paths`,
23/// falling back to a boot-scoped id.
24///
25/// Lives in the neutral leaf, not the Linux tree, so it compiles and is tested
26/// on every host. The rules it encodes are subtle enough to be worth testing
27/// where the tests actually run, and only the Linux implementation supplies
28/// real paths to it.
29// Only the Linux implementation supplies real paths to this, so other
30// hosts see it as dead code. It stays compiled on all of them anyway:
31// that is what keeps the tests below running everywhere rather than on
32// one host.
33#[allow(dead_code)]
34pub(crate) fn machine_id_from(machine_id_paths: &[&str], boot_id_path: &str) -> io::Result<String> {
35    for path in machine_id_paths {
36        match std::fs::read_to_string(path) {
37            Ok(s) => {
38                let trimmed = s.trim();
39                if !trimmed.is_empty() {
40                    return Ok(trimmed.to_string());
41                }
42            }
43            Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
44            // An unreadable machine-id stays a hard error rather than falling
45            // through: sibling processes of the same user may read the file
46            // fine, and deriving a different identity here would split the
47            // user across two identities -- two brokers, each believing it is
48            // the singleton.
49            Err(err) => return Err(io::Error::other(format!("read {path}: {err}"))),
50        }
51    }
52    // Read-only fallback for hosts that ship no machine-id file at all
53    // (minimal containers, machine-id-less musl distros): a boot-scoped
54    // identity from the kernel's boot_id. Every process in the same boot
55    // derives the same value -- exactly the lifetime this must cover -- and
56    // file *absence*, unlike readability, cannot differ between one user's
57    // processes, so the fallback stays consistent.
58    if let Ok(s) = std::fs::read_to_string(boot_id_path) {
59        let trimmed = s.trim();
60        if !trimmed.is_empty() {
61            return Ok(format!("boot:{trimmed}"));
62        }
63    }
64    Err(io::Error::other(
65        "no /etc/machine-id or /var/lib/dbus/machine-id found, and no usable boot_id fallback",
66    ))
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    /// A test process is not the machine's system account.
74    ///
75    /// Asserted as the property rather than against a uid or a SID: the point
76    /// of the facade is that a caller cannot tell which host answered. A run
77    /// that really is elevated is a broken environment, and this failing is
78    /// the correct outcome there.
79    #[test]
80    fn an_ordinary_test_process_is_not_privileged() {
81        let privilege = current_process_privilege().expect("privilege lookup must succeed");
82        assert_eq!(
83            privilege, None,
84            "test runs are expected unprivileged; got {privilege:?}"
85        );
86    }
87
88    mod machine_id_sources {
89        use super::super::machine_id_from;
90
91        fn temp_dir(label: &str) -> std::path::PathBuf {
92            let dir = std::env::temp_dir().join(format!(
93                "rp-host-{label}-{}-{:?}",
94                std::process::id(),
95                std::thread::current().id(),
96            ));
97            std::fs::create_dir_all(&dir).expect("create temp dir");
98            dir
99        }
100
101        fn write(dir: &std::path::Path, name: &str, content: &str) -> String {
102            let path = dir.join(name);
103            std::fs::write(&path, content).expect("write fixture file");
104            path.to_string_lossy().into_owned()
105        }
106
107        #[test]
108        fn machine_id_file_wins_over_boot_fallback() {
109            let dir = temp_dir("wins");
110            let machine = write(
111                &dir,
112                "machine-id",
113                "  abc123
114",
115            );
116            let boot = write(
117                &dir, "boot-id", "zzz
118",
119            );
120            assert_eq!(
121                machine_id_from(&[&machine], &boot).expect("resolve"),
122                "abc123"
123            );
124            let _ = std::fs::remove_dir_all(&dir);
125        }
126
127        #[test]
128        fn second_path_is_consulted_when_first_is_missing() {
129            let dir = temp_dir("second");
130            let missing = dir.join("absent").to_string_lossy().into_owned();
131            let machine = write(
132                &dir,
133                "machine-id",
134                "def456
135",
136            );
137            let boot = write(
138                &dir, "boot-id", "zzz
139",
140            );
141            assert_eq!(
142                machine_id_from(&[&missing, &machine], &boot).expect("resolve"),
143                "def456"
144            );
145            let _ = std::fs::remove_dir_all(&dir);
146        }
147
148        #[test]
149        fn missing_machine_id_files_fall_back_to_boot_id() {
150            let dir = temp_dir("fallback");
151            let missing = dir.join("absent").to_string_lossy().into_owned();
152            let boot = write(
153                &dir,
154                "boot-id",
155                "boot-value
156",
157            );
158            assert_eq!(
159                machine_id_from(&[&missing], &boot).expect("resolve"),
160                "boot:boot-value"
161            );
162            let _ = std::fs::remove_dir_all(&dir);
163        }
164
165        #[test]
166        fn empty_machine_id_file_falls_through_to_boot_id() {
167            let dir = temp_dir("empty");
168            let machine = write(
169                &dir,
170                "machine-id",
171                "   
172",
173            );
174            let boot = write(
175                &dir,
176                "boot-id",
177                "boot-value
178",
179            );
180            assert_eq!(
181                machine_id_from(&[&machine], &boot).expect("resolve"),
182                "boot:boot-value"
183            );
184            let _ = std::fs::remove_dir_all(&dir);
185        }
186
187        #[test]
188        fn unreadable_machine_id_stays_a_hard_error_despite_boot_fallback() {
189            let dir = temp_dir("unreadable");
190            // A directory in the machine-id slot yields a non-NotFound read
191            // error -- the split-identity hazard the hard error protects.
192            let as_dir = dir.join("machine-id-dir");
193            std::fs::create_dir_all(&as_dir).expect("create dir fixture");
194            let as_dir = as_dir.to_string_lossy().into_owned();
195            let boot = write(&dir, "boot-id", "boot-uuid\n");
196            machine_id_from(&[&as_dir], &boot)
197                .expect_err("unreadable machine-id must not fall through");
198            let _ = std::fs::remove_dir_all(&dir);
199        }
200
201        #[test]
202        fn everything_missing_is_an_error() {
203            let dir = temp_dir("nothing");
204            let missing = dir.join("absent").to_string_lossy().into_owned();
205            let no_boot = dir.join("absent-boot").to_string_lossy().into_owned();
206            assert!(machine_id_from(&[&missing], &no_boot).is_err());
207            let _ = std::fs::remove_dir_all(&dir);
208        }
209    }
210
211    /// Where the host has control groups, this process belongs to one and can
212    /// read it; where it has none, the answer says so rather than erroring.
213    #[test]
214    fn process_cgroup_is_readable_where_the_host_has_cgroups() {
215        if let Some(membership) = process_cgroup() {
216            let text = membership.expect("own cgroup membership must be readable");
217            assert!(!text.trim().is_empty(), "empty cgroup membership");
218        }
219    }
220
221    /// Each identity prints the detail an operator needs to recognise it.
222    #[test]
223    fn privileged_identities_describe_themselves_concretely() {
224        assert_eq!(
225            PrivilegedIdentity::UnixRoot.to_string(),
226            "root (effective uid 0)"
227        );
228        assert_eq!(
229            PrivilegedIdentity::WindowsLocalSystem.to_string(),
230            "Windows LocalSystem (S-1-5-18)"
231        );
232    }
233}