Skip to main content

lean_ctx/shell/containment/
gate.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Starting a process that does not run before its guard is ready.
3//!
4//! A guard joins the group of a process that already exists. If that process
5//! were already running its program, whoever started it could be killed
6//! between the start and the guard's readiness and leave the program running
7//! with nothing to end it. So the new process waits at a gate — after the
8//! fork, in its own group, before the program is executed — until the guard
9//! has pinned its group and reported ready. The gate is a pipe whose write
10//! end only the starting process holds: opened by one byte, and at its end
11//! when that process is gone, in which case the new process exits without
12//! ever executing the program.
13
14use std::io::Read as _;
15use std::os::fd::{AsRawFd as _, RawFd};
16use std::os::unix::process::CommandExt as _;
17use std::process::{Child, Command};
18use std::thread::ScopedJoinHandle;
19
20use super::CallGuard;
21
22/// Why nothing was started.
23pub enum Refusal {
24    /// The process could not be created or its program not executed.
25    NotStarted(std::io::Error),
26    /// The new process does not lead a group of its own. Nothing ran.
27    NotLeader,
28    /// No guard could be attached. Nothing ran.
29    NoGuard(String),
30}
31
32/// Starts `command` — which must ask for a process group of its own — with a
33/// guard on that group. The program is executed only after the guard is
34/// ready; on every refusal it was never executed, so there is nothing to end.
35///
36/// `command` is left with the gate's `pre_exec` step and must not be spawned
37/// a second time.
38pub fn start_behind_guard(command: &mut Command) -> Result<(Child, CallGuard), Refusal> {
39    let (gate, open) = std::io::pipe().map_err(Refusal::NotStarted)?;
40    let (mut announced, announce) = std::io::pipe().map_err(Refusal::NotStarted)?;
41    let (gate_fd, open_fd, announce_fd) =
42        (gate.as_raw_fd(), open.as_raw_fd(), announce.as_raw_fd());
43    // SAFETY: the closure runs in the forked child and calls only close,
44    // getpid, write and read, all async-signal-safe.
45    unsafe {
46        command.pre_exec(move || wait_at_gate(gate_fd, open_fd, announce_fd));
47    }
48    // `spawn` returns only once the program is executed, which the gate
49    // holds back: it runs on a thread of its own while this one sees to the
50    // guard. The thread owns the ends the child inherits, so they are open
51    // at the fork and closed here when `spawn` is done.
52    std::thread::scope(|scope| {
53        let spawning = scope.spawn(move || {
54            let spawned = command.spawn();
55            drop((gate, announce));
56            spawned
57        });
58        let mut pid = [0_u8; size_of::<libc::pid_t>()];
59        if announced.read_exact(&mut pid).is_err() {
60            // No process came into being, or it ended before it said who it is.
61            drop(open);
62            return Err(Refusal::NotStarted(match finish(spawning) {
63                Err(error) => error,
64                Ok(_) => std::io::Error::other("the process did not wait for its guard"),
65            }));
66        }
67        let pid = libc::pid_t::from_ne_bytes(pid);
68        #[cfg(any(test, feature = "test-support"))]
69        pause_before_guard(pid);
70        // SAFETY: reads the group of our own child, which waits at the gate.
71        let leads = unsafe { libc::getpgid(pid) } == pid;
72        let guard = if leads {
73            attach_guard(pid).map_err(Refusal::NoGuard)
74        } else {
75            Err(Refusal::NotLeader)
76        };
77        let guard = match guard {
78            Ok(guard) => guard,
79            Err(refusal) => {
80                // The gate ends unopened: the process exits, `spawn` reaps it.
81                drop(open);
82                let _ = finish(spawning);
83                return Err(refusal);
84            }
85        };
86        let opened = super::write_fd(open.as_raw_fd(), &[1]);
87        drop(open);
88        match finish(spawning) {
89            Ok(child) if opened => Ok((child, guard)),
90            // Dropping the guard lets go of it: it ends the group and is reaped.
91            Ok(_) => Err(Refusal::NotStarted(std::io::Error::other(
92                "the gate could not be opened",
93            ))),
94            Err(error) => Err(Refusal::NotStarted(error)),
95        }
96    })
97}
98
99fn finish(spawning: ScopedJoinHandle<'_, std::io::Result<Child>>) -> std::io::Result<Child> {
100    spawning
101        .join()
102        .unwrap_or_else(|_| Err(std::io::Error::other("the start did not finish")))
103}
104
105/// The guard of the process that waits at the gate and leads its own group.
106#[cfg(not(any(test, feature = "test-support")))]
107fn attach_guard(process: libc::pid_t) -> Result<CallGuard, String> {
108    CallGuard::attach_to(process, process)
109}
110
111/// As above; a test can make the attach fail the two ways it can fail.
112#[cfg(any(test, feature = "test-support"))]
113fn attach_guard(process: libc::pid_t) -> Result<CallGuard, String> {
114    let nowhere = super::tests::NO_SUCH_GROUP;
115    match GUARD_FAULT.with(std::cell::Cell::get) {
116        None => CallGuard::attach_to(process, process),
117        Some(GuardFault::CannotJoin) => CallGuard::attach_to(nowhere, nowhere),
118        Some(GuardFault::WrongGroup) => CallGuard::attach_to(process, nowhere),
119    }
120}
121
122/// Tests only: how the next guards started on this thread fail.
123#[cfg(any(test, feature = "test-support"))]
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum GuardFault {
126    /// The guard cannot be put into the group at all.
127    CannotJoin,
128    /// The guard is in the group but was told to guard another one: it never
129    /// reports ready.
130    WrongGroup,
131}
132
133#[cfg(any(test, feature = "test-support"))]
134thread_local! {
135    static GUARD_FAULT: std::cell::Cell<Option<GuardFault>> = const { std::cell::Cell::new(None) };
136    static LAST_GATED: std::cell::Cell<libc::pid_t> = const { std::cell::Cell::new(0) };
137}
138
139/// Tests only: every guard attached behind the gate on this thread fails as
140/// `fault` says, until it is reset with `None`.
141#[cfg(any(test, feature = "test-support"))]
142pub fn set_guard_fault_for_test(fault: Option<GuardFault>) {
143    GUARD_FAULT.with(|current| current.set(fault));
144}
145
146/// Tests only: the process that last waited at a gate started on this thread.
147#[cfg(any(test, feature = "test-support"))]
148pub(crate) fn last_gated_for_test() -> libc::pid_t {
149    LAST_GATED.with(std::cell::Cell::get)
150}
151
152/// Tests only: notes the waiting process, and — when asked to — holds the
153/// starting process between "the new process exists" and "attach its guard"
154/// and says so on standard output, so that a test can end the starting
155/// process exactly there.
156#[cfg(any(test, feature = "test-support"))]
157fn pause_before_guard(process: libc::pid_t) {
158    LAST_GATED.with(|last| last.set(process));
159    if std::env::var_os(super::tests::PAUSE_ENV).is_some() {
160        let _ = super::write_fd(
161            libc::STDOUT_FILENO,
162            format!("FORKED {process}\n").as_bytes(),
163        );
164        std::thread::sleep(std::time::Duration::from_mins(1));
165    }
166}
167
168/// The child's side, between fork and exec: says who it is, then waits until
169/// the gate is opened. An `Err` makes the child exit without executing.
170fn wait_at_gate(gate: RawFd, open: RawFd, announce: RawFd) -> std::io::Result<()> {
171    // SAFETY: integer-only calls on descriptors inherited through the fork.
172    // The child's copy of the gate's write end is closed first: only the
173    // starting process keeps the gate from ending.
174    unsafe {
175        libc::close(open);
176        let pid = libc::getpid().to_ne_bytes();
177        let told = libc::write(announce, pid.as_ptr().cast(), pid.len());
178        libc::close(announce);
179        if told != pid.len() as isize {
180            return Err(std::io::Error::from_raw_os_error(libc::EPIPE));
181        }
182        let mut byte = 0_u8;
183        loop {
184            match libc::read(gate, (&raw mut byte).cast(), 1) {
185                1 => return Ok(()),
186                0 => return Err(std::io::Error::from_raw_os_error(libc::ECANCELED)),
187                _ => {
188                    let error = std::io::Error::last_os_error();
189                    if error.raw_os_error() != Some(libc::EINTR) {
190                        return Err(error);
191                    }
192                }
193            }
194        }
195    }
196}