use std::{
io::{stdout, Write},
os::unix::process::CommandExt,
process::{Command, ExitCode},
};
use nix::errno::Errno;
use serde_json::json;
use syd::{
caps::securebits::{get_securebits, set_securebits, SecureBits},
compat::{get_no_new_privs, set_no_new_privs},
};
#[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_nnp = false;
let mut opt_sec = SecureBits::empty();
let mut opt_cmd = 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('p' | 'P') => opt_nnp = true,
Short('r') => opt_sec.insert(SecureBits::SECBIT_NOROOT),
Short('R') => opt_sec.insert(SecureBits::SECBIT_NOROOT_LOCKED),
Short('s') => opt_sec.insert(SecureBits::SECBIT_NO_SETUID_FIXUP),
Short('S') => opt_sec.insert(SecureBits::SECBIT_NO_SETUID_FIXUP_LOCKED),
Short('k') => opt_sec.insert(SecureBits::SECBIT_KEEP_CAPS),
Short('K') => opt_sec.insert(SecureBits::SECBIT_KEEP_CAPS_LOCKED),
Short('a') => opt_sec.insert(SecureBits::SECBIT_NO_CAP_AMBIENT_RAISE),
Short('A') => opt_sec.insert(SecureBits::SECBIT_NO_CAP_AMBIENT_RAISE_LOCKED),
Short('x') => opt_sec.insert(SecureBits::SECBIT_EXEC_RESTRICT_FILE),
Short('X') => opt_sec.insert(SecureBits::SECBIT_EXEC_RESTRICT_FILE_LOCKED),
Short('i') => opt_sec.insert(SecureBits::SECBIT_EXEC_DENY_INTERACTIVE),
Short('I') => opt_sec.insert(SecureBits::SECBIT_EXEC_DENY_INTERACTIVE_LOCKED),
Value(prog) => {
opt_cmd = Some(prog);
opt_arg.extend(parser.raw_args()?);
}
_ => return Err(arg.unexpected().into()),
}
}
let cmd = if let Some(cmd) = opt_cmd {
if !opt_nnp && opt_sec.is_empty() {
eprintln!("syd-sec: No secure bits specified for command!");
return Err(Errno::EINVAL.into());
}
cmd
} else if !opt_nnp && opt_sec.is_empty() {
let nnp = get_no_new_privs()?;
let sec = get_securebits()?;
#[expect(clippy::disallowed_methods)]
let data = json!({
"nnp": nnp,
"sec": sec,
});
#[expect(clippy::disallowed_methods)]
let mut data = serde_json::to_string(&data).expect("JSON");
data.push('\n');
stdout().write_all(data.as_bytes())?;
return Ok(ExitCode::SUCCESS);
} else {
if opt_nnp && !get_no_new_privs()? {
return Ok(ExitCode::FAILURE);
}
if !opt_sec.is_empty() && !get_securebits()?.contains(opt_sec) {
return Ok(ExitCode::FAILURE);
}
return Ok(ExitCode::SUCCESS);
};
if opt_nnp {
set_no_new_privs()?;
}
if !opt_sec.is_empty() {
opt_sec.insert(get_securebits()?);
set_securebits(opt_sec)?;
}
Ok(ExitCode::from(
127 + Command::new(cmd)
.args(opt_arg)
.exec()
.raw_os_error()
.unwrap_or(0) as u8,
))
}
fn help() {
println!("Usage: syd-sec [-ahikprsxAIKPRSX] {{command [args...]}}");
println!("Print secure bits or run command with secure bits set.");
println!("Given no arguments, print information on process secure bits in compact JSON.");
println!("Given command with arguments, set given secure bits and execute the command.");
println!("Given no commands and some arguments, test given secure bits and exit with success if all are set.");
println!("Use -p, -P to set/test no_new_privs attribute");
println!("Use -r, -R to set/test bit SECBIT_NOROOT");
println!("Use -s, -S to set/test bit SECBIT_NO_SETUID_FIXUP");
println!("Use -k, -K to set/test bit SECBIT_KEEP_CAPS");
println!("Use -a, -A to set/test bit SECBIT_NO_CAP_AMBIENT_RAISE");
println!("Use -x, -X to set/test bit SECBIT_EXEC_RESTRICT_FILE");
println!("Use -i, -I to set/test bit SECBIT_DENY_INTERACTIVE");
println!("Capital letter options set/test locked version of the respective secure bit.");
}