#[cfg(windows)]
use std::ffi::c_void;
use std::ffi::OsString;
use std::io;
pub fn user_baseline_environment() -> io::Result<Vec<(OsString, OsString)>> {
#[cfg(windows)]
{
let block = user_baseline_environment_block()?;
Ok(parse_windows_environment_block(&block))
}
#[cfg(unix)]
{
Ok(unix_login_baseline_environment().unwrap_or_else(|| std::env::vars_os().collect()))
}
#[cfg(not(any(windows, unix)))]
{
Ok(std::env::vars_os().collect())
}
}
#[cfg(unix)]
const UNIX_LOGIN_DEFAULT_PATH: &str = if cfg!(target_os = "macos") {
"/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
} else {
"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
};
#[cfg(unix)]
fn unix_login_baseline_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(UNIX_LOGIN_DEFAULT_PATH),
),
];
if let Some(shell) = field(passwd.pw_shell) {
env.push((OsString::from("SHELL"), shell));
}
for (key, value) in std::env::vars_os() {
let carry = key == "LANG" || key == "TZ" || key == "TMPDIR" || {
key.to_str().is_some_and(|k| k.starts_with("LC_"))
};
if carry {
env.push((key, value));
}
}
Some(env)
}
#[cfg(windows)]
pub fn user_baseline_environment_block() -> io::Result<Vec<u16>> {
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::Security::{TOKEN_DUPLICATE, TOKEN_IMPERSONATE, TOKEN_QUERY};
use windows_sys::Win32::System::Environment::{
CreateEnvironmentBlock, DestroyEnvironmentBlock,
};
use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
let mut token = std::ptr::null_mut();
let opened = unsafe {
OpenProcessToken(
GetCurrentProcess(),
TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_IMPERSONATE,
&mut token,
)
};
if opened == 0 {
return Err(io::Error::last_os_error());
}
let mut raw_block: *mut c_void = std::ptr::null_mut();
let created = unsafe { CreateEnvironmentBlock(&mut raw_block, token, 0) };
let create_error = if created == 0 {
Some(io::Error::last_os_error())
} else {
None
};
unsafe {
CloseHandle(token);
}
if let Some(error) = create_error {
return Err(error);
}
let copied = unsafe { copy_windows_environment_block(raw_block.cast::<u16>()) };
unsafe {
DestroyEnvironmentBlock(raw_block);
}
Ok(copied)
}
#[cfg(windows)]
unsafe fn copy_windows_environment_block(cursor: *const u16) -> Vec<u16> {
let mut len = 0usize;
loop {
if *cursor.add(len) == 0 && *cursor.add(len + 1) == 0 {
len += 2;
break;
}
len += 1;
}
std::slice::from_raw_parts(cursor, len).to_vec()
}
#[cfg(windows)]
fn parse_windows_environment_block(block: &[u16]) -> Vec<(OsString, OsString)> {
use std::os::windows::ffi::OsStringExt;
let mut env = Vec::new();
let mut offset = 0usize;
while offset < block.len() && block[offset] != 0 {
let Some(relative_end) = block[offset..].iter().position(|value| *value == 0) else {
break;
};
let end = offset + relative_end;
let entry = &block[offset..end];
if let Some(separator) = entry
.iter()
.enumerate()
.skip(1)
.find_map(|(index, value)| (*value == b'=' as u16).then_some(index))
{
let key = OsString::from_wide(&entry[..separator]);
let value = OsString::from_wide(&entry[separator + 1..]);
env.push((key, value));
}
offset = end + 1;
}
env
}
#[cfg(all(test, unix))]
mod unix_tests {
use super::*;
#[test]
fn login_baseline_contains_identity_and_default_path() {
let env = user_baseline_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));
let home = get("HOME").expect("baseline must contain HOME");
assert!(!home.is_empty());
let path = get("PATH").expect("baseline must contain PATH");
assert!(!path.is_empty());
}
#[test]
fn login_baseline_does_not_leak_arbitrary_process_vars() {
std::env::set_var("RUNNING_PROCESS_BASELINE_CANARY", "1");
let env = unix_login_baseline_environment().expect("test user must have a passwd entry");
assert!(
!env.iter()
.any(|(key, _)| key == "RUNNING_PROCESS_BASELINE_CANARY"),
"process-local variables must not leak into the login baseline"
);
std::env::remove_var("RUNNING_PROCESS_BASELINE_CANARY");
}
}
#[cfg(all(test, windows))]
mod tests {
use super::*;
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
#[test]
fn parser_preserves_drive_current_directory_entries() {
let block: Vec<u16> = OsStr::new("=C:=C:\\work")
.encode_wide()
.chain(std::iter::once(0))
.chain(OsStr::new("Path=C:\\Windows").encode_wide())
.chain(std::iter::once(0))
.chain(std::iter::once(0))
.collect();
assert_eq!(
parse_windows_environment_block(&block),
vec![
(OsString::from("=C:"), OsString::from("C:\\work")),
(OsString::from("Path"), OsString::from("C:\\Windows")),
]
);
}
#[test]
fn live_user_baseline_is_double_nul_terminated() {
let block = user_baseline_environment_block().unwrap();
assert!(block.len() >= 2);
assert_eq!(&block[block.len() - 2..], &[0, 0]);
}
#[test]
fn live_user_baseline_contains_username() {
let env = user_baseline_environment().unwrap();
let username = env
.iter()
.find(|(key, _)| key.eq_ignore_ascii_case("USERNAME"))
.map(|(_, value)| value.clone());
let username = username.expect("baseline environment must contain USERNAME");
assert!(!username.is_empty(), "USERNAME must be non-empty");
if let Ok(live) = std::env::var("USERNAME") {
assert_eq!(
username.to_string_lossy(),
live,
"baseline USERNAME must match the live login USERNAME"
);
}
}
}