comparison src/lib.rs @ 24:1941e9d9819c

Fix unsound manipulation of env vars Modifying env vars in multi-threaded process is unsound but this crate was neither checking the number of threads nor mark its functions as `unsafe`. This change fixes it by both adding a check and adding an `unsafe` function that can bypass that check if needed.
author Martin Habovstiak <martin.habovstiak@gmail.com>
date Fri, 28 Feb 2025 13:52:31 +0100
parents f33e2c048104
children 8e20daee41ed
comparison
equal deleted inserted replaced
23:729392c49b46 24:1941e9d9819c
1 //! A convenience crate for optionally supporting systemd socket activation. 1 //! A convenience crate for optionally supporting systemd socket activation.
2 //! 2 //!
3 //! ## About 3 //! ## About
4 //!
5 //! **Important:** because of various reasons it is recommended to call the [`init`] function at
6 //! the start of your program!
4 //! 7 //!
5 //! The goal of this crate is to make socket activation with systemd in your project trivial. 8 //! The goal of this crate is to make socket activation with systemd in your project trivial.
6 //! It provides a replacement for `std::net::SocketAddr` that allows parsing the bind address from string just like the one from `std` 9 //! It provides a replacement for `std::net::SocketAddr` that allows parsing the bind address from string just like the one from `std`
7 //! but on top of that also allows `systemd://socket_name` format that tells it to use systemd activation with given socket name. 10 //! but on top of that also allows `systemd://socket_name` format that tells it to use systemd activation with given socket name.
8 //! Then it provides a method to bind the address which will return the socket from systemd if available. 11 //! Then it provides a method to bind the address which will return the socket from systemd if available.
9 //! 12 //!
10 //! The provided type supports conversions from various types of strings and also `serde` and `parse_arg` via feature flag. 13 //! The provided type supports conversions from various types of strings and also `serde` and `parse_arg` via feature flag.
11 //! Thanks to this the change to your code should be minimal - parsing will continue to work, it'll just allow a new format. 14 //! Thanks to this the change to your code should be minimal - parsing will continue to work, it'll just allow a new format.
12 //! You only need to change the code to use `SocketAddr::bind()` instead of `TcpListener::bind()` for binding. 15 //! You only need to change the code to use `SocketAddr::bind()` instead of `TcpListener::bind()` for binding.
13 //! 16 //!
14 //! You also don't need to worry about conditional compilation to ensure OS compatibility. 17 //! You also don't need to worry about conditional compilation to ensure OS compatibility.
22 //! ```no_run 25 //! ```no_run
23 //! use systemd_socket::SocketAddr; 26 //! use systemd_socket::SocketAddr;
24 //! use std::convert::TryFrom; 27 //! use std::convert::TryFrom;
25 //! use std::io::Write; 28 //! use std::io::Write;
26 //! 29 //!
30 //! systemd_socket::init().expect("Failed to initialize systemd sockets");
27 //! let mut args = std::env::args_os(); 31 //! let mut args = std::env::args_os();
28 //! let program_name = args.next().expect("unknown program name"); 32 //! let program_name = args.next().expect("unknown program name");
29 //! let socket_addr = args.next().expect("missing socket address"); 33 //! let socket_addr = args.next().expect("missing socket address");
30 //! let socket_addr = SocketAddr::try_from(socket_addr).expect("failed to parse socket address"); 34 //! let socket_addr = SocketAddr::try_from(socket_addr).expect("failed to parse socket address");
31 //! let socket = socket_addr.bind().expect("failed to bind socket"); 35 //! let socket = socket_addr.bind().expect("failed to bind socket");
50 //! * `tokio` - adds `bind_tokio` method to `SocketAddr` (tokio 1.0) 54 //! * `tokio` - adds `bind_tokio` method to `SocketAddr` (tokio 1.0)
51 //! * `tokio_0_2` - adds `bind_tokio_0_2` method to `SocketAddr` 55 //! * `tokio_0_2` - adds `bind_tokio_0_2` method to `SocketAddr`
52 //! * `tokio_0_3` - adds `bind_tokio_0_3` method to `SocketAddr` 56 //! * `tokio_0_3` - adds `bind_tokio_0_3` method to `SocketAddr`
53 //! * `async_std` - adds `bind_async_std` method to `SocketAddr` 57 //! * `async_std` - adds `bind_async_std` method to `SocketAddr`
54 //! 58 //!
59 //! ## Soundness
60 //!
61 //! The systemd file descriptors are transferred using environment variables and since they are
62 //! file descriptors, they should have move semantics. However environment variables in Rust do not
63 //! have move semantics and even modifying them is very dangerous.
64 //!
65 //! Because of this, the crate only allows initialization when there's only one thread running.
66 //! However that still doesn't prevent all possible problems: if some other code closes file
67 //! descriptors stored in those environment variables you can get an invalid socket.
68 //!
69 //! This situation is obviously ridiculous because there shouldn't be a reason to use another
70 //! library to do the same thing. It could also be argued that whichever code doesn't clear the
71 //! evironment variable is broken (even though understandably) and it's not a fault of this library.
72 //!
55 //! ## MSRV 73 //! ## MSRV
56 //! 74 //!
57 //! This crate must always compile with the latest Rust available in the latest Debian stable. 75 //! This crate must always compile with the latest Rust available in the latest Debian stable.
58 //! That is currently Rust 1.41.1. (Debian 10 - Buster) 76 //! That is currently Rust 1.41.1. (Debian 10 - Buster)
59 77
77 pub(crate) mod systemd_sockets { 95 pub(crate) mod systemd_sockets {
78 use std::fmt; 96 use std::fmt;
79 use std::sync::Mutex; 97 use std::sync::Mutex;
80 use libsystemd::activation::FileDescriptor; 98 use libsystemd::activation::FileDescriptor;
81 use libsystemd::errors::SdError as LibSystemdError; 99 use libsystemd::errors::SdError as LibSystemdError;
82 type LibSystemdResult<T> = Result<T, LibSystemdError>;
83 100
84 #[derive(Debug)] 101 #[derive(Debug)]
85 pub(crate) struct Error(&'static Mutex<LibSystemdError>); 102 pub(crate) struct Error(&'static Mutex<InitError>);
86 103
87 impl fmt::Display for Error { 104 impl fmt::Display for Error {
88 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 105 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
89 fmt::Display::fmt(&*self.0.lock().expect("mutex poisoned"), f) 106 use std::error::Error as _;
107
108 let guard = self.0.lock().expect("mutex poisoned");
109 fmt::Display::fmt(&*guard, f)?;
110 let mut source_opt = guard.source();
111 while let Some(source) = source_opt {
112 write!(f, ": {}", source)?;
113 source_opt = source.source();
114 }
115 Ok(())
90 } 116 }
91 } 117 }
92 118
93 // No source we can't keep the mutex locked 119 // No source we can't keep the mutex locked
94 impl std::error::Error for Error {} 120 impl std::error::Error for Error {}
95 121
122 pub(crate) unsafe fn init(protected: bool) -> Result<(), InitError> {
123 SYSTEMD_SOCKETS.get_or_try_init(|| SystemdSockets::new(protected, true).map(Ok)).map(drop)
124 }
125
96 pub(crate) fn take(name: &str) -> Result<Option<FileDescriptor>, Error> { 126 pub(crate) fn take(name: &str) -> Result<Option<FileDescriptor>, Error> {
97 match &*SYSTEMD_SOCKETS { 127 let sockets = SYSTEMD_SOCKETS.get_or_init(|| SystemdSockets::new_protected(false).map_err(Mutex::new));
128 match sockets {
98 Ok(sockets) => Ok(sockets.take(name)), 129 Ok(sockets) => Ok(sockets.take(name)),
99 Err(error) => Err(Error(error)) 130 Err(error) => Err(Error(error))
100 } 131 }
101 } 132 }
102 133
134 #[derive(Debug)]
135 pub(crate) enum InitError {
136 OpenStatus(std::io::Error),
137 ReadStatus(std::io::Error),
138 ThreadCountNotFound,
139 MultipleThreads,
140 LibSystemd(LibSystemdError),
141 }
142
143 impl From<LibSystemdError> for InitError {
144 fn from(value: LibSystemdError) -> Self {
145 Self::LibSystemd(value)
146 }
147 }
148
149 impl fmt::Display for InitError {
150 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
151 match self {
152 Self::OpenStatus(_) => write!(f, "failed to open /proc/self/status"),
153 Self::ReadStatus(_) => write!(f, "failed to read /proc/self/status"),
154 Self::ThreadCountNotFound => write!(f, "/proc/self/status doesn't contain Threads entry"),
155 Self::MultipleThreads => write!(f, "there is more than one thread running"),
156 // We have nothing to say about the error, let's flatten it
157 Self::LibSystemd(error) => fmt::Display::fmt(error, f),
158 }
159 }
160 }
161
162 impl std::error::Error for InitError {
163 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
164 match self {
165 Self::OpenStatus(error) => Some(error),
166 Self::ReadStatus(error) => Some(error),
167 Self::ThreadCountNotFound => None,
168 Self::MultipleThreads => None,
169 // We have nothing to say about the error, let's flatten it
170 Self::LibSystemd(error) => error.source(),
171 }
172 }
173 }
174
103 struct SystemdSockets(std::sync::Mutex<std::collections::HashMap<String, FileDescriptor>>); 175 struct SystemdSockets(std::sync::Mutex<std::collections::HashMap<String, FileDescriptor>>);
104 176
105 impl SystemdSockets { 177 impl SystemdSockets {
106 fn new() -> LibSystemdResult<Self> { 178 fn new_protected(explicit: bool) -> Result<Self, InitError> {
179 unsafe { Self::new(true, explicit) }
180 }
181
182 unsafe fn new(protected: bool, explicit: bool) -> Result<Self, InitError> {
183 if explicit {
184 if std::env::var_os("LISTEN_PID").is_none() && std::env::var_os("LISTEN_FDS").is_none() && std::env::var_os("LISTEN_FDNAMES").is_none() {
185 // Systemd is not used - make the map empty
186 return Ok(SystemdSockets(Mutex::new(Default::default())));
187 }
188 }
189
190 if protected { Self::check_single_thread()? }
107 // MUST BE true FOR SAFETY!!! 191 // MUST BE true FOR SAFETY!!!
108 let map = libsystemd::activation::receive_descriptors_with_names(/*unset env = */ true)?.into_iter().map(|(fd, name)| (name, fd)).collect(); 192 let map = libsystemd::activation::receive_descriptors_with_names(/*unset env = */ protected)?.into_iter().map(|(fd, name)| (name, fd)).collect();
109 Ok(SystemdSockets(Mutex::new(map))) 193 Ok(SystemdSockets(Mutex::new(map)))
194 }
195
196 fn check_single_thread() -> Result<(), InitError> {
197 use std::io::BufRead;
198
199 let status = std::fs::File::open("/proc/self/status").map_err(InitError::OpenStatus)?;
200 let mut status = std::io::BufReader::new(status);
201 let mut line = String::new();
202 loop {
203 if status.read_line(&mut line).map_err(InitError::ReadStatus)? == 0 {
204 return Err(InitError::ThreadCountNotFound);
205 }
206 if let Some(threads) = line.strip_prefix("Threads:") {
207 if threads.trim() == "1" {
208 break;
209 } else {
210 return Err(InitError::MultipleThreads);
211 }
212 }
213 line.clear();
214 }
215 Ok(())
110 } 216 }
111 217
112 fn take(&self, name: &str) -> Option<FileDescriptor> { 218 fn take(&self, name: &str) -> Option<FileDescriptor> {
113 // MUST remove THE SOCKET FOR SAFETY!!! 219 // MUST remove THE SOCKET FOR SAFETY!!!
114 self.0.lock().expect("poisoned mutex").remove(name) 220 self.0.lock().expect("poisoned mutex").remove(name)
115 } 221 }
116 } 222 }
117 223
118 lazy_static::lazy_static! { 224 static SYSTEMD_SOCKETS: once_cell::sync::OnceCell<Result<SystemdSockets, Mutex<InitError>>> = once_cell::sync::OnceCell::new();
119 // We don't panic in order to let the application handle the error later
120 static ref SYSTEMD_SOCKETS: Result<SystemdSockets, Mutex<LibSystemdError>> = SystemdSockets::new().map_err(Mutex::new);
121 }
122 } 225 }
123 226
124 /// Socket address that can be an ordinary address or a systemd socket 227 /// Socket address that can be an ordinary address or a systemd socket
125 /// 228 ///
126 /// This is the core type of this crate that abstracts possible addresses. 229 /// This is the core type of this crate that abstracts possible addresses.
337 // This approach makes the rest of the code much simpler as it doesn't require sprinkling it 440 // This approach makes the rest of the code much simpler as it doesn't require sprinkling it
338 // with #[cfg(all(target_os = "linux", feature = "enable_systemd"))] yet still statically guarantees it won't execute. 441 // with #[cfg(all(target_os = "linux", feature = "enable_systemd"))] yet still statically guarantees it won't execute.
339 #[cfg(not(all(target_os = "linux", feature = "enable_systemd")))] 442 #[cfg(not(all(target_os = "linux", feature = "enable_systemd")))]
340 fn get_systemd(socket_name: Never, _prefixed: bool) -> Result<(std::net::TcpListener, SocketAddrInner), BindError> { 443 fn get_systemd(socket_name: Never, _prefixed: bool) -> Result<(std::net::TcpListener, SocketAddrInner), BindError> {
341 match socket_name {} 444 match socket_name {}
445 }
446 }
447
448 /// Initializes the library while there's only a single thread.
449 ///
450 /// Unfortunately, this library has to be initialized and, for soundness, this initialization must
451 /// happen when no other threads are running. This is attempted automatically when trying to bind a
452 /// systemd socket but at that time there may be other threads running and error reporting also
453 /// faces some restrictions. This function provides better control over the initialization point
454 /// and returns a more idiomatic error type.
455 ///
456 /// You should generally call this at around the top of `main`, where no threads were created yet.
457 #[inline]
458 pub fn init() -> Result<(), error::InitError> {
459 #[cfg(all(target_os = "linux", feature = "enable_systemd"))]
460 {
461 // Calling with true is always sound
462 unsafe { systemd_sockets::init(true) }.map_err(error::InitError)
463 }
464 #[cfg(not(all(target_os = "linux", feature = "enable_systemd")))]
465 {
466 Ok(())
467 }
468 }
469
470 /// Initializes the library without protection against double close.
471 ///
472 /// Unfortunately, this library has to be initialized and, because double closing file descriptors
473 /// is unsound, the library has some protections against double close. However these protections
474 /// come with the limitation that the library must be initailized with a single thread.
475 ///
476 /// If for any reason you're unable to call `init` in a single thread at around the top of `main`
477 /// (and this should be almost never) you may call this method if you've ensured that no other part
478 /// of your codebase is operating on systemd-provided file descriptors stored in the environment
479 /// variables.
480 pub unsafe fn init_unprotected() -> Result<(), error::InitError> {
481 #[cfg(all(target_os = "linux", feature = "enable_systemd"))]
482 {
483 systemd_sockets::init(false).map_err(error::InitError)
484 }
485 #[cfg(not(all(target_os = "linux", feature = "enable_systemd")))]
486 {
487 Ok(())
342 } 488 }
343 } 489 }
344 490
345 /// Displays the address in format that can be parsed again. 491 /// Displays the address in format that can be parsed again.
346 /// 492 ///