use std::ffi::CString; use std::io; use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}; use std::os::unix::ffi::OsStrExt; use std::os::unix::fs::PermissionsExt; use std::os::unix::process::ExitStatusExt; use std::path::PathBuf; use std::pin::Pin; use std::process::ExitStatus; use std::task::{Context, Poll}; use crate::Launch; use tokio::io::unix::AsyncFd; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; #[cfg(target_os = "linux")] #[path = "linux.rs"] mod os; #[cfg(target_os = "macos")] #[path = "macos.rs"] mod os; /// A descriptor handed to the child at its declared number. pub type Endpoint = OwnedFd; /// The terminal master, kept for resizing. pub type Window = OwnedFd; /// Leaves room above every requested child descriptor for the spawn /// handshake's own descriptors. pub const MAX_DESCRIPTOR: u32 = (i32::MAX - 32) as u32; use crate::signals::signal_number; pub use crate::signals::{exit_signal, known_signal, validate_signal}; fn reserve(fd: RawFd, minimum: RawFd) -> io::Result { let result = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, minimum) }; if result < 0 { Err(io::Error::last_os_error()) } else { Ok(unsafe { OwnedFd::from_raw_fd(result) }) } } /// Returns the parent's end and the child's end of a new pipe. pub fn pipe(parent_writes: bool) -> io::Result<(Pipe, Endpoint)> { let (read, write) = os::pipe()?; Ok(if parent_writes { (Pipe::new(write)?, read) } else { (Pipe::new(read)?, write) }) } pub fn null(read: bool) -> io::Result { Ok(std::fs::OpenOptions::new() .read(read) .write(!read) .open("/dev/null")? .into()) } pub fn inherit(fd: u32) -> io::Result { reserve(fd as RawFd, 3) } pub fn duplicate(endpoint: &Endpoint) -> io::Result { reserve(endpoint.as_raw_fd(), 3) } pub struct Pty { pub reader: Pipe, pub writer: Pipe, pub endpoints: [Endpoint; 3], pub window: Window, } pub fn pty(columns: u16, rows: u16) -> io::Result { let (master, slave) = os::pty()?; resize(&master, columns, rows)?; Ok(Pty { reader: Pipe::new(reserve(master.as_raw_fd(), 3)?)?, writer: Pipe::new(reserve(master.as_raw_fd(), 3)?)?, endpoints: [ reserve(slave.as_raw_fd(), 3)?, reserve(slave.as_raw_fd(), 3)?, slave, ], window: master, }) } pub fn resize(fd: &Window, columns: u16, rows: u16) -> io::Result<()> { let size = libc::winsize { ws_row: rows, ws_col: columns, ws_xpixel: 0, ws_ypixel: 0, }; if unsafe { libc::ioctl(fd.as_raw_fd(), libc::TIOCSWINSZ, &size) } < 0 { Err(io::Error::last_os_error()) } else { Ok(()) } } pub struct Pipe(AsyncFd); impl Pipe { fn new(fd: OwnedFd) -> io::Result { let flags = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GETFL) }; if flags < 0 || unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_SETFL, flags | libc::O_NONBLOCK) } < 0 { return Err(io::Error::last_os_error()); } Ok(Self(AsyncFd::new(fd)?)) } } impl AsyncRead for Pipe { fn poll_read( self: Pin<&mut Self>, cx: &mut Context<'_>, buffer: &mut ReadBuf<'_>, ) -> Poll> { loop { let mut ready = std::task::ready!(self.0.poll_read_ready(cx))?; match ready.try_io(|inner| { let bytes = buffer.initialize_unfilled(); let count = unsafe { libc::read( inner.get_ref().as_raw_fd(), bytes.as_mut_ptr().cast(), bytes.len(), ) }; if count < 0 { let error = io::Error::last_os_error(); // A PTY reports EIO after its last slave closes. if error.raw_os_error() == Some(libc::EIO) { return Ok(0); } Err(error) } else { Ok(count as usize) } }) { Ok(Ok(count)) => { buffer.advance(count); return Poll::Ready(Ok(())); } Ok(Err(error)) if error.kind() == io::ErrorKind::Interrupted => continue, Ok(Err(error)) => return Poll::Ready(Err(error)), Err(_) => continue, } } } } impl AsyncWrite for Pipe { fn poll_write( self: Pin<&mut Self>, cx: &mut Context<'_>, bytes: &[u8], ) -> Poll> { loop { let mut ready = std::task::ready!(self.0.poll_write_ready(cx))?; match ready.try_io(|inner| { let count = unsafe { libc::write( inner.get_ref().as_raw_fd(), bytes.as_ptr().cast(), bytes.len(), ) }; if count < 0 { Err(io::Error::last_os_error()) } else { Ok(count as usize) } }) { Ok(Err(error)) if error.kind() == io::ErrorKind::Interrupted => continue, Ok(result) => return Poll::Ready(result), Err(_) => continue, } } } fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } fn poll_shutdown(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } } pub fn is_executable(metadata: &std::fs::Metadata) -> bool { metadata.is_file() && metadata.permissions().mode() & 0o111 != 0 } /// Unix runs exactly the named entry. pub fn expand(path: PathBuf) -> Vec { vec![path] } pub use os::Executable; /// Whether this process can use a terminal: it has a controlling terminal /// and leads its foreground group. pub fn controlling_foreground() -> bool { let path = b"/dev/tty\0"; let fd = unsafe { libc::open(path.as_ptr().cast(), libc::O_RDONLY | libc::O_CLOEXEC) }; if fd < 0 { return false; } let foreground = unsafe { libc::tcgetpgrp(fd) }; unsafe { libc::close(fd); } foreground >= 0 && foreground == unsafe { libc::getpgrp() } } struct Exec { executable: Executable, /// argv[0] is the authorized path. _strings: Vec, argv: Vec, env: Vec, } impl Exec { fn execute(&self) -> io::Error { self.executable .execute(self.argv.as_ptr().cast(), self.env.as_ptr().cast()) } } pub struct Child { pid: u32, group: bool, reaped: bool, } impl Child { pub fn id(&self) -> u32 { self.pid } pub fn wait(&mut self) -> io::Result { loop { let mut status = 0; if unsafe { libc::waitpid(self.pid as i32, &mut status, 0) } >= 0 { self.reaped = true; return Ok(ExitStatus::from_raw(status)); } let error = io::Error::last_os_error(); if error.kind() != io::ErrorKind::Interrupted { return Err(error); } } } /// Observes exit without reaping, so the PID (and with it the process /// group) cannot be reused before group cleanup. pub fn observe_exit(&self) -> io::Result> { let mut info = std::mem::MaybeUninit::::zeroed(); let result = unsafe { libc::waitid( libc::P_PID, self.pid as libc::id_t, info.as_mut_ptr(), libc::WEXITED | libc::WNOHANG | libc::WNOWAIT, ) }; if result < 0 { return Err(io::Error::last_os_error()); } let info = unsafe { info.assume_init() }; if unsafe { info.si_pid() } == 0 { return Ok(None); } let status = unsafe { info.si_status() }; // Darwin's waitid has reported stopped children as waitable even // without WSTOPPED (golang/go#19314). Ok(match info.si_code { libc::CLD_EXITED => Some(ExitStatus::from_raw(status << 8)), libc::CLD_KILLED => Some(ExitStatus::from_raw(status)), libc::CLD_DUMPED => Some(ExitStatus::from_raw(status | 0x80)), _ => None, }) } pub fn signal(&self, group: bool, name: &str) -> io::Result { self.send(group, signal_number(name)?) } fn send(&self, group: bool, number: libc::c_int) -> io::Result { if self.reaped { return Ok(false); } crate::signals::send(self.pid, group, number) } } impl Drop for Child { fn drop(&mut self) { // Only reachable when the spawn handshake failed; the normal path reaps // in supervision. Reap blocking so a failed spawn never leaves a zombie. if !self.reaped { let _ = self.send(false, libc::SIGKILL); if self.group { let _ = self.send(true, libc::SIGKILL); } let mut status = 0; loop { if unsafe { libc::waitpid(self.pid as i32, &mut status, 0) } >= 0 { break; } if io::Error::last_os_error().kind() != io::ErrorKind::Interrupted { break; } } } } } pub(crate) fn spawn(launch: Launch) -> io::Result { use std::io::Read; let Launch { executable, program, args, env, cwd, group, join, terminal, endpoints, } = launch; let pgid = join.map(|group| group.pgid()).transpose()?.unwrap_or(0); // Every descriptor the handshake holds sits above all requested targets. let minimum = endpoints .iter() .map(|(fd, _)| *fd as RawFd + 16) .max() .unwrap_or(0) .max(64); let pointers = |strings: &[CString]| { strings .iter() .map(|value| value.as_ptr() as usize) .chain([0]) .collect() }; let argv = [CString::new(program.as_os_str().as_bytes())] .into_iter() .chain(args.iter().map(|arg| CString::new(arg.as_bytes()))) .collect::, _>>()?; let env = env .iter() .map(|(name, value)| CString::new([name.as_bytes(), b"=", value.as_bytes()].concat())) .collect::, _>>()?; let exec = Exec { executable: executable.reserve(minimum)?, argv: pointers(&argv), env: pointers(&env), _strings: argv.into_iter().chain(env).collect(), }; let entries = endpoints .into_iter() .map(|(target, fd)| Ok((reserve(fd.as_raw_fd(), minimum)?, target as RawFd))) .collect::>>()?; let cwd = CString::new(cwd.as_os_str().as_bytes())?; let (read_end, original_writer) = os::pipe()?; // The exec-error pipe must not collide with a requested child descriptor. let write_end = reserve(original_writer.as_raw_fd(), minimum)?; drop(original_writer); let mut descriptors = os::Descriptors::new()?; let owner = os::Owner::new(); let mut action: libc::sigaction = unsafe { std::mem::zeroed() }; action.sa_sigaction = libc::SIG_DFL; let mut mask: libc::sigset_t = unsafe { std::mem::zeroed() }; unsafe { libc::sigemptyset(&mut mask); } let pid = unsafe { libc::fork() }; if pid < 0 { return Err(io::Error::last_os_error()); } if pid == 0 { // All allocation and CString pointer construction precedes fork. let result = (|| -> io::Result<()> { unsafe { libc::close(read_end.as_raw_fd()); if libc::sigaction(libc::SIGPIPE, &action, std::ptr::null_mut()) < 0 || libc::sigprocmask(libc::SIG_SETMASK, &mask, std::ptr::null_mut()) < 0 || libc::chdir(cwd.as_ptr()) < 0 { return Err(io::Error::last_os_error()); } // Caught handlers reset on exec but ignores persist: // re-apply the startup ignores the supervisor or listener // feeds may have taken over, so children keep the fate // they were launched with. Reads the snapshot only, so no // allocation or locking happens after fork. for number in [ libc::SIGINT, libc::SIGTERM, libc::SIGHUP, libc::SIGUSR1, libc::SIGUSR2, libc::SIGWINCH, ] { if crate::signals::originally_ignored(number) && libc::signal(number, libc::SIG_IGN) == libc::SIG_ERR { return Err(io::Error::last_os_error()); } } if terminal { if libc::setsid() < 0 { return Err(io::Error::last_os_error()); } } else if (group || join.is_some()) && libc::setpgid(0, pgid as i32) < 0 { return Err(io::Error::last_os_error()); } owner.bind()?; for (source, target) in &entries { if libc::dup2(source.as_raw_fd(), *target) < 0 { return Err(io::Error::last_os_error()); } } descriptors.restrict()?; for (_, target) in &entries { if *target >= 3 && libc::fcntl(*target, libc::F_SETFD, 0) < 0 { return Err(io::Error::last_os_error()); } } if terminal && libc::ioctl(0, libc::TIOCSCTTY as _, 0) < 0 { return Err(io::Error::last_os_error()); } } Err(exec.execute()) })(); let error = result.unwrap_err().raw_os_error().unwrap_or(libc::EIO); unsafe { libc::write( write_end.as_raw_fd(), (&error as *const i32).cast(), std::mem::size_of::(), ); libc::_exit(127); } } drop(write_end); let mut child = Child { pid: pid as u32, group, reaped: false, }; let mut read_end = std::fs::File::from(read_end); let mut error = [0; 4]; match read_end.read(&mut error)? { 0 => Ok(child), 4 => { child.wait()?; Err(io::Error::from_raw_os_error(i32::from_ne_bytes(error))) } _ => Err(io::Error::other("Invalid subprocess exec handshake")), } }