use std::collections::BTreeMap;
use std::ffi::{CString, OsString};
use std::io;
use std::os::fd::{FromRawFd, OwnedFd as StdOwnedFd};
use std::os::unix::ffi::OsStrExt;
use super::super::{ChildHandle, PipedSpawn};
use super::{
OwnedFd, pipe_cloexec, posix_spawn_file_actions_addchdir_np, resolve_disclaim_fn, wait_for,
};
pub(crate) fn spawn_piped_disclaimed(cmd: std::process::Command) -> io::Result<PipedSpawn> {
let prog_c = CString::new(cmd.get_program().as_bytes())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "program contains NUL"))?;
let mut argv_c: Vec<CString> = vec![prog_c.clone()];
for a in cmd.get_args() {
argv_c.push(
CString::new(a.as_bytes()).map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "argument contains NUL")
})?,
);
}
let mut argv_ptr: Vec<*mut libc::c_char> = argv_c
.iter()
.map(|c| c.as_ptr() as *mut libc::c_char)
.collect();
argv_ptr.push(std::ptr::null_mut());
let mut env_map: BTreeMap<OsString, OsString> = std::env::vars_os().collect();
for (key, val) in cmd.get_envs() {
match val {
Some(v) => {
env_map.insert(key.to_os_string(), v.to_os_string());
}
None => {
env_map.remove(key);
}
}
}
let mut envp_c: Vec<CString> = Vec::with_capacity(env_map.len());
for (k, v) in &env_map {
let mut entry = k.as_bytes().to_vec();
entry.push(b'=');
entry.extend_from_slice(v.as_bytes());
envp_c.push(
CString::new(entry).map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "env entry contains NUL")
})?,
);
}
let mut envp_ptr: Vec<*mut libc::c_char> = envp_c
.iter()
.map(|c| c.as_ptr() as *mut libc::c_char)
.collect();
envp_ptr.push(std::ptr::null_mut());
let cwd_c = cmd
.get_current_dir()
.map(|dir| {
CString::new(dir.as_os_str().as_bytes())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "cwd contains NUL"))
})
.transpose()?;
let (in_rd, in_wr) = pipe_cloexec()?;
let (out_rd, out_wr) = pipe_cloexec()?;
let (err_rd, err_wr) = pipe_cloexec()?;
let in_rd = OwnedFd(in_rd);
let in_wr = OwnedFd(in_wr);
let out_rd = OwnedFd(out_rd);
let out_wr = OwnedFd(out_wr);
let err_rd = OwnedFd(err_rd);
let err_wr = OwnedFd(err_wr);
let pid = {
unsafe {
let mut file_actions: libc::posix_spawn_file_actions_t = std::mem::zeroed();
let mut attr: libc::posix_spawnattr_t = std::mem::zeroed();
if libc::posix_spawn_file_actions_init(&mut file_actions) != 0 {
return Err(io::Error::last_os_error());
}
if libc::posix_spawnattr_init(&mut attr) != 0 {
libc::posix_spawn_file_actions_destroy(&mut file_actions);
return Err(io::Error::last_os_error());
}
if let Some(dir_c) = &cwd_c {
let rc = posix_spawn_file_actions_addchdir_np(&mut file_actions, dir_c.as_ptr());
if rc != 0 {
libc::posix_spawnattr_destroy(&mut attr);
libc::posix_spawn_file_actions_destroy(&mut file_actions);
return Err(io::Error::from_raw_os_error(rc));
}
}
let rc = libc::posix_spawn_file_actions_adddup2(&mut file_actions, in_rd.0, 0);
if rc != 0 {
libc::posix_spawnattr_destroy(&mut attr);
libc::posix_spawn_file_actions_destroy(&mut file_actions);
return Err(io::Error::from_raw_os_error(rc));
}
let rc = libc::posix_spawn_file_actions_adddup2(&mut file_actions, out_wr.0, 1);
if rc != 0 {
libc::posix_spawnattr_destroy(&mut attr);
libc::posix_spawn_file_actions_destroy(&mut file_actions);
return Err(io::Error::from_raw_os_error(rc));
}
let rc = libc::posix_spawn_file_actions_adddup2(&mut file_actions, err_wr.0, 2);
if rc != 0 {
libc::posix_spawnattr_destroy(&mut attr);
libc::posix_spawn_file_actions_destroy(&mut file_actions);
return Err(io::Error::from_raw_os_error(rc));
}
if let Some(disclaim) = resolve_disclaim_fn() {
let _ = disclaim(&mut attr, 1);
}
let mut pid: libc::pid_t = 0;
let rc = libc::posix_spawnp(
&mut pid,
prog_c.as_ptr(),
&file_actions,
&attr,
argv_ptr.as_ptr(),
envp_ptr.as_ptr(),
);
libc::posix_spawnattr_destroy(&mut attr);
libc::posix_spawn_file_actions_destroy(&mut file_actions);
if rc != 0 {
return Err(io::Error::from_raw_os_error(rc));
}
pid
}
};
drop(in_rd);
drop(out_wr);
drop(err_wr);
let reaper = tokio::task::spawn_blocking(move || wait_for(pid));
let stdin_owned = unsafe { StdOwnedFd::from_raw_fd(in_wr.into_raw()) };
let stdout_owned = unsafe { StdOwnedFd::from_raw_fd(out_rd.into_raw()) };
let stderr_owned = unsafe { StdOwnedFd::from_raw_fd(err_rd.into_raw()) };
let sender = tokio::net::unix::pipe::Sender::from_owned_fd(stdin_owned)?;
let receiver = tokio::net::unix::pipe::Receiver::from_owned_fd(stdout_owned)?;
Ok(PipedSpawn {
handle: ChildHandle::Disclaimed {
pid,
reaper: Some(reaper),
_stderr: stderr_owned,
},
stdin: Box::new(sender),
stdout: Box::new(receiver),
})
}
#[cfg(test)]
mod tests {
use super::super::super::disclaimed_piped_spawn;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
#[tokio::test]
#[serial_test::serial]
async fn spawn_piped_disclaimed_writes_and_reads_via_cat() {
let cmd = std::process::Command::new("/bin/cat");
let mut spawned = disclaimed_piped_spawn(cmd).unwrap();
spawned
.stdin
.write_all(b"hello disclaimed\n")
.await
.unwrap();
drop(spawned.stdin); let mut reader = BufReader::new(spawned.stdout);
let mut line = String::new();
reader.read_line(&mut line).await.unwrap();
assert_eq!(line, "hello disclaimed\n");
let status = spawned.handle.wait().await.unwrap();
assert!(status.success());
}
#[tokio::test]
#[serial_test::serial]
async fn spawn_piped_disclaimed_preserves_cwd() {
let dir = std::env::temp_dir();
let mut cmd = std::process::Command::new("/bin/pwd");
cmd.current_dir(&dir);
let mut spawned = disclaimed_piped_spawn(cmd).unwrap();
drop(spawned.stdin);
let mut reader = BufReader::new(spawned.stdout);
let mut line = String::new();
reader.read_line(&mut line).await.unwrap();
let got = std::path::Path::new(line.trim())
.canonicalize()
.unwrap_or_else(|_| line.trim().into());
let want = dir.canonicalize().unwrap_or(dir);
assert_eq!(got, want);
spawned.handle.wait().await.unwrap();
}
#[tokio::test]
#[serial_test::serial]
async fn spawn_piped_disclaimed_removes_env_and_keeps_rest() {
unsafe { std::env::set_var("SPAWN_DISCLAIM_PIPED_TEST_KEEP", "kept") };
let mut cmd = std::process::Command::new("/bin/sh");
cmd.arg("-c").arg("printf '%s\\n' \"$SPAWN_DISCLAIM_PIPED_TEST_KEEP:${SPAWN_DISCLAIM_PIPED_TEST_REMOVE-unset}\"");
cmd.env("SPAWN_DISCLAIM_PIPED_TEST_REMOVE", "should-not-leak");
cmd.env_remove("SPAWN_DISCLAIM_PIPED_TEST_REMOVE");
let mut spawned = disclaimed_piped_spawn(cmd).unwrap();
drop(spawned.stdin);
let mut reader = BufReader::new(spawned.stdout);
let mut line = String::new();
reader.read_line(&mut line).await.unwrap();
unsafe { std::env::remove_var("SPAWN_DISCLAIM_PIPED_TEST_KEEP") };
assert_eq!(line, "kept:unset\n");
spawned.handle.wait().await.unwrap();
}
#[tokio::test]
#[serial_test::serial]
async fn spawn_piped_disclaimed_reports_spawn_error_for_missing_binary() {
let cmd = std::process::Command::new("/nonexistent/definitely-not-a-real-binary-2997");
let err = disclaimed_piped_spawn(cmd)
.err()
.expect("spawning a missing binary must error, not hang or panic");
assert!(matches!(
err.kind(),
std::io::ErrorKind::NotFound | std::io::ErrorKind::Other
));
}
#[tokio::test]
#[serial_test::serial]
async fn spawn_piped_disclaimed_kill_and_wait_reaps_child() {
let mut cmd = std::process::Command::new("/bin/sleep");
cmd.arg("30");
let mut spawned = disclaimed_piped_spawn(cmd).unwrap();
spawned.handle.start_kill().unwrap();
let status = spawned.handle.wait().await.unwrap();
assert!(!status.success());
}
}