running_process_platform_internal/platform_linux/
host.rs1use std::ffi::OsString;
4use std::io;
5use std::path::Path;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum PrivilegedIdentity {
15 UnixRoot,
17 WindowsLocalSystem,
19}
20
21impl std::fmt::Display for PrivilegedIdentity {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 match self {
24 Self::UnixRoot => f.write_str("root (effective uid 0)"),
25 Self::WindowsLocalSystem => f.write_str("Windows LocalSystem (S-1-5-18)"),
26 }
27 }
28}
29
30pub fn current_process_privilege() -> io::Result<Option<PrivilegedIdentity>> {
32 Ok(privilege_from_effective_uid(unsafe { libc::geteuid() }))
33}
34
35fn privilege_from_effective_uid(euid: libc::uid_t) -> Option<PrivilegedIdentity> {
40 (euid == 0).then_some(PrivilegedIdentity::UnixRoot)
41}
42
43
44pub fn user_machine_identity() -> io::Result<String> {
49 let uid = unsafe { libc::getuid() };
50 let machine_id = crate::platform::host::machine_id_from(&MACHINE_ID_PATHS, BOOT_ID_PATH)?;
51 Ok(format!("{uid}:{machine_id}"))
52}
53
54pub fn hostname() -> Option<String> {
60 let mut buf = [0_u8; 256];
61 let ok = unsafe { libc::gethostname(buf.as_mut_ptr().cast(), buf.len()) };
64 if ok != 0 {
65 return None;
66 }
67 let nul = buf.iter().position(|b| *b == 0).unwrap_or(buf.len());
68 let name = String::from_utf8_lossy(&buf[..nul]).into_owned();
69 (!name.is_empty()).then_some(name)
70}
71
72pub fn filesystem_device_id(path: &Path) -> Option<u64> {
74 use std::os::unix::fs::MetadataExt;
75
76 std::fs::metadata(path).ok().map(|meta| meta.dev())
77}
78
79pub fn machine_id() -> Option<String> {
92 MACHINE_ID_PATHS
93 .iter()
94 .find_map(|path| read_trimmed(path))
95 .or_else(|| read_trimmed(BOOT_ID_PATH).map(|id| format!("boot:{id}")))
96}
97
98pub fn boot_id() -> Option<String> {
100 read_trimmed(BOOT_ID_PATH)
101}
102
103pub fn namespace_id() -> Option<String> {
109 let mnt = read_link_lossy("/proc/self/ns/mnt").unwrap_or_else(|| "mntns:unknown".to_string());
110 let pid = read_link_lossy("/proc/self/ns/pid").unwrap_or_else(|| "pidns:unknown".to_string());
111 Some(format!("{mnt}:{pid}"))
112}
113
114const MACHINE_ID_PATHS: [&str; 2] = ["/etc/machine-id", "/var/lib/dbus/machine-id"];
115const BOOT_ID_PATH: &str = "/proc/sys/kernel/random/boot_id";
116
117fn read_trimmed(path: &str) -> Option<String> {
118 std::fs::read_to_string(path)
119 .ok()
120 .map(|s| s.trim().to_string())
121 .filter(|s| !s.is_empty())
122}
123
124fn read_link_lossy(path: &str) -> Option<String> {
125 std::fs::read_link(path)
126 .ok()
127 .map(|p| p.to_string_lossy().into_owned())
128}
129
130pub fn login_environment() -> io::Result<Vec<(OsString, OsString)>> {
146 Ok(passwd_login_environment().unwrap_or_else(|| std::env::vars_os().collect()))
147}
148
149pub fn environment_keys_are_case_insensitive() -> bool {
151 false
152}
153
154fn passwd_login_environment() -> Option<Vec<(OsString, OsString)>> {
157 use std::ffi::CStr;
158 use std::os::unix::ffi::OsStringExt;
159
160 let mut passwd: libc::passwd = unsafe { std::mem::zeroed() };
162 let mut result: *mut libc::passwd = std::ptr::null_mut();
163 let mut buf = vec![0u8; 1024];
167 loop {
168 let rc = unsafe {
171 libc::getpwuid_r(
172 libc::getuid(),
173 &mut passwd,
174 buf.as_mut_ptr().cast(),
175 buf.len(),
176 &mut result,
177 )
178 };
179 if rc == libc::ERANGE && buf.len() < 1 << 20 {
180 buf.resize(buf.len() * 2, 0);
181 continue;
182 }
183 if rc != 0 || result.is_null() {
184 return None;
185 }
186 break;
187 }
188
189 let field = |ptr: *const libc::c_char| -> Option<OsString> {
190 if ptr.is_null() {
191 return None;
192 }
193 let bytes = unsafe { CStr::from_ptr(ptr) }.to_bytes();
196 (!bytes.is_empty()).then(|| OsString::from_vec(bytes.to_vec()))
197 };
198 let name = field(passwd.pw_name)?;
199 let home = field(passwd.pw_dir)?;
200
201 let mut env: Vec<(OsString, OsString)> = vec![
202 (OsString::from("USER"), name.clone()),
203 (OsString::from("LOGNAME"), name),
204 (OsString::from("HOME"), home),
205 (OsString::from("PATH"), OsString::from(LOGIN_DEFAULT_PATH)),
206 ];
207 if let Some(shell) = field(passwd.pw_shell) {
208 env.push((OsString::from("SHELL"), shell));
209 }
210 env.extend(carried_session_variables());
211 Some(env)
212}
213
214fn carried_session_variables() -> Vec<(OsString, OsString)> {
227 std::env::vars_os()
228 .filter(|(key, _)| describes_the_login_session(key))
229 .collect()
230}
231
232fn describes_the_login_session(key: &OsString) -> bool {
233 key == "LANG"
234 || key == "TZ"
235 || key == "TMPDIR"
236 || key == "XDG_RUNTIME_DIR"
237 || key.to_str().is_some_and(|k| k.starts_with("LC_"))
238}
239
240const LOGIN_DEFAULT_PATH: &str = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
242
243pub fn login_environment_block() -> io::Result<Vec<u16>> {
251 Ok(encode_environment_block(&login_environment()?))
252}
253
254fn encode_environment_block(entries: &[(OsString, OsString)]) -> Vec<u16> {
261 let mut block = Vec::new();
262 for (key, value) in entries {
263 let entry = format!("{}={}", key.to_string_lossy(), value.to_string_lossy());
264 block.extend(entry.encode_utf16());
265 block.push(0);
266 }
267 block.push(0);
270 block
271}
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn root_detection_uses_effective_uid_zero() {
278 assert_eq!(
279 privilege_from_effective_uid(0),
280 Some(PrivilegedIdentity::UnixRoot)
281 );
282 assert_eq!(privilege_from_effective_uid(1000), None);
283 }
284
285 #[test]
289 fn host_identity_facts_are_never_empty_strings() {
290 let facts = [hostname(), machine_id(), boot_id(), namespace_id()];
291 for value in facts.into_iter().flatten() {
292 assert!(!value.is_empty(), "a reported fact must carry a value");
293 }
294 }
295
296 #[test]
298 fn this_host_reports_a_name_and_a_machine_id() {
299 assert!(hostname().is_some(), "a running host has a name");
300 assert!(machine_id().is_some(), "a running host has a machine id");
301 }
302
303 #[test]
306 fn filesystem_device_id_answers_for_an_existing_directory() {
307 let cwd = std::env::current_dir().expect("cwd");
308 let dev = filesystem_device_id(&cwd).expect("an existing directory has a device");
309 assert_eq!(filesystem_device_id(&cwd), Some(dev), "stable across reads");
310 }
311
312 #[test]
316 fn filesystem_device_id_declines_a_missing_path() {
317 let missing = std::env::temp_dir().join(format!(
318 "rp-host-absent-{}-{:?}",
319 std::process::id(),
320 std::thread::current().id()
321 ));
322 assert_eq!(filesystem_device_id(&missing), None);
323 }
324
325 #[test]
328 fn login_environment_contains_identity_and_default_path() {
329 let env = login_environment().unwrap();
330 let get = |name: &str| {
331 env.iter()
332 .find(|(key, _)| key == name)
333 .map(|(_, value)| value.clone())
334 };
335 let user = get("USER").expect("baseline must contain USER");
336 assert!(!user.is_empty());
337 assert_eq!(get("LOGNAME").as_ref(), Some(&user));
338 assert!(!get("HOME").expect("baseline must contain HOME").is_empty());
339 assert!(!get("PATH").expect("baseline must contain PATH").is_empty());
340 }
341
342 #[test]
345 fn login_environment_does_not_leak_arbitrary_process_vars() {
346 std::env::set_var("RUNNING_PROCESS_BASELINE_CANARY", "1");
347 let env = passwd_login_environment().expect("test user must have a passwd entry");
348 std::env::remove_var("RUNNING_PROCESS_BASELINE_CANARY");
349 assert!(
350 !env.iter()
351 .any(|(key, _)| key == "RUNNING_PROCESS_BASELINE_CANARY"),
352 "process-local variables must not leak into the login baseline"
353 );
354 }
355
356 #[test]
361 fn login_environment_carries_xdg_runtime_dir() {
362 std::env::set_var("XDG_RUNTIME_DIR", "/run/user/4242");
363 let env = passwd_login_environment().expect("test user must have a passwd entry");
364 let carried = env
365 .iter()
366 .find(|(key, _)| key == "XDG_RUNTIME_DIR")
367 .map(|(_, value)| value.clone());
368 std::env::remove_var("XDG_RUNTIME_DIR");
369 assert_eq!(
370 carried.as_deref(),
371 Some(std::ffi::OsStr::new("/run/user/4242")),
372 "login baseline must carry XDG_RUNTIME_DIR when the session sets it"
373 );
374 }
375
376 #[test]
379 fn only_session_describing_variables_are_carried() {
380 for carried in ["LANG", "TZ", "TMPDIR", "XDG_RUNTIME_DIR", "LC_ALL", "LC_TIME"] {
381 assert!(
382 describes_the_login_session(&OsString::from(carried)),
383 "{carried} describes the login session"
384 );
385 }
386 for dropped in ["PWD", "OLDPWD", "SSH_AUTH_SOCK", "LCD_BRIGHTNESS", "L"] {
387 assert!(
388 !describes_the_login_session(&OsString::from(dropped)),
389 "{dropped} belongs to this process, not the session"
390 );
391 }
392 }
393
394 #[test]
397 fn an_encoded_block_is_double_nul_terminated() {
398 let live = login_environment_block().expect("this host has a login environment");
399 assert!(live.len() >= 2);
400 assert_eq!(&live[live.len() - 2..], &[0, 0]);
401
402 let empty = encode_environment_block(&[]);
403 assert_eq!(empty, vec![0]);
404 }
405
406 #[test]
408 fn an_encoded_block_carries_every_entry_in_order() {
409 let block = encode_environment_block(&[
410 (OsString::from("FIRST"), OsString::from("one")),
411 (OsString::from("SECOND"), OsString::from("two")),
412 ]);
413 let text = String::from_utf16_lossy(&block);
414 let entries: Vec<&str> = text.split(' ').filter(|s| !s.is_empty()).collect();
415 assert_eq!(entries, vec!["FIRST=one", "SECOND=two"]);
416 }
417}