#![expect(clippy::disallowed_methods)]
#![expect(clippy::disallowed_types)]
use std::{
ffi::OsString,
io::{self, IoSlice, Write},
os::fd::{AsRawFd, RawFd},
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::DIRENT_BUF_SIZE,
confine::{run_cmd, syd_shell},
eprintfln,
err::{err2exit, err2no},
fd::{fd_status_flags, parse_fd, pidfd_getfd, pidfd_open, set_cloexec, PIDFD_THREAD},
path::{XPath, XPathBuf},
printfln,
rng::duprand,
};
#[cfg(all(
not(target_os = "android"),
not(target_arch = "loongarch64"),
not(target_arch = "riscv64"),
target_page_size_4k,
target_pointer_width = "64"
))]
#[global_allocator]
static GLOBAL: hardened_malloc::HardenedMalloc = hardened_malloc::HardenedMalloc;
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)),
_ => {
eprintfln!("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 => {
eprintfln!("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),
_ => {
eprintfln!("syd-fd: Invalid FD specified with -f!")?;
return Err(Errno::EINVAL.into());
}
}
}
}
if opt_cmd.is_some() {
eprintfln!("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,
_ => {
eprintfln!("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),
_ => {
eprintfln!("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,
_ => {
eprintfln!("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");
printfln!("{fd}")?;
}
return Ok(ExitCode::SUCCESS);
} else if let Some(pid) = opt_pid {
pid
} else {
eprintfln!("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());
eprintfln!("syd-fd: GETFD {remote_fd} -> {} (flags: {flags:?})",
fd.as_raw_fd())?;
set_cloexec(&fd, false)?;
std::mem::forget(fd);
}
let mut argv = match opt_cmd {
Some(cmd) => vec![cmd],
None => syd_shell(),
};
argv.extend(opt_arg);
let arg0 = argv.first().ok_or(Errno::EINVAL)?;
eprintfln!("syd-fd: EXEC {}", XPath::new(arg0))?;
Ok(err2exit(run_cmd(
Command::new(arg0).args(argv.get(1..).unwrap_or_default()),
)))
}
fn help() -> Result<(), Errno> {
printfln!("Usage: syd-fd [-h] [-p pid] [-f remote_fd[:local_fd]].. {{command [args...]}}")?;
printfln!(" syd-fd [-hw] -u socket [-m line] [-f fd]..")?;
printfln!("Interact with remote file descriptors")?;
printfln!("Execute the given command or `/bin/sh' with inherited remote fds.")?;
printfln!("List remote file descriptors with the given PID if no -f is given.")?;
printfln!("Use -p to specify PID.")?;
printfln!("Use -f remote_fd to specify remote file descriptor to transfer.")?;
printfln!("Optionally append a colon and a local fd to use as the target.")?;
printfln!("Use `rand' as target fd to duplicate to a random valid slot.")?;
printfln!("Use -u to send fds over the UNIX socket with SCM_RIGHTS.")?;
printfln!("Use -f fd1,fd2,... to specify file descriptors to send with -u.")?;
printfln!("Use -m message to specify message to send with -u.")?;
printfln!("Use -w to wait for response with -u.")?;
Ok(())
}
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)
}