use std::path::{Path, PathBuf};
use crate::{
error::SandboxError,
sandbox::{ProtectHome, ProtectSystem, SandboxPolicy},
};
use landlock::{
Access, AccessFs, AccessNet, BitFlags, CompatLevel, Compatible, PathBeneath, PathFd, Ruleset,
RulesetAttr, RulesetCreated, RulesetCreatedAttr, ABI,
};
#[cfg(target_env = "gnu")]
type RlimitResource = libc::__rlimit_resource_t;
#[cfg(not(target_env = "gnu"))]
type RlimitResource = libc::c_int;
pub fn apply_sandbox(policy: &SandboxPolicy) -> Result<(), SandboxError> {
let config = &policy.config;
if config.no_new_privileges {
apply_no_new_privs()?;
}
apply_resource_limits(config)?;
apply_landlock(policy)?;
Ok(())
}
fn apply_no_new_privs() -> Result<(), SandboxError> {
let ret = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
if ret != 0 {
return Err(SandboxError::Io(std::io::Error::last_os_error()));
}
Ok(())
}
fn apply_resource_limits(config: &crate::sandbox::Sandbox) -> Result<(), SandboxError> {
let limits: [(Option<u64>, RlimitResource); 8] = [
(config.limit_nofile, libc::RLIMIT_NOFILE),
(config.limit_nproc, libc::RLIMIT_NPROC),
(config.limit_core, libc::RLIMIT_CORE),
(config.limit_fsize, libc::RLIMIT_FSIZE),
(config.limit_data, libc::RLIMIT_DATA),
(config.limit_stack, libc::RLIMIT_STACK),
(config.limit_cpu, libc::RLIMIT_CPU),
(config.limit_memlock, libc::RLIMIT_MEMLOCK),
];
for (value, resource) in limits {
if let Some(limit) = value {
let rlim = libc::rlimit {
rlim_cur: limit,
rlim_max: limit,
};
let ret = unsafe { libc::setrlimit(resource, &rlim) };
if ret != 0 {
return Err(SandboxError::Io(std::io::Error::last_os_error()));
}
}
}
Ok(())
}
fn apply_landlock(policy: &SandboxPolicy) -> Result<(), SandboxError> {
let config = &policy.config;
let has_fs_restrictions = config.protect_system != ProtectSystem::No
|| config.protect_home != ProtectHome::No
|| !config.read_only_paths.is_empty()
|| !config.read_write_paths.is_empty()
|| !config.inaccessible_paths.is_empty()
|| !config.exec_paths.is_empty()
|| !config.no_exec_paths.is_empty();
let has_net_restrictions = config.private_network;
if !has_fs_restrictions && !has_net_restrictions {
return Ok(());
}
let read_access = AccessFs::ReadFile | AccessFs::ReadDir;
let write_access = AccessFs::WriteFile
| AccessFs::RemoveFile
| AccessFs::RemoveDir
| AccessFs::MakeChar
| AccessFs::MakeDir
| AccessFs::MakeReg
| AccessFs::MakeSock
| AccessFs::MakeFifo
| AccessFs::MakeBlock
| AccessFs::MakeSym
| AccessFs::Truncate;
let exec_access = AccessFs::Execute;
let all_fs_access = read_access | write_access | exec_access | AccessFs::Refer;
let mut ruleset_builder = Ruleset::default().handle_access(all_fs_access)?;
if has_net_restrictions {
ruleset_builder =
ruleset_builder.handle_access(AccessNet::BindTcp | AccessNet::ConnectTcp)?;
}
let mut ruleset = ruleset_builder.create()?;
let mut deny: Vec<PathBuf> = config.inaccessible_paths.clone();
if config.protect_home == ProtectHome::Yes {
deny.push(PathBuf::from("/home"));
deny.push(PathBuf::from("/root"));
deny.push(PathBuf::from("/run/user"));
}
let deny = canonicalize_existing(&deny);
let base_access = match config.protect_system {
ProtectSystem::Strict => read_access | exec_access,
ProtectSystem::No | ProtectSystem::Yes | ProtectSystem::Full => all_fs_access,
};
ruleset = grant_beneath_excluding(ruleset, Path::new("/"), base_access, &deny)?;
for path in &config.read_write_paths {
if let Ok(fd) = PathFd::new(path) {
ruleset = ruleset.add_rule(PathBeneath::new(fd, all_fs_access))?;
}
}
for path in &config.read_only_paths {
if let Ok(fd) = PathFd::new(path) {
ruleset = ruleset.add_rule(PathBeneath::new(fd, read_access | exec_access))?;
}
}
for path in &config.exec_paths {
if let Ok(fd) = PathFd::new(path) {
ruleset = ruleset.add_rule(PathBeneath::new(fd, exec_access))?;
}
}
let _status = ruleset.restrict_self()?;
Ok(())
}
fn grant_beneath_excluding(
mut ruleset: RulesetCreated,
dir: &Path,
access: BitFlags<AccessFs>,
deny: &[PathBuf],
) -> Result<RulesetCreated, SandboxError> {
if !deny.iter().any(|d| d.starts_with(dir)) {
if let Ok(fd) = PathFd::new(dir) {
ruleset = ruleset.add_rule(PathBeneath::new(fd, access))?;
}
return Ok(ruleset);
}
let Ok(entries) = std::fs::read_dir(dir) else {
return Ok(ruleset);
};
for entry in entries.flatten() {
let Ok(child) = std::fs::canonicalize(entry.path()) else {
continue;
};
if deny.contains(&child) {
continue;
}
if deny.iter().any(|d| d.starts_with(&child)) {
ruleset = grant_beneath_excluding(ruleset, &child, access, deny)?;
} else if let Ok(fd) = PathFd::new(&child) {
ruleset = ruleset.add_rule(PathBeneath::new(fd, access))?;
}
}
Ok(ruleset)
}
fn canonicalize_existing(paths: &[PathBuf]) -> Vec<PathBuf> {
paths
.iter()
.filter_map(|p| std::fs::canonicalize(p).ok())
.collect()
}
#[must_use]
pub fn landlock_available() -> bool {
Ruleset::default()
.set_compatibility(CompatLevel::HardRequirement)
.handle_access(AccessFs::from_all(ABI::V1))
.and_then(|r| r.create())
.is_ok()
}