Skip to main content

running_process/
environment.rs

1//! Process environment baselines.
2//!
3//! Windows exposes the logged-in user's machine + user environment through
4//! `CreateEnvironmentBlock`. Unix has no OS API that reconstructs a login
5//! environment, so the baseline is rebuilt from the user's identity:
6//! `getpwuid_r` supplies `USER`/`LOGNAME`/`HOME`/`SHELL`, `PATH` gets the
7//! platform's login default, and locale/timezone/tmpdir variables are
8//! carried over from the current process when present.
9
10#[cfg(windows)]
11use std::ffi::c_void;
12use std::ffi::OsString;
13use std::io;
14
15/// Return the logged-in user's baseline environment.
16///
17/// On Windows this is freshly constructed from machine and user settings via
18/// `CreateEnvironmentBlock` and therefore excludes variables that exist only
19/// in the current process. On Unix it is reconstructed from the user's
20/// identity (`getpwuid_r`): `USER`, `LOGNAME`, `HOME`, `SHELL`, a platform
21/// default `PATH`, plus locale (`LANG`, `LC_*`), `TZ`, and `TMPDIR` carried
22/// over from the current process when set. Non-Windows targets without a
23/// resolvable passwd entry fall back to the current process environment.
24pub fn user_baseline_environment() -> io::Result<Vec<(OsString, OsString)>> {
25    #[cfg(windows)]
26    {
27        let block = user_baseline_environment_block()?;
28        Ok(parse_windows_environment_block(&block))
29    }
30    #[cfg(unix)]
31    {
32        Ok(unix_login_baseline_environment().unwrap_or_else(|| std::env::vars_os().collect()))
33    }
34    #[cfg(not(any(windows, unix)))]
35    {
36        Ok(std::env::vars_os().collect())
37    }
38}
39
40/// The `PATH` a fresh login would start from. Matches `/etc/paths` order on
41/// macOS and the customary `login(1)` default elsewhere.
42#[cfg(unix)]
43const UNIX_LOGIN_DEFAULT_PATH: &str = if cfg!(target_os = "macos") {
44    "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
45} else {
46    "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
47};
48
49/// Build a clean login environment from the user's identity instead of the
50/// current process environment. Returns `None` when the passwd entry cannot
51/// be resolved (e.g. UID absent from NSS) so the caller can fall back.
52#[cfg(unix)]
53fn unix_login_baseline_environment() -> Option<Vec<(OsString, OsString)>> {
54    use std::ffi::CStr;
55    use std::os::unix::ffi::OsStringExt;
56
57    let mut passwd: libc::passwd = unsafe { std::mem::zeroed() };
58    let mut result: *mut libc::passwd = std::ptr::null_mut();
59    // sysconf(_SC_GETPW_R_SIZE_MAX) is allowed to return -1 ("no limit");
60    // 1 KiB covers real-world passwd entries and getpwuid_r reports ERANGE
61    // if it does not, in which case we grow and retry.
62    let mut buf = vec![0u8; 1024];
63    loop {
64        let rc = unsafe {
65            libc::getpwuid_r(
66                libc::getuid(),
67                &mut passwd,
68                buf.as_mut_ptr().cast(),
69                buf.len(),
70                &mut result,
71            )
72        };
73        if rc == libc::ERANGE && buf.len() < 1 << 20 {
74            buf.resize(buf.len() * 2, 0);
75            continue;
76        }
77        if rc != 0 || result.is_null() {
78            return None;
79        }
80        break;
81    }
82
83    let field = |ptr: *const libc::c_char| -> Option<OsString> {
84        if ptr.is_null() {
85            return None;
86        }
87        let bytes = unsafe { CStr::from_ptr(ptr) }.to_bytes();
88        (!bytes.is_empty()).then(|| OsString::from_vec(bytes.to_vec()))
89    };
90    let name = field(passwd.pw_name)?;
91    let home = field(passwd.pw_dir)?;
92
93    let mut env: Vec<(OsString, OsString)> = vec![
94        (OsString::from("USER"), name.clone()),
95        (OsString::from("LOGNAME"), name),
96        (OsString::from("HOME"), home),
97        (
98            OsString::from("PATH"),
99            OsString::from(UNIX_LOGIN_DEFAULT_PATH),
100        ),
101    ];
102    if let Some(shell) = field(passwd.pw_shell) {
103        env.push((OsString::from("SHELL"), shell));
104    }
105    // Locale, timezone, and per-user tmpdir describe the session rather
106    // than the parent process; carry them over when present so children
107    // keep rendering text and resolving paths the way the user expects.
108    for (key, value) in std::env::vars_os() {
109        let carry = key == "LANG" || key == "TZ" || key == "TMPDIR" || {
110            key.to_str().is_some_and(|k| k.starts_with("LC_"))
111        };
112        if carry {
113            env.push((key, value));
114        }
115    }
116    Some(env)
117}
118
119/// Return a CreateProcessW-compatible Unicode user environment block.
120///
121/// The returned buffer is sorted and double-NUL terminated by Windows. It is
122/// useful to callers that own a manual `CreateProcessW` path.
123#[cfg(windows)]
124pub fn user_baseline_environment_block() -> io::Result<Vec<u16>> {
125    use windows_sys::Win32::Foundation::CloseHandle;
126    use windows_sys::Win32::Security::{TOKEN_DUPLICATE, TOKEN_IMPERSONATE, TOKEN_QUERY};
127    use windows_sys::Win32::System::Environment::{
128        CreateEnvironmentBlock, DestroyEnvironmentBlock,
129    };
130    use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
131
132    // CreateEnvironmentBlock documents that the token needs TOKEN_QUERY,
133    // TOKEN_DUPLICATE, and TOKEN_IMPERSONATE. With TOKEN_QUERY alone the
134    // call still *succeeds* but silently omits the per-user dynamic
135    // variables (USERNAME, USERDOMAIN) — downstream consumers that key
136    // behavior on USERNAME (e.g. soldr's daemon pipe name) then diverge
137    // from processes holding the real login environment.
138    let mut token = std::ptr::null_mut();
139    let opened = unsafe {
140        OpenProcessToken(
141            GetCurrentProcess(),
142            TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_IMPERSONATE,
143            &mut token,
144        )
145    };
146    if opened == 0 {
147        return Err(io::Error::last_os_error());
148    }
149
150    let mut raw_block: *mut c_void = std::ptr::null_mut();
151    let created = unsafe { CreateEnvironmentBlock(&mut raw_block, token, 0) };
152    let create_error = if created == 0 {
153        Some(io::Error::last_os_error())
154    } else {
155        None
156    };
157    unsafe {
158        CloseHandle(token);
159    }
160    if let Some(error) = create_error {
161        return Err(error);
162    }
163
164    let copied = unsafe { copy_windows_environment_block(raw_block.cast::<u16>()) };
165    unsafe {
166        DestroyEnvironmentBlock(raw_block);
167    }
168    Ok(copied)
169}
170
171#[cfg(windows)]
172unsafe fn copy_windows_environment_block(cursor: *const u16) -> Vec<u16> {
173    let mut len = 0usize;
174    loop {
175        if *cursor.add(len) == 0 && *cursor.add(len + 1) == 0 {
176            len += 2;
177            break;
178        }
179        len += 1;
180    }
181    std::slice::from_raw_parts(cursor, len).to_vec()
182}
183
184#[cfg(windows)]
185fn parse_windows_environment_block(block: &[u16]) -> Vec<(OsString, OsString)> {
186    use std::os::windows::ffi::OsStringExt;
187
188    let mut env = Vec::new();
189    let mut offset = 0usize;
190    while offset < block.len() && block[offset] != 0 {
191        let Some(relative_end) = block[offset..].iter().position(|value| *value == 0) else {
192            break;
193        };
194        let end = offset + relative_end;
195        let entry = &block[offset..end];
196        // Drive-current-directory pseudo variables have the shape
197        // `=C:=C:\path`; skip index zero so their second '=' is the
198        // key/value separator.
199        if let Some(separator) = entry
200            .iter()
201            .enumerate()
202            .skip(1)
203            .find_map(|(index, value)| (*value == b'=' as u16).then_some(index))
204        {
205            let key = OsString::from_wide(&entry[..separator]);
206            let value = OsString::from_wide(&entry[separator + 1..]);
207            env.push((key, value));
208        }
209        offset = end + 1;
210    }
211    env
212}
213
214#[cfg(all(test, unix))]
215mod unix_tests {
216    use super::*;
217
218    #[test]
219    fn login_baseline_contains_identity_and_default_path() {
220        let env = user_baseline_environment().unwrap();
221        let get = |name: &str| {
222            env.iter()
223                .find(|(key, _)| key == name)
224                .map(|(_, value)| value.clone())
225        };
226        let user = get("USER").expect("baseline must contain USER");
227        assert!(!user.is_empty());
228        assert_eq!(get("LOGNAME").as_ref(), Some(&user));
229        let home = get("HOME").expect("baseline must contain HOME");
230        assert!(!home.is_empty());
231        let path = get("PATH").expect("baseline must contain PATH");
232        assert!(!path.is_empty());
233    }
234
235    #[test]
236    fn login_baseline_does_not_leak_arbitrary_process_vars() {
237        // A variable that only exists in this process must not survive
238        // into the login baseline (that's what Inherit is for).
239        std::env::set_var("RUNNING_PROCESS_BASELINE_CANARY", "1");
240        let env = unix_login_baseline_environment().expect("test user must have a passwd entry");
241        assert!(
242            !env.iter()
243                .any(|(key, _)| key == "RUNNING_PROCESS_BASELINE_CANARY"),
244            "process-local variables must not leak into the login baseline"
245        );
246        std::env::remove_var("RUNNING_PROCESS_BASELINE_CANARY");
247    }
248}
249
250#[cfg(all(test, windows))]
251mod tests {
252    use super::*;
253    use std::ffi::OsStr;
254    use std::os::windows::ffi::OsStrExt;
255
256    #[test]
257    fn parser_preserves_drive_current_directory_entries() {
258        let block: Vec<u16> = OsStr::new("=C:=C:\\work")
259            .encode_wide()
260            .chain(std::iter::once(0))
261            .chain(OsStr::new("Path=C:\\Windows").encode_wide())
262            .chain(std::iter::once(0))
263            .chain(std::iter::once(0))
264            .collect();
265        assert_eq!(
266            parse_windows_environment_block(&block),
267            vec![
268                (OsString::from("=C:"), OsString::from("C:\\work")),
269                (OsString::from("Path"), OsString::from("C:\\Windows")),
270            ]
271        );
272    }
273
274    #[test]
275    fn live_user_baseline_is_double_nul_terminated() {
276        let block = user_baseline_environment_block().unwrap();
277        assert!(block.len() >= 2);
278        assert_eq!(&block[block.len() - 2..], &[0, 0]);
279    }
280
281    /// Regression: with TOKEN_QUERY-only access, CreateEnvironmentBlock
282    /// succeeds but silently drops the per-user dynamic variables. The
283    /// baseline must contain USERNAME (and it must match the live value
284    /// when the current process has one).
285    #[test]
286    fn live_user_baseline_contains_username() {
287        let env = user_baseline_environment().unwrap();
288        let username = env
289            .iter()
290            .find(|(key, _)| key.eq_ignore_ascii_case("USERNAME"))
291            .map(|(_, value)| value.clone());
292        let username = username.expect("baseline environment must contain USERNAME");
293        assert!(!username.is_empty(), "USERNAME must be non-empty");
294        if let Ok(live) = std::env::var("USERNAME") {
295            assert_eq!(
296                username.to_string_lossy(),
297                live,
298                "baseline USERNAME must match the live login USERNAME"
299            );
300        }
301    }
302}