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