Skip to main content

ic_host_process/child/gate/
mod.rs

1//! Caller-released Unix command launch gate, without capture or readiness policy.
2#![allow(
3    unsafe_code,
4    reason = "reviewed async-signal-safe post-fork descriptor operations imported from Profiler"
5)]
6use rustix::io::{FdFlags, fcntl_setfd};
7use std::{
8    ffi::{OsStr, OsString},
9    fs::File,
10    io,
11    os::{fd::AsRawFd, unix::process::CommandExt},
12    process::Command,
13};
14
15/// Failure to prepare the gate's descriptors. No command has been spawned.
16#[derive(Debug)]
17pub enum LaunchGateError {
18    /// Anonymous pipe creation failed.
19    Pipe(io::Error),
20    /// Close-on-exec setup failed.
21    Descriptor(io::Error),
22}
23impl std::fmt::Display for LaunchGateError {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        match self {
26            Self::Pipe(e) | Self::Descriptor(e) => e.fmt(f),
27        }
28    }
29}
30impl std::error::Error for LaunchGateError {
31    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
32        match self {
33            Self::Pipe(e) | Self::Descriptor(e) => Some(e),
34        }
35    }
36}
37
38/// Prepare a shell launcher that waits for a newline before exec'ing exact argv.
39///
40/// The caller configures IO/environment, spawns once, admits its own readiness,
41/// then writes a line to the returned file. Closing without a line refuses exec.
42/// Original stdin is untouched. No process-group or cleanup policy is selected.
43/// # Errors
44/// Returns descriptor preparation failures; spawning and release are caller-owned.
45pub fn gated_command(
46    program: &OsStr,
47    arguments: &[OsString],
48    launcher_name: &OsStr,
49) -> Result<(Command, File), LaunchGateError> {
50    let (read, write) =
51        rustix::pipe::pipe().map_err(|source| LaunchGateError::Pipe(source.into()))?;
52    for fd in [&read, &write] {
53        fcntl_setfd(fd, FdFlags::CLOEXEC)
54            .map_err(|source| LaunchGateError::Descriptor(source.into()))?;
55    }
56    let write_fd = write.as_raw_fd();
57    let mut command = Command::new("/bin/sh");
58    command
59        .args(["-c", "read -r _ <&3 || exit 125; exec 3<&-; exec \"$@\""])
60        .arg(launcher_name)
61        .arg(program)
62        .args(arguments);
63    // SAFETY: the post-fork closure only invokes async-signal-safe FD
64    // operations. It never waits; the exec'ed shell owns the actual gate.
65    unsafe {
66        command.pre_exec(move || {
67            let source = read.as_raw_fd();
68            if libc::dup2(source, 3) < 0 || libc::fcntl(3, libc::F_SETFD, 0) < 0 {
69                return Err(std::io::Error::last_os_error());
70            }
71            if source != 3 {
72                libc::close(source);
73            }
74            if write_fd != 3 {
75                libc::close(write_fd);
76            }
77            Ok(())
78        });
79    }
80    Ok((command, std::fs::File::from(write)))
81}