Skip to main content

rightkit_daemon/
peer.rs

1//! OS-level peer authentication for the local transport.
2//!
3//! The directory mode (Unix) and the pipe DACL (Windows) already keep other
4//! users out. This module is the second, independent check: after a connection
5//! is accepted the kernel is asked who is on the other end, and anything that is
6//! not the current user is dropped before a single byte is read.
7//!
8//! Fail closed: every function here returns `Ok` only when the peer was positively
9//! identified as the current user. A platform that cannot answer returns
10//! [`PeerError::Unsupported`]; it never falls back to "assume it is me".
11
12use std::fmt;
13use std::io;
14
15/// Who the kernel says is on the other end of a connection.
16#[derive(Debug, Clone, PartialEq, Eq, Default)]
17#[non_exhaustive]
18pub struct PeerIdentity {
19    /// Unix: the peer's effective uid.
20    pub uid: Option<u32>,
21    /// Peer process id when the platform reports it (Linux, macOS, Windows; not the BSDs).
22    pub pid: Option<u32>,
23    /// Windows: the peer process token's user SID in `S-1-...` form.
24    pub sid: Option<String>,
25    /// macOS: the peer's audit token (`audit_token_t`, eight words: pid at index 5, pid version
26    /// at index 7) as the kernel captured it when the connection was made (`LOCAL_PEERTOKEN`).
27    /// The race-free handle for a code-signature check of the peer. `None` elsewhere, or when
28    /// the kernel did not provide one.
29    pub audit_token: Option<[u32; 8]>,
30    /// The peer process's start marker as read when the connection was accepted (Unix: start
31    /// time from the OS; Windows: creation time). Pins the pid to one process, so a pid reused
32    /// by another process after the peer exited is told apart at the server-side executable
33    /// check. `None` when it could not be read; that check then fails closed.
34    pub started: Option<(u64, u64)>,
35}
36
37/// Why a peer was not accepted as the current user.
38#[derive(Debug)]
39#[non_exhaustive]
40pub enum PeerError {
41    /// This platform cannot authenticate the peer, so no peer is accepted.
42    Unsupported(&'static str),
43    /// The peer is a different user.
44    Mismatch(PeerIdentity),
45    /// The OS could not tell who the peer is (including a peer that already left).
46    Query(io::Error),
47}
48
49impl fmt::Display for PeerError {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        match self {
52            PeerError::Unsupported(why) => write!(f, "peer authentication unsupported: {why}"),
53            PeerError::Mismatch(id) => {
54                write!(f, "peer is not the current user")?;
55                if let Some(uid) = id.uid {
56                    write!(f, " (uid {uid})")?;
57                }
58                if let Some(pid) = id.pid {
59                    write!(f, " (pid {pid})")?;
60                }
61                Ok(())
62            }
63            PeerError::Query(e) => write!(f, "could not identify peer: {e}"),
64        }
65    }
66}
67
68impl std::error::Error for PeerError {
69    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
70        match self {
71            PeerError::Query(e) => Some(e),
72            _ => None,
73        }
74    }
75}
76
77impl From<PeerError> for io::Error {
78    fn from(e: PeerError) -> Self {
79        let kind = match &e {
80            PeerError::Unsupported(_) => io::ErrorKind::Unsupported,
81            PeerError::Mismatch(_) => io::ErrorKind::PermissionDenied,
82            PeerError::Query(q) => q.kind(),
83        };
84        io::Error::new(kind, e.to_string())
85    }
86}
87
88impl PeerError {
89    /// The peer disconnected before it could be identified (a probe, or `wake`).
90    /// Not an attack; the connection is still dropped, but no rejection is logged.
91    pub(crate) fn peer_gone(&self) -> bool {
92        let PeerError::Query(e) = self else {
93            return false;
94        };
95        if matches!(
96            e.kind(),
97            io::ErrorKind::NotConnected
98                | io::ErrorKind::BrokenPipe
99                | io::ErrorKind::ConnectionReset
100                | io::ErrorKind::UnexpectedEof
101        ) {
102            return true;
103        }
104        // ERROR_BROKEN_PIPE 109, ERROR_NO_DATA 232, ERROR_PIPE_NOT_CONNECTED 233.
105        cfg!(windows) && matches!(e.raw_os_error(), Some(109 | 232 | 233))
106    }
107}
108
109/// `D:P(A;;GA;;;<sid>)`: protected DACL (no inheritance), one ACE granting
110/// GENERIC_ALL to exactly this SID.
111#[cfg(any(windows, test))]
112pub(crate) fn sddl_for_sid(sid: &str) -> String {
113    format!("D:P(A;;GA;;;{sid})")
114}
115
116// ---------------------------------------------------------------------------
117// Unix
118// ---------------------------------------------------------------------------
119
120#[cfg(unix)]
121mod unix {
122    use super::*;
123    use std::os::fd::AsRawFd;
124    use std::os::unix::net::UnixStream;
125
126    extern "C" {
127        fn geteuid() -> u32;
128    }
129
130    #[cfg(any(
131        target_os = "macos",
132        target_os = "ios",
133        target_os = "freebsd",
134        target_os = "openbsd",
135        target_os = "netbsd",
136        target_os = "dragonfly"
137    ))]
138    extern "C" {
139        fn getpeereid(fd: i32, euid: *mut u32, egid: *mut u32) -> i32;
140    }
141
142    // Linux/Android generic ABI: SOL_SOCKET = 1, SO_PEERCRED = 17. The mips, powerpc
143    // and sparc families number these differently and are deliberately left Unsupported
144    // rather than guessed.
145    #[cfg(all(
146        any(target_os = "linux", target_os = "android"),
147        not(any(
148            target_arch = "mips",
149            target_arch = "mips32r6",
150            target_arch = "mips64",
151            target_arch = "mips64r6",
152            target_arch = "powerpc",
153            target_arch = "powerpc64",
154            target_arch = "sparc",
155            target_arch = "sparc64"
156        ))
157    ))]
158    mod linux {
159        pub const SOL_SOCKET: i32 = 1;
160        pub const SO_PEERCRED: i32 = 17;
161        #[repr(C)]
162        #[derive(Default)]
163        pub struct Ucred {
164            pub pid: i32,
165            pub uid: u32,
166            pub gid: u32,
167        }
168        extern "C" {
169            pub fn getsockopt(
170                fd: i32,
171                level: i32,
172                name: i32,
173                val: *mut core::ffi::c_void,
174                len: *mut u32,
175            ) -> i32;
176        }
177    }
178
179    pub(crate) fn current_euid() -> u32 {
180        unsafe { geteuid() }
181    }
182
183    #[cfg(any(
184        target_os = "macos",
185        target_os = "ios",
186        target_os = "freebsd",
187        target_os = "openbsd",
188        target_os = "netbsd",
189        target_os = "dragonfly"
190    ))]
191    fn peer_identity(s: &UnixStream) -> Result<PeerIdentity, PeerError> {
192        let (mut uid, mut gid) = (u32::MAX, u32::MAX);
193        // SAFETY: valid fd borrowed from `s`; out-pointers are valid locals.
194        let rc = unsafe { getpeereid(s.as_raw_fd(), &mut uid, &mut gid) };
195        if rc != 0 {
196            return Err(PeerError::Query(io::Error::last_os_error()));
197        }
198        Ok(PeerIdentity {
199            uid: Some(uid),
200            pid: peer_pid(s),
201            sid: None,
202            audit_token: crate::host_identity::peer_audit_token(s),
203            started: peer_pid(s).and_then(crate::host_identity::process_start_marker),
204        })
205    }
206
207    /// The peer's process id from the `LOCAL_PEERPID` socket option. `None` when the option
208    /// fails: the peer is still authenticated by its uid, the pid is only extra detail.
209    #[cfg(any(target_os = "macos", target_os = "ios"))]
210    fn peer_pid(s: &UnixStream) -> Option<u32> {
211        const SOL_LOCAL: i32 = 0;
212        const LOCAL_PEERPID: i32 = 0x002;
213        extern "C" {
214            fn getsockopt(
215                fd: i32,
216                level: i32,
217                name: i32,
218                val: *mut core::ffi::c_void,
219                len: *mut u32,
220            ) -> i32;
221        }
222        let mut pid: i32 = 0;
223        let mut len = std::mem::size_of::<i32>() as u32;
224        // SAFETY: valid fd borrowed from `s`; `pid` and `len` are valid locals of the size
225        // the option writes (`pid_t`, `socklen_t`).
226        let rc = unsafe {
227            getsockopt(
228                s.as_raw_fd(),
229                SOL_LOCAL,
230                LOCAL_PEERPID,
231                (&mut pid as *mut i32).cast(),
232                &mut len,
233            )
234        };
235        if rc != 0 {
236            return None;
237        }
238        u32::try_from(pid).ok().filter(|p| *p != 0)
239    }
240
241    /// The BSDs have no peer-pid socket option of this shape.
242    #[cfg(any(
243        target_os = "freebsd",
244        target_os = "openbsd",
245        target_os = "netbsd",
246        target_os = "dragonfly"
247    ))]
248    fn peer_pid(_s: &UnixStream) -> Option<u32> {
249        None
250    }
251
252    #[cfg(all(
253        any(target_os = "linux", target_os = "android"),
254        not(any(
255            target_arch = "mips",
256            target_arch = "mips32r6",
257            target_arch = "mips64",
258            target_arch = "mips64r6",
259            target_arch = "powerpc",
260            target_arch = "powerpc64",
261            target_arch = "sparc",
262            target_arch = "sparc64"
263        ))
264    ))]
265    fn peer_identity(s: &UnixStream) -> Result<PeerIdentity, PeerError> {
266        let mut cred = linux::Ucred::default();
267        let mut len = std::mem::size_of::<linux::Ucred>() as u32;
268        // SAFETY: valid fd; `cred` and `len` describe a writable ucred.
269        let rc = unsafe {
270            linux::getsockopt(
271                s.as_raw_fd(),
272                linux::SOL_SOCKET,
273                linux::SO_PEERCRED,
274                &mut cred as *mut linux::Ucred as *mut core::ffi::c_void,
275                &mut len,
276            )
277        };
278        if rc != 0 {
279            return Err(PeerError::Query(io::Error::last_os_error()));
280        }
281        if (len as usize) < std::mem::size_of::<linux::Ucred>() {
282            return Err(PeerError::Query(io::Error::new(
283                io::ErrorKind::InvalidData,
284                "short SO_PEERCRED reply",
285            )));
286        }
287        Ok(PeerIdentity {
288            uid: Some(cred.uid),
289            pid: u32::try_from(cred.pid).ok().filter(|p| *p != 0),
290            sid: None,
291            audit_token: None,
292            started: u32::try_from(cred.pid)
293                .ok()
294                .filter(|p| *p != 0)
295                .and_then(crate::host_identity::process_start_marker),
296        })
297    }
298
299    #[cfg(not(any(
300        target_os = "macos",
301        target_os = "ios",
302        target_os = "freebsd",
303        target_os = "openbsd",
304        target_os = "netbsd",
305        target_os = "dragonfly",
306        all(
307            any(target_os = "linux", target_os = "android"),
308            not(any(
309                target_arch = "mips",
310                target_arch = "mips32r6",
311                target_arch = "mips64",
312                target_arch = "mips64r6",
313                target_arch = "powerpc",
314                target_arch = "powerpc64",
315                target_arch = "sparc",
316                target_arch = "sparc64"
317            ))
318        )
319    )))]
320    fn peer_identity(_s: &UnixStream) -> Result<PeerIdentity, PeerError> {
321        Err(PeerError::Unsupported(
322            "no peer-credential query is implemented for this Unix platform",
323        ))
324    }
325
326    /// The pure decision: the peer is accepted only when its uid is known and equals `me`.
327    pub(crate) fn accept_uid(id: PeerIdentity, me: u32) -> Result<PeerIdentity, PeerError> {
328        if id.uid == Some(me) {
329            Ok(id)
330        } else {
331            Err(PeerError::Mismatch(id))
332        }
333    }
334
335    /// Verify the peer of a connected Unix stream is the current user.
336    ///
337    /// macOS and the BSDs use `getpeereid`; Linux and Android use `SO_PEERCRED`. The
338    /// peer's effective uid must equal this process's `geteuid`. Any other Unix
339    /// returns [`PeerError::Unsupported`]: the answer is never `Ok` on a platform that
340    /// cannot check.
341    ///
342    /// The credentials are those the kernel captured when the connection was made, so a
343    /// peer that connects and then exits is still identified correctly.
344    pub fn verify_peer_is_current_user(stream: &UnixStream) -> Result<PeerIdentity, PeerError> {
345        accept_uid(peer_identity(stream)?, current_euid())
346    }
347}
348
349#[cfg(unix)]
350pub(crate) use unix::current_euid;
351#[cfg(unix)]
352pub use unix::verify_peer_is_current_user;
353
354// ---------------------------------------------------------------------------
355// Windows
356// ---------------------------------------------------------------------------
357
358#[cfg(windows)]
359mod win {
360    use super::*;
361    use std::os::windows::io::RawHandle;
362    use windows::core::PWSTR;
363    use windows::Win32::Foundation::{CloseHandle, LocalFree, HANDLE, HLOCAL};
364    use windows::Win32::Security::Authorization::{
365        ConvertSidToStringSidW, ConvertStringSecurityDescriptorToSecurityDescriptorW,
366        SDDL_REVISION_1,
367    };
368    use windows::Win32::Security::{
369        GetTokenInformation, TokenUser, PSECURITY_DESCRIPTOR, PSID, SECURITY_ATTRIBUTES,
370        TOKEN_QUERY, TOKEN_USER,
371    };
372    use windows::Win32::System::Pipes::{GetNamedPipeClientProcessId, GetNamedPipeServerProcessId};
373    use windows::Win32::System::Threading::{
374        GetCurrentProcess, OpenProcess, OpenProcessToken, PROCESS_QUERY_LIMITED_INFORMATION,
375    };
376
377    /// Win32 error as an `io::Error` that keeps the code, so `peer_gone` can see it.
378    fn win_err(e: windows::core::Error) -> io::Error {
379        io::Error::from_raw_os_error((e.code().0 as u32 & 0xFFFF) as i32)
380    }
381
382    struct Owned(HANDLE);
383    impl Drop for Owned {
384        fn drop(&mut self) {
385            unsafe {
386                let _ = CloseHandle(self.0);
387            }
388        }
389    }
390
391    fn sid_to_string(sid: PSID) -> io::Result<String> {
392        unsafe {
393            let mut p = PWSTR::null();
394            ConvertSidToStringSidW(sid, &mut p).map_err(win_err)?;
395            let s = p.to_string().map_err(io::Error::other);
396            let _ = LocalFree(Some(HLOCAL(p.0 as *mut _)));
397            s
398        }
399    }
400
401    /// The user SID string of an access token opened with TOKEN_QUERY.
402    fn token_user_sid(tok: HANDLE) -> io::Result<String> {
403        unsafe {
404            let mut need = 0u32;
405            let _ = GetTokenInformation(tok, TokenUser, None, 0, &mut need);
406            if need == 0 {
407                return Err(io::Error::other("token user size query returned 0"));
408            }
409            // u64 storage keeps the TOKEN_USER header pointer-aligned.
410            let mut buf = vec![0u64; (need as usize).div_ceil(8)];
411            GetTokenInformation(
412                tok,
413                TokenUser,
414                Some(buf.as_mut_ptr() as *mut _),
415                (buf.len() * 8) as u32,
416                &mut need,
417            )
418            .map_err(win_err)?;
419            let user = &*(buf.as_ptr() as *const TOKEN_USER);
420            sid_to_string(user.User.Sid)
421        }
422    }
423
424    /// The SID string of the user running this process.
425    pub(crate) fn current_user_sid_string() -> io::Result<String> {
426        unsafe {
427            let mut tok = HANDLE::default();
428            OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut tok).map_err(win_err)?;
429            let tok = Owned(tok);
430            token_user_sid(tok.0)
431        }
432    }
433
434    /// The SID string of the user running process `pid`. Fails closed: a process that
435    /// cannot be opened or queried yields an error, never a guess.
436    fn process_user_sid_string(pid: u32) -> io::Result<String> {
437        unsafe {
438            let proc =
439                Owned(OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid).map_err(win_err)?);
440            let mut tok = HANDLE::default();
441            OpenProcessToken(proc.0, TOKEN_QUERY, &mut tok).map_err(win_err)?;
442            let tok = Owned(tok);
443            token_user_sid(tok.0)
444        }
445    }
446
447    /// Compare a peer process's user SID with ours.
448    fn verify_pid(pid: u32) -> Result<PeerIdentity, PeerError> {
449        if pid == 0 {
450            return Err(PeerError::Query(io::Error::other("peer process id is 0")));
451        }
452        let me = current_user_sid_string().map_err(PeerError::Query)?;
453        let theirs = process_user_sid_string(pid).map_err(PeerError::Query)?;
454        let id = PeerIdentity {
455            uid: None,
456            pid: Some(pid),
457            sid: Some(theirs.clone()),
458            audit_token: None,
459            started: crate::host_identity::process_start_marker(pid),
460        };
461        // SID strings are canonical (`S-1-5-21-...`), so equality of the strings is
462        // equality of the SIDs.
463        if theirs.eq_ignore_ascii_case(&me) {
464            Ok(id)
465        } else {
466            Err(PeerError::Mismatch(id))
467        }
468    }
469
470    /// Server side: verify that the client connected to `handle` (a server pipe instance
471    /// on which `ConnectNamedPipe` has completed) runs as the current user.
472    ///
473    /// `GetNamedPipeClientProcessId`, then the process token's user SID against ours. Call
474    /// it before reading any request byte. Fails closed: a client that cannot be opened or
475    /// queried is rejected. The pid is the one the kernel recorded at connect time; the
476    /// pipe DACL (see [`owner_only_security_attributes`]) is the primary boundary and this
477    /// is the independent second check.
478    ///
479    /// `handle` must be a valid, connected named-pipe server handle for the duration of
480    /// the call.
481    pub fn verify_pipe_peer_is_current_user(handle: RawHandle) -> Result<PeerIdentity, PeerError> {
482        let mut pid = 0u32;
483        // SAFETY: the caller guarantees a valid pipe handle; `pid` is a valid out-pointer.
484        unsafe { GetNamedPipeClientProcessId(HANDLE(handle), &mut pid) }
485            .map_err(|e| PeerError::Query(win_err(e)))?;
486        verify_pid(pid)
487    }
488
489    /// Client side: verify that the server behind `handle` (a pipe opened with
490    /// `CreateFile`) runs as the current user, via `GetNamedPipeServerProcessId`. Catches
491    /// a different user squatting on the pipe name. Open the pipe with
492    /// `SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION` as well, so the server can
493    /// identify but never impersonate the caller.
494    pub fn verify_pipe_server_is_current_user(
495        handle: RawHandle,
496    ) -> Result<PeerIdentity, PeerError> {
497        let mut pid = 0u32;
498        // SAFETY: as above, for a client pipe handle.
499        unsafe { GetNamedPipeServerProcessId(HANDLE(handle), &mut pid) }
500            .map_err(|e| PeerError::Query(win_err(e)))?;
501        verify_pid(pid)
502    }
503
504    /// A security descriptor granting `GENERIC_ALL` to the current user and nobody else,
505    /// plus a `SECURITY_ATTRIBUTES` that points at it.
506    ///
507    /// SDDL: `D:P(A;;GA;;;<current user SID>)`, built from the process token's real SID
508    /// (never a literal such as `OW` or `BA`). The DACL is protected (`P`), so it inherits
509    /// nothing, and `bInheritHandle` is false.
510    ///
511    /// # Lifetime
512    ///
513    /// The descriptor is `LocalAlloc`ed and freed when this guard drops. The pointer from
514    /// [`as_ptr`](Self::as_ptr) is valid exactly as long as the guard is alive (the
515    /// attributes live on the heap, so moving the guard does not invalidate it). Windows
516    /// copies the descriptor into the kernel object inside `CreateNamedPipeW`,
517    /// `CreateMutexW`, `CreateEventW` and similar calls, so a single create call needs the
518    /// guard only until it returns. Keep the guard alive across every create call that
519    /// uses it (for example each new pipe instance).
520    pub struct OwnerOnlySecurityAttributes {
521        attrs: Box<SECURITY_ATTRIBUTES>,
522        sd: PSECURITY_DESCRIPTOR,
523        sddl: String,
524    }
525
526    // SAFETY: the descriptor and attributes are written once at construction and only
527    // read afterwards.
528    unsafe impl Send for OwnerOnlySecurityAttributes {}
529    unsafe impl Sync for OwnerOnlySecurityAttributes {}
530
531    impl OwnerOnlySecurityAttributes {
532        /// Pointer to the `SECURITY_ATTRIBUTES`, for the `lpSecurityAttributes` argument of
533        /// `CreateNamedPipeW`, `CreateMutexW`, `CreateEventW`, ... Cast it to the
534        /// `*const SECURITY_ATTRIBUTES` of whichever Windows binding crate you use. Valid
535        /// while `self` is alive; null is never returned.
536        pub fn as_ptr(&self) -> *const core::ffi::c_void {
537            &*self.attrs as *const SECURITY_ATTRIBUTES as *const core::ffi::c_void
538        }
539
540        /// The SDDL string the descriptor was built from.
541        pub fn sddl(&self) -> &str {
542            &self.sddl
543        }
544
545        pub(crate) fn raw(&self) -> &SECURITY_ATTRIBUTES {
546            &self.attrs
547        }
548    }
549
550    impl Drop for OwnerOnlySecurityAttributes {
551        fn drop(&mut self) {
552            unsafe {
553                let _ = LocalFree(Some(HLOCAL(self.sd.0)));
554            }
555        }
556    }
557
558    /// Build owner-only [`OwnerOnlySecurityAttributes`] for the current user. Usable for
559    /// named pipes, mutexes and events.
560    pub fn owner_only_security_attributes() -> io::Result<OwnerOnlySecurityAttributes> {
561        let sddl = sddl_for_sid(&current_user_sid_string()?);
562        let wide: Vec<u16> = sddl.encode_utf16().chain(std::iter::once(0)).collect();
563        let mut sd = PSECURITY_DESCRIPTOR::default();
564        unsafe {
565            ConvertStringSecurityDescriptorToSecurityDescriptorW(
566                windows::core::PCWSTR(wide.as_ptr()),
567                SDDL_REVISION_1,
568                &mut sd,
569                None,
570            )
571            .map_err(win_err)?;
572        }
573        let attrs = Box::new(SECURITY_ATTRIBUTES {
574            nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
575            lpSecurityDescriptor: sd.0,
576            bInheritHandle: false.into(),
577        });
578        Ok(OwnerOnlySecurityAttributes { attrs, sd, sddl })
579    }
580}
581
582#[cfg(all(windows, test))]
583pub(crate) use win::current_user_sid_string;
584#[cfg(windows)]
585pub use win::{
586    owner_only_security_attributes, verify_pipe_peer_is_current_user,
587    verify_pipe_server_is_current_user, OwnerOnlySecurityAttributes,
588};
589
590/// The SID string (`S-1-...`) of the user running this process, read from the process
591/// token. For callers building their own owner-only DACLs or comparing a peer's
592/// [`PeerIdentity::sid`] against the current user outside a pipe check.
593#[cfg(windows)]
594pub fn current_user_sid() -> io::Result<String> {
595    win::current_user_sid_string()
596}
597
598#[cfg(test)]
599mod tests {
600    use super::*;
601
602    #[test]
603    fn sddl_is_built_from_the_given_sid_with_a_protected_single_ace_dacl() {
604        assert_eq!(
605            sddl_for_sid("S-1-5-21-1-2-3-1001"),
606            "D:P(A;;GA;;;S-1-5-21-1-2-3-1001)"
607        );
608    }
609
610    #[test]
611    fn peer_error_maps_to_typed_io_kinds() {
612        let k = |e: PeerError| io::Error::from(e).kind();
613        assert_eq!(k(PeerError::Unsupported("x")), io::ErrorKind::Unsupported);
614        assert_eq!(
615            k(PeerError::Mismatch(PeerIdentity::default())),
616            io::ErrorKind::PermissionDenied
617        );
618    }
619
620    #[test]
621    fn a_vanished_peer_is_not_an_attack_but_a_mismatch_always_is() {
622        assert!(PeerError::Query(io::Error::from(io::ErrorKind::NotConnected)).peer_gone());
623        assert!(!PeerError::Query(io::Error::from(io::ErrorKind::PermissionDenied)).peer_gone());
624        assert!(!PeerError::Mismatch(PeerIdentity::default()).peer_gone());
625        assert!(!PeerError::Unsupported("x").peer_gone());
626    }
627
628    #[cfg(unix)]
629    mod unix_tests {
630        use super::*;
631        use std::os::unix::net::UnixStream;
632
633        #[test]
634        fn same_user_socketpair_is_accepted_with_our_uid() {
635            let (a, b) = UnixStream::pair().unwrap();
636            for s in [&a, &b] {
637                match verify_peer_is_current_user(s) {
638                    Ok(id) => assert_eq!(id.uid, Some(current_euid())),
639                    // A Unix with no credential query must refuse, never accept.
640                    Err(PeerError::Unsupported(_)) => {}
641                    Err(e) => panic!("same-user socketpair rejected: {e}"),
642                }
643            }
644        }
645
646        #[test]
647        fn a_different_uid_is_a_mismatch_and_an_unknown_uid_fails_closed() {
648            let me = current_euid();
649            let other = PeerIdentity {
650                uid: Some(me.wrapping_add(1)),
651                ..Default::default()
652            };
653            assert!(matches!(
654                super::super::unix::accept_uid(other, me),
655                Err(PeerError::Mismatch(id)) if id.uid == Some(me.wrapping_add(1))
656            ));
657            assert!(matches!(
658                super::super::unix::accept_uid(PeerIdentity::default(), me),
659                Err(PeerError::Mismatch(_))
660            ));
661            assert!(super::super::unix::accept_uid(
662                PeerIdentity {
663                    uid: Some(me),
664                    ..Default::default()
665                },
666                me
667            )
668            .is_ok());
669        }
670
671        // Cross-user (a real second account on the other end of a socket) cannot be
672        // exercised without privileges: it needs root to `setuid` a helper, or a second
673        // account. That branch is covered by the `accept_uid` test above, which feeds the
674        // decision function a foreign uid. Run a manual check as root with:
675        //   sudo -u nobody <test binary> --ignored cross_user
676        #[test]
677        #[ignore = "needs a second OS account; see comment"]
678        fn cross_user_socket_is_rejected() {
679            let path = std::env::var("RK_SERVICE_FOREIGN_SOCKET")
680                .expect("set RK_SERVICE_FOREIGN_SOCKET to a socket served by another user");
681            let s = UnixStream::connect(path).unwrap();
682            assert!(matches!(
683                verify_peer_is_current_user(&s),
684                Err(PeerError::Mismatch(_))
685            ));
686        }
687    }
688
689    #[cfg(windows)]
690    mod windows_tests {
691        use super::*;
692
693        #[test]
694        fn the_public_sid_helper_reports_this_processs_user() {
695            let sid = current_user_sid().unwrap();
696            assert!(sid.starts_with("S-1-"), "{sid}");
697            assert_eq!(sid, current_user_sid_string().unwrap());
698        }
699
700        #[test]
701        fn owner_only_attributes_use_the_real_current_sid() {
702            let sid = current_user_sid_string().unwrap();
703            assert!(sid.starts_with("S-1-"), "{sid}");
704            let a = owner_only_security_attributes().unwrap();
705            assert_eq!(a.sddl(), format!("D:P(A;;GA;;;{sid})"));
706            assert!(!a.as_ptr().is_null());
707            assert!(!a.raw().lpSecurityDescriptor.is_null());
708            assert!(!a.raw().bInheritHandle.as_bool());
709        }
710    }
711}