Skip to main content

coreshift_core/
unix_socket.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/
4
5//! Low-level Unix domain socket primitives.
6//!
7//! This module exposes Linux/Android `AF_UNIX` stream socket mechanics only:
8//! bind, listen, accept, connect, chmod for filesystem sockets, peer
9//! credentials, and byte I/O through [`Fd`]. Callers own all protocol, message
10//! framing, authentication policy, daemon behavior, and socket naming.
11//!
12//! Abstract socket names are Linux/Android-only. They are encoded with a
13//! leading NUL byte in `sun_path`; interior NUL bytes in the caller-provided
14//! abstract name are preserved because the kernel uses the explicit sockaddr
15//! length, not C string termination.
16
17use crate::CoreError;
18use crate::error::syscall_ret;
19use crate::reactor::Fd;
20use std::io::Error as IoError;
21use std::os::unix::ffi::OsStrExt;
22use std::os::unix::fs::FileTypeExt;
23use std::os::unix::io::AsRawFd;
24use std::path::Path;
25
26#[inline(always)]
27fn errno() -> i32 {
28    IoError::last_os_error().raw_os_error().unwrap_or(0)
29}
30
31/// Owned non-blocking Unix listener descriptor.
32pub struct UnixListenerFd {
33    /// Underlying descriptor for reactor registration and raw byte helpers.
34    pub fd: Fd,
35}
36
37/// Owned non-blocking Unix stream descriptor.
38pub struct UnixStreamFd {
39    /// Underlying descriptor for reactor registration and raw byte helpers.
40    pub fd: Fd,
41}
42
43/// Result of starting a non-blocking Unix stream connection.
44pub enum UnixConnectResult {
45    /// The socket connected immediately.
46    Connected(UnixStreamFd),
47    /// The socket connection is in progress; register for writability and call
48    /// [`UnixStreamFd::finish_connect`] or [`UnixStreamFd::check_connect_error`].
49    InProgress(UnixStreamFd),
50}
51
52/// Unix socket address.
53#[derive(Clone, Copy, Debug)]
54pub enum UnixSocketAddr<'a> {
55    /// Filesystem pathname socket.
56    Path(&'a Path),
57    /// Linux/Android abstract namespace socket name, without the leading NUL.
58    Abstract(&'a [u8]),
59}
60
61/// Explicit stale pathname behavior for filesystem socket binds.
62#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
63pub enum StaleSocketPolicy {
64    /// Preserve any existing path and let `bind` report the conflict.
65    #[default]
66    Preserve,
67    /// Unlink only if the existing path is itself a socket.
68    UnlinkSocketOnly,
69    /// Unlink any existing filesystem path.
70    ///
71    /// This may delete non-socket files and should only be used when the caller
72    /// owns the path namespace.
73    UnlinkAnyPath,
74}
75
76/// Bind options for a Unix stream listener.
77#[derive(Clone, Copy, Debug, Default)]
78pub struct UnixSocketBindOptions {
79    /// Explicit stale pathname handling for filesystem socket binds.
80    pub stale_socket_policy: StaleSocketPolicy,
81    /// Optional filesystem socket path mode applied after a successful bind.
82    pub mode: Option<u32>,
83}
84
85/// Peer process credentials when the platform exposes them.
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub struct PeerCred {
88    /// Peer process id when available.
89    pub pid: Option<i32>,
90    /// Peer user id.
91    pub uid: u32,
92    /// Peer group id.
93    pub gid: u32,
94}
95
96impl UnixListenerFd {
97    /// Accept one non-blocking client.
98    ///
99    /// ### Fork Safety
100    /// The listener's file descriptor is `O_CLOEXEC` and will be closed in the
101    /// child after `exec`.
102    ///
103    /// ### Errors
104    /// - `EAGAIN`/`EWOULDBLOCK`: No connection is pending.
105    /// - `ECONNABORTED`: A connection was aborted before it could be accepted.
106    /// - `EMFILE`: Process limit on open file descriptors hit.
107    /// - `ENFILE`: System-wide limit on open files hit.
108    ///
109    /// Returns `Ok(None)` if no client is ready.
110    pub fn accept(&self) -> Result<Option<UnixStreamFd>, CoreError> {
111        self.accept_timeout(0)
112    }
113
114    /// Accept a client with a raw timeout in milliseconds.
115    ///
116    /// - `-1`: Block indefinitely until a client connects.
117    /// - `0`: Return immediately (equivalent to [`Self::accept`]).
118    /// - `> 0`: Wait up to the specified milliseconds.
119    ///
120    /// ### Errors
121    /// Returns the same errors as [`Self::accept`], or `poll(2)` errors.
122    ///
123    /// # Example
124    /// ```no_run
125    /// # use coreshift_core::unix_socket::{self, UnixListenerFd, UnixSocketAddr, UnixSocketBindOptions};
126    /// # let listener = unix_socket::bind_unix_listener(UnixSocketAddr::Abstract(b"test"), UnixSocketBindOptions::default()).unwrap();
127    /// let stream = listener.accept_timeout(1000).unwrap();
128    /// ```
129    pub fn accept_timeout(&self, timeout_ms: i32) -> Result<Option<UnixStreamFd>, CoreError> {
130        if timeout_ms != 0 {
131            let mut pollfd = libc::pollfd {
132                fd: self.fd.as_raw_fd(),
133                events: libc::POLLIN,
134                revents: 0,
135            };
136            let ret = unsafe { libc::poll(&mut pollfd, 1, timeout_ms) };
137            if ret < 0 {
138                let e = errno();
139                if e == libc::EINTR {
140                    return Ok(None);
141                }
142                return Err(CoreError::sys(e, "poll(accept)"));
143            }
144            if ret == 0 {
145                return Ok(None);
146            }
147        }
148
149        loop {
150            let fd = unsafe {
151                libc::accept4(
152                    self.fd.as_raw_fd(),
153                    std::ptr::null_mut(),
154                    std::ptr::null_mut(),
155                    libc::SOCK_CLOEXEC | libc::SOCK_NONBLOCK,
156                )
157            };
158            if fd >= 0 {
159                return Ok(Some(UnixStreamFd {
160                    fd: Fd::new(fd, "accept4")?,
161                }));
162            }
163
164            let e = errno();
165            if e == libc::EINTR {
166                continue;
167            }
168            if e == libc::EAGAIN || e == libc::EWOULDBLOCK {
169                return Ok(None);
170            }
171            return Err(CoreError::sys(e, "accept4"));
172        }
173    }
174}
175
176impl UnixStreamFd {
177    /// Return peer credentials when the platform supports `SO_PEERCRED`.
178    ///
179    /// ### Errors
180    /// - `EBADF`: The file descriptor is invalid.
181    /// - `ENOPROTOOPT`: `SO_PEERCRED` is not supported by the socket.
182    ///
183    /// # Example
184    /// ```no_run
185    /// # use coreshift_core::unix_socket::UnixStreamFd;
186    /// # fn example(stream: UnixStreamFd) {
187    /// let creds = stream.peer_cred().unwrap();
188    /// if let Some(c) = creds {
189    ///     println!("Peer UID: {}", c.uid);
190    /// }
191    /// # }
192    /// ```
193    pub fn peer_cred(&self) -> Result<Option<PeerCred>, CoreError> {
194        peer_cred_raw(&self.fd)
195    }
196
197    /// Return the pending `SO_ERROR` connect status.
198    ///
199    /// `Ok(None)` means no pending socket error was reported. `Ok(Some(code))`
200    /// returns the raw connect error without making a policy decision.
201    ///
202    /// ### Errors
203    /// - `EBADF`: The file descriptor is invalid.
204    pub fn check_connect_error(&self) -> Result<Option<i32>, CoreError> {
205        let mut code: libc::c_int = 0;
206        let mut len = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
207        let ret = unsafe {
208            libc::getsockopt(
209                self.fd.as_raw_fd(),
210                libc::SOL_SOCKET,
211                libc::SO_ERROR,
212                (&mut code as *mut libc::c_int).cast(),
213                &mut len,
214            )
215        };
216        syscall_ret(ret, "getsockopt(SO_ERROR)")?;
217        if code == 0 { Ok(None) } else { Ok(Some(code)) }
218    }
219
220    /// Finish a non-blocking connect after the socket becomes writable.
221    ///
222    /// Returns the stream when `SO_ERROR` is clear; otherwise returns the raw
223    /// socket error as [`CoreError`].
224    ///
225    /// ### Errors
226    /// Returns the same errors as [`Self::check_connect_error`], or the
227    /// pending connection error itself.
228    pub fn finish_connect(self) -> Result<Self, CoreError> {
229        match self.check_connect_error()? {
230            None => Ok(self),
231            Some(code) => Err(CoreError::sys(code, "connect(SO_ERROR)")),
232        }
233    }
234}
235
236/// Bind a new Unix domain stream listener.
237///
238/// The socket is created with `SOCK_CLOEXEC` set.
239///
240/// ### Fork Safety
241/// The socket is `O_CLOEXEC` and will be closed in the child after `exec`.
242///
243/// ### Errors
244/// - `EACCES`: Permission denied for a component of the path.
245/// - `EADDRINUSE`: The address is already in use.
246/// - `EINVAL`: Invalid address.
247/// - `ELOOP`: Too many symbolic links encountered.
248/// - `ENAMETOOLONG`: Path is too long.
249/// - `ENOENT`: A component of the path prefix does not exist.
250pub fn bind_unix_listener(
251    addr: UnixSocketAddr<'_>,
252    opts: UnixSocketBindOptions,
253) -> Result<UnixListenerFd, CoreError> {
254    let encoded = UnixSockAddr::new(addr, "unix bind address")?;
255
256    match addr {
257        UnixSocketAddr::Path(path) => {
258            apply_stale_socket_policy(path, opts.stale_socket_policy)?;
259        }
260        UnixSocketAddr::Abstract(_) => {
261            if opts.stale_socket_policy != StaleSocketPolicy::Preserve || opts.mode.is_some() {
262                return Err(CoreError::sys(libc::EINVAL, "abstract unix bind options"));
263            }
264        }
265    }
266
267    let fd = new_unix_stream_socket()?;
268    let ret = unsafe { libc::bind(fd.as_raw_fd(), encoded.as_ptr(), encoded.len()) };
269    syscall_ret(ret, "bind")?;
270
271    if let (UnixSocketAddr::Path(path), Some(mode)) = (addr, opts.mode) {
272        if let Err(err) = chmod_unix_socket(UnixSocketAddr::Path(path), mode) {
273            cleanup_created_path(addr);
274            return Err(err);
275        }
276    }
277
278    let ret = unsafe { libc::listen(fd.as_raw_fd(), libc::SOMAXCONN) };
279    if let Err(err) = syscall_ret(ret, "listen") {
280        cleanup_created_path(addr);
281        return Err(err);
282    }
283
284    Ok(UnixListenerFd { fd })
285}
286
287/// Connect a non-blocking Unix stream socket.
288///
289/// ### Fork Safety
290/// The socket is `O_CLOEXEC` and will be closed in the child after `exec`.
291///
292/// ### Errors
293/// - `EACCES`: Permission denied.
294/// - `ECONNREFUSED`: No one listening on the remote address.
295/// - `EINPROGRESS`: Connection is in progress.
296/// - `ENOENT`: The socket path does not exist.
297pub fn connect_unix_stream(addr: UnixSocketAddr<'_>) -> Result<UnixConnectResult, CoreError> {
298    connect_unix_stream_as(addr, None)
299}
300
301/// Like [`connect_unix_stream`] but binds the client socket to `local` before connecting,
302/// so the peer name appears in `/proc/net/unix` with an identifiable label.
303pub fn connect_unix_stream_named(
304    remote: UnixSocketAddr<'_>,
305    local: UnixSocketAddr<'_>,
306) -> Result<UnixConnectResult, CoreError> {
307    connect_unix_stream_as(remote, Some(local))
308}
309
310fn connect_unix_stream_as(
311    remote: UnixSocketAddr<'_>,
312    local: Option<UnixSocketAddr<'_>>,
313) -> Result<UnixConnectResult, CoreError> {
314    let encoded = UnixSockAddr::new(remote, "unix connect address")?;
315    let fd = new_unix_stream_socket()?;
316
317    if let Some(la) = local {
318        let lb = UnixSockAddr::new(la, "unix bind local name")?;
319        let r = unsafe { libc::bind(fd.as_raw_fd(), lb.as_ptr(), lb.len()) };
320        if r < 0 {
321            return Err(CoreError::sys(errno(), "bind local name"));
322        }
323    }
324
325    loop {
326        let ret = unsafe { libc::connect(fd.as_raw_fd(), encoded.as_ptr(), encoded.len()) };
327        if ret == 0 {
328            return Ok(UnixConnectResult::Connected(UnixStreamFd { fd }));
329        }
330
331        let e = errno();
332        if e == libc::EINTR {
333            continue;
334        }
335        if e == libc::EINPROGRESS || e == libc::EALREADY {
336            return Ok(UnixConnectResult::InProgress(UnixStreamFd { fd }));
337        }
338        if e == libc::EISCONN {
339            return Ok(UnixConnectResult::Connected(UnixStreamFd { fd }));
340        }
341        return Err(CoreError::sys(e, "connect"));
342    }
343}
344
345/// Create a paired, non-blocking connected Unix stream socket set.
346///
347/// Both endpoints are `SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC` and owned
348/// by the caller. Useful for tests and in-process pipe-style channels where a
349/// full bind/connect round-trip is unnecessary.
350///
351/// ### Errors
352/// - `EAFNOSUPPORT`: `AF_UNIX` is unavailable.
353/// - `EMFILE`/`ENFILE`: Process or system file descriptor limit reached.
354pub fn socketpair() -> Result<(UnixStreamFd, UnixStreamFd), CoreError> {
355    let mut fds = [0 as libc::c_int; 2];
356    let ret = unsafe {
357        libc::socketpair(
358            libc::AF_UNIX,
359            libc::SOCK_STREAM | libc::SOCK_NONBLOCK | libc::SOCK_CLOEXEC,
360            0,
361            fds.as_mut_ptr(),
362        )
363    };
364    syscall_ret(ret, "socketpair")?;
365    let a = UnixStreamFd {
366        fd: Fd::new(fds[0], "socketpair.0")?,
367    };
368    let b = UnixStreamFd {
369        fd: Fd::new(fds[1], "socketpair.1")?,
370    };
371    Ok((a, b))
372}
373
374/// Change mode bits on a Unix socket filesystem path.
375///
376/// ### Errors
377/// - `EACCES`: Permission denied.
378/// - `ENOENT`: The socket path does not exist.
379/// - `EPERM`: The caller does not own the file.
380pub fn chmod_unix_socket(addr: UnixSocketAddr<'_>, mode: u32) -> Result<(), CoreError> {
381    match addr {
382        UnixSocketAddr::Path(path) => {
383            let metadata = std::fs::symlink_metadata(path).map_err(|err| {
384                CoreError::sys(
385                    err.raw_os_error().unwrap_or(libc::EIO),
386                    "lstat unix socket path",
387                )
388            })?;
389            if !metadata.file_type().is_socket() {
390                return Err(CoreError::sys(libc::EINVAL, "chmod unix socket path"));
391            }
392            let c_path = path_cstring(path, "chmod unix socket path")?;
393            let ret = unsafe { libc::chmod(c_path.as_ptr(), mode as libc::mode_t) };
394            syscall_ret(ret, "chmod")
395        }
396        UnixSocketAddr::Abstract(_) => Err(CoreError::sys(libc::EINVAL, "chmod abstract socket")),
397    }
398}
399
400/// Change mode bits on a Unix socket filesystem path.
401pub fn chmod_socket_path(path: impl AsRef<Path>, mode: u32) -> Result<(), CoreError> {
402    chmod_unix_socket(UnixSocketAddr::Path(path.as_ref()), mode)
403}
404
405/// Connect to a Unix domain stream socket.
406///
407/// The socket is created with `SOCK_CLOEXEC` and `SOCK_NONBLOCK` set.
408fn new_unix_stream_socket() -> Result<Fd, CoreError> {
409    let fd = unsafe {
410        libc::socket(
411            libc::AF_UNIX,
412            libc::SOCK_STREAM | libc::SOCK_CLOEXEC | libc::SOCK_NONBLOCK,
413            0,
414        )
415    };
416    syscall_ret(fd, "socket(AF_UNIX)")?;
417    Fd::new(fd, "socket(AF_UNIX)")
418}
419
420fn apply_stale_socket_policy(path: &Path, policy: StaleSocketPolicy) -> Result<(), CoreError> {
421    match policy {
422        StaleSocketPolicy::Preserve => Ok(()),
423        StaleSocketPolicy::UnlinkSocketOnly => {
424            let metadata = match std::fs::symlink_metadata(path) {
425                Ok(metadata) => metadata,
426                Err(err) if err.raw_os_error() == Some(libc::ENOENT) => return Ok(()),
427                Err(err) => {
428                    return Err(CoreError::sys(
429                        err.raw_os_error().unwrap_or(libc::EIO),
430                        "lstat unix socket path",
431                    ));
432                }
433            };
434            if !metadata.file_type().is_socket() {
435                return Err(CoreError::sys(libc::EEXIST, "stale unix socket path"));
436            }
437            unlink_path(path, "unlink stale unix socket")
438        }
439        StaleSocketPolicy::UnlinkAnyPath => unlink_path(path, "unlink unix socket path"),
440    }
441}
442
443fn unlink_path(path: &Path, op: &'static str) -> Result<(), CoreError> {
444    match std::fs::remove_file(path) {
445        Ok(()) => Ok(()),
446        Err(err) if err.raw_os_error() == Some(libc::ENOENT) => Ok(()),
447        Err(err) => Err(CoreError::sys(err.raw_os_error().unwrap_or(libc::EIO), op)),
448    }
449}
450
451fn cleanup_created_path(addr: UnixSocketAddr<'_>) {
452    if let UnixSocketAddr::Path(path) = addr {
453        let _ = std::fs::remove_file(path);
454    }
455}
456
457struct UnixSockAddr {
458    inner: libc::sockaddr_un,
459    len: libc::socklen_t,
460}
461
462impl UnixSockAddr {
463    fn new(addr: UnixSocketAddr<'_>, op: &'static str) -> Result<Self, CoreError> {
464        let mut inner: libc::sockaddr_un = unsafe { std::mem::zeroed() };
465        inner.sun_family = libc::AF_UNIX as libc::sa_family_t;
466        let sun_path_offset = std::mem::offset_of!(libc::sockaddr_un, sun_path);
467
468        let len = match addr {
469            UnixSocketAddr::Path(path) => {
470                let bytes = path.as_os_str().as_bytes();
471                if bytes.is_empty() {
472                    return Err(CoreError::sys(libc::EINVAL, op));
473                }
474                if bytes.contains(&0) {
475                    return Err(CoreError::sys(libc::EINVAL, op));
476                }
477                if bytes.len() >= inner.sun_path.len() {
478                    return Err(CoreError::sys(libc::ENAMETOOLONG, op));
479                }
480
481                for (slot, byte) in inner.sun_path.iter_mut().zip(bytes.iter().copied()) {
482                    *slot = byte as libc::c_char;
483                }
484                sun_path_offset + bytes.len() + 1
485            }
486            UnixSocketAddr::Abstract(name) => {
487                validate_abstract_supported()?;
488                if name.is_empty() {
489                    return Err(CoreError::sys(libc::EINVAL, op));
490                }
491                if name.len() + 1 > inner.sun_path.len() {
492                    return Err(CoreError::sys(libc::ENAMETOOLONG, op));
493                }
494
495                inner.sun_path[0] = 0;
496                for (slot, byte) in inner.sun_path[1..].iter_mut().zip(name.iter().copied()) {
497                    *slot = byte as libc::c_char;
498                }
499                sun_path_offset + 1 + name.len()
500            }
501        };
502        let len = libc::socklen_t::try_from(len).map_err(|_| CoreError::sys(libc::EINVAL, op))?;
503
504        Ok(Self { inner, len })
505    }
506
507    fn len(&self) -> libc::socklen_t {
508        self.len
509    }
510
511    fn as_ptr(&self) -> *const libc::sockaddr {
512        (&self.inner as *const libc::sockaddr_un).cast()
513    }
514}
515
516fn validate_abstract_supported() -> Result<(), CoreError> {
517    if cfg!(any(target_os = "linux", target_os = "android")) {
518        Ok(())
519    } else {
520        Err(CoreError::sys(libc::ENOSYS, "abstract unix socket"))
521    }
522}
523
524fn path_cstring(path: &Path, op: &'static str) -> Result<std::ffi::CString, CoreError> {
525    std::ffi::CString::new(path.as_os_str().as_bytes())
526        .map_err(|_| CoreError::sys(libc::EINVAL, op))
527}
528
529#[cfg(any(target_os = "linux", target_os = "android"))]
530fn peer_cred_raw(fd: &Fd) -> Result<Option<PeerCred>, CoreError> {
531    let mut cred: libc::ucred = unsafe { std::mem::zeroed() };
532    let mut len = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
533    let ret = unsafe {
534        libc::getsockopt(
535            fd.as_raw_fd(),
536            libc::SOL_SOCKET,
537            libc::SO_PEERCRED,
538            (&mut cred as *mut libc::ucred).cast(),
539            &mut len,
540        )
541    };
542    syscall_ret(ret, "getsockopt(SO_PEERCRED)")?;
543
544    Ok(Some(PeerCred {
545        pid: Some(cred.pid),
546        uid: cred.uid,
547        gid: cred.gid,
548    }))
549}
550
551#[cfg(not(any(target_os = "linux", target_os = "android")))]
552fn peer_cred_raw(_fd: &Fd) -> Result<Option<PeerCred>, CoreError> {
553    Ok(None)
554}