use std::collections::BTreeMap;
use std::path::Path;
use crate::policy::sandbox_types::{Cap, NetworkPolicy, PathMatch, RuleEffect, SandboxPolicy};
use landlock::{
ABI, Access, AccessFs, CompatLevel, Compatible, PathBeneath, PathFd, Ruleset, RulesetAttr,
RulesetCreatedAttr, RulesetStatus,
};
use seccompiler::{
BpfProgram, SeccompAction, SeccompCmpArgLen, SeccompCmpOp, SeccompCondition, SeccompFilter,
SeccompRule, TargetArch,
};
use tracing::{Level, instrument};
use super::{SandboxError, SupportLevel, do_exec};
#[instrument(level = Level::TRACE, skip(policy))]
pub fn exec_sandboxed(
policy: &SandboxPolicy,
cwd: &Path,
command: &[String],
) -> Result<std::convert::Infallible, SandboxError> {
let cwd_str = cwd.to_string_lossy();
set_no_new_privs()?;
match &policy.network {
NetworkPolicy::Deny => {
install_seccomp_network_filter()?;
}
NetworkPolicy::Localhost => {
install_seccomp_advisory_network_filter()?;
}
NetworkPolicy::AllowDomains(_) => {
install_seccomp_advisory_network_filter()?;
}
NetworkPolicy::Allow => {
}
}
install_landlock_rules(policy, &cwd_str)?;
do_exec(command)
}
#[instrument(level = Level::TRACE)]
pub fn check_support() -> SupportLevel {
let abi_result = std::panic::catch_unwind(|| {
Ruleset::default()
.set_compatibility(CompatLevel::BestEffort)
.handle_access(AccessFs::from_all(ABI::V5))
});
match abi_result {
Ok(Ok(_)) => SupportLevel::Full,
Ok(Err(_)) => SupportLevel::Partial {
missing: vec!["Landlock may not be fully supported on this kernel".into()],
},
Err(_) => SupportLevel::Unsupported {
reason: "Landlock not available on this kernel".into(),
},
}
}
#[instrument(level = Level::TRACE)]
fn set_no_new_privs() -> Result<(), SandboxError> {
let result = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
if result != 0 {
return Err(SandboxError::Apply(format!(
"prctl(PR_SET_NO_NEW_PRIVS) failed: {}",
std::io::Error::last_os_error()
)));
}
Ok(())
}
#[instrument(level = Level::TRACE)]
fn cap_to_access_fs(caps: Cap) -> landlock::BitFlags<AccessFs> {
let mut access = landlock::BitFlags::<AccessFs>::empty();
if caps.contains(Cap::READ) {
access |= AccessFs::ReadFile | AccessFs::ReadDir;
}
if caps.contains(Cap::WRITE) {
access |= AccessFs::WriteFile | AccessFs::Truncate | AccessFs::Refer;
}
if caps.contains(Cap::CREATE) {
access |= AccessFs::MakeReg
| AccessFs::MakeDir
| AccessFs::MakeSym
| AccessFs::MakeFifo
| AccessFs::MakeSock;
}
if caps.contains(Cap::DELETE) {
access |= AccessFs::RemoveFile | AccessFs::RemoveDir;
}
if caps.contains(Cap::EXECUTE) {
access |= AccessFs::Execute;
}
access
}
#[instrument(level = Level::TRACE, skip(policy))]
fn install_landlock_rules(policy: &SandboxPolicy, cwd: &str) -> Result<(), SandboxError> {
let abi = ABI::V5;
let all_access = AccessFs::from_all(abi);
let mut ruleset = Ruleset::default()
.set_compatibility(CompatLevel::BestEffort)
.handle_access(all_access)
.map_err(|e| SandboxError::Apply(format!("landlock handle_access: {}", e)))?
.create()
.map_err(|e| SandboxError::Apply(format!("landlock create: {}", e)))?;
let default_access = cap_to_access_fs(policy.default);
if !default_access.is_empty() {
ruleset = add_path_rule(ruleset, "/", default_access)?;
}
ruleset = add_path_rule(
ruleset,
"/dev/null",
AccessFs::WriteFile | AccessFs::Truncate | AccessFs::ReadFile,
)?;
for rule in &policy.rules {
if rule.path_match == PathMatch::Regex {
continue;
}
if rule.effect == RuleEffect::Allow {
let resolved = SandboxPolicy::resolve_path(&rule.path, cwd);
let access = cap_to_access_fs(rule.caps);
let total_access = access | default_access;
if !total_access.is_empty() {
ruleset = add_path_rule(ruleset, &resolved, total_access)?;
}
}
}
let status = ruleset
.restrict_self()
.map_err(|e| SandboxError::Apply(format!("landlock restrict_self: {}", e)))?;
if status.ruleset == RulesetStatus::NotEnforced {
return Err(SandboxError::Apply(
"landlock: ruleset not enforced (kernel may not support Landlock)".into(),
));
}
Ok(())
}
#[instrument(level = Level::TRACE)]
fn add_path_rule(
ruleset: landlock::RulesetCreated,
path: &str,
access: landlock::BitFlags<AccessFs>,
) -> Result<landlock::RulesetCreated, SandboxError> {
match PathFd::new(path) {
Ok(fd) => ruleset
.add_rule(PathBeneath::new(fd, access))
.map_err(|e| SandboxError::Apply(format!("landlock add_rule for '{}': {}", path, e))),
Err(_) => {
Ok(ruleset)
}
}
}
#[instrument(level = Level::TRACE)]
fn install_seccomp_network_filter() -> Result<(), SandboxError> {
let mut rules: BTreeMap<i64, Vec<SeccompRule>> = BTreeMap::new();
let deny_syscalls = [
libc::SYS_connect,
libc::SYS_accept,
libc::SYS_accept4,
libc::SYS_bind,
libc::SYS_listen,
libc::SYS_getpeername,
libc::SYS_getsockname,
libc::SYS_shutdown,
libc::SYS_sendto,
libc::SYS_sendmmsg,
libc::SYS_recvmmsg,
libc::SYS_getsockopt,
libc::SYS_setsockopt,
libc::SYS_ptrace,
];
for &syscall in &deny_syscalls {
rules.insert(syscall, vec![]);
}
let unix_only_rule = SeccompRule::new(vec![
SeccompCondition::new(
0, SeccompCmpArgLen::Dword,
SeccompCmpOp::Ne,
libc::AF_UNIX as u64,
)
.map_err(|e| SandboxError::Apply(format!("seccomp condition: {}", e)))?,
])
.map_err(|e| SandboxError::Apply(format!("seccomp rule: {}", e)))?;
rules.insert(libc::SYS_socket, vec![unix_only_rule.clone()]);
rules.insert(libc::SYS_socketpair, vec![unix_only_rule]);
let arch = seccomp_arch()?;
apply_seccomp_filter(rules, arch)
}
#[instrument(level = Level::TRACE)]
fn install_seccomp_advisory_network_filter() -> Result<(), SandboxError> {
let mut rules: BTreeMap<i64, Vec<SeccompRule>> = BTreeMap::new();
let deny_syscalls = [
libc::SYS_accept,
libc::SYS_accept4,
libc::SYS_bind,
libc::SYS_listen,
libc::SYS_ptrace,
];
for &syscall in &deny_syscalls {
rules.insert(syscall, vec![]);
}
let arch = seccomp_arch()?;
apply_seccomp_filter(rules, arch)
}
fn seccomp_arch() -> Result<TargetArch, SandboxError> {
if cfg!(target_arch = "x86_64") {
Ok(TargetArch::x86_64)
} else if cfg!(target_arch = "aarch64") {
Ok(TargetArch::aarch64)
} else {
Err(SandboxError::Apply(
"seccomp: unsupported architecture".into(),
))
}
}
fn apply_seccomp_filter(
rules: BTreeMap<i64, Vec<SeccompRule>>,
arch: TargetArch,
) -> Result<(), SandboxError> {
let filter = SeccompFilter::new(
rules,
SeccompAction::Allow, SeccompAction::Errno(1), arch,
)
.map_err(|e| SandboxError::Apply(format!("seccomp filter: {}", e)))?;
let prog: BpfProgram = filter
.try_into()
.map_err(|e| SandboxError::Apply(format!("seccomp compile: {}", e)))?;
seccompiler::apply_filter(&prog)
.map_err(|e| SandboxError::Apply(format!("seccomp apply: {}", e)))?;
Ok(())
}