Mercurial > crates > systemd-socket
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 /// |