subc_os/launch_nonce.rs
1//! The launch nonce: the secret the daemon gives each module it spawns, which
2//! the module presents to be admitted as itself.
3//!
4//! On macOS and Linux the daemon hands it over through a pipe rather than
5//! the environment, because any process of the same user can read another
6//! process's initial environment (`ps eww`, `sysctl KERN_PROCARGS2`). The
7//! daemon side is [`LaunchNonceHandoff`]: a pipe that already holds the nonce,
8//! whose read end becomes descriptor [`LAUNCH_NONCE_FD`] in the child. The
9//! module side is [`launch_nonce`]: it reads that descriptor once, closes it,
10//! and caches the value for the life of the process.
11//!
12//! While modules move over, the daemon also keeps setting the environment
13//! copy ([`LAUNCH_NONCE_ENV`]), and [`launch_nonce`] reads it when no
14//! descriptor is named. Windows has no descriptor handoff yet: an inheritable
15//! handle there leaks to every process any thread creates concurrently, so
16//! Windows keeps the environment copy only.
17
18use std::{
19 ffi::OsString,
20 fmt,
21 sync::{atomic::AtomicUsize, OnceLock},
22};
23
24/// The descriptor number the pipe's read end has in the child.
25pub const LAUNCH_NONCE_FD: i32 = 3;
26
27/// Names the descriptor holding the nonce, as `<fd>:<inode>`. The inode names
28/// the pipe itself, so the reader can tell it from an unrelated descriptor
29/// that happens to have the same number. A process a module spawns inherits
30/// this variable but not the pipe, and without the inode it would read and
31/// close whatever that process has at the number.
32pub const LAUNCH_NONCE_FD_ENV: &str = "SUBC_LAUNCH_NONCE_FD";
33
34/// The environment copy of the nonce, kept only while modules move to the
35/// descriptor. Same name as `subc_protocol::SUBC_LAUNCH_NONCE_ENV`; this crate
36/// does not depend on subc-protocol, so it states the name itself.
37pub const LAUNCH_NONCE_ENV: &str = "SUBC_LAUNCH_NONCE";
38
39/// Where a process got its launch nonce from.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41#[non_exhaustive]
42pub enum LaunchNonceSource {
43 /// The inherited descriptor named by [`LAUNCH_NONCE_FD_ENV`].
44 Fd,
45 /// The environment variable [`LAUNCH_NONCE_ENV`].
46 Env,
47}
48
49impl LaunchNonceSource {
50 /// The name modules report in their provenance: `fd` or `env`.
51 pub fn as_str(self) -> &'static str {
52 match self {
53 Self::Fd => "fd",
54 Self::Env => "env",
55 }
56 }
57}
58
59/// The nonce this process was launched with, and where it came from.
60/// `Debug` never prints the value.
61#[derive(Clone, PartialEq, Eq)]
62pub struct LaunchNonce {
63 value: String,
64 source: LaunchNonceSource,
65}
66
67impl LaunchNonce {
68 pub fn value(&self) -> &str {
69 &self.value
70 }
71
72 pub fn source(&self) -> LaunchNonceSource {
73 self.source
74 }
75}
76
77impl fmt::Debug for LaunchNonce {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 f.debug_struct("LaunchNonce")
80 .field(
81 "value",
82 &format_args!("<{} bytes redacted>", self.value.len()),
83 )
84 .field("source", &self.source)
85 .finish()
86 }
87}
88
89/// Why the descriptor named by [`LAUNCH_NONCE_FD_ENV`] gave no nonce.
90///
91/// None of these falls back to the environment copy. A named descriptor
92/// that cannot be read means the handoff went wrong, or that this process
93/// inherited the variable from a module without inheriting the pipe; reading
94/// the environment instead would hide the first and defeat the second.
95#[derive(Debug, Clone, PartialEq, Eq)]
96#[non_exhaustive]
97pub enum LaunchNonceError {
98 /// The variable is not `<fd>:<inode>`.
99 Malformed { value: String },
100 /// Nothing is open at that number: the variable was inherited without
101 /// the descriptor, which is what a process spawned by a module sees.
102 NotOpen { fd: i32, errno: i32 },
103 /// The descriptor is open but is not a pipe. It was left alone.
104 NotAPipe { fd: i32 },
105 /// The descriptor is a pipe, but not the one named. It was left alone.
106 WrongPipe {
107 fd: i32,
108 expected_inode: u64,
109 found_inode: u64,
110 },
111 /// The named pipe holds no bytes. It was left open and unread.
112 Empty { fd: i32 },
113 /// Reading the named pipe failed.
114 Unreadable { fd: i32, errno: Option<i32> },
115 /// The named pipe held bytes that are not UTF-8.
116 NotUtf8 { fd: i32 },
117}
118
119impl fmt::Display for LaunchNonceError {
120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 match self {
122 Self::Malformed { value } => write!(
123 f,
124 "{LAUNCH_NONCE_FD_ENV}={value:?} is not <fd>:<inode>"
125 ),
126 Self::NotOpen { fd, errno } => write!(
127 f,
128 "{LAUNCH_NONCE_FD_ENV} names descriptor {fd}, which is not open (errno {errno}); \
129 a process spawned by a module inherits the variable but not the descriptor"
130 ),
131 Self::NotAPipe { fd } => write!(
132 f,
133 "{LAUNCH_NONCE_FD_ENV} names descriptor {fd}, which is not a pipe; left it untouched"
134 ),
135 Self::WrongPipe {
136 fd,
137 expected_inode,
138 found_inode,
139 } => write!(
140 f,
141 "{LAUNCH_NONCE_FD_ENV} names descriptor {fd} with inode {expected_inode}, but it has \
142 inode {found_inode}; left it untouched"
143 ),
144 Self::Empty { fd } => write!(
145 f,
146 "the launch nonce pipe at descriptor {fd} is empty; left it untouched"
147 ),
148 Self::Unreadable { fd, errno } => write!(
149 f,
150 "could not read the launch nonce from descriptor {fd} (errno {errno:?})"
151 ),
152 Self::NotUtf8 { fd } => write!(
153 f,
154 "the launch nonce pipe at descriptor {fd} held bytes that are not UTF-8"
155 ),
156 }
157 }
158}
159
160impl std::error::Error for LaunchNonceError {}
161
162type Cached = Result<Option<LaunchNonce>, LaunchNonceError>;
163
164/// A launch nonce read at most once. The process has one, behind
165/// [`launch_nonce`]; tests make their own with a stand-in for the
166/// environment.
167pub(crate) struct LaunchNonceCell {
168 value: OnceLock<Cached>,
169 descriptor_reads: AtomicUsize,
170}
171
172impl LaunchNonceCell {
173 pub(crate) const fn new() -> Self {
174 Self {
175 value: OnceLock::new(),
176 descriptor_reads: AtomicUsize::new(0),
177 }
178 }
179
180 /// The cached result, reading it first if no caller has yet. Concurrent
181 /// first callers wait for the one that reads, so nobody reads twice.
182 pub(crate) fn get(&self, lookup: impl FnMut(&str) -> Option<OsString>) -> Cached {
183 self.value
184 .get_or_init(|| read_launch_nonce(lookup, &self.descriptor_reads))
185 .clone()
186 }
187
188 /// How many times this cell has taken a descriptor. At most one.
189 #[cfg(all(test, unix))]
190 pub(crate) fn descriptor_reads(&self) -> usize {
191 self.descriptor_reads
192 .load(std::sync::atomic::Ordering::SeqCst)
193 }
194}
195
196static PROCESS_NONCE: LaunchNonceCell = LaunchNonceCell::new();
197
198/// This process's launch nonce and where it came from.
199///
200/// The first call decides, and every later call returns the same answer
201/// without touching the descriptor or the environment again. So every reader
202/// in a process must come through here: after the first read closes the
203/// descriptor, its number is the next one the process hands out, and a second
204/// independent reader would read and close some unrelated socket or file.
205///
206/// - When [`LAUNCH_NONCE_FD_ENV`] is set (macOS and Linux), the descriptor it
207/// names is taken only if it is a pipe with the named inode and holds
208/// bytes; it is then read to end of file and closed. Anything else is a
209/// [`LaunchNonceError`] that leaves the descriptor as it was and never
210/// falls back to the environment.
211/// - Otherwise the value of [`LAUNCH_NONCE_ENV`] is used. Windows always
212/// takes this path.
213/// - `Ok(None)` means neither is set (or the environment copy is empty): the
214/// process was not started by the daemon.
215///
216/// It never changes the environment. Removing either variable would break
217/// any other reader in the process still on the environment copy, and
218/// changing the environment of a multi-threaded process is unsound.
219///
220/// Call it before the process spawns anything. Until the first read the
221/// descriptor is inheritable (it has to be, to survive the daemon's exec),
222/// so a child spawned earlier would inherit the pipe.
223pub fn launch_nonce() -> Result<Option<LaunchNonce>, LaunchNonceError> {
224 PROCESS_NONCE.get(|key| std::env::var_os(key))
225}
226
227fn read_launch_nonce(
228 mut lookup: impl FnMut(&str) -> Option<OsString>,
229 descriptor_reads: &AtomicUsize,
230) -> Cached {
231 #[cfg(unix)]
232 if let Some(value) = lookup(LAUNCH_NONCE_FD_ENV) {
233 return unix::read_descriptor(&value, descriptor_reads);
234 }
235 #[cfg(not(unix))]
236 let _ = descriptor_reads;
237 Ok(lookup(LAUNCH_NONCE_ENV)
238 .and_then(|value| value.into_string().ok())
239 .filter(|value| !value.is_empty())
240 .map(|value| LaunchNonce {
241 value,
242 source: LaunchNonceSource::Env,
243 }))
244}
245
246#[cfg(unix)]
247pub use unix::LaunchNonceHandoff;
248
249#[cfg(unix)]
250mod unix {
251 use std::{
252 ffi::OsStr,
253 fs::File,
254 io::{self, Read, Write},
255 os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd},
256 sync::atomic::{AtomicUsize, Ordering},
257 };
258
259 use super::{Cached, LaunchNonce, LaunchNonceError, LaunchNonceSource, LAUNCH_NONCE_FD};
260
261 /// The daemon half of the handoff, prepared before the spawn: a pipe that
262 /// already holds the nonce, its write end closed, and its inode.
263 ///
264 /// A module uses the same type to hand its own nonce to a helper process
265 /// that must connect as the module: never in argv, the environment or a
266 /// file, all of which another same-user process can read.
267 ///
268 /// ```no_run
269 /// # fn helper(nonce: &str) -> std::io::Result<()> {
270 /// use subc_os::launch_nonce::{LaunchNonceHandoff, LAUNCH_NONCE_FD_ENV};
271 ///
272 /// let mut command = std::process::Command::new("helper");
273 /// let handoff = LaunchNonceHandoff::new(nonce)?;
274 /// command.env(LAUNCH_NONCE_FD_ENV, handoff.fd_env_value());
275 /// // After every other pre-exec step the command has.
276 /// handoff.install_last(&mut command);
277 /// command.spawn()?;
278 /// # Ok(()) }
279 /// ```
280 #[derive(Debug)]
281 pub struct LaunchNonceHandoff {
282 read_end: OwnedFd,
283 inode: u64,
284 target: RawFd,
285 }
286
287 impl LaunchNonceHandoff {
288 /// Make the pipe, write `nonce` into it and close the write end, so a
289 /// reader gets exactly `nonce` and then end of file.
290 ///
291 /// Both ends are created close-on-exec (`std::io::pipe` does that),
292 /// so the read end reaches no child until [`Self::install_last`] puts
293 /// it into one. The nonce must fit in the pipe buffer, or the write
294 /// blocks with no reader: POSIX guarantees 512 bytes and macOS and
295 /// Linux give 16 KiB or more, and a daemon nonce is 64 characters.
296 pub fn new(nonce: &str) -> io::Result<Self> {
297 let (reader, mut writer) = io::pipe()?;
298 writer.write_all(nonce.as_bytes())?;
299 drop(writer);
300 let mut read_end = OwnedFd::from(reader);
301 // std configures the child's stdio before pre-exec callbacks. If a
302 // standard descriptor was closed in the parent, pipe() can use its
303 // number, which stdio setup would overwrite before the handoff runs.
304 // Move it out of that range now, keeping the parent copy close-on-exec.
305 if read_end.as_raw_fd() < LAUNCH_NONCE_FD {
306 // SAFETY: duplicates an owned descriptor; no memory is passed.
307 #[allow(unsafe_code)]
308 let copy = unsafe {
309 libc::fcntl(read_end.as_raw_fd(), libc::F_DUPFD_CLOEXEC, LAUNCH_NONCE_FD)
310 };
311 if copy == -1 {
312 return Err(io::Error::last_os_error());
313 }
314 // SAFETY: the successful fcntl returned a new descriptor owned here.
315 #[allow(unsafe_code)]
316 {
317 read_end = unsafe { OwnedFd::from_raw_fd(copy) };
318 }
319 }
320 let inode = fstat(read_end.as_raw_fd())?.st_ino as u64;
321 Ok(Self {
322 read_end,
323 inode,
324 target: LAUNCH_NONCE_FD,
325 })
326 }
327
328 /// The value to give the child as
329 /// [`LAUNCH_NONCE_FD_ENV`](super::LAUNCH_NONCE_FD_ENV):
330 /// `3:<inode of this pipe>`.
331 pub fn fd_env_value(&self) -> String {
332 format!("{}:{}", self.target, self.inode)
333 }
334
335 /// Arrange for the read end to be descriptor 3 in the process
336 /// `command` spawns, and in no other process. For a tokio `Command`,
337 /// pass `command.as_std_mut()`.
338 ///
339 /// It must be the LAST pre-exec step registered. The standard library
340 /// runs pre-exec steps in registration order, after its own stdio
341 /// setup, and this one closes whatever the child had at descriptor 3.
342 /// A step that runs later and writes through a descriptor it captured
343 /// (the Linux cgroup placement writes to `cgroup.procs`) would find
344 /// the pipe there instead if that descriptor had number 3.
345 ///
346 /// Registering any pre-exec step makes the standard library fork and
347 /// exec instead of using `posix_spawn`, on macOS as on Linux.
348 pub fn install_last(self, command: &mut std::process::Command) {
349 use std::os::unix::process::CommandExt;
350 // SAFETY: the closure runs between fork and exec in a copy of a
351 // possibly multi-threaded process, where only async-signal-safe
352 // calls are sound. `install_in_child` makes only dup2 and fcntl
353 // calls on a descriptor opened before the fork, and allocates
354 // nothing (see the_pre_exec_step_does_not_allocate).
355 #[allow(unsafe_code)]
356 unsafe {
357 command.pre_exec(move || self.install_in_child());
358 }
359 }
360
361 /// Put the read end at the target number without close-on-exec. Runs
362 /// in the forked child: only dup2 and fcntl, and errors built from
363 /// errno, which does not allocate.
364 ///
365 /// When the read end already has the target number, `dup2` would do
366 /// nothing and leave close-on-exec set, so exec would close the
367 /// descriptor; that case clears the flag instead.
368 pub(crate) fn install_in_child(&self) -> io::Result<()> {
369 let source = self.read_end.as_raw_fd();
370 if source == self.target {
371 // SAFETY: fcntl on a descriptor this struct owns; no memory is passed.
372 #[allow(unsafe_code)]
373 let flags = unsafe { libc::fcntl(source, libc::F_GETFD) };
374 if flags == -1 {
375 return Err(io::Error::last_os_error());
376 }
377 // SAFETY: as above.
378 #[allow(unsafe_code)]
379 let set = unsafe { libc::fcntl(source, libc::F_SETFD, flags & !libc::FD_CLOEXEC) };
380 if set == -1 {
381 return Err(io::Error::last_os_error());
382 }
383 return Ok(());
384 }
385 // SAFETY: dup2 takes two integers and touches no memory. It closes
386 // whatever the child had at the target, which is why this step
387 // must run after every other one (see `install_last`). The new
388 // descriptor does not carry close-on-exec, so it survives exec.
389 #[allow(unsafe_code)]
390 if unsafe { libc::dup2(source, self.target) } == -1 {
391 return Err(io::Error::last_os_error());
392 }
393 Ok(())
394 }
395
396 /// A handoff whose read end is placed at `target` instead of 3, so
397 /// tests can exercise the step without claiming descriptor 3 in the
398 /// test process itself.
399 #[cfg(test)]
400 pub(crate) fn with_target(mut self, target: RawFd) -> Self {
401 self.target = target;
402 self
403 }
404
405 #[cfg(test)]
406 pub(crate) fn read_end_fd(&self) -> RawFd {
407 self.read_end.as_raw_fd()
408 }
409
410 #[cfg(test)]
411 pub(crate) fn inode(&self) -> u64 {
412 self.inode
413 }
414 }
415
416 pub(super) fn read_descriptor(value: &OsStr, descriptor_reads: &AtomicUsize) -> Cached {
417 let text = value.to_string_lossy();
418 let malformed = || LaunchNonceError::Malformed {
419 value: text.to_string(),
420 };
421 let (fd_text, inode_text) = text.split_once(':').ok_or_else(malformed)?;
422 let fd: RawFd = fd_text.parse().map_err(|_| malformed())?;
423 let expected_inode: u64 = inode_text.parse().map_err(|_| malformed())?;
424 if fd < 0 {
425 return Err(malformed());
426 }
427
428 // Check what the descriptor is before taking ownership of it. Reading
429 // and closing a descriptor that belongs to other code in this process
430 // would break that code, and a process that inherited the variable
431 // without the descriptor may well have something else at this number.
432 let stat = fstat(fd).map_err(|error| LaunchNonceError::NotOpen {
433 fd,
434 errno: error.raw_os_error().unwrap_or(0),
435 })?;
436 if stat.st_mode & libc::S_IFMT != libc::S_IFIFO {
437 return Err(LaunchNonceError::NotAPipe { fd });
438 }
439 let found_inode = stat.st_ino as u64;
440 if found_inode != expected_inode {
441 return Err(LaunchNonceError::WrongPipe {
442 fd,
443 expected_inode,
444 found_inode,
445 });
446 }
447 // Ask how many bytes are waiting rather than reading to find out, so
448 // an empty pipe is refused without being consumed or closed.
449 let mut waiting: libc::c_int = 0;
450 // SAFETY: FIONREAD writes one int through the pointer, which points
451 // at a live local of that type.
452 #[allow(unsafe_code)]
453 if unsafe { libc::ioctl(fd, libc::FIONREAD, &mut waiting) } == -1 {
454 return Err(LaunchNonceError::Unreadable {
455 fd,
456 errno: io::Error::last_os_error().raw_os_error(),
457 });
458 }
459 if waiting <= 0 {
460 return Err(LaunchNonceError::Empty { fd });
461 }
462
463 descriptor_reads.fetch_add(1, Ordering::SeqCst);
464 // SAFETY: the descriptor is open and is the pipe the daemon named by
465 // inode, so it was handed to this process for this read. Nothing else
466 // in the process reads it: every reader goes through the one cached
467 // accessor, which reaches this line at most once.
468 #[allow(unsafe_code)]
469 let mut file = File::from(unsafe { OwnedFd::from_raw_fd(fd) });
470 let mut bytes = Vec::with_capacity(64);
471 let read = file.read_to_end(&mut bytes);
472 drop(file);
473 read.map_err(|error| LaunchNonceError::Unreadable {
474 fd,
475 errno: error.raw_os_error(),
476 })?;
477 let value = String::from_utf8(bytes).map_err(|_| LaunchNonceError::NotUtf8 { fd })?;
478 Ok(Some(LaunchNonce {
479 value,
480 source: LaunchNonceSource::Fd,
481 }))
482 }
483
484 pub(super) fn fstat(fd: RawFd) -> io::Result<libc::stat> {
485 let mut stat = std::mem::MaybeUninit::<libc::stat>::uninit();
486 // SAFETY: fstat writes one `struct stat` into the buffer, which is
487 // exactly that size, and writes nothing when it fails.
488 #[allow(unsafe_code)]
489 if unsafe { libc::fstat(fd, stat.as_mut_ptr()) } == -1 {
490 return Err(io::Error::last_os_error());
491 }
492 // SAFETY: fstat succeeded, so it filled the buffer.
493 #[allow(unsafe_code)]
494 Ok(unsafe { stat.assume_init() })
495 }
496}
497
498#[cfg(test)]
499pub(crate) mod tests;