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