Skip to main content

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