Skip to main content

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;