Skip to main content

capsule_runtime/
lifeline.rs

1//! Lifelines: pipes through which a process learns that another process it depends on has ended.
2//!
3//! There are two, and each has one meaning:
4//!
5//! | Variable | Created by | EOF means |
6//! |---|---|---|
7//! | [`FORMATION_LIFELINE_ENV`] | the formation launcher, once per member | the formation has ended |
8//! | [`SPAWNER_LIFELINE_ENV`] | a parent's runtime, once per delegated child | the process that spawned this one has ended |
9//!
10//! The creator hands the started process the read end and holds the only write end. It never
11//! writes to it. When the creator closes the write end, or its process dies by any means —
12//! `SIGKILL` and the OOM killer included — the kernel closes it, and the holder's read returns
13//! zero bytes. The two variables are never interchangeable: a delegated child is never handed its
14//! parent's formation lifeline, because EOF on that would mean the launcher had died, not the
15//! parent.
16//!
17//! * Bytes read are discarded and are not EOF; only a zero-byte read is. Nothing writes to a
18//!   lifeline, so no writer can block on a full pipe or be sent `SIGPIPE`.
19//! * Both ends are `FD_CLOEXEC` from creation. The one descriptor whose flag is cleared is the
20//!   started process's own read end, and only in that process's forked child, by
21//!   [`MemberLifeline::hand_to`] or [`ChildLifeline::hand_to`], so no other process ever holds a
22//!   write end that would keep a dead creator looking alive.
23//! * A holder reads its variable once per process, through [`FormationLifeline::from_env`] or
24//!   [`SpawnerLifeline::from_env`], each with a once-flag of its own. The read validates the
25//!   descriptor, sets `FD_CLOEXEC` on it so nothing the holder spawns inherits it, and takes sole
26//!   ownership of it.
27//! * A read error other than `EINTR` is reported and treated as EOF: a holder that can no longer
28//!   hear its lifeline could not be wound down by anything else once its creator is gone.
29//!
30//! Unlike the formation id, which belongs to the session, a lifeline belongs to the process: a
31//! descriptor is a property of the process holding it.
32//!
33//! Where `pipe2` is missing (darwin), `FD_CLOEXEC` is set after `pipe` returns, so a process
34//! another thread spawns in that instant can inherit a write end and delay that one holder's EOF
35//! until it exits.
36
37/// The variable a formation launcher sets in each member's environment: the decimal number of the
38/// descriptor holding that member's lifeline read end. Runtime-owned: never resolved from the host
39/// for a guest, a shell command or a hook, and never handed to a delegated child.
40pub const FORMATION_LIFELINE_ENV: &str = "MURMUR_FORMATION_LIFELINE";
41
42/// The variable a parent's runtime sets in each delegated child's environment, beside
43/// `MURMUR_SPAWNER`: the decimal number of the descriptor holding that child's spawner lifeline
44/// read end. Runtime-owned on the same terms as [`FORMATION_LIFELINE_ENV`], and never copied from
45/// a parent's environment into its child's: each child is handed a pipe of its own.
46pub const SPAWNER_LIFELINE_ENV: &str = "MURMUR_SPAWNER_LIFELINE";
47
48#[cfg(unix)]
49pub use unix::{ChildLifeline, FormationLifeline, MemberLifeline, SpawnerLifeline};
50
51#[cfg(unix)]
52mod unix {
53    use std::ffi::OsString;
54    use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
55    use std::sync::atomic::{AtomicBool, Ordering};
56
57    use super::{FORMATION_LIFELINE_ENV, SPAWNER_LIFELINE_ENV};
58    use crate::errors::RuntimeError;
59
60    /// Set by the first [`FormationLifeline::from_env`] that finds its variable set. A second read
61    /// would hand out a second owner of the same descriptor number.
62    static FORMATION_TAKEN: AtomicBool = AtomicBool::new(false);
63
64    /// [`FORMATION_TAKEN`] for [`SpawnerLifeline::from_env`]. Separate, because a formation
65    /// member's child holds no formation lifeline, and a member nobody delegated holds no spawner
66    /// lifeline, but a process may in principle hold one of each.
67    static SPAWNER_TAKEN: AtomicBool = AtomicBool::new(false);
68
69    /// The lowest descriptor a lifeline may be: 0, 1 and 2 are a process's standard streams, which
70    /// a spawn replaces in the child.
71    const FIRST_LIFELINE_FD: RawFd = 3;
72
73    /// What differs between the two lifelines: the variable, the error, and the words their
74    /// diagnostics use. Everything else is one mechanism.
75    struct Kind {
76        /// The name of the thread that blocks on the read end.
77        thread: &'static str,
78        /// Which lifeline this is, as a diagnostic names it.
79        name: &'static str,
80        /// Who holds the read end, as a refusal names them.
81        holder: &'static str,
82        /// What EOF says has ended, as a diagnostic names it.
83        ended: &'static str,
84        unreadable: fn(String) -> RuntimeError,
85    }
86
87    static FORMATION: Kind = Kind {
88        thread: "formation-lifeline",
89        name: "formation",
90        holder: "a member",
91        ended: "the formation",
92        unreadable: |reason| RuntimeError::FormationLifelineUnreadable { reason },
93    };
94
95    static SPAWNER: Kind = Kind {
96        thread: "spawner-lifeline",
97        name: "spawner",
98        holder: "a child",
99        ended: "the session that delegated to this one",
100        unreadable: |reason| RuntimeError::SpawnerLifelineUnreadable { reason },
101    };
102
103    impl Kind {
104        fn refuse(&self, reason: impl Into<String>) -> RuntimeError {
105            (self.unreadable)(reason.into())
106        }
107
108        /// The read end `value` names, validated and owned; `Ok(None)` for an absent or blank
109        /// value.
110        ///
111        /// Otherwise the value must be the decimal number, 3 or above, of an open descriptor that
112        /// is a FIFO opened read-only; anything else is an error, as is a second read of a set
113        /// variable under the same `taken`. On success the descriptor has `FD_CLOEXEC` set.
114        #[allow(unsafe_code)]
115        fn read_end(
116            &self,
117            value: Option<OsString>,
118            taken: &AtomicBool,
119        ) -> Result<Option<OwnedFd>, RuntimeError> {
120            let Some(raw) = value else {
121                return Ok(None);
122            };
123            let raw = raw.to_string_lossy();
124            let value = raw.trim();
125            if value.is_empty() {
126                return Ok(None);
127            }
128            if taken.swap(true, Ordering::SeqCst) {
129                return Err(self.refuse(
130                    "it was already read by this process, and a descriptor has one owner",
131                ));
132            }
133            let fd = self.parse_fd(value)?;
134            self.check_read_end(fd)?;
135            set_cloexec(fd).map_err(|error| {
136                self.refuse(format!(
137                    "descriptor {fd} could not be made close-on-exec: {error}"
138                ))
139            })?;
140            // SAFETY: `fd` is open (`check_read_end` read its flags and status), and this is the
141            // one read of the variable in this process (`taken`), so no other owner of the number
142            // was created from it. The creator hands each process a descriptor nothing else in
143            // that process opened, so ownership passes here and nowhere else.
144            Ok(Some(unsafe { OwnedFd::from_raw_fd(fd) }))
145        }
146
147        fn parse_fd(&self, value: &str) -> Result<RawFd, RuntimeError> {
148            if !value.bytes().all(|byte| byte.is_ascii_digit()) {
149                return Err(self.refuse("it is not a decimal descriptor number"));
150            }
151            let fd: RawFd = value
152                .parse()
153                .map_err(|_| self.refuse("it is not a descriptor number this platform has"))?;
154            if fd < FIRST_LIFELINE_FD {
155                return Err(self.refuse(format!(
156                    "descriptor {fd} is a standard stream, and a lifeline is {FIRST_LIFELINE_FD} or above"
157                )));
158            }
159            Ok(fd)
160        }
161
162        /// Refuse `fd` unless it is open, a FIFO, and opened read-only.
163        #[allow(unsafe_code)]
164        fn check_read_end(&self, fd: RawFd) -> Result<(), RuntimeError> {
165            // SAFETY: `F_GETFL` takes no third argument and dereferences nothing; an unopened `fd`
166            // fails with `EBADF`.
167            let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
168            if flags < 0 {
169                return Err(self.refuse(format!("descriptor {fd} is not open")));
170            }
171            // SAFETY: `stat` is a zeroed plain-data struct `fstat` fills in; `fd` is open.
172            let mode = unsafe {
173                let mut stat: libc::stat = std::mem::zeroed();
174                if libc::fstat(fd, &mut stat) != 0 {
175                    return Err(self.refuse(format!(
176                        "descriptor {fd} could not be examined: {}",
177                        std::io::Error::last_os_error()
178                    )));
179                }
180                stat.st_mode
181            };
182            if mode & libc::S_IFMT != libc::S_IFIFO {
183                let kind = match mode & libc::S_IFMT {
184                    libc::S_IFCHR => "a character device",
185                    libc::S_IFREG => "a regular file",
186                    libc::S_IFDIR => "a directory",
187                    libc::S_IFSOCK => "a socket",
188                    _ => "not a pipe",
189                };
190                return Err(self.refuse(format!(
191                    "descriptor {fd} is {kind}, and a lifeline is a pipe's read end"
192                )));
193            }
194            if flags & libc::O_ACCMODE != libc::O_RDONLY {
195                return Err(self.refuse(format!(
196                    "descriptor {fd} is open for writing, and {} holds only a read end",
197                    self.holder
198                )));
199            }
200            Ok(())
201        }
202
203        /// Hand `fd` to a detached thread that blocks on it and calls `on_closed` once, at EOF.
204        /// EOF that came before this call is seen at once. Errors only when the thread cannot be
205        /// started.
206        fn watch(
207            &'static self,
208            fd: OwnedFd,
209            on_closed: impl FnOnce() + Send + 'static,
210        ) -> std::io::Result<()> {
211            std::thread::Builder::new()
212                .name(self.thread.to_string())
213                .spawn(move || {
214                    self.wait_for_eof(fd);
215                    on_closed();
216                })
217                .map(|_| ())
218        }
219
220        /// Block until `fd` reads EOF, discarding any bytes and retrying `EINTR`.
221        fn wait_for_eof(&self, fd: OwnedFd) {
222            use std::io::Read;
223            let mut pipe = std::fs::File::from(fd);
224            let mut buffer = [0u8; 64];
225            loop {
226                match pipe.read(&mut buffer) {
227                    Ok(0) => return,
228                    Ok(_) => {}
229                    Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}
230                    Err(error) => {
231                        crate::runtime_err!(
232                            "[capsule-runtime] the {} lifeline could not be read ({error}); \
233                             treating {} as ended",
234                            self.name,
235                            self.ended
236                        );
237                        return;
238                    }
239                }
240            }
241        }
242    }
243
244    /// A member's read end of its formation lifeline, owned by this process alone.
245    #[derive(Debug)]
246    pub struct FormationLifeline {
247        fd: OwnedFd,
248    }
249
250    impl FormationLifeline {
251        /// This process's formation lifeline, read from [`FORMATION_LIFELINE_ENV`].
252        ///
253        /// `Ok(None)` when the variable is absent or blank. Otherwise the value must be the decimal
254        /// number, 3 or above, of an open descriptor that is a FIFO opened read-only; anything else
255        /// is an error, as is a second read of a set variable in the same process. On success the
256        /// descriptor has `FD_CLOEXEC` set and is owned by the returned value.
257        pub fn from_env() -> Result<Option<Self>, RuntimeError> {
258            Self::from_env_once(&FORMATION_TAKEN)
259        }
260
261        pub(crate) fn from_env_once(taken: &AtomicBool) -> Result<Option<Self>, RuntimeError> {
262            let value = std::env::var_os(FORMATION_LIFELINE_ENV);
263            Ok(FORMATION.read_end(value, taken)?.map(|fd| Self { fd }))
264        }
265
266        /// Hand the lifeline to a detached thread that blocks on it and calls `on_closed` once,
267        /// at EOF. EOF that came before this call is seen at once.
268        ///
269        /// The thread owns the descriptor from here on; nothing else in the process holds or
270        /// closes it. Errors only when the thread cannot be started.
271        pub fn watch(self, on_closed: impl FnOnce() + Send + 'static) -> std::io::Result<()> {
272            FORMATION.watch(self.fd, on_closed)
273        }
274    }
275
276    /// A delegated child's read end of its spawner lifeline, owned by this process alone. EOF on
277    /// it means the process that spawned this one has ended.
278    #[derive(Debug)]
279    pub struct SpawnerLifeline {
280        fd: OwnedFd,
281    }
282
283    impl SpawnerLifeline {
284        /// This process's spawner lifeline, read from [`SPAWNER_LIFELINE_ENV`].
285        ///
286        /// Validated on exactly the terms of [`FormationLifeline::from_env`], under a once-flag of
287        /// its own; errors are [`RuntimeError::SpawnerLifelineUnreadable`].
288        pub fn from_env() -> Result<Option<Self>, RuntimeError> {
289            Self::from_env_once(&SPAWNER_TAKEN)
290        }
291
292        pub(crate) fn from_env_once(taken: &AtomicBool) -> Result<Option<Self>, RuntimeError> {
293            let value = std::env::var_os(SPAWNER_LIFELINE_ENV);
294            Ok(SPAWNER.read_end(value, taken)?.map(|fd| Self { fd }))
295        }
296
297        /// Hand the lifeline to a detached `spawner-lifeline` thread that blocks on it and calls
298        /// `on_closed` once, at EOF. EOF that came before this call is seen at once.
299        ///
300        /// The thread owns the descriptor from here on. Errors only when the thread cannot be
301        /// started.
302        pub fn watch(self, on_closed: impl FnOnce() + Send + 'static) -> std::io::Result<()> {
303            SPAWNER.watch(self.fd, on_closed)
304        }
305    }
306
307    #[allow(unsafe_code)]
308    fn set_cloexec(fd: RawFd) -> std::io::Result<()> {
309        // SAFETY: `F_GETFD`/`F_SETFD` take and return integers and dereference nothing; `fd` is
310        // open.
311        let set = unsafe {
312            let flags = libc::fcntl(fd, libc::F_GETFD);
313            flags >= 0 && libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) == 0
314        };
315        if set {
316            Ok(())
317        } else {
318            Err(std::io::Error::last_os_error())
319        }
320    }
321
322    /// The creator's side of one lifeline: the write end, and the read end until the process it
323    /// is for has been spawned with it. Dropping it closes whatever it still holds.
324    #[derive(Debug)]
325    struct PipeEnds {
326        read: Option<OwnedFd>,
327        write: Option<OwnedFd>,
328    }
329
330    impl PipeEnds {
331        /// A new pipe, both ends `FD_CLOEXEC`, the read end numbered 3 or above.
332        #[allow(unsafe_code)]
333        fn new() -> std::io::Result<Self> {
334            let [read, write] = cloexec_pipe()?;
335            // A process started with its standard input closed is handed descriptor 0 by `pipe`,
336            // and a spawn replaces a child's 0, 1 and 2.
337            let read = if read.as_raw_fd() < FIRST_LIFELINE_FD {
338                // SAFETY: `F_DUPFD_CLOEXEC` takes an integer and dereferences nothing; `read` is
339                // open and stays owned by its `OwnedFd`, which is dropped below.
340                let duplicate = unsafe {
341                    libc::fcntl(read.as_raw_fd(), libc::F_DUPFD_CLOEXEC, FIRST_LIFELINE_FD)
342                };
343                if duplicate < 0 {
344                    return Err(std::io::Error::last_os_error());
345                }
346                // SAFETY: `duplicate` was just returned by `fcntl` and is owned by nothing else.
347                unsafe { OwnedFd::from_raw_fd(duplicate) }
348            } else {
349                read
350            };
351            Ok(Self {
352                read: Some(read),
353                write: Some(write),
354            })
355        }
356
357        /// The `(name, value)` pair naming the read end, while it is still held.
358        fn env_pair(&self, name: &'static str) -> Option<(&'static str, String)> {
359            self.read
360                .as_ref()
361                .map(|read| (name, read.as_raw_fd().to_string()))
362        }
363
364        /// Set `name` to the read end's number and register a `pre_exec` that clears
365        /// `FD_CLOEXEC` on that one descriptor in the child. Does nothing once the read end has
366        /// been released.
367        #[allow(unsafe_code)]
368        fn hand_to(&self, name: &'static str, command: &mut std::process::Command) {
369            use std::os::unix::process::CommandExt;
370            let Some((name, value)) = self.env_pair(name) else {
371                return;
372            };
373            let fd = self.read.as_ref().map_or(-1, AsRawFd::as_raw_fd);
374            command.env(name, value);
375            // SAFETY: the closure runs in the forked child between `fork` and `exec`, where only
376            // async-signal-safe calls are allowed. It makes two `fcntl` calls on an integer it
377            // captured by value: no allocation, no lock, no reference into the parent.
378            unsafe {
379                command.pre_exec(move || {
380                    let flags = libc::fcntl(fd, libc::F_GETFD);
381                    if flags < 0 || libc::fcntl(fd, libc::F_SETFD, flags & !libc::FD_CLOEXEC) != 0 {
382                        return Err(std::io::Error::last_os_error());
383                    }
384                    Ok(())
385                });
386            }
387        }
388
389        fn write_fd(&self) -> Option<RawFd> {
390            self.write.as_ref().map(AsRawFd::as_raw_fd)
391        }
392    }
393
394    /// The launcher's side of one member's lifeline: the write end it holds for the formation's
395    /// life, and the read end until the member has been spawned with it.
396    ///
397    /// Dropping it closes whatever it still holds, so a launcher that is gone has closed every
398    /// lifeline.
399    #[derive(Debug)]
400    pub struct MemberLifeline {
401        ends: PipeEnds,
402    }
403
404    impl MemberLifeline {
405        /// A new pipe, both ends `FD_CLOEXEC`, the read end numbered 3 or above.
406        ///
407        /// Where `pipe2` is missing the flag is set after `pipe` returns, so a process that spawns
408        /// from other threads must create every lifeline before its first spawn.
409        pub fn new() -> std::io::Result<Self> {
410            Ok(Self {
411                ends: PipeEnds::new()?,
412            })
413        }
414
415        /// Give the member `command` will start its read end: set [`FORMATION_LIFELINE_ENV`] and
416        /// register a `pre_exec` that clears `FD_CLOEXEC` on that one descriptor in the child.
417        ///
418        /// Register it after any fd-hygiene `pre_exec`, which marks every inherited descriptor
419        /// close-on-exec; `pre_exec` closures run in registration order. The child never gets the
420        /// write end, which stays close-on-exec. Does nothing once [`Self::spawned`] has run.
421        pub fn hand_to(&self, command: &mut std::process::Command) {
422            self.ends.hand_to(FORMATION_LIFELINE_ENV, command);
423        }
424
425        /// Close the launcher's copy of the read end once the member holds its own, so the member
426        /// is the read end's only holder.
427        pub fn spawned(&mut self) {
428            self.ends.read = None;
429        }
430
431        /// Close the write end: the member reads EOF, which tells it the formation has ended.
432        /// Idempotent.
433        pub fn close(&mut self) {
434            self.ends.write = None;
435        }
436
437        /// The write end's descriptor number, while it is open.
438        pub fn write_fd(&self) -> Option<RawFd> {
439            self.ends.write_fd()
440        }
441    }
442
443    /// A parent runtime's side of one delegated child's spawner lifeline: the write end it holds
444    /// for as long as the child is its to end, and the read end until the child has been spawned
445    /// with it.
446    ///
447    /// Dropping it closes whatever it still holds; so does this process ending, by any means.
448    #[derive(Debug)]
449    pub struct ChildLifeline {
450        ends: PipeEnds,
451    }
452
453    impl ChildLifeline {
454        /// A new pipe, both ends `FD_CLOEXEC`, the read end numbered 3 or above.
455        ///
456        /// Where `pipe2` is missing the flag is set after `pipe` returns, so a process another
457        /// thread spawns in that instant can inherit a write end and hold that child's EOF back
458        /// until it exits.
459        pub fn new() -> std::io::Result<Self> {
460            Ok(Self {
461                ends: PipeEnds::new()?,
462            })
463        }
464
465        /// Give the child `command` will start its read end: set [`SPAWNER_LIFELINE_ENV`] and
466        /// register a `pre_exec` that clears `FD_CLOEXEC` on that one descriptor in the child.
467        ///
468        /// The `pre_exec` makes two `fcntl` calls and nothing else; register it last. The child
469        /// never gets the write end, which stays close-on-exec. Does nothing once
470        /// [`Self::spawned`] has run.
471        pub fn hand_to(&self, command: &mut std::process::Command) {
472            self.ends.hand_to(SPAWNER_LIFELINE_ENV, command);
473        }
474
475        /// The `(name, value)` pair [`Self::hand_to`] sets, while the read end is still held.
476        pub fn env_pair(&self) -> Option<(&'static str, String)> {
477            self.ends.env_pair(SPAWNER_LIFELINE_ENV)
478        }
479
480        /// Close this process's copy of the read end once the child holds its own, so the child
481        /// is the read end's only holder.
482        pub fn spawned(&mut self) {
483            self.ends.read = None;
484        }
485
486        /// Close the write end: the child reads EOF, which tells it its spawner has ended.
487        /// Idempotent.
488        pub fn close(&mut self) {
489            self.ends.write = None;
490        }
491
492        /// The write end's descriptor number, while it is open.
493        pub fn write_fd(&self) -> Option<RawFd> {
494            self.ends.write_fd()
495        }
496    }
497
498    #[cfg(any(target_os = "linux", target_os = "android", target_os = "freebsd"))]
499    #[allow(unsafe_code)]
500    fn cloexec_pipe() -> std::io::Result<[OwnedFd; 2]> {
501        let mut fds = [0 as RawFd; 2];
502        // SAFETY: `fds` is a live two-element array `pipe2` writes the two new descriptors into.
503        let created = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC) };
504        if created != 0 {
505            return Err(std::io::Error::last_os_error());
506        }
507        // SAFETY: `pipe2` succeeded, so both numbers are open descriptors owned by nothing else.
508        Ok(unsafe { [OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1])] })
509    }
510
511    #[cfg(not(any(target_os = "linux", target_os = "android", target_os = "freebsd")))]
512    #[allow(unsafe_code)]
513    fn cloexec_pipe() -> std::io::Result<[OwnedFd; 2]> {
514        let mut fds = [0 as RawFd; 2];
515        // SAFETY: `fds` is a live two-element array `pipe` writes the two new descriptors into.
516        let created = unsafe { libc::pipe(fds.as_mut_ptr()) };
517        if created != 0 {
518            return Err(std::io::Error::last_os_error());
519        }
520        // SAFETY: `pipe` succeeded, so both numbers are open descriptors owned by nothing else.
521        let ends = unsafe { [OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1])] };
522        for end in &ends {
523            set_cloexec(end.as_raw_fd())?;
524        }
525        Ok(ends)
526    }
527
528    #[cfg(test)]
529    #[allow(unsafe_code)]
530    mod tests {
531        use super::*;
532        use std::os::fd::IntoRawFd;
533        use std::sync::mpsc;
534        use std::time::Duration;
535
536        /// Serializes the tests that set a lifeline variable in this test binary's process
537        /// environment.
538        fn env_guard() -> std::sync::MutexGuard<'static, ()> {
539            crate::formation::FORMATION_ENV_LOCK
540                .lock()
541                .unwrap_or_else(|poisoned| poisoned.into_inner())
542        }
543
544        fn fd_flags(fd: RawFd) -> libc::c_int {
545            // SAFETY: `F_GETFD` takes and returns integers.
546            unsafe { libc::fcntl(fd, libc::F_GETFD) }
547        }
548
549        /// Read the formation lifeline the variable names, with a fresh once-flag standing in for
550        /// the process-wide one.
551        fn read_with(
552            value: &str,
553            taken: &AtomicBool,
554        ) -> Result<Option<FormationLifeline>, RuntimeError> {
555            std::env::set_var(FORMATION_LIFELINE_ENV, value);
556            let read = FormationLifeline::from_env_once(taken);
557            std::env::remove_var(FORMATION_LIFELINE_ENV);
558            read
559        }
560
561        /// [`read_with`] for the spawner lifeline.
562        fn read_spawner_with(
563            value: &str,
564            taken: &AtomicBool,
565        ) -> Result<Option<SpawnerLifeline>, RuntimeError> {
566            std::env::set_var(SPAWNER_LIFELINE_ENV, value);
567            let read = SpawnerLifeline::from_env_once(taken);
568            std::env::remove_var(SPAWNER_LIFELINE_ENV);
569            read
570        }
571
572        /// A pipe whose read end has been released by its creator, as a started process's would
573        /// be: the descriptor number, and the creator side holding only the write end.
574        fn released(ends: &mut PipeEnds) -> RawFd {
575            ends.read.take().unwrap().into_raw_fd()
576        }
577
578        fn member_side() -> (RawFd, MemberLifeline) {
579            let mut lifeline = MemberLifeline::new().unwrap();
580            (released(&mut lifeline.ends), lifeline)
581        }
582
583        fn child_side() -> (RawFd, ChildLifeline) {
584            let mut lifeline = ChildLifeline::new().unwrap();
585            (released(&mut lifeline.ends), lifeline)
586        }
587
588        /// The highest descriptor this process may have: other test threads open and close
589        /// descriptors concurrently, and the kernel hands out the lowest free number, so a number
590        /// just closed here could be reused before it is read.
591        fn closed_fd() -> RawFd {
592            // SAFETY: `rlimit` is a zeroed plain-data struct `getrlimit` fills in.
593            let limit = unsafe {
594                let mut limit: libc::rlimit = std::mem::zeroed();
595                assert_eq!(libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit), 0);
596                limit.rlim_cur
597            };
598            let fd = RawFd::try_from(limit.saturating_sub(1)).unwrap_or(RawFd::MAX);
599            assert!(fd_flags(fd) < 0, "descriptor {fd} is open");
600            fd
601        }
602
603        #[test]
604        fn lifeline_from_env_sets_cloexec_and_a_second_read_is_refused() {
605            let _guard = env_guard();
606            let (fd, _launcher) = member_side();
607            // SAFETY: `F_SETFD` on a descriptor this test owns.
608            unsafe {
609                libc::fcntl(fd, libc::F_SETFD, 0);
610            }
611            assert_eq!(fd_flags(fd) & libc::FD_CLOEXEC, 0);
612            let taken = AtomicBool::new(false);
613            let lifeline = read_with(&fd.to_string(), &taken).unwrap().unwrap();
614            assert_eq!(lifeline.fd.as_raw_fd(), fd);
615            assert_ne!(fd_flags(fd) & libc::FD_CLOEXEC, 0);
616
617            let again = read_with(&fd.to_string(), &taken).unwrap_err().to_string();
618            assert!(again.contains(FORMATION_LIFELINE_ENV), "{again}");
619            assert!(again.contains("already read"), "{again}");
620        }
621
622        #[test]
623        fn spawner_lifeline_from_env_sets_cloexec_and_a_second_read_is_refused() {
624            let _guard = env_guard();
625            let (fd, _parent) = child_side();
626            // SAFETY: `F_SETFD` on a descriptor this test owns.
627            unsafe {
628                libc::fcntl(fd, libc::F_SETFD, 0);
629            }
630            let taken = AtomicBool::new(false);
631            let lifeline = read_spawner_with(&fd.to_string(), &taken).unwrap().unwrap();
632            assert_eq!(lifeline.fd.as_raw_fd(), fd);
633            assert_ne!(fd_flags(fd) & libc::FD_CLOEXEC, 0);
634
635            let again = read_spawner_with(&fd.to_string(), &taken)
636                .unwrap_err()
637                .to_string();
638            assert!(again.contains(SPAWNER_LIFELINE_ENV), "{again}");
639            assert!(again.contains("already read"), "{again}");
640        }
641
642        /// The two reads are guarded separately: taking the spawner lifeline leaves the formation
643        /// lifeline readable, and the other way round.
644        #[test]
645        fn spawner_and_formation_lifelines_do_not_share_a_once_flag() {
646            assert!(!std::ptr::eq(&SPAWNER_TAKEN, &FORMATION_TAKEN));
647            let _guard = env_guard();
648            let (spawner_fd, _parent) = child_side();
649            let (formation_fd, _launcher) = member_side();
650            std::env::set_var(SPAWNER_LIFELINE_ENV, spawner_fd.to_string());
651            std::env::set_var(FORMATION_LIFELINE_ENV, formation_fd.to_string());
652            let spawner_taken = AtomicBool::new(false);
653            let formation_taken = AtomicBool::new(false);
654            let spawner = SpawnerLifeline::from_env_once(&spawner_taken);
655            let formation = FormationLifeline::from_env_once(&formation_taken);
656            std::env::remove_var(SPAWNER_LIFELINE_ENV);
657            std::env::remove_var(FORMATION_LIFELINE_ENV);
658            assert_eq!(spawner.unwrap().unwrap().fd.as_raw_fd(), spawner_fd);
659            assert_eq!(formation.unwrap().unwrap().fd.as_raw_fd(), formation_fd);
660        }
661
662        #[test]
663        fn lifeline_absent_or_blank_is_none() {
664            let _guard = env_guard();
665            std::env::remove_var(FORMATION_LIFELINE_ENV);
666            std::env::remove_var(SPAWNER_LIFELINE_ENV);
667            let taken = AtomicBool::new(false);
668            assert!(FormationLifeline::from_env_once(&taken).unwrap().is_none());
669            assert!(read_with("  ", &taken).unwrap().is_none());
670            assert!(SpawnerLifeline::from_env_once(&taken).unwrap().is_none());
671            assert!(read_spawner_with("  ", &taken).unwrap().is_none());
672            assert!(
673                !taken.load(Ordering::SeqCst),
674                "an absent lifeline takes nothing"
675            );
676        }
677
678        /// Values that are not a read end, each with the words its refusal must carry, and the
679        /// descriptors they name, which stay open while the caller holds them.
680        #[allow(clippy::type_complexity)]
681        fn refusals() -> (
682            Vec<(String, &'static str)>,
683            (std::fs::File, std::fs::File, ChildLifeline),
684        ) {
685            let file = tempfile::tempfile().unwrap();
686            let null = std::fs::File::open("/dev/null").unwrap();
687            let launcher = ChildLifeline::new().unwrap();
688            let write = launcher.write_fd().unwrap();
689            let cases = vec![
690                ("three".to_string(), "not a decimal descriptor number"),
691                ("-4".to_string(), "not a decimal descriptor number"),
692                ("2".to_string(), "standard stream"),
693                (closed_fd().to_string(), "is not open"),
694                (null.as_raw_fd().to_string(), "a character device"),
695                (file.as_raw_fd().to_string(), "a regular file"),
696                (write.to_string(), "open for writing"),
697            ];
698            (cases, (file, null, launcher))
699        }
700
701        #[test]
702        fn lifeline_values_that_are_not_a_read_end_are_refused() {
703            let _guard = env_guard();
704            let (cases, _held) = refusals();
705            for (value, expected) in cases {
706                let taken = AtomicBool::new(false);
707                let refused = read_with(&value, &taken).unwrap_err().to_string();
708                assert!(refused.contains(FORMATION_LIFELINE_ENV), "{refused}");
709                assert!(refused.contains(expected), "{value}: {refused}");
710                if expected == "open for writing" {
711                    assert!(refused.contains("a member holds only"), "{refused}");
712                }
713            }
714        }
715
716        #[test]
717        fn spawner_lifeline_values_that_are_not_a_read_end_are_refused() {
718            let _guard = env_guard();
719            let (cases, _held) = refusals();
720            for (value, expected) in cases {
721                let taken = AtomicBool::new(false);
722                let refused = read_spawner_with(&value, &taken).unwrap_err();
723                assert!(
724                    matches!(refused, RuntimeError::SpawnerLifelineUnreadable { .. }),
725                    "{refused:?}"
726                );
727                let refused = refused.to_string();
728                assert!(refused.contains(SPAWNER_LIFELINE_ENV), "{refused}");
729                assert!(refused.contains(expected), "{value}: {refused}");
730                if expected == "open for writing" {
731                    assert!(refused.contains("a child holds only"), "{refused}");
732                }
733            }
734        }
735
736        /// What `sh` reports about the descriptors it inherited: the variable's value, then
737        /// `read` and `write` for whichever end it holds.
738        fn handed(variable: &str, read: RawFd, write: RawFd) -> std::process::Command {
739            let mut command = std::process::Command::new("sh");
740            command.arg("-c").arg(format!(
741                "echo \"${variable}\"; \
742                 if [ -e /dev/fd/{read} ]; then echo read; fi; \
743                 if [ -e /dev/fd/{write} ]; then echo write; fi"
744            ));
745            command.stdout(std::process::Stdio::piped());
746            command
747        }
748
749        #[test]
750        fn lifeline_hand_to_gives_a_child_its_read_end_and_no_write_end() {
751            let mut lifeline = MemberLifeline::new().unwrap();
752            let (read, write) = (
753                lifeline.ends.read.as_ref().unwrap().as_raw_fd(),
754                lifeline.write_fd().unwrap(),
755            );
756            let mut command = handed(FORMATION_LIFELINE_ENV, read, write);
757            lifeline.hand_to(&mut command);
758            let child = command.spawn().unwrap();
759            lifeline.spawned();
760            let output = child.wait_with_output().unwrap();
761            let stdout = String::from_utf8_lossy(&output.stdout);
762            assert_eq!(
763                stdout.lines().collect::<Vec<_>>(),
764                [&read.to_string(), "read"]
765            );
766            assert!(
767                lifeline.ends.env_pair(FORMATION_LIFELINE_ENV).is_none(),
768                "the launcher released its read end"
769            );
770            assert!(lifeline.write_fd().is_some());
771        }
772
773        #[test]
774        fn spawner_lifeline_hand_to_gives_a_child_its_read_end_and_no_write_end() {
775            let mut lifeline = ChildLifeline::new().unwrap();
776            let (read, write) = (
777                lifeline.ends.read.as_ref().unwrap().as_raw_fd(),
778                lifeline.write_fd().unwrap(),
779            );
780            assert_eq!(
781                lifeline.env_pair(),
782                Some((SPAWNER_LIFELINE_ENV, read.to_string()))
783            );
784            let mut command = handed(SPAWNER_LIFELINE_ENV, read, write);
785            lifeline.hand_to(&mut command);
786            let child = command.spawn().unwrap();
787            lifeline.spawned();
788            let output = child.wait_with_output().unwrap();
789            let stdout = String::from_utf8_lossy(&output.stdout);
790            assert_eq!(
791                stdout.lines().collect::<Vec<_>>(),
792                [&read.to_string(), "read"]
793            );
794            assert!(
795                lifeline.env_pair().is_none(),
796                "the parent released its read end"
797            );
798            assert!(lifeline.write_fd().is_some());
799        }
800
801        /// Watch `fd` as `kind` would, and report each call of `on_closed` on the channel.
802        fn watched(kind: &'static Kind, fd: RawFd) -> mpsc::Receiver<()> {
803            // SAFETY: `fd` is a read end the calling test released and owns nothing else of.
804            let fd = unsafe { OwnedFd::from_raw_fd(fd) };
805            let (tx, rx) = mpsc::channel();
806            kind.watch(fd, move || tx.send(()).unwrap()).unwrap();
807            rx
808        }
809
810        #[test]
811        fn lifeline_closing_the_write_end_fires_the_watcher_once() {
812            let _guard = env_guard();
813            let (fd, mut launcher) = member_side();
814            let taken = AtomicBool::new(false);
815            let lifeline = read_with(&fd.to_string(), &taken).unwrap().unwrap();
816            let (tx, rx) = mpsc::channel();
817            lifeline.watch(move || tx.send(()).unwrap()).unwrap();
818            assert!(rx.recv_timeout(Duration::from_millis(300)).is_err());
819            launcher.close();
820            rx.recv_timeout(Duration::from_secs(5))
821                .expect("EOF fires the watcher");
822            assert!(
823                matches!(
824                    rx.recv_timeout(Duration::from_millis(300)),
825                    Err(mpsc::RecvTimeoutError::Disconnected)
826                ),
827                "the watcher fires once and is gone"
828            );
829        }
830
831        #[test]
832        fn spawner_lifeline_closing_the_write_end_fires_the_watcher_once() {
833            let _guard = env_guard();
834            let (fd, mut parent) = child_side();
835            let taken = AtomicBool::new(false);
836            let lifeline = read_spawner_with(&fd.to_string(), &taken).unwrap().unwrap();
837            let (tx, rx) = mpsc::channel();
838            lifeline.watch(move || tx.send(()).unwrap()).unwrap();
839            assert!(rx.recv_timeout(Duration::from_millis(300)).is_err());
840            parent.close();
841            parent.close();
842            rx.recv_timeout(Duration::from_secs(5))
843                .expect("EOF fires the watcher");
844            assert!(
845                matches!(
846                    rx.recv_timeout(Duration::from_millis(300)),
847                    Err(mpsc::RecvTimeoutError::Disconnected)
848                ),
849                "the watcher fires once and is gone"
850            );
851        }
852
853        /// Dropping the parent's side is a close: a parent that is gone has closed every child's
854        /// lifeline.
855        #[test]
856        fn spawner_lifeline_dropping_the_parent_side_fires_the_watcher() {
857            let (fd, parent) = child_side();
858            let rx = watched(&SPAWNER, fd);
859            assert!(rx.recv_timeout(Duration::from_millis(300)).is_err());
860            drop(parent);
861            rx.recv_timeout(Duration::from_secs(5))
862                .expect("dropping the write end fires the watcher");
863        }
864
865        #[test]
866        fn lifeline_eof_before_watch_fires_at_once() {
867            let _guard = env_guard();
868            let (fd, mut launcher) = member_side();
869            launcher.close();
870            let taken = AtomicBool::new(false);
871            let lifeline = read_with(&fd.to_string(), &taken).unwrap().unwrap();
872            let (tx, rx) = mpsc::channel();
873            lifeline.watch(move || tx.send(()).unwrap()).unwrap();
874            rx.recv_timeout(Duration::from_secs(5))
875                .expect("sticky EOF fires the watcher");
876        }
877
878        #[test]
879        fn spawner_lifeline_eof_before_watch_fires_at_once() {
880            let (fd, mut parent) = child_side();
881            parent.close();
882            let rx = watched(&SPAWNER, fd);
883            rx.recv_timeout(Duration::from_secs(5))
884                .expect("sticky EOF fires the watcher");
885        }
886
887        fn bytes_do_not_fire(kind: &'static Kind, fd: RawFd, write: RawFd) -> mpsc::Receiver<()> {
888            let rx = watched(kind, fd);
889            let bytes = [7u8; 64];
890            // SAFETY: writes from a live 64-byte array to a write end the caller holds.
891            let written =
892                unsafe { libc::write(write, bytes.as_ptr().cast::<libc::c_void>(), bytes.len()) };
893            assert_eq!(written, 64);
894            assert!(
895                rx.recv_timeout(Duration::from_millis(500)).is_err(),
896                "bytes are not EOF"
897            );
898            rx
899        }
900
901        #[test]
902        fn lifeline_bytes_with_the_write_end_open_do_not_fire_the_watcher() {
903            let (fd, mut launcher) = member_side();
904            let rx = bytes_do_not_fire(&FORMATION, fd, launcher.write_fd().unwrap());
905            launcher.close();
906            rx.recv_timeout(Duration::from_secs(5))
907                .expect("EOF after the bytes fires");
908        }
909
910        #[test]
911        fn spawner_lifeline_bytes_with_the_write_end_open_do_not_fire_the_watcher() {
912            let (fd, mut parent) = child_side();
913            let rx = bytes_do_not_fire(&SPAWNER, fd, parent.write_fd().unwrap());
914            parent.close();
915            rx.recv_timeout(Duration::from_secs(5))
916                .expect("EOF after the bytes fires");
917        }
918
919        #[test]
920        fn the_watcher_threads_are_named_for_their_lifeline() {
921            assert_eq!(FORMATION.thread, "formation-lifeline");
922            assert_eq!(SPAWNER.thread, "spawner-lifeline");
923        }
924    }
925}