//! Linux specifics of the Unix backend. use std::ffi::CString; use std::io; use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd, RawFd}; use std::os::unix::ffi::OsStrExt; use super::reserve; pub(super) fn pipe() -> io::Result<(OwnedFd, OwnedFd)> { let mut fds = [0; 2]; if unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC) } < 0 { return Err(io::Error::last_os_error()); } Ok(unsafe { (OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1])) }) } /// Returns the master and slave of a new terminal. pub(super) fn pty() -> io::Result<(OwnedFd, OwnedFd)> { let fd = unsafe { libc::posix_openpt(libc::O_RDWR | libc::O_NOCTTY | libc::O_CLOEXEC) }; if fd < 0 { return Err(io::Error::last_os_error()); } let master = unsafe { OwnedFd::from_raw_fd(fd) }; if unsafe { libc::grantpt(fd) } < 0 || unsafe { libc::unlockpt(fd) } < 0 { return Err(io::Error::last_os_error()); } let fd = unsafe { libc::ioctl( fd, libc::TIOCGPTPEER, libc::O_RDWR | libc::O_NOCTTY | libc::O_CLOEXEC, ) }; if fd < 0 { return Err(io::Error::last_os_error()); } Ok((master, unsafe { OwnedFd::from_raw_fd(fd) })) } /// The authorized executable, pinned by an `O_PATH` descriptor through exec. pub struct Executable { fd: OwnedFd, } impl Executable { pub fn open_at( directory: &impl AsFd, name: &std::ffi::OsStr, ) -> io::Result<(Self, std::fs::Metadata)> { let label = CString::new(name.as_bytes())?; // Follow the final link (multi-call/symlinked entries) while the parent // directory fd pins the lookup; the entry authorization covers the // lexical name, and argv[0] preserves it for multicall dispatch. let flags = libc::O_PATH | libc::O_CLOEXEC; let fd = unsafe { libc::openat(directory.as_fd().as_raw_fd(), label.as_ptr(), flags) }; if fd < 0 { return Err(io::Error::last_os_error()); } let fd = unsafe { OwnedFd::from_raw_fd(fd) }; let metadata = std::fs::File::from(reserve(fd.as_raw_fd(), 3)?).metadata()?; Ok((Self { fd }, metadata)) } /// Moves the descriptor clear of every requested child descriptor. pub(super) fn reserve(&self, minimum: RawFd) -> io::Result { Ok(Self { fd: reserve(self.fd.as_raw_fd(), minimum)?, }) } /// Runs after fork; returns only on failure. pub(super) fn execute( &self, argv: *const *const libc::c_char, env: *const *const libc::c_char, ) -> io::Error { let execute = || unsafe { libc::fexecve(self.fd.as_raw_fd(), argv, env) }; execute(); let error = io::Error::last_os_error(); // Script interpreters need the pinned script fd after exec; binaries do not. if error.raw_os_error() == Some(libc::ENOENT) { if unsafe { libc::fcntl(self.fd.as_raw_fd(), libc::F_SETFD, 0) } < 0 { return io::Error::last_os_error(); } execute(); return io::Error::last_os_error(); } error } } /// Taken before fork, so the child can tell whether its owner already died. pub(super) struct Owner(libc::pid_t); impl Owner { pub fn new() -> Self { Self(unsafe { libc::getpid() }) } /// Orphaned children must not outlive an abruptly killed owner. pub fn bind(&self) -> io::Result<()> { if unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL as u64) } < 0 { return Err(io::Error::last_os_error()); } if unsafe { libc::getppid() } != self.0 { return Err(io::Error::new(io::ErrorKind::NotConnected, "owner exited")); } Ok(()) } } pub(super) struct Descriptors; impl Descriptors { pub fn new() -> io::Result { Ok(Self) } /// Marks every descriptor from 3 up CLOEXEC; spawn re-arms the declared /// ones. Marking rather than closing keeps the executable and exec-error /// pipe open until exec. pub fn restrict(&mut self) -> io::Result<()> { let result = unsafe { libc::syscall( libc::SYS_close_range, 3u32, u32::MAX, libc::CLOSE_RANGE_CLOEXEC, ) }; if result == 0 { return Ok(()); } let error = io::Error::last_os_error(); if !matches!( error.raw_os_error(), Some(libc::ENOSYS | libc::EINVAL | libc::EPERM) ) { return Err(error); } // RLIMIT_NOFILE can be lowered below existing descriptors, so scanning // only to that limit would leak them. getdents64 avoids allocation after // fork and visits exactly the child's open descriptors, including high FDs. let directory = unsafe { libc::open( c"/proc/self/fd".as_ptr(), libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC, ) }; if directory < 0 { let error = io::Error::last_os_error(); return Err( if matches!(error.raw_os_error(), Some(libc::ENOENT | libc::ENOTDIR)) { io::Error::from_raw_os_error(libc::ENOTSUP) } else { error }, ); } let directory = unsafe { OwnedFd::from_raw_fd(directory) }; #[repr(align(8))] struct Buffer([u8; 8192]); let mut buffer = Buffer([0; 8192]); loop { let count = unsafe { libc::syscall( libc::SYS_getdents64, directory.as_raw_fd(), buffer.0.as_mut_ptr(), buffer.0.len(), ) }; if count < 0 { let error = io::Error::last_os_error(); if error.kind() == io::ErrorKind::Interrupted { continue; } return Err(error); } if count == 0 { return Ok(()); } let mut entries = &buffer.0[..count as usize]; while !entries.is_empty() { // linux_dirent64: ino(8), off(8), reclen(2), type(1), name. if entries.len() < 20 { return Err(io::Error::from_raw_os_error(libc::EIO)); } let length = u16::from_ne_bytes([entries[16], entries[17]]) as usize; if length < 20 || length > entries.len() { return Err(io::Error::from_raw_os_error(libc::EIO)); } let name = &entries[19..length]; let end = name .iter() .position(|byte| *byte == 0) .ok_or_else(|| io::Error::from_raw_os_error(libc::EIO))?; let fd = name[..end].iter().try_fold(0i32, |value, byte| { if !byte.is_ascii_digit() { return None; } value.checked_mul(10)?.checked_add((byte - b'0') as i32) }); if let Some(fd) = fd.filter(|fd| *fd >= 3) { let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) }; if flags < 0 || unsafe { libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) } < 0 { return Err(io::Error::last_os_error()); } } entries = &entries[length..]; } } } }