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