Skip to main content

listen_fds/
lib.rs

1// SPDX-FileCopyrightText: 2026 The listen-fds-rs authors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use pidfd_util::{self, PidFdExt};
5use std::{
6    borrow::Cow,
7    env,
8    num::ParseIntError,
9    os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd},
10};
11use thiserror::Error;
12
13/// Errors that can occur when retrieving socket-activated file descriptors.
14#[derive(Error, Debug)]
15pub enum ListenFdsError {
16    /// The process was not socket-activated.
17    ///
18    /// This error occurs when the `LISTEN_FDS` environment variable is not present,
19    /// indicating that systemd did not socket-activate this process.
20    #[error("Not socket activated")]
21    NoListenFds,
22
23    /// The socket activation targets a different process.
24    ///
25    /// This error occurs when the PID or pidfd ID in the environment variables
26    /// does not match the current process, indicating the file descriptors were
27    /// intended for a different process.
28    #[error("The socket activation targets another PID (target: {listen_pid}, self: {self_pid}")]
29    PidMissmatch { listen_pid: u64, self_pid: u64 },
30
31    /// The `LISTEN_FDS` value is out of valid range.
32    #[error("LISTEN_FDS contains out of range FDs")]
33    OutOfRangeListenFds,
34
35    /// The `LISTEN_PID` environment variable contains an invalid PID.
36    #[error("LISTEN_PID does not contain a valid PID")]
37    BadListenPid(ParseIntError),
38
39    /// The `LISTEN_PIDFDID` environment variable contains an invalid pidfd ID.
40    #[error("LISTEN_PIDFDID does not contain a valid Pidfd ID")]
41    BadListenPidfdId(ParseIntError),
42
43    /// The `LISTEN_FDS` environment variable contains an invalid number.
44    #[error("LISTEN_FDS does not contain a valid Pidfd ID")]
45    BadListenFds(ParseIntError),
46
47    /// The number of names in `LISTEN_FDNAMES` doesn't match the number of file descriptors.
48    #[error("LISTEN_FDNAMES contains a wrong number of names")]
49    BadListenFdNames,
50}
51
52/// Container for socket-activated file descriptors passed from systemd.
53///
54/// This struct provides safe access to file descriptors passed via systemd socket activation.
55/// It validates that the file descriptors are intended for this process and automatically sets
56/// the `FD_CLOEXEC` flag on all descriptors for security.
57pub struct ListenFds {
58    fds: Vec<Option<OwnedFd>>,
59    names: Option<Vec<String>>,
60}
61
62impl ListenFds {
63    /// Creates a new `ListenFds` instance from systemd socket activation environment variables.
64    ///
65    /// This function reads and validates the following environment variables:
66    /// - `LISTEN_PID`: The target process ID (validated against current PID)
67    /// - `LISTEN_PIDFDID`: The target pidfd ID (validated against current pidfd, more secure)
68    /// - `LISTEN_FDS`: The number of file descriptors passed
69    /// - `LISTEN_FDNAMES`: Optional colon-separated names for the file descriptors
70    ///
71    /// After reading, all these environment variables are removed from the process environment
72    /// for security reasons.
73    ///
74    /// # Safety
75    ///
76    /// This function is `unsafe` because:
77    /// - It modifies the process environment by removing variables
78    /// - It must be called before spawning any threads (to avoid race conditions)
79    /// - It must be called at most once per process
80    ///
81    /// Call this early in `main()`, before any thread spawning occurs.
82    pub unsafe fn new() -> Result<ListenFds, ListenFdsError> {
83        const LISTEN_FDS_START: usize = 3;
84
85        let listen_pid = env::var("LISTEN_PID");
86        let listen_pidfdid = env::var("LISTEN_PIDFDID");
87        let listen_fds = env::var("LISTEN_FDS");
88        let listen_fdnames = env::var("LISTEN_FDNAMES");
89
90        unsafe {
91            env::remove_var("LISTEN_PID");
92            env::remove_var("LISTEN_PIDFDID");
93            env::remove_var("LISTEN_FDS");
94            env::remove_var("LISTEN_FDNAMES");
95        }
96
97        if let Ok(listen_pid) = listen_pid {
98            let listen_pid: u32 = listen_pid
99                .trim()
100                .parse()
101                .map_err(ListenFdsError::BadListenPid)?;
102            let self_pid = std::process::id();
103            if listen_pid != self_pid {
104                return Err(ListenFdsError::PidMissmatch {
105                    self_pid: self_pid.into(),
106                    listen_pid: listen_pid.into(),
107                });
108            }
109        }
110
111        if let Ok(listen_pidfdid) = listen_pidfdid {
112            let listen_pidfdid: u64 = listen_pidfdid
113                .trim()
114                .parse()
115                .map_err(ListenFdsError::BadListenPidfdId)?;
116            if let Some(self_pidfdid) = pidfd_util::PidFd::from_self()
117                .and_then(|pfd| pfd.get_id())
118                .ok()
119                && listen_pidfdid != self_pidfdid
120            {
121                return Err(ListenFdsError::PidMissmatch {
122                    self_pid: self_pidfdid,
123                    listen_pid: listen_pidfdid,
124                });
125            }
126        }
127
128        let listen_fds = listen_fds.map_err(|_| ListenFdsError::NoListenFds)?;
129        let listen_fds: usize = listen_fds
130            .trim()
131            .parse()
132            .map_err(ListenFdsError::BadListenFds)?;
133        if LISTEN_FDS_START + listen_fds > std::mem::size_of::<std::ffi::c_int>() {
134            return Err(ListenFdsError::OutOfRangeListenFds);
135        }
136
137        let fds: Vec<Option<OwnedFd>> = (0..listen_fds)
138            .map(|i| unsafe { Some(OwnedFd::from_raw_fd((LISTEN_FDS_START + i) as RawFd)) })
139            .collect();
140        fds.iter()
141            .flatten()
142            .for_each(|fd| unsafe { Self::ensure_cloexec(fd) });
143
144        let names: Option<Vec<String>> = listen_fdnames
145            .ok()
146            .map(|names| names.split(':').map(|s| s.to_owned()).collect::<Vec<_>>());
147
148        if let Some(n) = &names
149            && n.len() != fds.len()
150        {
151            return Err(ListenFdsError::BadListenFdNames);
152        }
153
154        Ok(ListenFds { fds, names })
155    }
156
157    unsafe fn ensure_cloexec<Fd: AsFd>(fd: Fd) {
158        let raw_fd = fd.as_fd().as_raw_fd();
159        let flags = unsafe { libc::fcntl(raw_fd, libc::F_GETFD) };
160
161        if flags >= 0 && (flags & libc::FD_CLOEXEC) != libc::FD_CLOEXEC {
162            unsafe {
163                libc::fcntl(raw_fd, libc::F_SETFD, flags | libc::FD_CLOEXEC);
164            }
165        }
166    }
167
168    /// Returns the number of file descriptors received from systemd.
169    pub fn len(&self) -> usize {
170        self.fds.len()
171    }
172
173    /// Returns `true` if no file descriptors were received from systemd.
174    pub fn is_empty(&self) -> bool {
175        self.fds.is_empty()
176    }
177
178    /// Takes ownership of the file descriptor at the given index.
179    ///
180    /// This removes the file descriptor from the internal storage and returns it along with
181    /// its name. Subsequent calls with the same index will return `None`.
182    ///
183    /// Returns `None` if the index is out of bounds or the FD has already been taken.
184    ///
185    /// # Examples
186    ///
187    /// ```no_run
188    /// # use listen_fds::ListenFds;
189    /// # use std::os::unix::net::UnixListener;
190    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
191    /// let mut fds = unsafe { ListenFds::new()? };
192    ///
193    /// if let Some((fd, name)) = fds.take_fd(0) {
194    ///     println!("Taking FD named: {}", name);
195    ///     let listener = UnixListener::from(fd);
196    ///     // Use the listener...
197    /// }
198    /// # Ok(())
199    /// # }
200    /// ```
201    pub fn take_fd(&mut self, idx: usize) -> Option<(OwnedFd, &str)> {
202        let fd = self.fds.get_mut(idx)?.take()?;
203        let name = self.get_name(idx);
204        Some((fd, name))
205    }
206
207    /// Borrows the file descriptor at the given index without taking ownership.
208    ///
209    /// This returns a borrowed reference to the file descriptor along with its name.
210    /// The file descriptor remains in the internal storage and can be borrowed again
211    /// or taken later.
212    ///
213    /// Returns `None` if the index is out of bounds or the FD has been taken.
214    ///
215    /// # Examples
216    ///
217    /// ```no_run
218    /// # use listen_fds::ListenFds;
219    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
220    /// let fds = unsafe { ListenFds::new()? };
221    ///
222    /// if let Some((fd, name)) = fds.get_fd(0) {
223    ///     println!("FD named: {}", name);
224    ///     // Use the borrowed fd...
225    /// }
226    /// # Ok(())
227    /// # }
228    /// ```
229    pub fn get_fd(&self, idx: usize) -> Option<(BorrowedFd<'_>, &str)> {
230        let fd = self.fds.get(idx)?.as_ref()?.as_fd();
231        let name = self.get_name(idx);
232        Some((fd, name))
233    }
234
235    fn get_name(&self, idx: usize) -> &str {
236        self.names
237            .as_ref()
238            .and_then(|v| v.get(idx))
239            .map(|v| v.as_str())
240            .unwrap_or("unknown")
241    }
242
243    /// Takes ownership of all file descriptors with the given name.
244    ///
245    /// This searches for all file descriptors that match the given name (from `LISTEN_FDNAMES`)
246    /// and returns an iterator that yields owned file descriptors. The matching FDs are removed
247    /// from internal storage.
248    ///
249    /// # Examples
250    ///
251    /// ```no_run
252    /// # use listen_fds::ListenFds;
253    /// # use std::net::TcpListener;
254    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
255    /// let mut fds = unsafe { ListenFds::new()? };
256    ///
257    /// // Take all FDs named "http"
258    /// for fd in fds.take("http") {
259    ///     let listener = TcpListener::from(fd);
260    ///     // Handle HTTP connections...
261    /// }
262    /// # Ok(())
263    /// # }
264    /// ```
265    pub fn take(&mut self, name: impl Into<Cow<'static, str>>) -> impl Iterator<Item = OwnedFd> {
266        let name = name.into();
267        self.names
268            .iter()
269            .flatten()
270            .zip(&mut self.fds)
271            .filter_map(move |(s, fd)| (*s == name).then(|| fd.take()).flatten())
272    }
273}