Skip to main content

net_io/
inherit.rs

1//! Inherited listeners: systemd socket activation and FastCGI's descriptor 0.
2//!
3//! Inherited descriptors are verified before net-io takes them, taken once per
4//! process (a second call gets what is left, never a second owner of the same
5//! number), and set close-on-exec. The `LISTEN_*` variables are left in the
6//! environment (decision D-036): the pid check already keeps a child from
7//! using them.
8
9use std::fmt;
10use std::io;
11use std::sync::{Mutex, PoisonError};
12
13use crate::handoff::ListenerHandle;
14#[cfg(unix)]
15use crate::sys::KindError;
16
17/// Why inherited descriptors were refused.
18///
19/// ```
20/// assert_eq!(net_io::InheritError::BadCount.to_string(), "LISTEN_FDS is not a valid count");
21/// ```
22#[non_exhaustive]
23#[derive(Debug)]
24pub enum InheritError {
25    /// `LISTEN_FDS` is not a non-negative number.
26    BadCount,
27    /// More descriptors than allowed.
28    TooMany {
29        /// The maximum.
30        max: u32,
31    },
32    /// The descriptor is not a socket.
33    NotASocket {
34        /// The descriptor.
35        fd: i32,
36    },
37    /// The socket is not listening.
38    NotListening {
39        /// The descriptor.
40        fd: i32,
41    },
42    /// The socket's family or type is not one net-io serves.
43    WrongKind {
44        /// The descriptor.
45        fd: i32,
46    },
47    /// `LISTEN_FDNAMES` does not match `LISTEN_FDS`.
48    NamesMismatch,
49    /// `LISTEN_PIDFDID` does not match this process.
50    PidFdMismatch,
51    /// An OS call failed.
52    Os(io::Error),
53}
54
55impl fmt::Display for InheritError {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        match self {
58            InheritError::BadCount => f.write_str("LISTEN_FDS is not a valid count"),
59            InheritError::TooMany { max } => write!(f, "more than {max} inherited descriptors"),
60            InheritError::NotASocket { fd } => write!(f, "descriptor {fd} is not a socket"),
61            InheritError::NotListening { fd } => {
62                write!(f, "descriptor {fd} is not a listening socket")
63            }
64            InheritError::WrongKind { fd } => {
65                write!(f, "descriptor {fd} is not a TCP or Unix stream socket")
66            }
67            InheritError::NamesMismatch => f.write_str("LISTEN_FDNAMES does not match LISTEN_FDS"),
68            InheritError::PidFdMismatch => {
69                f.write_str("LISTEN_PIDFDID does not match this process")
70            }
71            InheritError::Os(e) => write!(f, "{e}"),
72        }
73    }
74}
75
76impl std::error::Error for InheritError {
77    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
78        match self {
79            InheritError::Os(e) => Some(e),
80            _ => None,
81        }
82    }
83}
84
85/// A listener systemd passed to this process, with its `LISTEN_FDNAMES` name.
86#[derive(Debug)]
87pub struct InheritedListener {
88    /// Its name from `LISTEN_FDNAMES`, when systemd gave names.
89    pub name: Option<String>,
90    /// The socket.
91    pub handle: ListenerHandle,
92}
93
94/// The most descriptors systemd may pass (FR-42).
95#[cfg(unix)]
96const MAX_FDS: u32 = 64;
97/// systemd's first passed descriptor (`SD_LISTEN_FDS_START`).
98#[cfg(unix)]
99const FIRST_FD: i32 = 3;
100
101/// What systemd passed, taken once per process.
102struct Taken {
103    read: bool,
104    left: Vec<InheritedListener>,
105}
106
107static SYSTEMD: Mutex<Taken> = Mutex::new(Taken {
108    read: false,
109    left: Vec::new(),
110});
111
112/// The listeners systemd passed to this process (socket activation). Reads
113/// `LISTEN_PID` (it must name this process, otherwise the variables were meant
114/// for another and the list is empty), `LISTEN_PIDFDID` where present (Linux:
115/// must match this process's pidfd), `LISTEN_FDS` (at most 64), and
116/// `LISTEN_FDNAMES`. Every descriptor from 3 is verified (a socket, listening,
117/// TCP or Unix stream) before any is taken. Not started by systemd: an empty
118/// list. A second call returns what earlier calls (including
119/// [`systemd_named`]) left.
120///
121/// # Errors
122///
123/// `BadCount`, `TooMany`, `NamesMismatch`, `PidFdMismatch`, `NotASocket`,
124/// `NotListening`, `WrongKind`, `Os`; nothing is taken on an error.
125///
126/// ```no_run
127/// use net_io::{AllowAll, ListenerSpec, ServerBuilder};
128/// let mut builder = ServerBuilder::new();
129/// for inherited in net_io::systemd()? {
130///     builder = builder.listener(ListenerSpec::adopt(inherited.handle));
131/// }
132/// let server = builder.handler(|conn| { conn.send_all(b"hi\n")?; Ok(()) }).gate(AllowAll).build()?;
133/// server.open_door()?;
134/// server.wait_for_stop();
135/// # Ok::<(), Box<dyn std::error::Error>>(())
136/// ```
137pub fn systemd() -> Result<Vec<InheritedListener>, InheritError> {
138    let mut taken = SYSTEMD.lock().unwrap_or_else(PoisonError::into_inner);
139    read_once(&mut taken)?;
140    Ok(std::mem::take(&mut taken.left))
141}
142
143/// The one listener systemd passed under `name` (`LISTEN_FDNAMES`), or `None`.
144/// The others stay for later calls.
145///
146/// # Errors
147///
148/// As [`systemd`].
149pub fn systemd_named(name: &str) -> Result<Option<InheritedListener>, InheritError> {
150    let mut taken = SYSTEMD.lock().unwrap_or_else(PoisonError::into_inner);
151    read_once(&mut taken)?;
152    Ok(taken
153        .left
154        .iter()
155        .position(|l| l.name.as_deref() == Some(name))
156        .map(|i| taken.left.remove(i)))
157}
158
159fn read_once(taken: &mut Taken) -> Result<(), InheritError> {
160    if taken.read {
161        return Ok(());
162    }
163    #[cfg(unix)]
164    {
165        let env = |k: &str| std::env::var(k).ok();
166        taken.left = from_systemd(&env, std::process::id(), FIRST_FD)?;
167    }
168    taken.read = true;
169    Ok(())
170}
171
172/// The systemd protocol over `env`, with descriptors from `first` (3 outside tests).
173#[cfg(unix)]
174fn from_systemd(
175    env: &dyn Fn(&str) -> Option<String>,
176    pid: u32,
177    first: i32,
178) -> Result<Vec<InheritedListener>, InheritError> {
179    // Not for this process (or not systemd at all): nothing inherited.
180    match env("LISTEN_PID").and_then(|p| p.trim().parse::<u32>().ok()) {
181        Some(p) if p == pid => {}
182        _ => return Ok(Vec::new()),
183    }
184    #[cfg(target_os = "linux")]
185    if let Some(want) = env("LISTEN_PIDFDID") {
186        let want: u64 = want
187            .trim()
188            .parse()
189            .map_err(|_| InheritError::PidFdMismatch)?;
190        if crate::sys::pidfd_inode().map_err(InheritError::Os)? != want {
191            return Err(InheritError::PidFdMismatch);
192        }
193    }
194    let Some(count) = env("LISTEN_FDS") else {
195        return Ok(Vec::new());
196    };
197    let count: u32 = count.trim().parse().map_err(|_| InheritError::BadCount)?;
198    if count > MAX_FDS {
199        return Err(InheritError::TooMany { max: MAX_FDS });
200    }
201    let names: Option<Vec<String>> =
202        env("LISTEN_FDNAMES").map(|n| n.split(':').map(str::to_owned).collect());
203    if names.as_ref().is_some_and(|n| n.len() != count as usize) {
204        return Err(InheritError::NamesMismatch);
205    }
206    // Verify every descriptor before taking any.
207    let fds: Vec<i32> = (0..count as i32).map(|i| first + i).collect();
208    for &fd in &fds {
209        crate::sys::inherited_kind(fd).map_err(|e| kind_error(e, fd))?;
210    }
211    let mut out = Vec::with_capacity(fds.len());
212    for (i, fd) in fds.into_iter().enumerate() {
213        let owned = crate::sys::claim_inherited(fd).map_err(InheritError::Os)?;
214        let name = names
215            .as_ref()
216            .and_then(|n| n.get(i))
217            .filter(|n| !n.is_empty())
218            .cloned();
219        let handle =
220            ListenerHandle::verified(owned, name.clone()).map_err(|e| kind_error(e, fd))?;
221        out.push(InheritedListener { name, handle });
222    }
223    Ok(out)
224}
225
226/// The fuzz target's entry: `LISTEN_FDS`, `LISTEN_FDNAMES`, and
227/// `LISTEN_PIDFDID` from `data` (split at NUL), descriptors looked for from
228/// 1,000,000 (never open), so nothing real is touched.
229#[cfg(all(fuzzing, unix))]
230pub(crate) fn fuzz_systemd_env(data: &[u8]) {
231    let text = String::from_utf8_lossy(data);
232    let mut parts = text.split('\0');
233    let pid = std::process::id();
234    let (fds, names, pidfd) = (parts.next(), parts.next(), parts.next());
235    let env = |k: &str| match k {
236        "LISTEN_PID" => Some(pid.to_string()),
237        "LISTEN_FDS" => fds.map(str::to_owned),
238        "LISTEN_FDNAMES" => names.map(str::to_owned),
239        "LISTEN_PIDFDID" => pidfd.map(str::to_owned),
240        _ => None,
241    };
242    if let Ok(list) = from_systemd(&env, pid, 1_000_000) {
243        assert!(list.is_empty(), "no descriptor is open up there");
244    }
245}
246
247#[cfg(all(fuzzing, not(unix)))]
248pub(crate) fn fuzz_systemd_env(_data: &[u8]) {}
249
250#[cfg(unix)]
251fn kind_error(e: KindError, fd: i32) -> InheritError {
252    match e {
253        KindError::NotASocket => InheritError::NotASocket { fd },
254        KindError::NotListening => InheritError::NotListening { fd },
255        KindError::WrongKind => InheritError::WrongKind { fd },
256        KindError::Os(e) => InheritError::Os(e),
257    }
258}
259
260#[cfg(unix)]
261static FASTCGI: Mutex<bool> = Mutex::new(false);
262
263/// FastCGI's listening socket (`FCGI_LISTENSOCK_FILENO`, as `mod_fcgid` starts a
264/// FastCGI server): descriptor 0 is taken only if it is a socket, listening,
265/// and has no peer; otherwise `None` (a terminal, a file, a pipe, a connected
266/// socket). Taken once per process: later calls return `None`.
267///
268/// # Errors
269///
270/// `Os` when an OS call fails unexpectedly.
271///
272/// ```no_run
273/// if let Some(handle) = net_io::fastcgi_listener()? {
274///     println!("started by a FastCGI manager on {}", handle.local());
275/// }
276/// # Ok::<(), net_io::InheritError>(())
277/// ```
278#[cfg(unix)]
279pub fn fastcgi_listener() -> Result<Option<ListenerHandle>, InheritError> {
280    let mut taken = FASTCGI.lock().unwrap_or_else(PoisonError::into_inner);
281    if *taken {
282        return Ok(None);
283    }
284    let got = fastcgi_from(0)?;
285    if got.is_some() {
286        *taken = true;
287    }
288    Ok(got)
289}
290
291#[cfg(unix)]
292fn fastcgi_from(fd: i32) -> Result<Option<ListenerHandle>, InheritError> {
293    match crate::sys::inherited_kind(fd) {
294        Ok(_) => {}
295        Err(KindError::Os(e)) => return Err(InheritError::Os(e)),
296        Err(_) => return Ok(None),
297    }
298    // A listener has no peer: `inherited_kind` refused a connected socket (on
299    // macOS by `getpeername`), so descriptor 0 is taken only when it is ours.
300    let owned = crate::sys::claim_inherited(fd).map_err(InheritError::Os)?;
301    ListenerHandle::verified(owned, None)
302        .map(Some)
303        .map_err(|e| kind_error(e, fd))
304}
305
306#[cfg(all(test, unix))]
307mod tests {
308    use super::*;
309    use std::collections::HashMap;
310    use std::os::fd::AsFd;
311
312    fn env(vars: &[(&str, &str)]) -> impl Fn(&str) -> Option<String> {
313        let map: HashMap<String, String> = vars
314            .iter()
315            .map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
316            .collect();
317        move |k| map.get(k).cloned()
318    }
319
320    /// Places listeners at `base`, `base + 1`, ... like systemd at 3.
321    fn place(base: i32, n: usize) -> Vec<std::net::TcpListener> {
322        (0..n)
323            .map(|i| {
324                let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
325                crate::sys::dup_to(l.as_fd(), base + i as i32).unwrap();
326                l
327            })
328            .collect()
329    }
330
331    #[test]
332    fn test_systemd_protocol() {
333        let me = std::process::id();
334        let pid = me.to_string();
335        // Edge cases 95 and 97: nothing for us.
336        assert!(from_systemd(&env(&[]), me, 900).unwrap().is_empty());
337        assert!(
338            from_systemd(&env(&[("LISTEN_PID", "1"), ("LISTEN_FDS", "1")]), me, 900)
339                .unwrap()
340                .is_empty()
341        );
342        assert!(
343            from_systemd(&env(&[("LISTEN_PID", &pid), ("LISTEN_FDS", "0")]), me, 900)
344                .unwrap()
345                .is_empty()
346        );
347        // Edge case 96.
348        for bad in ["x", "-1", ""] {
349            assert!(matches!(
350                from_systemd(&env(&[("LISTEN_PID", &pid), ("LISTEN_FDS", bad)]), me, 900),
351                Err(InheritError::BadCount)
352            ));
353        }
354        assert!(matches!(
355            from_systemd(&env(&[("LISTEN_PID", &pid), ("LISTEN_FDS", "65")]), me, 900),
356            Err(InheritError::TooMany { max: 64 })
357        ));
358        // Edge case 98.
359        assert!(matches!(
360            from_systemd(
361                &env(&[
362                    ("LISTEN_PID", &pid),
363                    ("LISTEN_FDS", "2"),
364                    ("LISTEN_FDNAMES", "a")
365                ]),
366                me,
367                900
368            ),
369            Err(InheritError::NamesMismatch)
370        ));
371        // Edge case 99: a closed number and a regular file.
372        assert!(matches!(
373            from_systemd(&env(&[("LISTEN_PID", &pid), ("LISTEN_FDS", "1")]), me, 990),
374            Err(InheritError::NotASocket { fd: 990 })
375        ));
376        let file = std::fs::File::open("/dev/null").unwrap();
377        crate::sys::dup_to(file.as_fd(), 991).unwrap();
378        assert!(matches!(
379            from_systemd(&env(&[("LISTEN_PID", &pid), ("LISTEN_FDS", "1")]), me, 991),
380            Err(InheritError::NotASocket { fd: 991 })
381        ));
382        crate::sys::close_number(991);
383    }
384
385    #[test]
386    fn test_systemd_takes_verified_listeners_with_names() {
387        let me = std::process::id();
388        let pid = me.to_string();
389        let ls = place(910, 2);
390        let got = from_systemd(
391            &env(&[
392                ("LISTEN_PID", &pid),
393                ("LISTEN_FDS", "2"),
394                ("LISTEN_FDNAMES", "web:"),
395            ]),
396            me,
397            910,
398        )
399        .unwrap();
400        assert_eq!(got.len(), 2);
401        assert_eq!(got[0].name.as_deref(), Some("web"));
402        assert_eq!(got[1].name, None, "an empty name is no name");
403        assert_eq!(
404            got[0].handle.local().socket_addr(),
405            Some(ls[0].local_addr().unwrap())
406        );
407        // Edge case 100: a connected socket is refused, and nothing is taken.
408        let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
409        let c = std::net::TcpStream::connect(l.local_addr().unwrap()).unwrap();
410        crate::sys::dup_to(c.as_fd(), 920).unwrap();
411        assert!(matches!(
412            from_systemd(&env(&[("LISTEN_PID", &pid), ("LISTEN_FDS", "1")]), me, 920),
413            Err(InheritError::NotListening { fd: 920 })
414        ));
415        crate::sys::close_number(920);
416    }
417
418    #[test]
419    fn test_fastcgi_descriptor_checks() {
420        // Edge case 101: a file, a connected socket: None; a listener: taken.
421        let file = std::fs::File::open("/dev/null").unwrap();
422        crate::sys::dup_to(file.as_fd(), 930).unwrap();
423        assert!(fastcgi_from(930).unwrap().is_none());
424        crate::sys::close_number(930);
425        let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
426        let c = std::net::TcpStream::connect(l.local_addr().unwrap()).unwrap();
427        crate::sys::dup_to(c.as_fd(), 931).unwrap();
428        assert!(fastcgi_from(931).unwrap().is_none());
429        crate::sys::close_number(931);
430        crate::sys::dup_to(l.as_fd(), 932).unwrap();
431        let h = fastcgi_from(932).unwrap().unwrap();
432        assert_eq!(h.local().socket_addr(), Some(l.local_addr().unwrap()));
433        assert!(fastcgi_from(-1).unwrap().is_none());
434    }
435}