#![expect(clippy::disallowed_methods)]
#![expect(clippy::disallowed_types)]
use std::{
env,
ffi::OsString,
io::{self, IoSlice, Write},
os::{
fd::{AsRawFd, RawFd},
unix::process::CommandExt,
},
process::{Command, ExitCode},
};
use btoi::btoi;
use memchr::memchr;
use nix::{
errno::Errno,
fcntl::{open, OFlag},
sys::{
socket::{
connect, sendmsg, socket, AddressFamily, ControlMessage, MsgFlags, SockFlag, SockType,
UnixAddr,
},
stat::Mode,
},
unistd::{dup2_raw, getpid, read, Pid},
};
use syd::{
compat::{getdents64, readlinkat},
config::*,
err::err2no,
fd::{fd_status_flags, parse_fd, pidfd_getfd, pidfd_open, set_cloexec, PIDFD_THREAD},
path::{XPath, XPathBuf},
rng::duprand,
};
#[cfg(all(
not(coverage),
not(feature = "prof"),
not(target_os = "android"),
not(target_arch = "riscv64"),
target_page_size_4k,
target_pointer_width = "64"
))]
#[global_allocator]
static GLOBAL: hardened_malloc::HardenedMalloc = hardened_malloc::HardenedMalloc;
#[cfg(feature = "prof")]
#[global_allocator]
static GLOBAL: tcmalloc::TCMalloc = tcmalloc::TCMalloc;
syd::main! {
use lexopt::prelude::*;
syd::set_sigpipe_dfl()?;
let mut opt_pid = None;
let mut opt_cmd = None;
let mut opt_arg = Vec::new();
let mut opt_fds = Vec::new();
let mut opt_msg: Option<OsString> = None;
let mut opt_unix: Option<OsString> = None;
let mut opt_wait = false;
let mut parser = lexopt::Parser::from_env();
while let Some(arg) = parser.next()? {
match arg {
Short('h') => {
help();
return Ok(ExitCode::SUCCESS);
}
Short('p') => {
let pid = parser.value()?;
opt_pid = match pid.parse::<libc::pid_t>() {
Ok(pid) if pid > 0 => Some(Pid::from_raw(pid)),
_ => {
eprintln!("syd-fd: Invalid PID specified with -p!");
return Err(Errno::EINVAL.into());
}
};
}
Short('f') => {
let fd = parser.value()?;
match fd.to_str() {
Some(fd) => opt_fds.push(fd.to_string()),
None => {
eprintln!("syd-fd: Invalid UTF-8 in FD argument!");
return Err(Errno::EILSEQ.into());
}
};
}
Short('u') => {
opt_unix = Some(parser.value()?);
}
Short('m') => {
opt_msg = Some(parser.value()?);
}
Short('w') => opt_wait = true,
Value(prog) => {
opt_cmd = Some(prog);
opt_arg.extend(parser.raw_args()?);
}
_ => return Err(arg.unexpected().into()),
}
}
if let Some(addr) = opt_unix {
let mut fds = Vec::new();
for val in &opt_fds {
for fd in val.split(',') {
match btoi::<RawFd>(fd.as_bytes()) {
Ok(fd) if fd >= 0 => fds.push(fd),
_ => {
eprintln!("syd-fd: Invalid FD specified with -f!");
return Err(Errno::EINVAL.into());
}
}
}
}
if opt_cmd.is_some() {
eprintln!("syd-fd: Unexpected positional argument with -u!");
return Err(Errno::EINVAL.into());
}
unix_send_fds(XPathBuf::from(addr), opt_msg, fds, opt_wait)?;
return Ok(ExitCode::SUCCESS);
}
let mut fds = Vec::new();
for fd in &opt_fds {
if let Some(idx) = memchr(b':', fd.as_bytes()) {
let remote_fd = &fd[..idx];
let remote_fd = match btoi::<RawFd>(remote_fd.as_bytes()) {
Ok(fd) if fd >= 0 => fd,
_ => {
eprintln!("syd-fd: Invalid FD specified with -f!");
return Err(Errno::EINVAL.into());
}
};
let local_fd = &fd[idx + 1..];
let local_fd = match local_fd {
"rand" => Some(libc::AT_FDCWD),
fd => match btoi::<RawFd>(fd.as_bytes()) {
Ok(fd) if fd >= 0 => Some(fd),
_ => {
eprintln!("syd-fd: Invalid FD specified with -f!");
return Err(Errno::EINVAL.into());
}
},
};
fds.push((remote_fd, local_fd));
} else {
let remote_fd = match btoi::<RawFd>(fd.as_bytes()) {
Ok(fd) if fd >= 0 => fd,
_ => {
eprintln!("syd-fd: Invalid FD specified with -f!");
return Err(Errno::EINVAL.into());
}
};
fds.push((remote_fd, None));
}
}
let opt_fds = fds;
let pid = if opt_fds.is_empty() {
let fds = proc_pid_fd(opt_pid)?;
for fd in fds {
#[expect(clippy::disallowed_methods)]
let fd = serde_json::to_string(&fd).expect("JSON");
println!("{fd}");
}
return Ok(ExitCode::SUCCESS);
} else if let Some(pid) = opt_pid {
pid
} else {
eprintln!("PID must be specified with -p!");
return Err(Errno::EINVAL.into());
};
let pid_fd = pidfd_open(pid, PIDFD_THREAD)?;
for (remote_fd, local_fd) in opt_fds {
let fd = pidfd_getfd(&pid_fd, remote_fd)?;
let fd = match local_fd {
Some(libc::AT_FDCWD) => {
let fd_rand = duprand(fd.as_raw_fd(), OFlag::empty())?;
drop(fd);
fd_rand
}
Some(newfd) => {
let fd_dup = unsafe { dup2_raw(&fd, newfd) }?;
drop(fd);
fd_dup.into()
}
None => fd,
};
let flags = fd_status_flags(&fd).unwrap_or(OFlag::empty());
eprintln!("syd-fd: GETFD {remote_fd} -> {} (flags: {flags:?})",
fd.as_raw_fd());
set_cloexec(&fd, false)?;
std::mem::forget(fd);
}
let opt_cmd = opt_cmd.unwrap_or_else(|| env::var_os(ENV_SH).unwrap_or(OsString::from(SYD_SH)));
eprintln!("syd-fd: EXEC {}", XPathBuf::from(opt_cmd.clone()));
Ok(ExitCode::from(
127 + Command::new(opt_cmd)
.args(opt_arg)
.exec()
.raw_os_error()
.unwrap_or(0) as u8,
))
}
fn help() {
println!("Usage: syd-fd [-h] [-p pid] [-f remote_fd[:local_fd]].. {{command [args...]}}");
println!(" syd-fd [-hw] -u socket [-m line] [-f fd]..");
println!("Interact with remote file descriptors");
println!("Execute the given command or `/bin/sh' with inherited remote fds.");
println!("List remote file descriptors with the given PID if no -f is given.");
println!("Use -p to specify PID.");
println!("Use -f remote_fd to specify remote file descriptor to transfer.");
println!("Optionally append a colon and a local fd to use as the target.");
println!("Use `rand' as target fd to duplicate to a random valid slot.");
println!("Use -u to send fds over the UNIX socket with SCM_RIGHTS.");
println!("Use -f fd1,fd2,... to specify file descriptors to send with -u.");
println!("Use -m message to specify message to send with -u.");
println!("Use -w to wait for response with -u.");
}
fn unix_send_fds(
addr: XPathBuf,
line: Option<OsString>,
fds: Vec<RawFd>,
wait: bool,
) -> Result<(), Errno> {
let addr = if matches!(addr.first(), Some(b'@')) {
UnixAddr::new_abstract(&addr.as_bytes()[1..])?
} else {
UnixAddr::new(addr.as_bytes())?
};
let sock = socket(
AddressFamily::Unix,
SockType::Stream,
SockFlag::SOCK_CLOEXEC,
None,
)?;
connect(sock.as_raw_fd(), &addr)?;
let mut data = line
.map(|line| XPathBuf::from(line).into_vec())
.unwrap_or_default();
if !data.ends_with(b"\n") {
data.push(b'\n');
}
let iov = [IoSlice::new(&data)];
let cmsgs = if fds.is_empty() {
Vec::new()
} else {
vec![ControlMessage::ScmRights(&fds)]
};
sendmsg::<UnixAddr>(sock.as_raw_fd(), &iov, &cmsgs, MsgFlags::empty(), None)?;
if !wait {
return Ok(());
}
let mut buf = [0u8; 4096];
let mut stdout = io::stdout().lock();
loop {
match read(&sock, &mut buf) {
Ok(0) => break,
Ok(n) => {
stdout.write_all(&buf[..n]).map_err(|e| err2no(&e))?;
stdout.flush().map_err(|e| err2no(&e))?;
}
Err(Errno::EINTR) => {}
Err(errno) => return Err(errno),
}
}
Ok(())
}
#[expect(clippy::type_complexity)]
fn proc_pid_fd(pid: Option<Pid>) -> Result<Vec<(RawFd, XPathBuf)>, Errno> {
let pid = pid.unwrap_or_else(getpid);
let mut dir = XPathBuf::try_from("/proc")?;
dir.try_push_pid(pid)?;
dir.try_push(b"fd")?;
#[expect(clippy::disallowed_methods)]
let dir = open(
&dir,
OFlag::O_RDONLY | OFlag::O_DIRECTORY | OFlag::O_CLOEXEC,
Mode::empty(),
)?;
let mut res = vec![];
let mut seen_dot = false;
let mut seen_dotdot = false;
loop {
let mut entries = match getdents64(&dir, DIRENT_BUF_SIZE) {
Ok(entries) => entries,
Err(Errno::ECANCELED) => break, Err(errno) => return Err(errno),
};
for entry in &mut entries {
if !seen_dot && entry.is_dot() {
seen_dot = true;
continue;
}
if !seen_dotdot && entry.is_dotdot() {
seen_dotdot = true;
continue;
}
let entry = XPath::from_bytes(entry.name_bytes());
let fd = parse_fd(entry)?;
let target = readlinkat(&dir, entry)?;
res.push((fd, target));
}
}
Ok(res)
}