Skip to main content

running_process/broker/lifecycle/
privilege.rs

1//! Broker startup privilege checks.
2//!
3//! The broker control socket is a per-user boundary. Starting it as
4//! root or Windows LocalSystem would make that boundary ambiguous, so
5//! the binary refuses privileged startup unless a test environment
6//! explicitly opts out.
7
8/// Environment variable that permits privileged broker startup.
9///
10/// This exists for controlled test fixtures only. Production launchers
11/// should run the broker as the target user instead.
12pub const ALLOW_PRIVILEGED_ENV: &str = "RUNNING_PROCESS_BROKER_ALLOW_PRIVILEGED";
13
14/// Errors returned while checking broker startup privileges.
15#[derive(Debug, thiserror::Error)]
16pub enum PrivilegeError {
17    /// The current process is running as a privileged OS identity.
18    #[error(
19        "running-process-broker-v1 refuses to run as {identity} by default; set {ALLOW_PRIVILEGED_ENV}=1 only for isolated test environments"
20    )]
21    Privileged {
22        /// Privileged identity detected for the current process.
23        identity: PrivilegedIdentity,
24    },
25    /// The platform privilege lookup failed.
26    #[error("failed to determine broker process privilege: {0}")]
27    PlatformLookup(String),
28}
29
30/// Privileged identities that are forbidden for the broker by default.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum PrivilegedIdentity {
33    /// Unix effective UID 0.
34    UnixRoot,
35    /// Windows LocalSystem account (`S-1-5-18`).
36    WindowsLocalSystem,
37}
38
39impl std::fmt::Display for PrivilegedIdentity {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        match self {
42            Self::UnixRoot => f.write_str("root (effective uid 0)"),
43            Self::WindowsLocalSystem => f.write_str("Windows LocalSystem (S-1-5-18)"),
44        }
45    }
46}
47
48/// Refuse to start the broker when the current process is privileged.
49///
50/// The check runs before the binary binds any socket. Set
51/// [`ALLOW_PRIVILEGED_ENV`] to `1` only for isolated test environments
52/// that intentionally exercise privileged startup behavior.
53pub fn refuse_privileged_run() -> Result<(), PrivilegeError> {
54    if allow_privileged_from_env() {
55        return Ok(());
56    }
57    refuse_process_privilege(current_process_privilege()?)
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61struct ProcessPrivilege {
62    identity: Option<PrivilegedIdentity>,
63}
64
65impl ProcessPrivilege {
66    const fn unprivileged() -> Self {
67        Self { identity: None }
68    }
69
70    const fn privileged(identity: PrivilegedIdentity) -> Self {
71        Self {
72            identity: Some(identity),
73        }
74    }
75}
76
77fn refuse_process_privilege(privilege: ProcessPrivilege) -> Result<(), PrivilegeError> {
78    match privilege.identity {
79        Some(identity) => Err(PrivilegeError::Privileged { identity }),
80        None => Ok(()),
81    }
82}
83
84fn allow_privileged_from_env() -> bool {
85    let value = std::env::var(ALLOW_PRIVILEGED_ENV).ok();
86    allow_privileged_env_value(value.as_deref())
87}
88
89fn allow_privileged_env_value(value: Option<&str>) -> bool {
90    value == Some("1")
91}
92
93fn current_process_privilege() -> Result<ProcessPrivilege, PrivilegeError> {
94    platform_current_process_privilege()
95}
96
97#[cfg(unix)]
98fn platform_current_process_privilege() -> Result<ProcessPrivilege, PrivilegeError> {
99    let euid = unsafe { libc::geteuid() };
100    Ok(privilege_from_unix_euid(euid))
101}
102
103#[cfg(unix)]
104fn privilege_from_unix_euid(euid: libc::uid_t) -> ProcessPrivilege {
105    if euid == 0 {
106        ProcessPrivilege::privileged(PrivilegedIdentity::UnixRoot)
107    } else {
108        ProcessPrivilege::unprivileged()
109    }
110}
111
112#[cfg(windows)]
113fn platform_current_process_privilege() -> Result<ProcessPrivilege, PrivilegeError> {
114    let sid = windows_current_user_sid_bytes()?;
115    if is_windows_local_system_sid(&sid) {
116        Ok(ProcessPrivilege::privileged(
117            PrivilegedIdentity::WindowsLocalSystem,
118        ))
119    } else {
120        Ok(ProcessPrivilege::unprivileged())
121    }
122}
123
124#[cfg(all(not(unix), not(windows)))]
125fn platform_current_process_privilege() -> Result<ProcessPrivilege, PrivilegeError> {
126    Ok(ProcessPrivilege::unprivileged())
127}
128
129#[cfg(windows)]
130fn windows_current_user_sid_bytes() -> Result<Vec<u8>, PrivilegeError> {
131    use std::ptr;
132    use winapi::shared::winerror::ERROR_INSUFFICIENT_BUFFER;
133    use winapi::um::errhandlingapi::GetLastError;
134    use winapi::um::processthreadsapi::{GetCurrentProcess, OpenProcessToken};
135    use winapi::um::securitybaseapi::{GetLengthSid, GetTokenInformation, IsValidSid};
136    use winapi::um::winnt::{TokenUser, HANDLE, TOKEN_QUERY, TOKEN_USER};
137
138    // SAFETY: this follows the standard Windows token query pattern:
139    // open the current process token, ask for the required TOKEN_USER
140    // buffer size, then copy the SID bytes out while the buffer is
141    // still alive.
142    unsafe {
143        let mut token: HANDLE = ptr::null_mut();
144        if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) == 0 {
145            return Err(PrivilegeError::PlatformLookup(format!(
146                "OpenProcessToken failed (GetLastError={})",
147                GetLastError()
148            )));
149        }
150        let token = TokenHandle(token);
151
152        let mut required_size = 0_u32;
153        let ok = GetTokenInformation(token.0, TokenUser, ptr::null_mut(), 0, &mut required_size);
154        let last = GetLastError();
155        if ok != 0 || last != ERROR_INSUFFICIENT_BUFFER {
156            return Err(PrivilegeError::PlatformLookup(format!(
157                "GetTokenInformation size query failed (ok={ok}, GetLastError={last})"
158            )));
159        }
160        if required_size == 0 {
161            return Err(PrivilegeError::PlatformLookup(
162                "GetTokenInformation reported 0 required bytes".into(),
163            ));
164        }
165
166        let mut buf = vec![0_u8; required_size as usize];
167        if GetTokenInformation(
168            token.0,
169            TokenUser,
170            buf.as_mut_ptr().cast(),
171            required_size,
172            &mut required_size,
173        ) == 0
174        {
175            return Err(PrivilegeError::PlatformLookup(format!(
176                "GetTokenInformation real query failed (GetLastError={})",
177                GetLastError()
178            )));
179        }
180
181        let token_user: *const TOKEN_USER = buf.as_ptr().cast();
182        let sid = (*token_user).User.Sid;
183        if sid.is_null() {
184            return Err(PrivilegeError::PlatformLookup(
185                "TOKEN_USER returned a null SID pointer".into(),
186            ));
187        }
188        if IsValidSid(sid) == 0 {
189            return Err(PrivilegeError::PlatformLookup(
190                "IsValidSid returned false".into(),
191            ));
192        }
193
194        let len = GetLengthSid(sid) as usize;
195        if len == 0 || len > 1024 {
196            return Err(PrivilegeError::PlatformLookup(format!(
197                "GetLengthSid returned implausible length {len}"
198            )));
199        }
200        Ok(std::slice::from_raw_parts(sid as *const u8, len).to_vec())
201    }
202}
203
204#[cfg(windows)]
205struct TokenHandle(winapi::um::winnt::HANDLE);
206
207#[cfg(windows)]
208impl Drop for TokenHandle {
209    fn drop(&mut self) {
210        unsafe {
211            winapi::um::handleapi::CloseHandle(self.0);
212        }
213    }
214}
215
216#[cfg(windows)]
217fn is_windows_local_system_sid(sid: &[u8]) -> bool {
218    const LOCAL_SYSTEM_SID: &[u8] = &[1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0];
219    sid == LOCAL_SYSTEM_SID
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn refuses_privileged_identity() {
228        let err =
229            refuse_process_privilege(ProcessPrivilege::privileged(PrivilegedIdentity::UnixRoot))
230                .unwrap_err();
231        assert!(matches!(
232            err,
233            PrivilegeError::Privileged {
234                identity: PrivilegedIdentity::UnixRoot
235            }
236        ));
237    }
238
239    #[test]
240    fn allows_unprivileged_identity() {
241        refuse_process_privilege(ProcessPrivilege::unprivileged()).unwrap();
242    }
243
244    #[test]
245    fn allow_env_value_requires_exact_one() {
246        assert!(allow_privileged_env_value(Some("1")));
247        assert!(!allow_privileged_env_value(None));
248        assert!(!allow_privileged_env_value(Some("")));
249        assert!(!allow_privileged_env_value(Some("true")));
250        assert!(!allow_privileged_env_value(Some("yes")));
251    }
252
253    #[cfg(unix)]
254    #[test]
255    fn unix_root_detection_uses_effective_uid_zero() {
256        assert_eq!(
257            privilege_from_unix_euid(0),
258            ProcessPrivilege::privileged(PrivilegedIdentity::UnixRoot)
259        );
260        assert_eq!(
261            privilege_from_unix_euid(1000),
262            ProcessPrivilege::unprivileged()
263        );
264    }
265
266    #[cfg(windows)]
267    #[test]
268    fn windows_local_system_sid_is_detected() {
269        assert!(is_windows_local_system_sid(&[
270            1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0
271        ]));
272        assert!(!is_windows_local_system_sid(&[
273            1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0
274        ]));
275    }
276}