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;
14use std::path::PathBuf;
15
16/// Who the kernel says is on the other end of a connection.
17#[derive(Debug, Clone, PartialEq, Eq, Default)]
18#[non_exhaustive]
19pub struct PeerIdentity {
20    /// Unix: the peer's effective uid.
21    pub uid: Option<u32>,
22    /// Peer process id when the platform reports it (Linux, macOS, Windows; not the BSDs).
23    pub pid: Option<u32>,
24    /// Windows: the peer process token's user SID in `S-1-...` form.
25    pub sid: Option<String>,
26    /// macOS: the peer's audit token (`audit_token_t`, eight words: pid at index 5, pid version
27    /// at index 7) as the kernel captured it when the connection was made (`LOCAL_PEERTOKEN`).
28    /// The race-free handle for a code-signature check of the peer. `None` elsewhere, or when
29    /// the kernel did not provide one.
30    pub audit_token: Option<[u32; 8]>,
31    /// The peer process's start marker as read when the connection was accepted (Unix: start
32    /// time from the OS; Windows: creation time). Pins the pid to one process, so a pid reused
33    /// by another process after the peer exited is told apart at the server-side executable
34    /// check. `None` when it could not be read; that check then fails closed.
35    pub started: Option<(u64, u64)>,
36}
37
38/// Why a peer was not accepted as the current user.
39#[derive(Debug)]
40#[non_exhaustive]
41pub enum PeerError {
42    /// This platform cannot authenticate the peer, so no peer is accepted.
43    Unsupported(&'static str),
44    /// The peer is a different user.
45    Mismatch(PeerIdentity),
46    /// The OS could not tell who the peer is (including a peer that already left).
47    Query(io::Error),
48    /// The peer process was found but the OS would not let this process read its identity. It
49    /// carries what is known about the process so a host can say which program was refused
50    /// (rightkit#1287). On Windows this is the usual answer for a client at a higher integrity
51    /// level (an elevated process) than a medium-integrity server: the pid and image path are
52    /// still readable ([`PeerFailureKind::Elevated`]) and the refusal is right, only the missing
53    /// identity was the problem.
54    Unreadable {
55        /// The peer's process id, when the kernel reported one.
56        pid: Option<u32>,
57        /// The peer's executable path, when it could be read (Windows `QueryFullProcessImageNameW`
58        /// works with `PROCESS_QUERY_LIMITED_INFORMATION` even for an elevated client).
59        image: Option<PathBuf>,
60        /// A stable kind a host can switch on instead of parsing text.
61        kind: PeerFailureKind,
62        /// The OS error.
63        source: io::Error,
64    },
65}
66
67/// Why a peer could not be read, as a stable kind (see [`PeerError::Unreadable`]).
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
69#[non_exhaustive]
70pub enum PeerFailureKind {
71    /// The OS refused access to the peer's token: the client runs at a higher integrity level
72    /// than this host (typically elevated). Tell the user to run the client without
73    /// administrator rights, or the host elevated too.
74    Elevated,
75    /// Any other failure to read the peer.
76    Other,
77}
78
79impl PeerFailureKind {
80    /// A stable lowercase name for logs and events.
81    pub fn as_str(self) -> &'static str {
82        match self {
83            PeerFailureKind::Elevated => "elevated",
84            PeerFailureKind::Other => "other",
85        }
86    }
87}
88
89/// A peer that could not be identified, as a plain value a [`ServiceEvent`](crate::ServiceEvent)
90/// or a log line can carry (the OS error is flattened to text).
91#[derive(Debug, Clone, PartialEq, Eq)]
92#[non_exhaustive]
93pub struct PeerRefusal {
94    /// The peer's process id, when known.
95    pub pid: Option<u32>,
96    /// The peer's executable path, when it could be read.
97    pub image: Option<PathBuf>,
98    /// The stable kind.
99    pub kind: PeerFailureKind,
100    /// The OS error text.
101    pub reason: String,
102}
103
104impl fmt::Display for PeerError {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        match self {
107            PeerError::Unsupported(why) => write!(f, "peer authentication unsupported: {why}"),
108            PeerError::Mismatch(id) => {
109                write!(f, "peer is not the current user")?;
110                if let Some(uid) = id.uid {
111                    write!(f, " (uid {uid})")?;
112                }
113                if let Some(pid) = id.pid {
114                    write!(f, " (pid {pid})")?;
115                }
116                Ok(())
117            }
118            PeerError::Query(e) => write!(f, "could not identify peer: {e}"),
119            PeerError::Unreadable {
120                pid,
121                image,
122                kind,
123                source,
124            } => {
125                write!(f, "could not identify peer")?;
126                if let Some(pid) = pid {
127                    write!(f, " (pid {pid})")?;
128                }
129                if let Some(image) = image {
130                    write!(f, " ({})", image.display())?;
131                }
132                if *kind == PeerFailureKind::Elevated {
133                    write!(
134                        f,
135                        " (the client runs at a higher integrity level, for example elevated)"
136                    )?;
137                }
138                write!(f, ": {source}")
139            }
140        }
141    }
142}
143
144impl std::error::Error for PeerError {
145    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
146        match self {
147            PeerError::Query(e) => Some(e),
148            PeerError::Unreadable { source, .. } => Some(source),
149            _ => None,
150        }
151    }
152}
153
154impl From<PeerError> for io::Error {
155    fn from(e: PeerError) -> Self {
156        let kind = match &e {
157            PeerError::Unsupported(_) => io::ErrorKind::Unsupported,
158            PeerError::Mismatch(_) => io::ErrorKind::PermissionDenied,
159            PeerError::Query(q) => q.kind(),
160            PeerError::Unreadable { source, .. } => source.kind(),
161        };
162        io::Error::new(kind, e.to_string())
163    }
164}
165
166impl PeerError {
167    /// The peer's pid, when the error knows it.
168    pub fn pid(&self) -> Option<u32> {
169        match self {
170            PeerError::Mismatch(id) => id.pid,
171            PeerError::Unreadable { pid, .. } => *pid,
172            _ => None,
173        }
174    }
175
176    /// The peer's executable path, when the error carries it.
177    pub fn image(&self) -> Option<&std::path::Path> {
178        match self {
179            PeerError::Unreadable { image, .. } => image.as_deref(),
180            _ => None,
181        }
182    }
183
184    /// The stable kind of an unreadable peer; `None` for the other errors.
185    pub fn failure_kind(&self) -> Option<PeerFailureKind> {
186        match self {
187            PeerError::Unreadable { kind, .. } => Some(*kind),
188            _ => None,
189        }
190    }
191
192    /// This error as a [`PeerRefusal`] when it is an unreadable peer (the server's rejection
193    /// event carries it).
194    pub(crate) fn refusal(&self) -> Option<PeerRefusal> {
195        match self {
196            PeerError::Unreadable {
197                pid,
198                image,
199                kind,
200                source,
201            } => Some(PeerRefusal {
202                pid: *pid,
203                image: image.clone(),
204                kind: *kind,
205                reason: source.to_string(),
206            }),
207            _ => None,
208        }
209    }
210
211    /// The peer disconnected before it could be identified (a probe, or `wake`).
212    /// Not an attack; the connection is still dropped, but no rejection is logged.
213    pub(crate) fn peer_gone(&self) -> bool {
214        let e = match self {
215            PeerError::Query(e) => e,
216            PeerError::Unreadable { source, .. } => source,
217            _ => return false,
218        };
219        if matches!(
220            e.kind(),
221            io::ErrorKind::NotConnected
222                | io::ErrorKind::BrokenPipe
223                | io::ErrorKind::ConnectionReset
224                | io::ErrorKind::UnexpectedEof
225        ) {
226            return true;
227        }
228        // ERROR_BROKEN_PIPE 109, ERROR_NO_DATA 232, ERROR_PIPE_NOT_CONNECTED 233.
229        cfg!(windows) && matches!(e.raw_os_error(), Some(109 | 232 | 233))
230    }
231}
232
233/// `GENERIC_ALL` as an SDDL access mask.
234pub const ACCESS_GENERIC_ALL: u32 = 0x1000_0000;
235/// The restricted mask an elevated service grants one interactive owner (rightkit#1042): read and
236/// write data and attributes, `READ_CONTROL` and `SYNCHRONIZE`, and no delete, write-DAC or
237/// execute (`0x12019b`, the mask Pulse's `winadmin` service uses).
238pub const ACCESS_PIPE_OWNER_RW: u32 = 0x0012_019b;
239
240/// One principal of a pipe DACL: a SID string and an access mask (rightkit#1042).
241#[derive(Debug, Clone, PartialEq, Eq)]
242pub struct AclEntry {
243    /// The SID in `S-1-...` form, or a two-letter SDDL alias such as `SY`. Only ASCII letters,
244    /// digits and `-` are accepted, so a value can never add an ACE or end the descriptor.
245    pub sid: String,
246    /// The access mask granted (see [`ACCESS_GENERIC_ALL`], [`ACCESS_PIPE_OWNER_RW`]).
247    pub mask: u32,
248}
249
250impl AclEntry {
251    pub fn new(sid: impl Into<String>, mask: u32) -> Self {
252        Self {
253            sid: sid.into(),
254            mask,
255        }
256    }
257
258    /// `GENERIC_ALL` for `sid`.
259    pub fn full_control(sid: impl Into<String>) -> Self {
260        Self::new(sid, ACCESS_GENERIC_ALL)
261    }
262
263    #[cfg_attr(not(windows), allow(dead_code))]
264    fn valid(&self) -> bool {
265        !self.sid.is_empty()
266            && self.sid.len() <= 184
267            && self
268                .sid
269                .bytes()
270                .all(|b| b.is_ascii_alphanumeric() || b == b'-')
271    }
272}
273
274/// A mandatory integrity label for a pipe (rightkit#1123): the label is the level below which a
275/// process cannot write to the object. A confined (AppContainer) child needs the label lowered.
276#[derive(Debug, Clone, Copy, PartialEq, Eq)]
277#[non_exhaustive]
278pub enum IntegrityLabel {
279    Untrusted,
280    Low,
281    Medium,
282    High,
283}
284
285impl IntegrityLabel {
286    #[cfg_attr(not(windows), allow(dead_code))]
287    fn sid(self) -> &'static str {
288        match self {
289            IntegrityLabel::Untrusted => "S-1-16-0",
290            IntegrityLabel::Low => "S-1-16-4096",
291            IntegrityLabel::Medium => "S-1-16-8192",
292            IntegrityLabel::High => "S-1-16-12288",
293        }
294    }
295}
296
297/// The SDDL for `entries` (a protected DACL, one ACE each) and an optional label with the
298/// no-write-up policy. `None` when `entries` is empty or an entry's SID is not plain.
299#[cfg_attr(not(windows), allow(dead_code))]
300pub(crate) fn sddl_for_entries(
301    entries: &[AclEntry],
302    label: Option<IntegrityLabel>,
303) -> Option<String> {
304    if entries.is_empty() || !entries.iter().all(AclEntry::valid) {
305        return None;
306    }
307    let mut out = String::from("D:P");
308    for e in entries {
309        out.push_str(&format!("(A;;0x{:x};;;{})", e.mask, e.sid));
310    }
311    if let Some(label) = label {
312        out.push_str(&format!("S:(ML;;NW;;;{})", label.sid()));
313    }
314    Some(out)
315}
316
317/// `D:P(A;;GA;;;<sid>)`: protected DACL (no inheritance), one ACE granting
318/// GENERIC_ALL to exactly this SID.
319#[cfg(windows)]
320pub(crate) fn sddl_for_sid(sid: &str) -> String {
321    format!("D:P(A;;GA;;;{sid})")
322}
323
324// ---------------------------------------------------------------------------
325// Unix
326// ---------------------------------------------------------------------------
327
328#[cfg(unix)]
329mod unix {
330    use super::*;
331    use std::os::fd::AsRawFd;
332    use std::os::unix::net::UnixStream;
333
334    extern "C" {
335        fn geteuid() -> u32;
336    }
337
338    #[cfg(any(
339        target_os = "macos",
340        target_os = "ios",
341        target_os = "freebsd",
342        target_os = "openbsd",
343        target_os = "netbsd",
344        target_os = "dragonfly"
345    ))]
346    extern "C" {
347        fn getpeereid(fd: i32, euid: *mut u32, egid: *mut u32) -> i32;
348    }
349
350    // Linux/Android generic ABI: SOL_SOCKET = 1, SO_PEERCRED = 17. The mips, powerpc
351    // and sparc families number these differently and are deliberately left Unsupported
352    // rather than guessed.
353    #[cfg(all(
354        any(target_os = "linux", target_os = "android"),
355        not(any(
356            target_arch = "mips",
357            target_arch = "mips32r6",
358            target_arch = "mips64",
359            target_arch = "mips64r6",
360            target_arch = "powerpc",
361            target_arch = "powerpc64",
362            target_arch = "sparc",
363            target_arch = "sparc64"
364        ))
365    ))]
366    mod linux {
367        pub const SOL_SOCKET: i32 = 1;
368        pub const SO_PEERCRED: i32 = 17;
369        #[repr(C)]
370        #[derive(Default)]
371        pub struct Ucred {
372            pub pid: i32,
373            pub uid: u32,
374            pub gid: u32,
375        }
376        extern "C" {
377            pub fn getsockopt(
378                fd: i32,
379                level: i32,
380                name: i32,
381                val: *mut core::ffi::c_void,
382                len: *mut u32,
383            ) -> i32;
384        }
385    }
386
387    pub(crate) fn current_euid() -> u32 {
388        unsafe { geteuid() }
389    }
390
391    #[cfg(any(
392        target_os = "macos",
393        target_os = "ios",
394        target_os = "freebsd",
395        target_os = "openbsd",
396        target_os = "netbsd",
397        target_os = "dragonfly"
398    ))]
399    fn peer_identity(s: &UnixStream) -> Result<PeerIdentity, PeerError> {
400        let (mut uid, mut gid) = (u32::MAX, u32::MAX);
401        // SAFETY: valid fd borrowed from `s`; out-pointers are valid locals.
402        let rc = unsafe { getpeereid(s.as_raw_fd(), &mut uid, &mut gid) };
403        if rc != 0 {
404            return Err(PeerError::Query(io::Error::last_os_error()));
405        }
406        Ok(PeerIdentity {
407            uid: Some(uid),
408            pid: peer_pid(s),
409            sid: None,
410            audit_token: crate::host_identity::peer_audit_token(s),
411            started: peer_pid(s).and_then(crate::host_identity::process_start_marker),
412        })
413    }
414
415    /// The peer's process id from the `LOCAL_PEERPID` socket option. `None` when the option
416    /// fails: the peer is still authenticated by its uid, the pid is only extra detail.
417    #[cfg(any(target_os = "macos", target_os = "ios"))]
418    fn peer_pid(s: &UnixStream) -> Option<u32> {
419        const SOL_LOCAL: i32 = 0;
420        const LOCAL_PEERPID: i32 = 0x002;
421        extern "C" {
422            fn getsockopt(
423                fd: i32,
424                level: i32,
425                name: i32,
426                val: *mut core::ffi::c_void,
427                len: *mut u32,
428            ) -> i32;
429        }
430        let mut pid: i32 = 0;
431        let mut len = std::mem::size_of::<i32>() as u32;
432        // SAFETY: valid fd borrowed from `s`; `pid` and `len` are valid locals of the size
433        // the option writes (`pid_t`, `socklen_t`).
434        let rc = unsafe {
435            getsockopt(
436                s.as_raw_fd(),
437                SOL_LOCAL,
438                LOCAL_PEERPID,
439                (&mut pid as *mut i32).cast(),
440                &mut len,
441            )
442        };
443        if rc != 0 {
444            return None;
445        }
446        u32::try_from(pid).ok().filter(|p| *p != 0)
447    }
448
449    /// The BSDs have no peer-pid socket option of this shape.
450    #[cfg(any(
451        target_os = "freebsd",
452        target_os = "openbsd",
453        target_os = "netbsd",
454        target_os = "dragonfly"
455    ))]
456    fn peer_pid(_s: &UnixStream) -> Option<u32> {
457        None
458    }
459
460    #[cfg(all(
461        any(target_os = "linux", target_os = "android"),
462        not(any(
463            target_arch = "mips",
464            target_arch = "mips32r6",
465            target_arch = "mips64",
466            target_arch = "mips64r6",
467            target_arch = "powerpc",
468            target_arch = "powerpc64",
469            target_arch = "sparc",
470            target_arch = "sparc64"
471        ))
472    ))]
473    fn peer_identity(s: &UnixStream) -> Result<PeerIdentity, PeerError> {
474        let mut cred = linux::Ucred::default();
475        let mut len = std::mem::size_of::<linux::Ucred>() as u32;
476        // SAFETY: valid fd; `cred` and `len` describe a writable ucred.
477        let rc = unsafe {
478            linux::getsockopt(
479                s.as_raw_fd(),
480                linux::SOL_SOCKET,
481                linux::SO_PEERCRED,
482                &mut cred as *mut linux::Ucred as *mut core::ffi::c_void,
483                &mut len,
484            )
485        };
486        if rc != 0 {
487            return Err(PeerError::Query(io::Error::last_os_error()));
488        }
489        if (len as usize) < std::mem::size_of::<linux::Ucred>() {
490            return Err(PeerError::Query(io::Error::new(
491                io::ErrorKind::InvalidData,
492                "short SO_PEERCRED reply",
493            )));
494        }
495        Ok(PeerIdentity {
496            uid: Some(cred.uid),
497            pid: u32::try_from(cred.pid).ok().filter(|p| *p != 0),
498            sid: None,
499            audit_token: None,
500            started: u32::try_from(cred.pid)
501                .ok()
502                .filter(|p| *p != 0)
503                .and_then(crate::host_identity::process_start_marker),
504        })
505    }
506
507    #[cfg(not(any(
508        target_os = "macos",
509        target_os = "ios",
510        target_os = "freebsd",
511        target_os = "openbsd",
512        target_os = "netbsd",
513        target_os = "dragonfly",
514        all(
515            any(target_os = "linux", target_os = "android"),
516            not(any(
517                target_arch = "mips",
518                target_arch = "mips32r6",
519                target_arch = "mips64",
520                target_arch = "mips64r6",
521                target_arch = "powerpc",
522                target_arch = "powerpc64",
523                target_arch = "sparc",
524                target_arch = "sparc64"
525            ))
526        )
527    )))]
528    fn peer_identity(_s: &UnixStream) -> Result<PeerIdentity, PeerError> {
529        Err(PeerError::Unsupported(
530            "no peer-credential query is implemented for this Unix platform",
531        ))
532    }
533
534    /// The pure decision: the peer is accepted only when its uid is known and equals `me`.
535    pub(crate) fn accept_uid(id: PeerIdentity, me: u32) -> Result<PeerIdentity, PeerError> {
536        if id.uid == Some(me) {
537            Ok(id)
538        } else {
539            Err(PeerError::Mismatch(id))
540        }
541    }
542
543    /// Verify the peer of a connected Unix stream is the current user.
544    ///
545    /// macOS and the BSDs use `getpeereid`; Linux and Android use `SO_PEERCRED`. The
546    /// peer's effective uid must equal this process's `geteuid`. Any other Unix
547    /// returns [`PeerError::Unsupported`]: the answer is never `Ok` on a platform that
548    /// cannot check.
549    ///
550    /// The credentials are those the kernel captured when the connection was made, so a
551    /// peer that connects and then exits is still identified correctly.
552    pub fn verify_peer_is_current_user(stream: &UnixStream) -> Result<PeerIdentity, PeerError> {
553        accept_uid(peer_identity(stream)?, current_euid())
554    }
555}
556
557#[cfg(unix)]
558pub(crate) use unix::current_euid;
559#[cfg(unix)]
560pub use unix::verify_peer_is_current_user;
561
562// ---------------------------------------------------------------------------
563// Windows
564// ---------------------------------------------------------------------------
565
566#[cfg(windows)]
567mod win {
568    use super::*;
569    use std::os::windows::ffi::OsStringExt;
570    use std::os::windows::io::RawHandle;
571    use windows::core::PWSTR;
572    use windows::Win32::Foundation::{CloseHandle, LocalFree, ERROR_ACCESS_DENIED, HANDLE, HLOCAL};
573    use windows::Win32::Security::Authorization::{
574        ConvertSidToStringSidW, ConvertStringSecurityDescriptorToSecurityDescriptorW,
575        SDDL_REVISION_1,
576    };
577    use windows::Win32::Security::{
578        GetTokenInformation, TokenUser, PSECURITY_DESCRIPTOR, PSID, SECURITY_ATTRIBUTES,
579        TOKEN_QUERY, TOKEN_USER,
580    };
581    use windows::Win32::System::Pipes::{GetNamedPipeClientProcessId, GetNamedPipeServerProcessId};
582    use windows::Win32::System::Threading::{
583        GetCurrentProcess, OpenProcess, OpenProcessToken, QueryFullProcessImageNameW,
584        PROCESS_NAME_WIN32, PROCESS_QUERY_LIMITED_INFORMATION,
585    };
586
587    /// Win32 error as an `io::Error` that keeps the code, so `peer_gone` can see it.
588    fn win_err(e: windows::core::Error) -> io::Error {
589        io::Error::from_raw_os_error((e.code().0 as u32 & 0xFFFF) as i32)
590    }
591
592    struct Owned(HANDLE);
593    impl Drop for Owned {
594        fn drop(&mut self) {
595            unsafe {
596                let _ = CloseHandle(self.0);
597            }
598        }
599    }
600
601    fn sid_to_string(sid: PSID) -> io::Result<String> {
602        unsafe {
603            let mut p = PWSTR::null();
604            ConvertSidToStringSidW(sid, &mut p).map_err(win_err)?;
605            let s = p.to_string().map_err(io::Error::other);
606            let _ = LocalFree(Some(HLOCAL(p.0 as *mut _)));
607            s
608        }
609    }
610
611    /// The user SID string of an access token opened with TOKEN_QUERY.
612    fn token_user_sid(tok: HANDLE) -> io::Result<String> {
613        unsafe {
614            let mut need = 0u32;
615            let _ = GetTokenInformation(tok, TokenUser, None, 0, &mut need);
616            if need == 0 {
617                return Err(io::Error::other("token user size query returned 0"));
618            }
619            // u64 storage keeps the TOKEN_USER header pointer-aligned.
620            let mut buf = vec![0u64; (need as usize).div_ceil(8)];
621            GetTokenInformation(
622                tok,
623                TokenUser,
624                Some(buf.as_mut_ptr() as *mut _),
625                (buf.len() * 8) as u32,
626                &mut need,
627            )
628            .map_err(win_err)?;
629            let user = &*(buf.as_ptr() as *const TOKEN_USER);
630            sid_to_string(user.User.Sid)
631        }
632    }
633
634    /// The SID string of the user running this process.
635    pub(crate) fn current_user_sid_string() -> io::Result<String> {
636        unsafe {
637            let mut tok = HANDLE::default();
638            OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut tok).map_err(win_err)?;
639            let tok = Owned(tok);
640            token_user_sid(tok.0)
641        }
642    }
643
644    /// The executable path of an open process, `None` when it cannot be read.
645    fn process_image(proc: HANDLE) -> Option<PathBuf> {
646        let mut buf = vec![0u16; 32768];
647        let mut len = buf.len() as u32;
648        // SAFETY: `buf` holds `len` UTF-16 units; the handle is open for the call.
649        unsafe {
650            QueryFullProcessImageNameW(proc, PROCESS_NAME_WIN32, PWSTR(buf.as_mut_ptr()), &mut len)
651        }
652        .ok()?;
653        (len > 0).then(|| PathBuf::from(std::ffi::OsString::from_wide(&buf[..len as usize])))
654    }
655
656    /// `ERROR_ACCESS_DENIED` as the raw OS code `win_err` keeps (the low 16 bits).
657    fn is_access_denied(e: &io::Error) -> bool {
658        e.raw_os_error() == Some(ERROR_ACCESS_DENIED.0 as i32)
659    }
660
661    /// The SID string of the user running process `pid`. Fails closed: a process that
662    /// cannot be opened or queried yields an error, never a guess. A process that opens but
663    /// whose token is refused (a client at a higher integrity level) is
664    /// [`PeerError::Unreadable`] with its pid and image path (rightkit#1287).
665    fn process_user_sid_string(pid: u32) -> Result<String, PeerError> {
666        unsafe {
667            let proc = Owned(
668                OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
669                    .map_err(|e| unreadable(pid, None, win_err(e)))?,
670            );
671            let mut tok = HANDLE::default();
672            if let Err(e) = OpenProcessToken(proc.0, TOKEN_QUERY, &mut tok) {
673                return Err(unreadable(pid, process_image(proc.0), win_err(e)));
674            }
675            let tok = Owned(tok);
676            token_user_sid(tok.0).map_err(|e| unreadable(pid, process_image(proc.0), e))
677        }
678    }
679
680    fn unreadable(pid: u32, image: Option<PathBuf>, source: io::Error) -> PeerError {
681        let kind = if is_access_denied(&source) {
682            PeerFailureKind::Elevated
683        } else {
684            PeerFailureKind::Other
685        };
686        PeerError::Unreadable {
687            pid: Some(pid),
688            image,
689            kind,
690            source,
691        }
692    }
693
694    /// Compare a peer process's user SID with `expected` (a canonical `S-1-...` string).
695    fn verify_pid_against(pid: u32, expected: &str) -> Result<PeerIdentity, PeerError> {
696        if pid == 0 {
697            return Err(PeerError::Query(io::Error::other("peer process id is 0")));
698        }
699        let theirs = process_user_sid_string(pid)?;
700        let id = PeerIdentity {
701            uid: None,
702            pid: Some(pid),
703            sid: Some(theirs.clone()),
704            audit_token: None,
705            started: crate::host_identity::process_start_marker(pid),
706        };
707        // SID strings are canonical (`S-1-5-21-...`), so equality of the strings is
708        // equality of the SIDs.
709        if theirs.eq_ignore_ascii_case(expected) {
710            Ok(id)
711        } else {
712            Err(PeerError::Mismatch(id))
713        }
714    }
715
716    /// Compare a peer process's user SID with ours.
717    fn verify_pid(pid: u32) -> Result<PeerIdentity, PeerError> {
718        let me = current_user_sid_string().map_err(PeerError::Query)?;
719        verify_pid_against(pid, &me)
720    }
721
722    /// Server side: verify that the client connected to `handle` (a server pipe instance
723    /// on which `ConnectNamedPipe` has completed) runs as the current user.
724    ///
725    /// `GetNamedPipeClientProcessId`, then the process token's user SID against ours. Call
726    /// it before reading any request byte. Fails closed: a client that cannot be opened or
727    /// queried is rejected. The pid is the one the kernel recorded at connect time; the
728    /// pipe DACL (see [`owner_only_security_attributes`]) is the primary boundary and this
729    /// is the independent second check.
730    ///
731    /// `handle` must be a valid, connected named-pipe server handle for the duration of
732    /// the call.
733    pub fn verify_pipe_peer_is_current_user(handle: RawHandle) -> Result<PeerIdentity, PeerError> {
734        let mut pid = 0u32;
735        // SAFETY: the caller guarantees a valid pipe handle; `pid` is a valid out-pointer.
736        unsafe { GetNamedPipeClientProcessId(HANDLE(handle), &mut pid) }
737            .map_err(|e| PeerError::Query(win_err(e)))?;
738        verify_pid(pid)
739    }
740
741    /// Server side, for a service that does not run as the user it serves (an elevated service
742    /// serving one interactive owner, rightkit#1042): like [`verify_pipe_peer_is_current_user`],
743    /// but the client's process-token user must equal `sid` (an `S-1-...` string), not the
744    /// current user's SID, which for a SYSTEM service would refuse the owner. Same fail-closed
745    /// rules: a client that cannot be opened or queried is rejected.
746    ///
747    /// `handle` must be a valid, connected named-pipe server handle for the duration of the
748    /// call.
749    pub fn verify_pipe_peer_is_sid(
750        handle: RawHandle,
751        sid: &str,
752    ) -> Result<PeerIdentity, PeerError> {
753        if sid.is_empty() {
754            return Err(PeerError::Query(io::Error::other(
755                "no SID to compare the peer with",
756            )));
757        }
758        let mut pid = 0u32;
759        // SAFETY: the caller guarantees a valid pipe handle; `pid` is a valid out-pointer.
760        unsafe { GetNamedPipeClientProcessId(HANDLE(handle), &mut pid) }
761            .map_err(|e| PeerError::Query(win_err(e)))?;
762        verify_pid_against(pid, sid)
763    }
764
765    /// Client side: verify that the server behind `handle` (a pipe opened with
766    /// `CreateFile`) runs as the current user, via `GetNamedPipeServerProcessId`. Catches
767    /// a different user squatting on the pipe name. Open the pipe with
768    /// `SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION` as well, so the server can
769    /// identify but never impersonate the caller.
770    pub fn verify_pipe_server_is_current_user(
771        handle: RawHandle,
772    ) -> Result<PeerIdentity, PeerError> {
773        let mut pid = 0u32;
774        // SAFETY: as above, for a client pipe handle.
775        unsafe { GetNamedPipeServerProcessId(HANDLE(handle), &mut pid) }
776            .map_err(|e| PeerError::Query(win_err(e)))?;
777        verify_pid(pid)
778    }
779
780    /// A security descriptor granting `GENERIC_ALL` to the current user and nobody else,
781    /// plus a `SECURITY_ATTRIBUTES` that points at it.
782    ///
783    /// SDDL: `D:P(A;;GA;;;<current user SID>)`, built from the process token's real SID
784    /// (never a literal such as `OW` or `BA`). The DACL is protected (`P`), so it inherits
785    /// nothing, and `bInheritHandle` is false.
786    ///
787    /// # Lifetime
788    ///
789    /// The descriptor is `LocalAlloc`ed and freed when this guard drops. The pointer from
790    /// [`as_ptr`](Self::as_ptr) is valid exactly as long as the guard is alive (the
791    /// attributes live on the heap, so moving the guard does not invalidate it). Windows
792    /// copies the descriptor into the kernel object inside `CreateNamedPipeW`,
793    /// `CreateMutexW`, `CreateEventW` and similar calls, so a single create call needs the
794    /// guard only until it returns. Keep the guard alive across every create call that
795    /// uses it (for example each new pipe instance).
796    pub struct OwnerOnlySecurityAttributes {
797        attrs: Box<SECURITY_ATTRIBUTES>,
798        sd: PSECURITY_DESCRIPTOR,
799        sddl: String,
800    }
801
802    // SAFETY: the descriptor and attributes are written once at construction and only
803    // read afterwards.
804    unsafe impl Send for OwnerOnlySecurityAttributes {}
805    unsafe impl Sync for OwnerOnlySecurityAttributes {}
806
807    impl OwnerOnlySecurityAttributes {
808        /// Pointer to the `SECURITY_ATTRIBUTES`, for the `lpSecurityAttributes` argument of
809        /// `CreateNamedPipeW`, `CreateMutexW`, `CreateEventW`, ... Cast it to the
810        /// `*const SECURITY_ATTRIBUTES` of whichever Windows binding crate you use. Valid
811        /// while `self` is alive; null is never returned.
812        pub fn as_ptr(&self) -> *const core::ffi::c_void {
813            &*self.attrs as *const SECURITY_ATTRIBUTES as *const core::ffi::c_void
814        }
815
816        /// The SDDL string the descriptor was built from.
817        pub fn sddl(&self) -> &str {
818            &self.sddl
819        }
820
821        /// The `SECURITY_ATTRIBUTES` itself, for `CreateNamedPipeW(.., Some(attrs.as_raw()))`
822        /// with the `windows` crate this crate uses. Valid while `self` is alive.
823        pub fn as_raw(&self) -> &SECURITY_ATTRIBUTES {
824            &self.attrs
825        }
826
827        pub(crate) fn raw(&self) -> &SECURITY_ATTRIBUTES {
828            &self.attrs
829        }
830    }
831
832    impl Drop for OwnerOnlySecurityAttributes {
833        fn drop(&mut self) {
834            unsafe {
835                let _ = LocalFree(Some(HLOCAL(self.sd.0)));
836            }
837        }
838    }
839
840    /// Build owner-only [`OwnerOnlySecurityAttributes`] for the current user. Usable for
841    /// named pipes, mutexes and events.
842    pub fn owner_only_security_attributes() -> io::Result<OwnerOnlySecurityAttributes> {
843        let sddl = sddl_for_sid(&current_user_sid_string()?);
844        attributes_from_sddl(sddl)
845    }
846
847    /// Owner-only attributes for the current user plus `extra` principals and an optional
848    /// integrity `label` (rightkit#1123): the descriptor for a pipe a confined child must write,
849    /// `D:P(A;;GA;;;<user>)(A;;GA;;;<container>)S:(ML;;NW;;;S-1-16-0)`. The current user always
850    /// keeps full control; every extra SID is checked to be plain (letters, digits, `-`).
851    pub fn owner_only_security_attributes_with(
852        extra: &[AclEntry],
853        label: Option<IntegrityLabel>,
854    ) -> io::Result<OwnerOnlySecurityAttributes> {
855        let mut entries = vec![AclEntry::full_control(current_user_sid_string()?)];
856        entries.extend(extra.iter().cloned());
857        pipe_security_attributes(&entries, label)
858    }
859
860    /// Attributes for exactly the principals in `entries` (rightkit#1042), for example SYSTEM
861    /// with full control and one interactive owner with [`ACCESS_PIPE_OWNER_RW`]:
862    /// `D:P(A;;0x10000000;;;SY)(A;;0x12019b;;;<owner>)`. The DACL is protected, so it inherits
863    /// nothing; nobody not listed has access. An empty list or an entry whose SID is not plain
864    /// is `InvalidInput`. Hand [`as_raw`](OwnerOnlySecurityAttributes::as_raw) to
865    /// `CreateNamedPipeW`, and check each client with [`verify_pipe_peer_is_sid`].
866    pub fn pipe_security_attributes(
867        entries: &[AclEntry],
868        label: Option<IntegrityLabel>,
869    ) -> io::Result<OwnerOnlySecurityAttributes> {
870        let sddl = sddl_for_entries(entries, label).ok_or_else(|| {
871            io::Error::new(
872                io::ErrorKind::InvalidInput,
873                "a pipe DACL needs at least one entry and plain SID strings",
874            )
875        })?;
876        attributes_from_sddl(sddl)
877    }
878
879    fn attributes_from_sddl(sddl: String) -> io::Result<OwnerOnlySecurityAttributes> {
880        let wide: Vec<u16> = sddl.encode_utf16().chain(std::iter::once(0)).collect();
881        let mut sd = PSECURITY_DESCRIPTOR::default();
882        unsafe {
883            ConvertStringSecurityDescriptorToSecurityDescriptorW(
884                windows::core::PCWSTR(wide.as_ptr()),
885                SDDL_REVISION_1,
886                &mut sd,
887                None,
888            )
889            .map_err(win_err)?;
890        }
891        let attrs = Box::new(SECURITY_ATTRIBUTES {
892            nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
893            lpSecurityDescriptor: sd.0,
894            bInheritHandle: false.into(),
895        });
896        Ok(OwnerOnlySecurityAttributes { attrs, sd, sddl })
897    }
898}
899
900#[cfg(windows)]
901pub use win::{
902    owner_only_security_attributes, owner_only_security_attributes_with, pipe_security_attributes,
903    verify_pipe_peer_is_current_user, verify_pipe_peer_is_sid, verify_pipe_server_is_current_user,
904    OwnerOnlySecurityAttributes,
905};
906
907/// The SID string (`S-1-...`) of the user running this process, read from the process
908/// token. For callers building their own owner-only DACLs or comparing a peer's
909/// [`PeerIdentity::sid`] against the current user outside a pipe check.
910#[cfg(windows)]
911pub fn current_user_sid() -> io::Result<String> {
912    win::current_user_sid_string()
913}