use std::ffi::OsString;
use std::io;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PrivilegedIdentity {
UnixRoot,
WindowsLocalSystem,
}
impl std::fmt::Display for PrivilegedIdentity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnixRoot => f.write_str("root (effective uid 0)"),
Self::WindowsLocalSystem => f.write_str("Windows LocalSystem (S-1-5-18)"),
}
}
}
pub fn current_process_privilege() -> io::Result<Option<PrivilegedIdentity>> {
Ok(privilege_from_effective_uid(unsafe { libc::geteuid() }))
}
fn privilege_from_effective_uid(euid: libc::uid_t) -> Option<PrivilegedIdentity> {
(euid == 0).then_some(PrivilegedIdentity::UnixRoot)
}
pub fn user_machine_identity() -> io::Result<String> {
let uid = unsafe { libc::getuid() };
let machine_id = crate::platform::host::machine_id_from(&MACHINE_ID_PATHS, BOOT_ID_PATH)?;
Ok(format!("{uid}:{machine_id}"))
}
pub fn hostname() -> Option<String> {
let mut buf = [0_u8; 256];
let ok = unsafe { libc::gethostname(buf.as_mut_ptr().cast(), buf.len()) };
if ok != 0 {
return None;
}
let nul = buf.iter().position(|b| *b == 0).unwrap_or(buf.len());
let name = String::from_utf8_lossy(&buf[..nul]).into_owned();
(!name.is_empty()).then_some(name)
}
pub fn filesystem_device_id(path: &Path) -> Option<u64> {
use std::os::unix::fs::MetadataExt;
std::fs::metadata(path).ok().map(|meta| meta.dev())
}
pub fn machine_id() -> Option<String> {
MACHINE_ID_PATHS
.iter()
.find_map(|path| read_trimmed(path))
.or_else(|| read_trimmed(BOOT_ID_PATH).map(|id| format!("boot:{id}")))
}
pub fn boot_id() -> Option<String> {
read_trimmed(BOOT_ID_PATH)
}
pub fn namespace_id() -> Option<String> {
let mnt = read_link_lossy("/proc/self/ns/mnt").unwrap_or_else(|| "mntns:unknown".to_string());
let pid = read_link_lossy("/proc/self/ns/pid").unwrap_or_else(|| "pidns:unknown".to_string());
Some(format!("{mnt}:{pid}"))
}
const MACHINE_ID_PATHS: [&str; 2] = ["/etc/machine-id", "/var/lib/dbus/machine-id"];
const BOOT_ID_PATH: &str = "/proc/sys/kernel/random/boot_id";
fn read_trimmed(path: &str) -> Option<String> {
std::fs::read_to_string(path)
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
}
fn read_link_lossy(path: &str) -> Option<String> {
std::fs::read_link(path)
.ok()
.map(|p| p.to_string_lossy().into_owned())
}
pub fn login_environment() -> io::Result<Vec<(OsString, OsString)>> {
Ok(passwd_login_environment().unwrap_or_else(|| std::env::vars_os().collect()))
}
pub fn environment_keys_are_case_insensitive() -> bool {
false
}
fn passwd_login_environment() -> Option<Vec<(OsString, OsString)>> {
use std::ffi::CStr;
use std::os::unix::ffi::OsStringExt;
let mut passwd: libc::passwd = unsafe { std::mem::zeroed() };
let mut result: *mut libc::passwd = std::ptr::null_mut();
let mut buf = vec![0u8; 1024];
loop {
let rc = unsafe {
libc::getpwuid_r(
libc::getuid(),
&mut passwd,
buf.as_mut_ptr().cast(),
buf.len(),
&mut result,
)
};
if rc == libc::ERANGE && buf.len() < 1 << 20 {
buf.resize(buf.len() * 2, 0);
continue;
}
if rc != 0 || result.is_null() {
return None;
}
break;
}
let field = |ptr: *const libc::c_char| -> Option<OsString> {
if ptr.is_null() {
return None;
}
let bytes = unsafe { CStr::from_ptr(ptr) }.to_bytes();
(!bytes.is_empty()).then(|| OsString::from_vec(bytes.to_vec()))
};
let name = field(passwd.pw_name)?;
let home = field(passwd.pw_dir)?;
let mut env: Vec<(OsString, OsString)> = vec![
(OsString::from("USER"), name.clone()),
(OsString::from("LOGNAME"), name),
(OsString::from("HOME"), home),
(OsString::from("PATH"), OsString::from(LOGIN_DEFAULT_PATH)),
];
if let Some(shell) = field(passwd.pw_shell) {
env.push((OsString::from("SHELL"), shell));
}
env.extend(carried_session_variables());
Some(env)
}
fn carried_session_variables() -> Vec<(OsString, OsString)> {
std::env::vars_os()
.filter(|(key, _)| describes_the_login_session(key))
.collect()
}
fn describes_the_login_session(key: &OsString) -> bool {
key == "LANG"
|| key == "TZ"
|| key == "TMPDIR"
|| key == "XDG_RUNTIME_DIR"
|| key.to_str().is_some_and(|k| k.starts_with("LC_"))
}
const LOGIN_DEFAULT_PATH: &str = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
pub fn login_environment_block() -> io::Result<Vec<u16>> {
Ok(encode_environment_block(&login_environment()?))
}
fn encode_environment_block(entries: &[(OsString, OsString)]) -> Vec<u16> {
let mut block = Vec::new();
for (key, value) in entries {
let entry = format!("{}={}", key.to_string_lossy(), value.to_string_lossy());
block.extend(entry.encode_utf16());
block.push(0);
}
block.push(0);
block
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn root_detection_uses_effective_uid_zero() {
assert_eq!(
privilege_from_effective_uid(0),
Some(PrivilegedIdentity::UnixRoot)
);
assert_eq!(privilege_from_effective_uid(1000), None);
}
#[test]
fn host_identity_facts_are_never_empty_strings() {
let facts = [hostname(), machine_id(), boot_id(), namespace_id()];
for value in facts.into_iter().flatten() {
assert!(!value.is_empty(), "a reported fact must carry a value");
}
}
#[test]
fn this_host_reports_a_name_and_a_machine_id() {
assert!(hostname().is_some(), "a running host has a name");
assert!(machine_id().is_some(), "a running host has a machine id");
}
#[test]
fn filesystem_device_id_answers_for_an_existing_directory() {
let cwd = std::env::current_dir().expect("cwd");
let dev = filesystem_device_id(&cwd).expect("an existing directory has a device");
assert_eq!(filesystem_device_id(&cwd), Some(dev), "stable across reads");
}
#[test]
fn filesystem_device_id_declines_a_missing_path() {
let missing = std::env::temp_dir().join(format!(
"rp-host-absent-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
assert_eq!(filesystem_device_id(&missing), None);
}
#[test]
fn login_environment_contains_identity_and_default_path() {
let env = login_environment().unwrap();
let get = |name: &str| {
env.iter()
.find(|(key, _)| key == name)
.map(|(_, value)| value.clone())
};
let user = get("USER").expect("baseline must contain USER");
assert!(!user.is_empty());
assert_eq!(get("LOGNAME").as_ref(), Some(&user));
assert!(!get("HOME").expect("baseline must contain HOME").is_empty());
assert!(!get("PATH").expect("baseline must contain PATH").is_empty());
}
#[test]
fn login_environment_does_not_leak_arbitrary_process_vars() {
std::env::set_var("RUNNING_PROCESS_BASELINE_CANARY", "1");
let env = passwd_login_environment().expect("test user must have a passwd entry");
std::env::remove_var("RUNNING_PROCESS_BASELINE_CANARY");
assert!(
!env.iter()
.any(|(key, _)| key == "RUNNING_PROCESS_BASELINE_CANARY"),
"process-local variables must not leak into the login baseline"
);
}
#[test]
fn login_environment_carries_xdg_runtime_dir() {
std::env::set_var("XDG_RUNTIME_DIR", "/run/user/4242");
let env = passwd_login_environment().expect("test user must have a passwd entry");
let carried = env
.iter()
.find(|(key, _)| key == "XDG_RUNTIME_DIR")
.map(|(_, value)| value.clone());
std::env::remove_var("XDG_RUNTIME_DIR");
assert_eq!(
carried.as_deref(),
Some(std::ffi::OsStr::new("/run/user/4242")),
"login baseline must carry XDG_RUNTIME_DIR when the session sets it"
);
}
#[test]
fn only_session_describing_variables_are_carried() {
for carried in ["LANG", "TZ", "TMPDIR", "XDG_RUNTIME_DIR", "LC_ALL", "LC_TIME"] {
assert!(
describes_the_login_session(&OsString::from(carried)),
"{carried} describes the login session"
);
}
for dropped in ["PWD", "OLDPWD", "SSH_AUTH_SOCK", "LCD_BRIGHTNESS", "L"] {
assert!(
!describes_the_login_session(&OsString::from(dropped)),
"{dropped} belongs to this process, not the session"
);
}
}
#[test]
fn an_encoded_block_is_double_nul_terminated() {
let live = login_environment_block().expect("this host has a login environment");
assert!(live.len() >= 2);
assert_eq!(&live[live.len() - 2..], &[0, 0]);
let empty = encode_environment_block(&[]);
assert_eq!(empty, vec![0]);
}
#[test]
fn an_encoded_block_carries_every_entry_in_order() {
let block = encode_environment_block(&[
(OsString::from("FIRST"), OsString::from("one")),
(OsString::from("SECOND"), OsString::from("two")),
]);
let text = String::from_utf16_lossy(&block);
let entries: Vec<&str> = text.split('