use std::{os::fd::AsRawFd, process::ExitCode};
use libseccomp::{scmp_cmp, ScmpAction, ScmpFilterContext, ScmpSyscall};
use nix::{errno::Errno, sys::resource::Resource};
use syd::{
config::{ALLOC_SYSCALLS, ENV_SKIP_SCMP, VDSO_SYSCALLS},
confine::{
confine_mdwe, confine_rlimit_zero, confine_scmp_madvise, confine_scmp_wx_all, secure_getenv,
},
elf::{ElfError, ElfType, ExecutableFile, LinkingType},
err::SydResult,
landlock_policy::LandlockPolicy,
path::XPathBuf,
};
#[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 etyp = false;
let mut is_32bit = false;
let mut is_64bit = false;
let mut is_dynamic = false;
let mut is_static = false;
let mut is_pie = false;
let mut is_script = false;
let mut is_xstack = false;
let mut opt_path = None;
let mut parser = lexopt::Parser::from_env();
while let Some(arg) = parser.next()? {
match arg {
Short('h') => {
help();
return Ok(ExitCode::SUCCESS);
}
Short('3') => is_32bit = true,
Short('6') => is_64bit = true,
Short('d') => is_dynamic = true,
Short('s') => is_static = true,
Short('p') => is_pie = true,
Short('x') => is_script = true,
Short('X') => is_xstack = true,
Short('t') => etyp = true,
Value(path) => opt_path = Some(XPathBuf::from(path)),
_ => return Err(arg.unexpected().into()),
}
}
let flags = [
is_32bit, is_64bit, is_dynamic, is_static, is_pie, etyp, is_script, is_xstack,
];
let info = match flags.iter().filter(|&&flag| flag).count() {
0 => true,
1 => false,
_ => {
eprintln!("syd-elf: At most one of -3, -6, -d, -s, -p, -t, -x and -X must be given!");
return Err(Errno::EINVAL.into());
}
};
let path = if let Some(path) = opt_path {
path
} else {
eprintln!("syd-elf: Expected exactly one path as argument!");
return Err(Errno::EINVAL.into());
};
let check_linking = info || is_dynamic || is_static || is_pie || is_xstack;
#[expect(clippy::disallowed_methods)]
#[expect(clippy::disallowed_types)]
let file = std::fs::File::open(&path)?;
if secure_getenv(ENV_SKIP_SCMP).is_none() {
confine(&file)?;
}
let exe = match ExecutableFile::parse(file, check_linking) {
Ok(exe) => Some(exe),
Err(ElfError::BadMagic) => None,
Err(error) => return Err(error.into()),
};
if is_script {
return Ok(match exe {
Some(ExecutableFile::Script) => ExitCode::SUCCESS,
_ => ExitCode::FAILURE,
});
} else if is_32bit {
return Ok(match exe {
Some(ExecutableFile::Elf {
elf_type: ElfType::Elf32,
..
}) => ExitCode::SUCCESS,
_ => ExitCode::FAILURE,
});
} else if is_64bit {
return Ok(match exe {
Some(ExecutableFile::Elf {
elf_type: ElfType::Elf64,
..
}) => ExitCode::SUCCESS,
_ => ExitCode::FAILURE,
});
} else if is_dynamic {
return Ok(match exe {
Some(ExecutableFile::Elf {
linking_type: Some(LinkingType::Dynamic),
..
}) => ExitCode::SUCCESS,
_ => ExitCode::FAILURE,
});
} else if is_static {
return Ok(match exe {
Some(ExecutableFile::Elf {
linking_type: Some(LinkingType::Static),
..
}) => ExitCode::SUCCESS,
_ => ExitCode::FAILURE,
});
} else if is_pie {
return Ok(match exe {
Some(ExecutableFile::Elf { pie: true, .. }) => ExitCode::SUCCESS,
_ => ExitCode::FAILURE,
});
} else if is_xstack {
return Ok(match exe {
Some(ExecutableFile::Elf { xs: true, .. }) => ExitCode::SUCCESS,
_ => ExitCode::FAILURE,
});
} else if etyp {
let name = match exe {
Some(ExecutableFile::Elf { file_type, .. }) => file_type.to_string(),
Some(ExecutableFile::Script) => "script".to_string(),
None => "unknown".to_string(),
};
println!("{name}");
return Ok(ExitCode::SUCCESS);
} else if let Some(exe) = exe {
println!("{path}:{exe}");
} else {
println!("{path}:UNKNOWN");
}
Ok(ExitCode::SUCCESS)
}
fn help() {
println!("Usage: syd-elf [-36dhpstxX] binary|script");
println!("Given a binary, print file name and ELF information.");
println!("Given a script, print file name and \"SCRIPT\".");
println!("The information line is a list of fields delimited by colons.");
println!("Given -3, exit with success if the given binary is 32-bit.");
println!("Given -6, exit with success if the given binary is 64-bit.");
println!("Given -d, exit with success if the given binary is dynamically linked.");
println!("Given -s, exit with success if the given binary is statically linked.");
println!("Given -p, exit with success if the given binary is PIE.");
println!("Given -t, print the type of the file.");
println!("Given -x, exit with success if the given executable is a script.");
println!("Given -X, exit with success if the given binary has executable stack.");
}
fn confine<Fd: AsRawFd>(fd: &Fd) -> SydResult<()> {
let _ = confine_rlimit_zero(&[
Resource::RLIMIT_FSIZE,
Resource::RLIMIT_NOFILE,
Resource::RLIMIT_NPROC,
]);
let abi = syd::landlock::ABI::new_current();
let policy = LandlockPolicy {
scoped_abs: true,
scoped_sig: true,
..Default::default()
};
let _ = policy.restrict_self(abi);
let _ = confine_mdwe(false);
let _ = confine_scmp_wx_all();
let mut ctx = ScmpFilterContext::new(ScmpAction::KillProcess)?;
ctx.set_ctl_nnp(true)?;
ctx.set_act_badarch(ScmpAction::KillProcess)?;
ctx.set_api_sysrawrc(true)?;
let _ = ctx.set_ctl_optimize(2);
const BASE_SET: &[&str] = &[
"brk",
"exit",
"exit_group",
"mmap",
"mmap2",
"mprotect",
"mremap",
"munmap",
"rt_sigprocmask",
"sigaltstack",
"sigprocmask",
];
for sysname in BASE_SET.iter().chain(ALLOC_SYSCALLS).chain(VDSO_SYSCALLS) {
let syscall = if let Ok(syscall) = ScmpSyscall::from_name(sysname) {
syscall
} else {
continue;
};
ctx.add_rule(ScmpAction::Allow, syscall)?;
}
confine_scmp_madvise(&mut ctx)?;
let fd = fd.as_raw_fd() as u64;
for sysname in ["close", "read", "readv", "_llseek", "lseek"] {
ctx.add_rule_conditional(
ScmpAction::Allow,
ScmpSyscall::from_name(sysname)?,
&[scmp_cmp!($arg0 == fd)],
)?;
}
const F_GETFD: u64 = nix::libc::F_GETFD as u64;
const F_SETFD: u64 = nix::libc::F_SETFD as u64;
for sysname in ["fcntl", "fcntl64"] {
if let Ok(syscall) = ScmpSyscall::from_name(sysname) {
for op in [F_GETFD, F_SETFD] {
ctx.add_rule_conditional(
ScmpAction::Allow,
syscall,
&[scmp_cmp!($arg0 == fd), scmp_cmp!($arg1 == op)],
)?;
}
}
}
let sysname = "prctl";
if let Ok(syscall) = ScmpSyscall::from_name(sysname) {
let op = libc::PR_SET_VMA as u64;
ctx.add_rule_conditional(ScmpAction::Allow, syscall, &[scmp_cmp!($arg0 == op)])?;
}
const FD_1: u64 = nix::libc::STDOUT_FILENO as u64;
const FD_2: u64 = nix::libc::STDERR_FILENO as u64;
if let Ok(syscall) = ScmpSyscall::from_name("write") {
for fd in [FD_1, FD_2] {
ctx.add_rule_conditional(ScmpAction::Allow, syscall, &[scmp_cmp!($arg0 == fd)])?;
}
}
if let Err(error) = ctx.load() {
if error
.sysrawrc()
.map(|errno| errno.abs())
.unwrap_or(libc::ECANCELED)
!= libc::EINVAL
{
return Err(error.into());
}
}
Ok(())
}