use std::{
os::unix::ffi::OsStrExt,
process::{Command, ExitCode},
};
use memchr::arch::all::is_equal;
use nix::errno::Errno;
use syd::{
confine::{lock_enabled, run_cmd},
err::err2no,
landlock::{
AddRuleError, AddRulesError, CompatLevel, CreateRulesetError, Errata, RestrictSelfError,
RestrictSelfFlags, RulesetError, RulesetStatus, ABI,
},
landlock_policy::{LandlockPolicy, LANDLOCK_ACCESS_FS, LANDLOCK_ACCESS_NET},
parsers::sandbox::parse_landlock_cmd,
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;
const COMPAT_LEVEL_HARD: &[&[u8]] = &[b"h", b"hard", b"hard-requirement"];
const COMPAT_LEVEL_SOFT: &[&[u8]] = &[b"s", b"soft", b"soft-requirement"];
const COMPAT_LEVEL_BEST: &[&[u8]] = &[b"b", b"best", b"best-effort"];
syd::main! {
use lexopt::prelude::*;
syd::set_sigpipe_dfl()?;
let mut opt_abick = false;
let mut opt_check = false;
let mut opt_verbose = false;
let mut opt_cmd = None;
let mut opt_arg = Vec::new();
let mut policy = LandlockPolicy {
compat_level: Some(CompatLevel::HardRequirement),
..Default::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_abick = true,
Short('V') => opt_check = true,
Short('v') => opt_verbose = true,
Short('E') => {
let errata = parser.value()?;
let errata = errata.as_bytes();
if is_equal(errata, b"list") {
let errata = Errata::query();
for fix in errata {
match fix {
Errata::TCP_SOCKET_IDENTIFICATION => println!("tcp_socket_identification"),
Errata::SCOPED_SIGNAL_SAME_TGID => println!("scoped_signal_same_tgid"),
unknown => { let mut unknown = unknown.bits();
while unknown != 0 {
let lsb = unknown & unknown.wrapping_neg();
println!("{lsb:#x}");
unknown &= unknown - 1;
}
}
}
}
return Ok(ExitCode::SUCCESS);
}
let requested = LandlockPolicy::parse_errata(errata)?;
let supported = Errata::query();
let code = if supported.contains(requested) {
0 } else if supported.intersects(requested) {
1 } else {
2 };
return Ok(ExitCode::from(code));
}
Short('S') => policy.scoped_sig = true,
Short('U') => policy.scoped_abs = true,
Short('C') => {
let level = parser.value()?;
let level = level.as_bytes();
if is_equal(level, b"list") {
println!("hard-requirement");
println!("soft-requirement");
println!("best-effort");
return Ok(ExitCode::SUCCESS);
}
if COMPAT_LEVEL_HARD.iter().any(|&l| is_equal(level, l)) {
policy.compat_level = Some(CompatLevel::HardRequirement);
} else if COMPAT_LEVEL_SOFT.iter().any(|&l| is_equal(level, l)) {
policy.compat_level = Some(CompatLevel::SoftRequirement);
} else if COMPAT_LEVEL_BEST.iter().any(|&l| is_equal(level, l)) {
policy.compat_level = Some(CompatLevel::BestEffort);
} else {
return Err(Errno::EINVAL.into());
}
}
Short('F') => {
let flags = parser.value()?;
let flags = flags.as_bytes();
if is_equal(flags, b"list") {
for flag in RestrictSelfFlags::all().iter() {
println!("{flag}\t{}", flag.bits());
}
return Ok(ExitCode::SUCCESS);
}
let flags = LandlockPolicy::parse_restrict_self_flags(flags, true)?;
policy.restrict_self_flags.insert(flags);
}
Short('l') => {
let cmd = parser.value().map(XPathBuf::from)?;
if cmd.is_equal(b"list") {
for (name, access) in LANDLOCK_ACCESS_FS.iter() {
println!("{name}\t{}", access.bits());
}
for (name, access) in LANDLOCK_ACCESS_NET.iter() {
println!("{name}\t{}", access.bits());
}
return Ok(ExitCode::SUCCESS);
}
let cmd = parse_landlock_cmd(&format!("allow/lock/{cmd}"))?;
policy.edit(cmd, None)?;
}
Short('r') => {
let path = parser.value().map(XPathBuf::from)?;
let cmd = parse_landlock_cmd(&format!("allow/lock/read,readdir,exec,ioctl+{path}"))?;
policy.edit(cmd, None)?;
}
Short('w') => {
let path = parser.value().map(XPathBuf::from)?;
let cmd = parse_landlock_cmd(&format!("allow/lock/all+{path}"))?;
policy.edit(cmd, None)?;
}
Short('b') => {
let port = parser.value().map(XPathBuf::from)?;
let cmd = parse_landlock_cmd(&format!("allow/lock/bind+{port}"))?;
policy.edit(cmd, None)?;
}
Short('c') => {
let port = parser.value().map(XPathBuf::from)?;
let cmd = parse_landlock_cmd(&format!("allow/lock/connect+{port}"))?;
policy.edit(cmd, None)?;
}
Value(prog) => {
opt_cmd = Some(prog);
opt_arg.extend(parser.raw_args()?);
}
_ => return Err(arg.unexpected().into()),
}
}
if opt_abick && opt_check {
eprintln!("-A and -V are mutually exclusive!");
return Err(Errno::EINVAL.into());
}
let abi = ABI::new_current();
if opt_abick {
let abi = abi as i32 as u8;
print!("{abi}");
return Ok(ExitCode::from(abi));
} else if opt_check {
if abi == ABI::Unsupported {
println!("Landlock is not supported.");
return Ok(ExitCode::from(127));
}
let state = lock_enabled(abi);
let state_verb = match state {
0 => "fully enforced",
1 => "partially enforced",
2 => "not enforced",
_ => "unsupported",
};
println!("Landlock ABI {} is {state_verb}.", abi as i32);
return Ok(ExitCode::from(state));
}
let cmd = if let Some(cmd) = opt_cmd {
cmd
} else {
help();
return Ok(ExitCode::SUCCESS);
};
macro_rules! vprintln {
($($arg:tt)*) => {
if opt_verbose {
eprintln!($($arg)*);
}
};
}
match policy.restrict_self(abi) {
Ok(status) => match status.ruleset {
RulesetStatus::FullyEnforced => {
vprintln!("syd-lock: Landlock ABI {} is fully enforced.", abi as i32)
}
RulesetStatus::PartiallyEnforced => {
vprintln!(
"syd-lock: Landlock ABI {} is partially enforced.",
abi as i32
)
}
RulesetStatus::NotEnforced => {
eprintln!("syd-lock: Landlock ABI {} is not enforced!", abi as i32);
return Ok(ExitCode::FAILURE);
}
},
Err(error) if policy.compat_level == Some(CompatLevel::BestEffort) => {
eprintln!("syd-lock: Landlock ABI {} is unsupported: {error}!", abi as i32);
eprintln!("syd-lock: Compatibility level is best effort, resuming...");
}
Err(
RulesetError::AddRules(AddRulesError::Fs(AddRuleError::AddRuleCall { source, .. }))
| RulesetError::AddRules(AddRulesError::Net(AddRuleError::AddRuleCall {
source, ..
}))
| RulesetError::AddRules(AddRulesError::Scope(AddRuleError::AddRuleCall {
source, ..
})),
) => {
let errno = err2no(&source);
eprintln!("syd-lock: Landlock add rules error: {source}!");
return Err(errno.into());
}
Err(RulesetError::CreateRuleset(CreateRulesetError::CreateRulesetCall {
source, ..
})) => {
let errno = err2no(&source);
eprintln!("syd-lock: Landlock create ruleset error: {source}!");
return Err(errno.into());
}
Err(RulesetError::RestrictSelf(RestrictSelfError::SetNoNewPrivsCall {
source, ..
})) => {
let errno = err2no(&source);
eprintln!("syd-lock: Set no new privs error: {source}!");
return Err(errno.into());
}
Err(RulesetError::RestrictSelf(RestrictSelfError::RestrictSelfCall { source, .. })) => {
let errno = err2no(&source);
eprintln!("syd-lock: Landlock restrict self error: {source}!");
return Err(errno.into());
}
Err(source) => {
eprintln!("syd-lock: Landlock handle accesses error: {source}!");
return Err(Errno::ENOTSUP.into());
}
};
let mut cmd = Command::new(cmd);
let cmd = cmd.args(opt_arg);
Ok(ExitCode::from(run_cmd(cmd)))
}
fn help() {
println!("Usage: syd-lock [-bchrvwASUV] [-C level] [-E errata] [-F flag]... [-l cat[,cat...]{{+|-}}path|port[-port]]... {{command [args...]}}");
println!("Run a program under landlock(7).");
println!("Use -v to increase verbosity.");
println!("Use -A to exit with Landlock ABI version.");
println!("Use -V to check for Landlock support.");
println!("Use -l cat[,cat...]{{+|-}}path|port[-port] to specify categories with path or closed port range.");
println!("Use -C level to set ABI compatibility level, one of hard-requirement, soft-requirement, best-effort.");
println!("Use -E errata to check for fixes in current ABI. Argument may be a name or number.");
println!("Use -F flags to set landlock_restrict_self(2) flags.");
println!("Use -S to enabled scoped signals.");
println!("Use -U to enabled scoped UNIX abstract sockets.");
println!(
"Use `list' with -l, -C, -E, -F to list categories, compat-levels, erratas and flags."
);
println!("Use -r path as a shorthand for -l read,readdir,exec,ioctl+path.");
println!("Use -w path as a shorthand for -l all+path.");
println!("Use -b port as a shorthand for -l bind+port.");
println!("Use -c port as a shorthand for -l connect+port.");
println!("Categories:");
println!("\tall = *");
println!("\trpath = read + readdir");
println!("\twpath = write + truncate");
println!("\tcpath = create + delete + rename");
println!("\tdpath = mkbdev + mkcdev");
println!("\tspath = mkfifo + symlink");
println!("\ttpath = mkdir + rmdir");
println!("\tinet = bind + connect");
println!("\t ioctl");
println!(
"Refer to the \"Sandboxing\" and \"Lock Sandboxing\" sections of the syd(7) manual page."
);
}