use super::flog_safe::flog_safe;
use crate::null_terminated_array::OwningNullTerminatedArray;
use crate::redirection::Dup2List;
use crate::signal::signal_reset_handlers;
use crate::wutil::fstat;
use fish_common::exit_without_destructors;
use libc::{pid_t, O_RDONLY};
use nix::unistd::getpid;
use std::ffi::CStr;
use std::num::NonZeroU32;
use std::os::unix::fs::MetadataExt as _;
use std::time::Duration;
const FORK_LAPS: usize = 5;
const FORK_SLEEP_TIME: Duration = Duration::from_nanos(1000000);
fn clear_cloexec(fd: i32) -> i32 {
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD, 0) };
if flags < 0 {
return -1;
}
let new_flags = flags & !libc::FD_CLOEXEC;
if flags == new_flags {
0
} else {
unsafe { libc::fcntl(fd, libc::F_SETFD, new_flags) }
}
}
pub(crate) fn report_setpgid_error(
err: i32,
is_parent: bool,
pid: libc::pid_t,
desired_pgid: libc::pid_t,
job_id: i64,
command: &CStr,
argv0: &CStr,
) {
let cur_group = unsafe { libc::getpgid(pid) };
flog_safe!(
warning,
"Could not send ",
if is_parent { "child" } else { "self" },
" ",
pid,
", '",
argv0,
"' in job ",
job_id,
", '",
command,
"' from group ",
cur_group,
" to group ",
desired_pgid,
);
match err {
libc::EACCES => flog_safe!(error, "setpgid: Process ", pid, " has already exec'd"),
libc::EINVAL => flog_safe!(error, "setpgid: pgid ", cur_group, " unsupported"),
libc::EPERM => {
flog_safe!(
error,
"setpgid: Process ",
pid,
" is a session leader or pgid ",
cur_group,
" does not match"
);
}
libc::ESRCH => flog_safe!(error, "setpgid: Process ID ", pid, " does not match"),
_ => flog_safe!(error, "setpgid: Unknown error number ", err),
}
}
pub fn execute_setpgid(pid: libc::pid_t, pgroup: libc::pid_t, is_parent: bool) -> i32 {
let mut eperm_count = 0;
loop {
if unsafe { libc::setpgid(pid, pgroup) } == 0 {
return 0;
}
let err = errno::errno().0;
assert_ne!(err, libc::EINTR);
if err == libc::EACCES && is_parent {
return 0;
} else if err == libc::EPERM && eperm_count < 100 {
eperm_count += 1;
flog_safe!(proc_pgroup, "setpgid(2) returned EPERM. Retrying");
continue;
}
#[cfg(any(apple, bsd))]
if err == libc::ESRCH && is_parent {
return 0;
}
return err;
}
}
pub fn child_setup_process(
claim_tty_from: Option<NonZeroU32>,
sigmask: Option<&libc::sigset_t>,
is_forked: bool,
dup2s: &Dup2List,
) -> i32 {
for act in &dup2s.actions {
let err;
if act.target < 0 {
err = unsafe { libc::close(act.src) };
} else if act.target != act.src {
err = unsafe { libc::dup2(act.src, act.target) };
} else {
err = clear_cloexec(act.src);
}
if err < 0 {
if is_forked {
flog_safe!(
warning,
"failed to set up file descriptors in child_setup_process"
);
exit_without_destructors(1);
}
return err;
}
}
if claim_tty_from
.is_some_and(|pid| unsafe { libc::tcgetpgrp(libc::STDIN_FILENO) } == pid.get() as i32)
{
unsafe {
libc::signal(libc::SIGTTIN, libc::SIG_IGN);
libc::signal(libc::SIGTTOU, libc::SIG_IGN);
let _ = libc::tcsetpgrp(libc::STDIN_FILENO, getpid().as_raw());
}
}
if let Some(sigmask) = sigmask {
unsafe { libc::sigprocmask(libc::SIG_SETMASK, sigmask, std::ptr::null_mut()) };
}
signal_reset_handlers();
0
}
pub fn execute_fork() -> pid_t {
let mut err = 0;
for i in 0..FORK_LAPS {
let pid = unsafe { libc::fork() };
if pid >= 0 {
return pid;
}
err = errno::errno().0;
if err != libc::EAGAIN {
break;
}
if i != FORK_LAPS - 1 {
std::thread::sleep(FORK_SLEEP_TIME);
}
}
match err {
libc::EAGAIN => {
flog_safe!(
error,
"fork: Out of resources. Check RLIMIT_NPROC and pid_max."
);
}
libc::ENOMEM => {
flog_safe!(error, "fork: Out of memory.");
}
_ => {
flog_safe!(error, "fork: Unknown error number", err);
}
}
exit_without_destructors(1)
}
pub(crate) fn signal_safe_report_exec_error(
err: i32,
actual_cmd: &CStr,
argvv: &OwningNullTerminatedArray,
envv: &OwningNullTerminatedArray,
) {
match err {
libc::E2BIG => {
let szenv = envv.iter().map(|s| s.to_bytes().len()).sum::<usize>();
let sz = szenv + argvv.iter().map(|s| s.to_bytes().len()).sum::<usize>();
let arg_max = unsafe { libc::sysconf(libc::_SC_ARG_MAX) };
if arg_max > 0 {
let arg_max = arg_max as usize;
if sz >= arg_max {
flog_safe!(
exec,
"Failed to execute process '",
actual_cmd,
"': the total size of the argument list and exported variables (",
sz,
") exceeds the OS limit of ",
arg_max,
"."
);
} else {
flog_safe!(
exec,
"Failed to execute process '",
actual_cmd,
"': An argument or exported variable exceeds the OS argument length limit."
);
}
if szenv >= arg_max / 2 {
flog_safe!(
exec,
"Hint: Your exported variables take up over half the limit. Try \
erasing or unexporting variables."
);
}
} else {
flog_safe!(
exec,
"Failed to execute process '",
actual_cmd,
"': the total size of the argument list and exported variables (",
sz,
") exceeds the operating system limit.",
);
}
}
libc::ENOEXEC => {
flog_safe!(
exec,
"Failed to execute process: '",
actual_cmd,
"' the file could not be run by the operating system."
);
let mut interpreter_buf = [b'\0'; 128];
if get_interpreter(actual_cmd, &mut interpreter_buf).is_none() {
if actual_cmd.to_bytes().ends_with(b".fish") {
flog_safe!(
exec,
"fish scripts require an interpreter directive (must \
start with '#!/path/to/fish')."
);
} else {
flog_safe!(exec, "Maybe the interpreter directive (#! line) is broken?");
}
}
}
libc::EACCES | libc::ENOENT => {
let mut interpreter_buf = [b'\0'; 128];
if let Some(interpreter) = get_interpreter(actual_cmd, &mut interpreter_buf) {
let fd = unsafe { libc::open(interpreter.as_ptr(), O_RDONLY) };
let md = if fd == -1 {
Err(())
} else {
fstat(fd).map_err(|_| ())
};
fn err_or_no_exec_handling(interpreter: &CStr, actual_cmd: &CStr) {
let interpreter = interpreter.to_bytes();
if interpreter.last() == Some(&b'\r') {
flog_safe!(
exec,
"Failed to execute process '",
actual_cmd,
"': The file uses Windows line endings (\\r\\n). Run dos2unix or similar to fix it."
);
} else {
flog_safe!(
exec,
"Failed to execute process '",
actual_cmd,
"': The file specified the interpreter '",
interpreter,
"', which is not an executable command."
);
}
}
if let Ok(metadata) = md {
#[allow(clippy::useless_conversion)] if unsafe { libc::access(interpreter.as_ptr(), libc::X_OK) } != 0 {
err_or_no_exec_handling(interpreter, actual_cmd);
} else if metadata.mode() & u32::from(libc::S_IFMT) == u32::from(libc::S_IFDIR)
{
flog_safe!(
exec,
"Failed to execute process '",
actual_cmd,
"': The file specified the interpreter '",
interpreter,
"', which is a directory."
);
}
} else {
err_or_no_exec_handling(interpreter, actual_cmd);
}
} else if unsafe { libc::access(actual_cmd.as_ptr(), libc::X_OK) } == 0 {
flog_safe!(
exec,
"Failed to execute process '",
actual_cmd,
"': The file exists and is executable. Check the interpreter or linker?"
);
} else if err == libc::ENOENT {
flog_safe!(
exec,
"Failed to execute process '",
actual_cmd,
"': The file does not exist or could not be executed."
);
} else {
flog_safe!(
exec,
"Failed to execute process '",
actual_cmd,
"': The file could not be accessed."
);
}
}
libc::ENOMEM => {
flog_safe!(exec, "Out of memory");
}
libc::ETXTBSY => {
flog_safe!(
exec,
"Failed to execute process '",
actual_cmd,
"': File is currently open for writing.",
);
}
libc::ELOOP => {
flog_safe!(
exec,
"Failed to execute process '",
actual_cmd,
"': Too many layers of symbolic links. Maybe a loop?"
);
}
libc::EINVAL => {
flog_safe!(
exec,
"Failed to execute process '",
actual_cmd,
"': Unsupported format."
);
}
libc::EISDIR => {
flog_safe!(
exec,
"Failed to execute process '",
actual_cmd,
"': File is a directory."
);
}
libc::ENOTDIR => {
flog_safe!(
exec,
"Failed to execute process '",
actual_cmd,
"': A path component is not a directory."
);
}
libc::EMFILE => {
flog_safe!(
exec,
"Failed to execute process '",
actual_cmd,
"': Too many open files in this process."
);
}
libc::ENFILE => {
flog_safe!(
exec,
"Failed to execute process '",
actual_cmd,
"': Too many open files on the system."
);
}
libc::ENAMETOOLONG => {
flog_safe!(
exec,
"Failed to execute process '",
actual_cmd,
"': Name is too long."
);
}
libc::EPERM => {
flog_safe!(
exec,
"Failed to execute process '",
actual_cmd,
"': No permission. \
Either suid/sgid is forbidden or you lack capabilities."
);
}
#[cfg(apple)]
libc::EBADARCH => {
flog_safe!(
exec,
"Failed to execute process '",
actual_cmd,
"': Bad CPU type in executable."
);
}
#[cfg(apple)]
libc::EBADMACHO => {
flog_safe!(
exec,
"Failed to execute process '",
actual_cmd,
"': Malformed Mach-O file."
);
}
err => {
flog_safe!(
exec,
"Failed to execute process '",
actual_cmd,
"', unknown error number ",
err,
);
}
}
}
fn get_interpreter<'a>(command: &CStr, buffer: &'a mut [u8]) -> Option<&'a CStr> {
let fd = unsafe { libc::open(command.as_ptr(), libc::O_RDONLY) };
let mut idx = 0;
if fd >= 0 {
while idx + 1 < buffer.len() {
let mut ch = b'\0';
let amt = unsafe { libc::read(fd, (&raw mut ch).cast(), size_of_val(&ch)) };
if amt <= 0 || ch == b'\n' {
break;
}
buffer[idx] = ch;
idx += 1;
}
buffer[idx] = b'\0';
idx += 1;
unsafe { libc::close(fd) };
}
#[allow(clippy::if_same_then_else)]
let offset = if buffer.starts_with(b"#! /") {
3
} else if buffer.starts_with(b"#!/") {
2
} else if buffer.starts_with(b"#!") {
2
} else {
return None;
};
CStr::from_bytes_with_nul(&buffer[offset..idx.max(offset)]).ok()
}
#[cfg(test)]
mod tests {
use super::get_interpreter;
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt as _;
#[test]
fn test_get_interpreter_returns_none_on_embedded_nul() {
let script = fish_tempfile::new_file().unwrap();
std::fs::write(script.path(), b"#!/bin/\0sh\n").unwrap();
let command = CString::new(script.path().as_os_str().as_bytes()).unwrap();
let mut buffer = [0u8; 64];
assert!(get_interpreter(command.as_c_str(), &mut buffer).is_none());
}
}