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}