#![allow(clippy::disallowed_types)]
use std::{
collections::HashSet,
env,
fs::{canonicalize, read_dir, File},
io::Write,
os::unix::ffi::OsStrExt,
path::Path,
process::ExitCode,
};
use ahash::RandomState;
use data_encoding::HEXLOWER;
use nix::unistd::{access, AccessFlags};
use syd::{
elf::{ElfType, ExecutableFile, LinkingType},
fd::open_static_proc,
hash::{hash, hash_auto, hash_list},
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;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
enum Filter {
ElfFilter32,
ElfFilter64,
ElfFilterDynamic,
ElfFilterStatic,
ElfFilterPIE,
ElfFilterNoPIE,
ElfFilterXStack,
Script,
}
syd::main! {
use lexopt::prelude::*;
syd::set_sigpipe_dfl()?;
let mut opt_dsyd = false;
#[expect(clippy::disallowed_methods)]
let mut opt_path = env::var("PATH").unwrap_or("/usr/bin:/bin".to_string());
let mut opt_func = "auto".to_string();
let mut opt_action = None;
let mut opt_limit = 0usize; let mut elf_set: HashSet<Filter, RandomState> = HashSet::default();
let mut parser = lexopt::Parser::from_env();
while let Some(arg) = parser.next()? {
match arg {
Short('h') => {
help();
return Ok(ExitCode::SUCCESS);
}
Short('a') => opt_func = parser.value()?.to_str().ok_or(nix::errno::Errno::EINVAL)?.to_string(),
Short('k') => opt_action = Some("kill"),
Short('w') => opt_action = Some("warn"),
Short('p') => opt_path = parser.value()?.parse::<String>()?,
Short('l') => opt_limit = parser.value()?.parse::<usize>()?,
Short('s') => opt_dsyd = true,
Short('e') => match parser.value()?.parse::<String>()?.as_str() {
"32" => {
if elf_set.contains(&Filter::ElfFilter64) {
eprintln!("The option -e32 conflicts with -e64!");
return Ok(ExitCode::FAILURE);
}
elf_set.insert(Filter::ElfFilter32);
}
"64" => {
if elf_set.contains(&Filter::ElfFilter32) {
eprintln!("The option -e64 conflicts with -e32!");
return Ok(ExitCode::FAILURE);
}
elf_set.insert(Filter::ElfFilter64);
}
"d" => {
if elf_set.contains(&Filter::ElfFilterStatic) {
eprintln!("The option -ed conflicts with -es!");
return Ok(ExitCode::FAILURE);
}
elf_set.insert(Filter::ElfFilterDynamic);
}
"s" => {
if elf_set.contains(&Filter::ElfFilterDynamic) {
eprintln!("The option -es conflicts with -ed!");
return Ok(ExitCode::FAILURE);
}
elf_set.insert(Filter::ElfFilterStatic);
}
"p" => {
if elf_set.contains(&Filter::ElfFilterNoPIE) {
eprintln!("The option -ep conflicts with -eP!");
return Ok(ExitCode::FAILURE);
}
elf_set.insert(Filter::ElfFilterPIE);
}
"P" => {
if elf_set.contains(&Filter::ElfFilterPIE) {
eprintln!("The option -eP conflicts with -ep!");
return Ok(ExitCode::FAILURE);
}
elf_set.insert(Filter::ElfFilterNoPIE);
}
"x" => {
elf_set.insert(Filter::Script);
}
"X" => {
elf_set.insert(Filter::ElfFilterXStack);
}
value => {
eprintln!("Unknown ELF option: -e{value}");
return Ok(ExitCode::FAILURE);
}
},
_ => return Err(arg.unexpected().into()),
}
}
if opt_func == "list" {
open_static_proc()?;
for name in hash_list()? {
println!("{name}");
}
return Ok(ExitCode::SUCCESS);
}
if elf_set.is_empty() && opt_func == "auto" {
match hash_auto() {
Some(func) => opt_func = func,
None => {
eprintln!("Error: No supported hash algorithm found!");
return Ok(ExitCode::FAILURE);
}
}
}
let mut count = 0usize;
let mut path_set: HashSet<XPathBuf, RandomState> = HashSet::default();
let dirs = opt_path.split(':');
for dir in dirs {
if !Path::new(dir).is_dir() {
continue;
}
#[expect(clippy::disallowed_methods)]
if let Ok(entries) = read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() && access(&path, AccessFlags::X_OK).is_ok() {
if let Ok(path) = canonicalize(path).map(XPathBuf::from) {
if !path_set.insert(path.clone()) {
continue;
}
if !elf_set.is_empty() {
#[expect(non_snake_case)]
let filter = if let Ok(file) = File::open(&path) {
let filter_32 = elf_set.contains(&Filter::ElfFilter32);
let filter_64 = elf_set.contains(&Filter::ElfFilter64);
let filter_d = elf_set.contains(&Filter::ElfFilterDynamic);
let filter_s = elf_set.contains(&Filter::ElfFilterStatic);
let filter_p = elf_set.contains(&Filter::ElfFilterPIE);
let filter_P = elf_set.contains(&Filter::ElfFilterNoPIE);
let filter_x = elf_set.contains(&Filter::Script);
let filter_X = elf_set.contains(&Filter::ElfFilterXStack);
let check_linking =
filter_d || filter_s || filter_p || filter_P || filter_X;
if let Ok(exe) = ExecutableFile::parse(file, check_linking) {
match exe {
ExecutableFile::Elf {
elf_type: ElfType::Elf32,
..
} if filter_32 => true,
ExecutableFile::Elf {
elf_type: ElfType::Elf64,
..
} if filter_64 => true,
ExecutableFile::Elf {
linking_type: Some(LinkingType::Dynamic),
..
} if filter_d => true,
ExecutableFile::Elf {
linking_type: Some(LinkingType::Static),
..
} if filter_s => true,
ExecutableFile::Elf { pie: true, .. } if filter_p => true,
ExecutableFile::Elf { pie: false, .. } if filter_P => true,
ExecutableFile::Elf { xs: true, .. } if filter_X => true,
ExecutableFile::Script if filter_x => true,
_ => false,
}
} else {
false
}
} else {
false
};
#[expect(clippy::disallowed_methods)]
if filter {
let stdout = std::io::stdout();
let mut handle = stdout.lock();
handle.write_all(path.as_os_str().as_bytes()).unwrap();
handle.write_all(b"\n").unwrap();
}
} else if let Ok(mut file) = File::open(&path) {
if let Ok(true) = ExecutableFile::is_elf_file(&mut file) {
if let Ok(key) = hash(&opt_func, &file) {
let key = HEXLOWER.encode(&key);
let pre = if opt_dsyd { "/dev/syd/" } else { "" };
if let Some(act) = opt_action {
println!("{pre}force+{path}:{opt_func}:{key}:{act}");
} else {
println!("{pre}force+{path}:{opt_func}:{key}");
}
if opt_limit > 0 {
count += 1;
if count >= opt_limit {
return Ok(ExitCode::SUCCESS);
}
}
}
}
}
}
}
}
}
}
Ok(ExitCode::SUCCESS)
}
fn help() {
println!("Usage: syd-path [-a <algorithm>] [-heklpsw]");
println!("Write Integrity Force rules for binaries under PATH.");
println!("If at least one of the various *-e* options is specified,");
println!("List executables with specified information under PATH.");
println!();
println!(" -a <alg> Hash algorithm (default: auto-detect best available).");
println!(" Any algorithm listed in proc_crypto(5) with type ahash or shash.");
println!(" Use `-a list' to list available algorithms.");
println!(" Use `-a auto' to auto-detect the best algorithm (default).");
println!(" Examples: sha256, sha512, sha3-512, blake2b-256, md5, crc32c");
println!(" -k Use action kill (default).");
println!(" -w Use action warn.");
println!(" -p <path> Specify alternative PATH.");
println!(" -l <num> Limit by number of entries.");
println!(" -s Prefix rules with /dev/syd/.");
println!(" -e32 List 32-bit ELF executables (conflicts with -e64).");
println!(" -e64 List 64-bit ELF executables (conflicts with -e32).");
println!(" -ed List dynamically linked ELF executables (conflicts with -es).");
println!(" -es List statically linked ELF executables (conflicts with -ed).");
println!(" -ep List PIE executables (conflicts with -eP).");
println!(" -eP List non-PIE executables (conflicts with -ep).");
println!(" -ex List scripts under PATH.");
println!(" -eX List binaries with executable stack.");
println!(" -h Display this help.");
}