Skip to main content

vissue_control/
peercred.rs

1//! Same-uid check for an accepted Unix socket. No `unsafe` in this crate.
2
3use std::io;
4use std::os::fd::AsFd;
5
6use nix::unistd::Uid;
7
8/// Accept the peer when its uid matches the current process uid.
9#[must_use]
10pub fn accept_peer(uid: u32) -> bool {
11    uid == current_uid()
12}
13
14/// Real uid of this process (`getuid`), matching `Uid::current`.
15#[must_use]
16pub fn current_uid() -> u32 {
17    Uid::current().as_raw()
18}
19
20/// Peer uid from `SO_PEERCRED` on Linux, or `getpeereid` on BSD/macOS.
21pub fn peer_uid<F: AsFd>(sock: &F) -> io::Result<u32> {
22    peer_uid_impl(sock)
23}
24
25/// Whether an accepted socket may stay open.
26///
27/// `Ok(uid)` uses [`accept_peer`]. `ErrorKind::Unsupported` means this OS
28/// cannot read peer credentials: return true so dir 0700 / sock 0600 are the
29/// check. Every other IO error and a uid mismatch fail closed.
30#[must_use]
31pub fn accept_from_result(result: io::Result<u32>) -> bool {
32    match result {
33        Ok(uid) => accept_peer(uid),
34        Err(err) if err.kind() == io::ErrorKind::Unsupported => true,
35        Err(_) => false,
36    }
37}
38
39/// [`accept_from_result`] over [`peer_uid`].
40#[must_use]
41pub fn accept_socket<F: AsFd>(sock: &F) -> bool {
42    accept_from_result(peer_uid(sock))
43}
44
45#[cfg(any(target_os = "linux", target_os = "android"))]
46fn peer_uid_impl<F: AsFd>(sock: &F) -> io::Result<u32> {
47    use nix::sys::socket::{getsockopt, sockopt};
48    let creds = getsockopt(sock, sockopt::PeerCredentials)?;
49    Ok(creds.uid())
50}
51
52#[cfg(any(
53    target_os = "macos",
54    target_os = "ios",
55    target_os = "freebsd",
56    target_os = "dragonfly",
57    target_os = "openbsd",
58    target_os = "netbsd"
59))]
60fn peer_uid_impl<F: AsFd>(sock: &F) -> io::Result<u32> {
61    let (uid, _) = nix::unistd::getpeereid(sock)?;
62    Ok(uid.as_raw())
63}
64
65#[cfg(not(any(
66    target_os = "linux",
67    target_os = "android",
68    target_os = "macos",
69    target_os = "ios",
70    target_os = "freebsd",
71    target_os = "dragonfly",
72    target_os = "openbsd",
73    target_os = "netbsd"
74)))]
75fn peer_uid_impl<F: AsFd>(_sock: &F) -> io::Result<u32> {
76    Err(io::Error::new(
77        io::ErrorKind::Unsupported,
78        "peer credentials are unavailable; rely on dir 0700 / sock 0600",
79    ))
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn same_uid_is_accepted() {
88        assert!(accept_peer(current_uid()));
89    }
90
91    #[test]
92    fn other_uid_is_rejected() {
93        let other = current_uid().wrapping_add(1);
94        assert!(!accept_peer(other));
95        assert!(!accept_from_result(Ok(other)));
96    }
97
98    #[test]
99    fn unsupported_peercred_falls_back_to_mode_bits() {
100        let err = io::Error::new(io::ErrorKind::Unsupported, "no SO_PEERCRED");
101        assert!(accept_from_result(Err(err)));
102        let err = io::Error::other("getsockopt failed");
103        assert!(!accept_from_result(Err(err)));
104        assert!(accept_from_result(Ok(current_uid())));
105    }
106
107    #[test]
108    fn same_process_peer_is_accepted() {
109        use std::os::unix::net::{UnixListener, UnixStream};
110
111        let dir = tempfile::tempdir().unwrap();
112        let path = dir.path().join("peer.sock");
113        let listener = UnixListener::bind(&path).unwrap();
114        let client = UnixStream::connect(&path).unwrap();
115        let (server, _) = listener.accept().unwrap();
116        let uid = peer_uid(&server).unwrap();
117        assert_eq!(uid, current_uid());
118        assert!(accept_peer(uid));
119        assert!(accept_socket(&server));
120        assert!(accept_socket(&client));
121    }
122}