use std::{os::unix::ffi::OsStrExt, process::ExitCode};
use memchr::arch::all::is_equal;
use nix::errno::Errno;
use syd::{
confine::{lock_enabled, run_syd_shell},
eprintfln,
err::{err2exit, 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,
printf, printfln,
};
#[cfg(all(
not(target_os = "android"),
not(target_arch = "loongarch64"),
not(target_arch = "riscv64"),
target_page_size_4k,
target_pointer_width = "64"
))]
#[global_allocator]
static GLOBAL: hardened_malloc::HardenedMalloc = hardened_malloc::HardenedMalloc;
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 => printfln!("tcp_socket_identification")?,
Errata::SCOPED_SIGNAL_SAME_TGID => printfln!("scoped_signal_same_tgid")?,
unknown => { let mut unknown = unknown.bits();
while unknown != 0 {
let lsb = unknown & unknown.wrapping_neg();
printfln!("{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") {
printfln!("hard-requirement")?;
printfln!("soft-requirement")?;
printfln!("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() {
printfln!("{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() {
printfln!("{name}\t{}", access.bits())?;
}
for (name, access) in LANDLOCK_ACCESS_NET.iter() {
printfln!("{name}\t{}", access.bits())?;
}
return Ok(ExitCode::SUCCESS);
}
let rule = format!("allow/lock/{cmd}");
let cmd = match parse_landlock_cmd(&rule) {
Ok(cmd) => cmd,
Err(errno) => {
let _ = eprintfln!("Failed to parse Landlock rule `{rule}': {errno}!");
return Err(errno.into());
}
};
if let Err(errno) = policy.edit(cmd, None) {
let _ = eprintfln!("Failed to apply Landlock rule `{rule}': {errno}!");
return Err(errno.into());
}
}
Short('r') => {
let path = parser.value().map(XPathBuf::from)?;
let rule = format!("allow/lock/read,readdir,exec,ioctl+{path}");
let cmd = match parse_landlock_cmd(&rule) {
Ok(cmd) => cmd,
Err(errno) => {
let _ = eprintfln!("Failed to parse Landlock rule `{rule}': {errno}!");
return Err(errno.into());
}
};
if let Err(errno) = policy.edit(cmd, None) {
let _ = eprintfln!("Failed to apply Landlock rule `{rule}': {errno}!");
return Err(errno.into());
}
}
Short('w') => {
let path = parser.value().map(XPathBuf::from)?;
let rule = format!("allow/lock/all+{path}");
let cmd = match parse_landlock_cmd(&rule) {
Ok(cmd) => cmd,
Err(errno) => {
let _ = eprintfln!("Failed to parse Landlock rule `{rule}': {errno}!");
return Err(errno.into());
}
};
if let Err(errno) = policy.edit(cmd, None) {
let _ = eprintfln!("Failed to apply Landlock rule `{rule}': {errno}!");
return Err(errno.into());
}
}
Short('b') => {
let port = parser.value().map(XPathBuf::from)?;
let rule = format!("allow/lock/bind+{port}");
let cmd = match parse_landlock_cmd(&rule) {
Ok(cmd) => cmd,
Err(errno) => {
let _ = eprintfln!("Failed to parse Landlock rule `{rule}': {errno}!");
return Err(errno.into());
}
};
if let Err(errno) = policy.edit(cmd, None) {
let _ = eprintfln!("Failed to apply Landlock rule `{rule}': {errno}!");
return Err(errno.into());
}
}
Short('c') => {
let port = parser.value().map(XPathBuf::from)?;
let rule = format!("allow/lock/connect+{port}");
let cmd = match parse_landlock_cmd(&rule) {
Ok(cmd) => cmd,
Err(errno) => {
let _ = eprintfln!("Failed to parse Landlock rule `{rule}': {errno}!");
return Err(errno.into());
}
};
if let Err(errno) = policy.edit(cmd, None) {
let _ = eprintfln!("Failed to apply Landlock rule `{rule}': {errno}!");
return Err(errno.into());
}
}
Value(prog) => {
opt_cmd = Some(prog);
opt_arg.extend(parser.raw_args()?);
}
_ => return Err(arg.unexpected().into()),
}
}
if opt_abick && opt_check {
eprintfln!("-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;
printf!("{abi}")?;
return Ok(ExitCode::from(abi));
} else if opt_check {
if abi == ABI::Unsupported {
printfln!("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",
};
printfln!("Landlock ABI {} is {state_verb}.", abi as i32)?;
return Ok(ExitCode::from(state));
}
macro_rules! vprintln {
($($arg:tt)*) => {
if opt_verbose {
eprintfln!($($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 => {
eprintfln!("syd-lock: Landlock ABI {} is not enforced!", abi as i32)?;
return Ok(ExitCode::FAILURE);
}
},
Err(error) if policy.compat_level == Some(CompatLevel::BestEffort) => {
eprintfln!("syd-lock: Landlock ABI {} is unsupported: {error}!", abi as i32)?;
eprintfln!("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);
let _ = eprintfln!("syd-lock: Landlock add rules error: {source}!");
return Err(errno.into());
}
Err(RulesetError::CreateRuleset(CreateRulesetError::CreateRulesetCall {
source, ..
})) => {
let errno = err2no(&source);
let _ = eprintfln!("syd-lock: Landlock create ruleset error: {source}!");
return Err(errno.into());
}
Err(RulesetError::RestrictSelf(RestrictSelfError::SetNoNewPrivsCall {
source, ..
})) => {
let errno = err2no(&source);
let _ = eprintfln!("syd-lock: Set no new privs error: {source}!");
return Err(errno.into());
}
Err(RulesetError::RestrictSelf(RestrictSelfError::RestrictSelfCall { source, .. })) => {
let errno = err2no(&source);
let _ = eprintfln!("syd-lock: Landlock restrict self error: {source}!");
return Err(errno.into());
}
Err(source) => {
eprintfln!("syd-lock: Landlock handle accesses error: {source}!")?;
return Err(Errno::ENOTSUP.into());
}
};
Ok(err2exit(run_syd_shell(opt_cmd, opt_arg)))
}
fn help() -> Result<(), Errno> {
printfln!("Usage: syd-lock [-bchrvwASUV] [-C level] [-E errata] [-F flag]... [-l cat[,cat...]{{+|-}}path|port[-port]]... {{command [args...]}}")?;
printfln!("Run a program under landlock(7).")?;
printfln!("Use -v to increase verbosity.")?;
printfln!("Use -A to exit with Landlock ABI version.")?;
printfln!("Use -V to check for Landlock support.")?;
printfln!("Use -l cat[,cat...]{{+|-}}path|port[-port] to specify categories with path or closed port range.")?;
printfln!("Use -C level to set ABI compatibility level, one of hard-requirement, soft-requirement, best-effort.")?;
printfln!(
"Use -E errata to check for fixes in current ABI. Argument may be a name or number."
)?;
printfln!("Use -F flags to set landlock_restrict_self(2) flags.")?;
printfln!("Use -S to enabled scoped signals.")?;
printfln!("Use -U to enabled scoped UNIX abstract sockets.")?;
printfln!(
"Use `list' with -l, -C, -E, -F to list categories, compat-levels, erratas and flags."
)?;
printfln!("Use -r path as a shorthand for -l read,readdir,exec,ioctl+path.")?;
printfln!("Use -w path as a shorthand for -l all+path.")?;
printfln!("Use -b port as a shorthand for -l bind+port.")?;
printfln!("Use -c port as a shorthand for -l connect+port.")?;
printfln!("Categories:")?;
printfln!("\tall = *")?;
printfln!("\trpath = read + readdir")?;
printfln!("\twpath = write + truncate")?;
printfln!("\tcpath = create + delete + rename")?;
printfln!("\tdpath = mkbdev + mkcdev")?;
printfln!("\tspath = mkfifo + symlink")?;
printfln!("\ttpath = mkdir + rmdir")?;
printfln!("\tinet = bind + connect")?;
printfln!("\t ioctl")?;
printfln!("Refer to \"Sandboxing\" and \"Lock Sandboxing\" sections of syd(7) manual page.")?;
Ok(())
}