use std::env;
use std::fs::OpenOptions;
use std::os::fd::IntoRawFd;
#[cfg(any(
feature = "frontend",
all(test, feature = "test-support", feature = "unix-runtime")
))]
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
#[cfg(any(feature = "frontend", test))]
use crate::args::InitArgs;
use crate::ast::AndOrList;
#[cfg(any(feature = "frontend", test))]
use crate::policy::ShellIdentity;
use crate::sys;
use crate::sys::Runtime;
use super::{
BackgroundMachinePayload, ChildFdDisposition, ChildLaunchPlan, ProcessGroupPlan, ShellState,
shell_resolve,
};
pub(super) const MAX_MACHINE_PAYLOAD_BYTES: usize = 16 * 1024 * 1024;
const PREFERRED_MACHINE_PAYLOAD_CHILD_FD: i32 = 255;
#[cfg(any(
feature = "frontend",
all(test, feature = "test-support", feature = "unix-runtime")
))]
const BACKGROUND_FORWARD_SIGNALS: [i32; 4] =
[libc::SIGHUP, libc::SIGINT, libc::SIGQUIT, libc::SIGTERM];
#[cfg(any(
feature = "frontend",
all(test, feature = "test-support", feature = "unix-runtime")
))]
static BACKGROUND_FORWARD_PROCESS_GROUP: AtomicI32 = AtomicI32::new(0);
#[cfg(any(
feature = "frontend",
all(test, feature = "test-support", feature = "unix-runtime")
))]
static BACKGROUND_RECEIVED_CONTINUE: AtomicBool = AtomicBool::new(false);
#[cfg(any(
feature = "frontend",
all(test, feature = "test-support", feature = "unix-runtime")
))]
pub(super) struct BackgroundSignalForwardGuard {
_signals: sys::SignalDispositionGuard,
previous_process_group: i32,
}
#[cfg(any(
feature = "frontend",
all(test, feature = "test-support", feature = "unix-runtime")
))]
extern "C" fn forward_background_signal(sig: i32) {
let process_group = BACKGROUND_FORWARD_PROCESS_GROUP.load(Ordering::Relaxed);
unsafe {
if process_group > 0 {
let _ = libc::kill(-process_group, sig);
}
libc::_exit(128 + sig);
}
}
#[cfg(any(
feature = "frontend",
all(test, feature = "test-support", feature = "unix-runtime")
))]
extern "C" fn note_background_continue(_sig: i32) {
BACKGROUND_RECEIVED_CONTINUE.store(true, Ordering::Relaxed);
}
#[cfg(any(
feature = "frontend",
all(test, feature = "test-support", feature = "unix-runtime")
))]
impl BackgroundSignalForwardGuard {
pub(super) fn install() -> Self {
BACKGROUND_RECEIVED_CONTINUE.store(false, Ordering::Relaxed);
let process_group = unsafe {
let pid = libc::getpid();
let pgid = libc::getpgrp();
if pgid == pid { pgid } else { 0 }
};
let previous_process_group =
BACKGROUND_FORWARD_PROCESS_GROUP.swap(process_group, Ordering::Relaxed);
let mut signals = sys::SignalDispositionGuard::new();
for sig in BACKGROUND_FORWARD_SIGNALS {
signals.set_preserving_ignore(
sig,
forward_background_signal as *const () as libc::sighandler_t,
);
}
signals.set_preserving_ignore(
libc::SIGCONT,
note_background_continue as *const () as libc::sighandler_t,
);
Self {
_signals: signals,
previous_process_group,
}
}
}
#[cfg(any(
feature = "frontend",
all(test, feature = "test-support", feature = "unix-runtime")
))]
pub(super) fn take_background_continue_signal() -> bool {
BACKGROUND_RECEIVED_CONTINUE.swap(false, Ordering::Relaxed)
}
#[cfg(not(any(
feature = "frontend",
all(test, feature = "test-support", feature = "unix-runtime")
)))]
pub(super) fn take_background_continue_signal() -> bool {
false
}
#[cfg(any(
feature = "frontend",
all(test, feature = "test-support", feature = "unix-runtime")
))]
impl Drop for BackgroundSignalForwardGuard {
fn drop(&mut self) {
BACKGROUND_FORWARD_PROCESS_GROUP.store(self.previous_process_group, Ordering::Relaxed);
}
}
pub(super) fn encode_background_payload(
state: &ShellState,
and_or_list: &AndOrList,
command_id: &str,
) -> Result<String, String> {
let payload = BackgroundMachinePayload {
definition: state.definition.portable_background_checkpoint(),
state: state.background_checkpoint(),
and_or_list: and_or_list.clone(),
command_id: command_id.to_string(),
};
let payload_text =
serde_json::to_string(&payload).map_err(|err| format!("invalid machine payload: {err}"))?;
validate_machine_payload_size(payload_text.len())?;
Ok(payload_text)
}
#[cfg(any(
feature = "frontend",
all(test, feature = "test-support", feature = "unix-runtime")
))]
pub(super) fn parse_machine_payload(
payload_text: &str,
) -> Result<BackgroundMachinePayload, String> {
serde_json::from_str(payload_text).map_err(|err| format!("invalid machine payload: {err}"))
}
fn validate_machine_payload_size(len: usize) -> Result<(), String> {
if len <= MAX_MACHINE_PAYLOAD_BYTES {
Ok(())
} else {
Err(format!(
"machine payload too large: {len} bytes exceeds limit of {MAX_MACHINE_PAYLOAD_BYTES} bytes"
))
}
}
fn background_machine_env(
state: &ShellState,
payload_child_fd: sys::FileDescriptor,
) -> Vec<(String, String)> {
let payload_env = state.definition.identity.machine_payload_fd_env_var();
let default_payload_env = crate::policy::ShellIdentity::default()
.machine_payload_fd_env_var()
.to_string();
let mut env = shell_resolve::exported_exec_environment(state);
env.retain(|(key, _)| key != payload_env && key != &default_payload_env);
env.push((
payload_env.to_string(),
payload_child_fd.as_i32().to_string(),
));
if default_payload_env != payload_env {
env.push((default_payload_env, payload_child_fd.as_i32().to_string()));
}
env
}
fn background_payload_child_fd(state: &ShellState) -> Result<sys::FileDescriptor, String> {
let available = |fd| fd > 2 && !state.fd_table.raw_fds.contains_key(&fd);
if available(PREFERRED_MACHINE_PAYLOAD_CHILD_FD) {
return Ok(sys::FileDescriptor::new(PREFERRED_MACHINE_PAYLOAD_CHILD_FD));
}
if let Some(fd) = (3..PREFERRED_MACHINE_PAYLOAD_CHILD_FD)
.rev()
.find(|&fd| available(fd))
{
return Ok(sys::FileDescriptor::new(fd));
}
let limit = machine_payload_child_fd_scan_limit();
if let Some(fd) = ((PREFERRED_MACHINE_PAYLOAD_CHILD_FD + 1)..limit).find(|&fd| available(fd)) {
return Ok(sys::FileDescriptor::new(fd));
}
Err("no collision-free machine payload fd is available".to_string())
}
fn machine_payload_child_fd_scan_limit() -> i32 {
let max_fd = unsafe { libc::sysconf(libc::_SC_OPEN_MAX) };
if max_fd > 0 {
max_fd.min(i32::MAX as libc::c_long) as i32
} else {
1024
}
}
pub(super) fn background_stdin_fd(
state: &ShellState,
) -> Result<Option<sys::FileDescriptor>, String> {
if state.interactive {
return Ok(None);
}
let file = OpenOptions::new()
.read(true)
.open("/dev/null")
.map_err(|err| format!("failed to open /dev/null for background stdin: {err}"))?;
Ok(Some(sys::FileDescriptor::from(file.into_raw_fd())))
}
#[cfg(any(feature = "frontend", test))]
pub(crate) fn load_machine_payload_text(
init: &InitArgs,
identity: &ShellIdentity,
) -> Result<String, String> {
if let Some(payload_text) = init.command_str.as_ref() {
validate_machine_payload_size(payload_text.len())?;
return Ok(payload_text.clone());
}
let mut env_names = vec![identity.machine_payload_fd_env_var().to_string()];
let default_env = crate::policy::ShellIdentity::default()
.machine_payload_fd_env_var()
.to_string();
if default_env != env_names[0] {
env_names.push(default_env);
}
for payload_env in env_names {
if let Ok(raw_fd) = env::var(&payload_env) {
let fd_num = raw_fd.parse::<i32>().map_err(|_| {
format!("{}: invalid {payload_env} value: {raw_fd}", identity.name())
})?;
if fd_num < 0 {
return Err(format!(
"{}: invalid {payload_env} value: {fd_num}",
identity.name()
));
}
let payload_text = sys::FileDescriptor::from(fd_num)
.read_to_string_with_limit(MAX_MACHINE_PAYLOAD_BYTES + 1)
.map_err(|err| {
format!(
"{}: failed to read machine payload fd {fd_num}: {err}",
identity.name()
)
})?;
validate_machine_payload_size(payload_text.len())
.map_err(|err| format!("{}: {err}", identity.name()))?;
return Ok(payload_text);
}
}
let payload_env = identity.machine_payload_fd_env_var();
Err(format!(
"{}: --machine requires -c <payload> or {payload_env}=<fd>",
identity.name()
))
}
pub(crate) fn resolve_machine_program_path(shell_name: &str) -> Option<String> {
let cargo_bin_env = format!("CARGO_BIN_EXE_{shell_name}");
if let Some(program) = env::var_os(&cargo_bin_env) {
let candidate = std::path::PathBuf::from(program);
if candidate.is_file() {
return Some(candidate.to_string_lossy().into_owned());
}
}
let current = env::current_exe().ok()?;
if let Some(parent) = current.parent()
&& parent.file_name().is_some_and(|name| name == "deps")
&& let Some(bin_dir) = parent.parent()
{
let candidate = bin_dir.join(shell_name);
if candidate.is_file() {
return Some(candidate.to_string_lossy().into_owned());
}
}
Some(current.to_string_lossy().into_owned())
}
pub(super) fn spawn_background_job<R: Runtime>(
state: &ShellState,
runtime: &mut R,
and_or_list: &AndOrList,
command_id: &str,
) -> Result<sys::SpawnedProcess, String> {
let payload_text = encode_background_payload(state, and_or_list, command_id)?;
let Some((program, argv)) = state
.definition
.background_launcher
.machine_command(state.shell_name())
else {
return Err("failed to resolve machine executable path".to_string());
};
let payload_child_fd = background_payload_child_fd(state)?;
let payload_pipe = sys::OsPipe::new()
.map_err(|err| format!("failed to create machine payload pipe: {err}"))?;
let env = background_machine_env(state, payload_child_fd);
let background_stdin = match background_stdin_fd(state) {
Ok(fd) => fd,
Err(err) => {
payload_pipe.read_fd.close();
payload_pipe.write_fd.close();
return Err(err);
}
};
let stdin_fd = background_stdin.unwrap_or(state.stdin_fd);
let launch = ChildLaunchPlan::new(state, program, argv, ProcessGroupPlan::New)
.with_env(env)
.with_stdio(stdin_fd, state.stdout_fd, state.stderr_fd)
.with_extra_fd(
payload_child_fd.as_i32(),
ChildFdDisposition::OpenFrom(payload_pipe.read_fd),
);
let spawned = match launch.spawn(runtime, sys::SpawnMode::BackgroundJob) {
Ok(spawned) => spawned,
Err(err) => {
if let Some(fd) = background_stdin {
fd.close();
}
payload_pipe.read_fd.close();
payload_pipe.write_fd.close();
return Err(format!("failed to spawn background job: {err}"));
}
};
if let Some(fd) = background_stdin {
fd.close();
}
if let Err(err) = payload_pipe.write_fd.write_all(payload_text.as_bytes()) {
payload_pipe.read_fd.close();
payload_pipe.write_fd.close();
let _ = runtime.signal_process_group(spawned.handle, sys::RuntimeSignal::Terminate);
let _ = runtime.wait_child(spawned.handle);
return Err(format!("failed to write machine payload: {err}"));
}
payload_pipe.read_fd.close();
payload_pipe.write_fd.close();
Ok(spawned)
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use super::*;
static PAYLOAD_ENV_LOCK: Mutex<()> = Mutex::new(());
static PROGRAM_ENV_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn load_machine_payload_text_reads_payload_from_fd_env() {
let _guard = PAYLOAD_ENV_LOCK.lock().expect("payload env lock poisoned");
let identity = ShellIdentity::default();
let payload_env = identity.machine_payload_fd_env_var();
let saved_fd = env::var(payload_env).ok();
let pipe = sys::OsPipe::new().expect("payload pipe");
pipe.write_fd
.write_all(br#"{"kind":"payload"}"#)
.expect("write payload");
pipe.write_fd.close();
unsafe {
env::set_var(payload_env, pipe.read_fd.as_i32().to_string());
}
let init = InitArgs::default();
let payload = load_machine_payload_text(&init, &identity).expect("load payload from fd");
assert_eq!(payload, r#"{"kind":"payload"}"#);
unsafe {
if let Some(value) = saved_fd {
env::set_var(payload_env, value);
} else {
env::remove_var(payload_env);
}
}
}
#[test]
fn background_payload_child_fd_prefers_reserved_fd_when_available() {
let state = ShellState::new();
assert_eq!(
background_payload_child_fd(&state).expect("payload fd should resolve"),
sys::FileDescriptor::new(PREFERRED_MACHINE_PAYLOAD_CHILD_FD)
);
}
#[test]
fn background_payload_child_fd_avoids_shell_fd_table_entries() {
let mut state = ShellState::new();
state.fd_table.raw_fds.insert(
PREFERRED_MACHINE_PAYLOAD_CHILD_FD,
sys::FileDescriptor::STDOUT,
);
assert_eq!(
background_payload_child_fd(&state).expect("payload fd should resolve"),
sys::FileDescriptor::new(PREFERRED_MACHINE_PAYLOAD_CHILD_FD - 1)
);
}
#[test]
fn resolve_machine_program_path_prefers_cargo_bin_env() {
let _guard = PROGRAM_ENV_LOCK.lock().expect("program env lock poisoned");
let saved = env::var_os("CARGO_BIN_EXE_mxsh");
let current = env::current_exe().expect("test executable path");
unsafe {
env::set_var("CARGO_BIN_EXE_mxsh", ¤t);
}
assert_eq!(
resolve_machine_program_path("mxsh"),
Some(current.to_string_lossy().into_owned())
);
unsafe {
if let Some(saved) = saved {
env::set_var("CARGO_BIN_EXE_mxsh", saved);
} else {
env::remove_var("CARGO_BIN_EXE_mxsh");
}
}
}
}