Skip to main content

hotl_platform/ipc/
unix.rs

1//! A `0600` unix-domain socket under the runtime directory.
2
3use super::{Ipc, IpcListener, Liveness, PeerReject};
4use crate::KnownPaths as _;
5use std::io;
6use std::path::PathBuf;
7
8pub type UnixIpcStream = tokio::net::UnixStream;
9
10#[derive(Debug, Clone, Copy, Default)]
11pub struct UnixIpc;
12
13impl UnixIpc {
14    pub const fn new() -> Self {
15        Self
16    }
17}
18
19impl crate::sealed::Sealed for UnixIpc {}
20
21/// `<runtime>/run`, holding one `<id>.sock` per live session.
22fn run_dir() -> PathBuf {
23    crate::KNOWN_PATHS
24        .data()
25        .unwrap_or_else(|| PathBuf::from("."))
26        .join("run")
27}
28
29fn sock_path(id: &str) -> PathBuf {
30    run_dir().join(format!("{id}.sock"))
31}
32
33pub struct UnixIpcListener(tokio::net::UnixListener);
34
35impl IpcListener for UnixIpcListener {
36    type Stream = UnixIpcStream;
37
38    async fn accept(&mut self) -> io::Result<Self::Stream> {
39        self.0.accept().await.map(|(s, _)| s)
40    }
41}
42
43impl Ipc for UnixIpc {
44    type Listener = UnixIpcListener;
45    type Stream = UnixIpcStream;
46
47    /// A socket file outlives the process that bound it, so the caller must
48    /// sweep it — inode-matched, or a stale guard deletes a successor's live
49    /// socket.
50    const LEAVES_STALE_ARTIFACT: bool = true;
51
52    fn bind_private(&self, id: &str) -> io::Result<Self::Listener> {
53        use crate::PrivateFs as _;
54        let dir = run_dir();
55        crate::PRIVATE_FS.create_dir_all(&dir)?;
56        let path = sock_path(id);
57        // A stale socket from a dead server is cleared; a *live* one is the
58        // caller's business to refuse, which is why `liveness` exists.
59        if path.exists() && self.liveness(id) == Liveness::Dead {
60            let _ = std::fs::remove_file(&path);
61        }
62        let listener = tokio::net::UnixListener::bind(&path)?;
63        // Owner-only: only this uid can connect, even on a shared host. This is
64        // the authorization boundary; peer auth is defence in depth.
65        crate::PRIVATE_FS.harden_existing(&path)?;
66        Ok(UnixIpcListener(listener))
67    }
68
69    async fn connect(&self, id: &str) -> io::Result<Self::Stream> {
70        tokio::net::UnixStream::connect(sock_path(id)).await
71    }
72
73    fn authenticate_peer(&self, stream: &Self::Stream) -> Result<(), PeerReject> {
74        // `peer_cred` is the portable spelling: `SO_PEERCRED` on Linux,
75        // `getpeereid` on BSD/macOS.
76        let cred = stream
77            .peer_cred()
78            .map_err(|e| PeerReject(format!("the peer's credentials are unreadable: {e}")))?;
79        // SAFETY: `getuid` takes nothing and cannot fail.
80        let me = unsafe { libc::getuid() };
81        if cred.uid() != me {
82            return Err(PeerReject(format!(
83                "the peer runs as uid {}, not {me}",
84                cred.uid()
85            )));
86        }
87        Ok(())
88    }
89
90    fn liveness(&self, id: &str) -> Liveness {
91        // A blocking connect, deliberately: this runs from `gc` and from the
92        // bind path, neither of which has a reactor.
93        match std::os::unix::net::UnixStream::connect(sock_path(id)) {
94            Ok(_) => Liveness::Live,
95            Err(_) => Liveness::Dead,
96        }
97    }
98
99    fn list_live(&self) -> Vec<String> {
100        let Ok(entries) = std::fs::read_dir(run_dir()) else {
101            return Vec::new();
102        };
103        entries
104            .flatten()
105            .filter_map(|e| {
106                let p = e.path();
107                if p.extension()? != "sock" {
108                    return None;
109                }
110                let id = p.file_stem()?.to_str()?.to_string();
111                (self.liveness(&id) == Liveness::Live).then_some(id)
112            })
113            .collect()
114    }
115
116    fn artifact_path(&self, id: &str) -> Option<PathBuf> {
117        Some(sock_path(id))
118    }
119}