use std::{os::unix::ffi::OsStrExt, process::ExitCode, str::FromStr, time::Duration};
use libc::c_int;
use libseccomp::{ScmpArch, ScmpSyscall};
#[expect(clippy::disallowed_types)]
use nix::unistd::ForkResult;
use nix::{
errno::Errno,
fcntl::OFlag,
sys::{
signal::{kill, Signal},
stat::lstat,
wait::{waitpid, WaitPidFlag, WaitStatus},
},
unistd::{fork, Pid},
};
use syd::{
confine::print_seccomp_architectures,
fd::{open_static_proc, unix_inodes},
ioctl::{Ioctl, IoctlMap},
parsers::sandbox::str2u64,
proc::proc_unix_inodes,
wildmatch::inamematch,
};
#[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_arch = ScmpArch::native(); let mut opt_errno = false; let mut opt_ioctl = false; let mut opt_ghost = false; let mut opt_probe = false; let mut opt_open = false; let mut opt_signal = false; let mut opt_unix_nl = false; let mut opt_unix_pn = false; let mut opt_tmout = Duration::from_secs(3); let mut opt_sys = None;
let mut opt_arg = Vec::new();
let mut parser = lexopt::Parser::from_env();
while let Some(arg) = parser.next()? {
match arg {
Short('h') => {
help();
return Ok(ExitCode::SUCCESS);
}
Short('e') => opt_errno = true,
Short('i') => opt_ioctl = true,
Short('p') => opt_probe = true,
Short('o') => opt_open = true,
Short('s') => opt_signal = true,
Short('u') => opt_unix_nl = true,
Short('U') => opt_unix_pn = true,
Short('g') => {
opt_ghost = true;
opt_probe = true;
}
Short('t') => {
opt_tmout = parser
.value()?
.parse::<f64>()
.map(Duration::from_secs_f64)?
}
Short('a') => {
let value = parser.value()?.parse::<String>()?;
if matches!(value.to_ascii_lowercase().as_str(), "help" | "list") {
print_seccomp_architectures();
return Ok(ExitCode::SUCCESS);
}
opt_arch = match ScmpArch::from_str(&format!(
"SCMP_ARCH_{}",
value.to_ascii_uppercase()
)) {
Ok(opt_arch) => opt_arch,
Err(_) => {
eprintln!("Invalid architecture `{value}', use `-a list' for a list.");
return Ok(ExitCode::FAILURE);
}
};
}
Value(sys) if opt_sys.is_none() => opt_sys = Some(sys),
Value(arg) => {
opt_arg.push(arg);
opt_arg.extend(parser.raw_args()?);
}
_ => return Err(arg.unexpected().into()),
}
}
let flags = [opt_errno, opt_ioctl, opt_open, opt_signal, opt_unix_nl, opt_unix_pn];
if flags.iter().filter(|&&flag| flag).count() > 1 {
eprintln!("sys-sys: At most one of -e, -i, -o, -s, -u, and -U must be given!");
return Err(Errno::EINVAL.into());
}
if opt_unix_nl {
if opt_sys.is_some() {
eprintln!("syd-sys: -u does not accept a parameter!");
return Err(Errno::EINVAL.into());
}
for inode in unix_inodes()? {
println!("{inode}");
}
return Ok(ExitCode::SUCCESS);
} else if opt_unix_pn {
if opt_sys.is_some() {
eprintln!("syd-sys: -U does not accept a parameter!");
return Err(Errno::EINVAL.into());
}
open_static_proc()?;
for inode in proc_unix_inodes(Pid::this())? {
println!("{inode}");
}
return Ok(ExitCode::SUCCESS);
}
let sysarg = if let Some(value) = opt_sys {
value
} else {
let what = if opt_errno {
"errno"
} else if opt_ioctl {
"ioctl"
} else if opt_open {
"open"
} else if opt_signal {
"signal"
} else {
"syscall"
};
eprintln!("syd-sys: Expected {what} number or name regex as first argument!");
return Ok(ExitCode::FAILURE);
};
if opt_errno {
return match sysarg.parse::<u16>() {
Ok(0) => Ok(ExitCode::FAILURE),
Ok(num) => {
let errno = Errno::from_raw(i32::from(num));
if errno == Errno::UnknownErrno {
return Ok(ExitCode::FAILURE);
}
let estr = errno.to_string();
let mut iter = estr.split(": ");
let name = iter.next().unwrap_or("?");
let desc = iter.next().unwrap_or("?");
println!("{num}\t{name}\t{desc}");
Ok(ExitCode::SUCCESS)
}
Err(_) => {
let glob = sysarg.to_str().ok_or(Errno::EINVAL)?;
let mut ok = false;
for errno in (1..=4096).map(Errno::from_raw) {
if errno == Errno::UnknownErrno {
continue;
}
let estr = errno.to_string();
let mut iter = estr.split(": ");
let name = iter.next().unwrap_or("?");
let desc = iter.next().unwrap_or("?");
if inamematch(glob, &estr) {
println!("{}\t{}\t{}", errno as i32, name, desc);
ok = true;
}
}
Ok(if ok {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
})
}
};
} else if opt_ioctl {
let ioctl = IoctlMap::new(Some(opt_arch), false);
return match str2u64(sysarg.as_bytes()).map(|arg| arg as Ioctl) {
Ok(num) => {
if let Ok(Some(names)) = ioctl.get_names(num, opt_arch) {
for name in names {
println!("{name}\t{num}");
}
Ok(ExitCode::SUCCESS)
} else {
Ok(ExitCode::FAILURE)
}
}
Err(_) => {
let glob = sysarg.to_str().ok_or(Errno::EINVAL)?;
let iter = ioctl.iter(opt_arch).ok_or(Errno::EINVAL)?;
let mut ok = false;
for (name, num) in iter {
if inamematch(glob, name) {
println!("{name}\t{num}");
ok = true;
}
}
Ok(if ok {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
})
}
};
} else if opt_open {
return match sysarg
.parse::<c_int>()
.ok()
.and_then(OFlag::from_bits)
{
Some(OFlag::O_RDONLY) => {
println!("O_RDONLY\t0");
Ok(ExitCode::SUCCESS)
}
Some(flags) => {
for flag in flags {
let name = oflag_name(flag);
println!("{name}\t{}", flag.bits());
}
Ok(ExitCode::SUCCESS)
}
None => {
let mut glob = sysarg
.to_str()
.ok_or(Errno::EINVAL)?
.to_ascii_uppercase();
if !glob.starts_with("O_") {
glob.insert_str(0, "O_");
}
let mut ok = false;
for flag in OFlag::all() {
if flag == OFlag::O_ACCMODE {
for flag in [OFlag::O_RDONLY, OFlag::O_RDWR, OFlag::O_WRONLY] {
let name = oflag_name(flag);
if inamematch(&glob, &name) {
println!("{name}\t{}", flag.bits());
ok = true;
}
}
continue;
}
let name = oflag_name(flag);
if inamematch(&glob, &name) {
println!("{name}\t{}", flag.bits());
ok = true;
}
}
Ok(if ok {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
})
}
};
} else if opt_signal {
return match sysarg
.parse::<i32>()
.ok()
.and_then(|num| Signal::try_from(num).ok())
{
Some(sig) => {
println!("{sig}\t{}", sig as i32);
Ok(ExitCode::SUCCESS)
}
None => {
let mut glob = sysarg
.to_str()
.ok_or(Errno::EINVAL)?
.to_ascii_uppercase();
if !glob.starts_with("SIG") {
glob.insert_str(0, "SIG");
}
let mut ok = false;
for sig in Signal::iterator() {
if inamematch(&glob, sig.as_str()) {
println!("{sig}\t{}", sig as i32);
ok = true;
}
}
Ok(if ok {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
})
}
}
}
let syscalls = match sysarg.parse::<i32>() {
Ok(num) => {
let syscall = ScmpSyscall::from(num);
if !opt_probe {
if let Ok(name) = syscall.get_name_by_arch(opt_arch) {
println!("{name}\t{num}");
return Ok(ExitCode::SUCCESS);
} else {
return Ok(ExitCode::FAILURE);
}
}
vec![syscall]
}
Err(_) => {
let glob = sysarg.to_str().ok_or(Errno::EINVAL)?;
let mut ok = false;
let mut syscalls = vec![];
for (call, name) in (0..1024)
.map(|n| {
let call = ScmpSyscall::from(n);
(call, call.get_name_by_arch(opt_arch).unwrap_or_default())
})
.filter(|(_, name)| !name.is_empty())
{
if inamematch(glob, &name) {
if opt_probe {
syscalls.push(call);
} else {
let num = i32::from(call);
println!("{name}\t{num}");
ok = true;
}
}
}
if !opt_probe {
return Ok(if ok {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
});
}
syscalls
}
};
if opt_ghost {
if let Err(errno) = enable_ghost_mode() {
eprintln!("syd-sys: Failed to enable Syd's Ghost mode: {errno}");
if errno == Errno::ENOENT {
eprintln!("sys-sys: Ensure you're running under Syd, and the sandbox lock is off.");
}
return Ok(ExitCode::FAILURE);
}
}
let mut args: [Option<libc::c_long>; 6] = [None; 6];
for argc in 0..6 {
if let Some(value) = opt_arg.get(argc) {
args[argc] = match value.parse::<libc::c_long>() {
Ok(value) => Some(value),
Err(error) => {
eprintln!("syd-sys: Argument {argc} is invalid: {error}");
return Ok(ExitCode::FAILURE);
}
};
} else {
break;
}
}
for syscall in syscalls {
println!("{}", probe_syscall(syscall, &args, opt_tmout));
}
Ok(ExitCode::SUCCESS)
}
fn help() {
println!("Usage: syd-sys [-hgeiopstuU] [-a list|native|x86|x86_64|aarch64...] number|name-glob [<probe-args>...]");
println!("Given a number, print the matching syscall name and exit.");
println!("Given a glob, print case-insensitively matching syscall names and exit.");
println!("Given -e, query errnos rather than syscalls.");
println!("Given -i, query ioctls rather than syscalls.");
println!("Given -o, query open flags rather than syscalls.");
println!("Given -s, query signals rather than syscalls.");
println!("Given -p, probe the system call and print result.");
println!("Given -g with -p, enable Syd's Ghost mode prior to probing.");
println!("Use -t to specify syscall probe timeout in seconds, defaults to 3 seconds.");
println!("Use -u to list UNIX domain socket inodes using netlink(7)");
println!("Use -U to list UNIX domain socket inodes using proc_net(5)");
}
fn probe_syscall(
syscall: ScmpSyscall,
args: &[Option<libc::c_long>; 6],
timeout: Duration,
) -> String {
let snum = i32::from(syscall);
let name = syscall.get_name().unwrap_or(snum.to_string());
let argc = args
.iter()
.enumerate()
.rev()
.find(|&(_, elem)| elem.is_some())
.map_or(0, |(idx, _)| idx + 1);
#[expect(clippy::disallowed_methods)]
match unsafe { fork() }.expect("fork") {
ForkResult::Child => unsafe {
match argc {
0 => libc::syscall(snum.into()),
1 => libc::syscall(snum.into(), args[0].unwrap()),
2 => libc::syscall(snum.into(), args[0].unwrap(), args[1].unwrap()),
3 => libc::syscall(
snum.into(),
args[0].unwrap(),
args[1].unwrap(),
args[2].unwrap(),
),
4 => libc::syscall(
snum.into(),
args[0].unwrap(),
args[1].unwrap(),
args[2].unwrap(),
args[3].unwrap(),
),
5 => libc::syscall(
snum.into(),
args[0].unwrap(),
args[1].unwrap(),
args[2].unwrap(),
args[3].unwrap(),
args[4].unwrap(),
),
6 => libc::syscall(
snum.into(),
args[0].unwrap(),
args[1].unwrap(),
args[2].unwrap(),
args[3].unwrap(),
args[4].unwrap(),
args[5].unwrap(),
),
_ => unreachable!(),
};
libc::_exit(Errno::last() as i32);
},
ForkResult::Parent { child, .. } => {
let start = std::time::Instant::now();
let result = loop {
match waitpid(child, Some(WaitPidFlag::WNOHANG)) {
Ok(WaitStatus::Exited(_, code)) => {
if code == 0 {
break "0".to_string();
} else {
break errstr(code).to_string();
}
}
Ok(WaitStatus::Signaled(_, sig, core)) => {
if core {
break format!("{sig}!");
} else {
break format!("{sig}");
}
}
Ok(WaitStatus::StillAlive) if start.elapsed() >= timeout => {
let _ = kill(child, Signal::SIGKILL);
break "TMOUT".to_string();
}
Err(Errno::ECHILD) => break "ECHILD".to_string(),
_ => {}
}
};
match argc {
0 => format!("{name}()={result}"),
1 => format!("{name}(0x{:x})={result}", args[0].unwrap()),
2 => format!(
"{name}(0x{:x}, 0x{:x})={result}",
args[0].unwrap(),
args[1].unwrap()
),
3 => format!(
"{name}(0x{:x}, 0x{:x}, 0x{:x})={result}",
args[0].unwrap(),
args[1].unwrap(),
args[2].unwrap()
),
4 => format!(
"{name}(0x{:x}, 0x{:x}, 0x{:x}, 0x{:x})={result}",
args[0].unwrap(),
args[1].unwrap(),
args[2].unwrap(),
args[3].unwrap()
),
5 => format!(
"{name}(0x{:x}, 0x{:x}, 0x{:x}, 0x{:x}, 0x{:x})={result}",
args[0].unwrap(),
args[1].unwrap(),
args[2].unwrap(),
args[3].unwrap(),
args[4].unwrap()
),
6 => format!(
"{name}(0x{:x}, 0x{:x}, 0x{:x}, 0x{:x}, 0x{:x}, 0x{:x})={result}",
args[0].unwrap(),
args[1].unwrap(),
args[2].unwrap(),
args[3].unwrap(),
args[4].unwrap(),
args[5].unwrap()
),
_ => unreachable!(),
}
}
}
}
fn enable_ghost_mode() -> Result<(), Errno> {
match lstat("/dev/syd/ghost") {
Err(Errno::EOWNERDEAD) => Ok(()),
Err(errno) => Err(errno),
Ok(_) => Err(Errno::EOWNERDEAD),
}
}
fn errstr(errno: i32) -> String {
if let Some((name, _)) = Errno::from_raw(errno).to_string().split_once(':') {
name.to_string()
} else {
errno.to_string()
}
}
fn oflag_name(flag: OFlag) -> String {
if flag == OFlag::O_RDONLY {
return "O_RDONLY".to_string();
}
let s = format!("{flag:?}");
let s = s
.strip_prefix("OFlag(")
.and_then(|i| i.strip_suffix(")"))
.unwrap_or(&s);
s.replace(' ', "")
}