#![deny(non_ascii_idents)]
#![deny(unsafe_code)]
#![deny(unused_results)]
#![allow(clippy::unwrap_or_default)] #![allow(clippy::new_without_default)]
#![warn(missing_docs)]
#![warn(trivial_casts, trivial_numeric_casts)]
pub use seccompiler::SeccompFilter as SeccompilerFilter;
pub use seccompiler::SeccompRule as SeccompilerRule;
pub use seccompiler::SeccompCondition as SeccompilerArgumentFilter;
pub use seccompiler::Error as SeccompilerError;
pub use seccompiler::SeccompCmpOp as SeccompilerComparator;
use seccompiler::SeccompAction;
pub use syscalls;
pub mod error;
pub use error::*;
#[macro_use]
pub mod macros;
pub mod builtins;
#[cfg(feature = "landlock")]
mod landlock;
#[cfg(feature = "landlock")]
pub use landlock::*;
#[cfg(feature = "isolate")]
pub mod isolate;
#[cfg(feature = "landlock")]
use std::path::PathBuf;
use std::collections::{BTreeMap, HashMap};
#[derive(Debug, Clone, PartialEq)]
pub struct SeccompArgumentFilter {
pub arg_idx: u8,
comparator: SeccompilerComparator,
pub value: u64,
pub is_64bit: bool,
}
impl SeccompArgumentFilter {
#[must_use]
pub fn new(arg_idx: u8, comparator: SeccompilerComparator, value: u64) -> SeccompArgumentFilter {
SeccompArgumentFilter::new64(arg_idx, comparator, value)
}
#[must_use]
pub fn new64(arg_idx: u8, comparator: SeccompilerComparator, value: u64) -> SeccompArgumentFilter {
SeccompArgumentFilter {
arg_idx,
comparator,
value,
is_64bit: true,
}
}
#[must_use]
pub fn new32(arg_idx: u8, comparator: SeccompilerComparator, value: u32) -> SeccompArgumentFilter {
let value = u64::from(value);
SeccompArgumentFilter {
arg_idx,
comparator,
value,
is_64bit: false,
}
}
pub(crate) fn into_seccompiler(self) -> Result<SeccompilerArgumentFilter, ExtraSafeError> {
use seccompiler::SeccompCmpArgLen;
let arg_len = if self.is_64bit { SeccompCmpArgLen::Qword } else { SeccompCmpArgLen::Dword };
Ok(SeccompilerArgumentFilter::new(self.arg_idx, arg_len,
self.comparator, self.value)?)
}
}
#[derive(Debug, Clone)]
#[must_use]
pub struct SeccompRule {
pub syscall: syscalls::Sysno,
pub argument_filters: Vec<SeccompArgumentFilter>,
}
impl SeccompRule {
pub fn new(syscall: syscalls::Sysno) -> SeccompRule {
SeccompRule {
syscall,
argument_filters: Vec::new(),
}
}
pub fn and_condition(mut self, argument_filter: SeccompArgumentFilter) -> SeccompRule {
self.argument_filters.push(argument_filter);
self
}
pub(crate) fn into_seccompiler(self) -> Result<Option<SeccompilerRule>, ExtraSafeError> {
if self.argument_filters.is_empty() {
return Ok(None);
}
let argument_filters: Vec<SeccompilerArgumentFilter> = self.argument_filters.into_iter()
.map(SeccompArgumentFilter::into_seccompiler).collect::<Result::<_, ExtraSafeError>>()?;
Ok(Some(SeccompilerRule::new(argument_filters)?))
}
}
#[derive(Debug, Clone)]
struct LabeledSeccompRule(pub &'static str, pub SeccompRule);
pub trait RuleSet {
fn simple_rules(&self) -> Vec<syscalls::Sysno>;
fn conditional_rules(&self) -> HashMap<syscalls::Sysno, Vec<SeccompRule>> {
HashMap::new()
}
fn name(&self) -> &'static str;
#[cfg(feature = "landlock")]
fn landlock_rules(&self) -> Vec<LandlockRule> {
Vec::new()
}
}
impl<T: ?Sized + RuleSet> RuleSet for &T {
#[inline]
fn simple_rules(&self) -> Vec<syscalls::Sysno> {
T::simple_rules(self)
}
#[inline]
fn conditional_rules(&self) -> HashMap<syscalls::Sysno, Vec<SeccompRule>> {
T::conditional_rules(self)
}
#[inline]
fn name(&self) -> &'static str {
T::name(self)
}
#[cfg(feature = "landlock")]
#[inline]
fn landlock_rules(&self) -> Vec<LandlockRule> {
T::landlock_rules(self)
}
}
impl RuleSet for syscalls::Sysno {
fn simple_rules(&self) -> Vec<syscalls::Sysno> {
Vec::from([*self])
}
fn conditional_rules(&self) -> HashMap<syscalls::Sysno, Vec<SeccompRule>> {
HashMap::new()
}
fn name(&self) -> &'static str {
self.name()
}
}
#[must_use]
#[derive(Debug)]
pub struct SafetyContext {
seccomp_rules: HashMap<syscalls::Sysno, Vec<LabeledSeccompRule>>,
#[cfg(feature = "landlock")]
landlock_rules: HashMap<PathBuf, LabeledLandlockRule>,
errno: u32,
all_threads: bool,
#[cfg(feature = "landlock")]
only_landlock: bool,
}
impl SafetyContext {
pub fn new() -> SafetyContext {
SafetyContext {
seccomp_rules: HashMap::new(),
#[cfg(feature = "landlock")]
landlock_rules: HashMap::new(),
errno: 1,
all_threads: false,
#[cfg(feature = "landlock")]
only_landlock: false,
}
}
pub fn with_errno(mut self, errno: u32) -> SafetyContext {
self.errno = errno;
self
}
#[allow(clippy::needless_pass_by_value)]
fn gather_rules<R: RuleSet>(rules: R) -> Vec<SeccompRule> {
let base_syscalls = rules.simple_rules();
let mut rules = rules.conditional_rules();
for syscall in base_syscalls {
let rule = SeccompRule::new(syscall);
rules.entry(syscall)
.or_insert_with(Vec::new)
.push(rule);
}
rules.into_values().flatten()
.collect()
}
pub fn enable<R: RuleSet>(mut self, policy: R) -> Result<SafetyContext, ExtraSafeError> {
#[cfg(feature = "landlock")]
self.enable_landlock_rules(&policy)?;
self.enable_seccomp_rules(policy)?;
Ok(self)
}
#[cfg(feature = "landlock")]
fn enable_landlock_rules<R: RuleSet>(&mut self, policy: &R) -> Result<(), ExtraSafeError> {
let name = policy.name();
let rules = policy.landlock_rules().into_iter()
.map(|rule| (rule.path.clone(), LabeledLandlockRule(name, rule)));
for (path, labeled_rule) in rules {
if let Some(existing_rule) = self.landlock_rules.get(&path) {
return Err(ExtraSafeError::DuplicatePath(path.clone(), existing_rule.0, labeled_rule.0));
}
let _always_none = self.landlock_rules.insert(path, labeled_rule);
}
Ok(())
}
fn enable_seccomp_rules<R: RuleSet>(&mut self, policy: R) -> Result<(), ExtraSafeError> {
let policy_name = policy.name();
let new_rules = SafetyContext::gather_rules(policy)
.into_iter()
.map(|rule| LabeledSeccompRule(policy_name, rule));
for labeled_new_rule in new_rules {
let new_rule = &labeled_new_rule.1;
let syscall = &new_rule.syscall;
if let Some(existing_rules) = self.seccomp_rules.get(syscall) {
for labeled_existing_rule in existing_rules {
let existing_rule = &labeled_existing_rule.1;
let new_is_simple = new_rule.argument_filters.is_empty();
let existing_is_simple = existing_rule.argument_filters.is_empty();
if new_is_simple && !existing_is_simple {
return Err(ExtraSafeError::ConditionalNoEffectError(
new_rule.syscall,
labeled_existing_rule.0,
labeled_new_rule.0,
));
}
else if !new_is_simple && existing_is_simple {
return Err(ExtraSafeError::ConditionalNoEffectError(
new_rule.syscall,
labeled_new_rule.0,
labeled_existing_rule.0,
));
}
}
}
self.seccomp_rules
.entry(*syscall)
.or_insert_with(Vec::new)
.push(labeled_new_rule);
}
Ok(())
}
#[cfg(feature = "landlock")]
pub fn landlock_only(mut self) -> SafetyContext {
self.only_landlock = true;
self
}
pub fn apply_to_current_thread(mut self) -> Result<(), ExtraSafeError> {
self.all_threads = false;
self.apply()
}
pub fn apply_to_all_threads(mut self) -> Result<(), ExtraSafeError> {
#[cfg(feature = "landlock")]
if !self.landlock_rules.is_empty() {
return Err(ExtraSafeError::LandlockNoThreadSync);
}
self.all_threads = true;
self.apply()
}
fn apply(mut self) -> Result<(), ExtraSafeError> {
#[cfg(feature = "landlock")]
if self.seccomp_rules.is_empty() && self.landlock_rules.is_empty() {
return Err(ExtraSafeError::NoRulesEnabled);
}
#[cfg(not(feature = "landlock"))]
if self.seccomp_rules.is_empty() {
return Err(ExtraSafeError::NoRulesEnabled);
}
self = self.enable(builtins::BasicCapabilities)?;
#[cfg(feature = "landlock")]
if self.only_landlock {
return self.apply_landlock_rules();
}
else if self.landlock_rules.is_empty() {
return self.apply_seccomp_rules();
}
#[cfg(feature = "landlock")]
self.apply_landlock_rules()?;
self.apply_seccomp_rules()
}
fn apply_seccomp_rules(self) -> Result<(), ExtraSafeError> {
let mut rules_map: BTreeMap<i64, Vec<SeccompilerRule>> = BTreeMap::new();
for (syscall, labeled_rules) in self.seccomp_rules {
let syscall = syscall.id().into();
let mut seccompiler_rules = Vec::new();
for LabeledSeccompRule(_origin, rule) in labeled_rules {
if let Some(seccompiler_rule) = rule.into_seccompiler()? {
seccompiler_rules.push(seccompiler_rule);
}
}
let result = rules_map.insert(syscall, seccompiler_rules);
assert!(result.is_none(), "extrasafe logic error: somehow inserted the same syscall's rules twice");
}
#[cfg(not(all(target_arch = "x86_64", target_os = "linux")))]
compile_error!("extrasafe is currently only supported on linux x86_64");
let seccompiler_filter = SeccompilerFilter::new(
rules_map,
SeccompAction::Errno(self.errno),
SeccompAction::Allow,
std::env::consts::ARCH.try_into().expect("invalid arches are prevented above"),
)?;
let bpf_filter: seccompiler::BpfProgram = seccompiler_filter.try_into()?;
if self.all_threads {
seccompiler::apply_filter_all_threads(&bpf_filter)?;
}
else {
seccompiler::apply_filter(&bpf_filter)?;
}
Ok(())
}
#[cfg(feature = "landlock")]
fn apply_landlock_rules(&self) -> Result<(), ExtraSafeError> {
let abi = ABI::V2;
let mut landlock_ruleset = Ruleset::default()
.set_compatibility(CompatLevel::HardRequirement)
.handle_access(AccessFs::from_all(abi))?
.create()?;
for LabeledLandlockRule(_policy_name, rule) in self.landlock_rules.values() {
if let Ok(fd) = PathFd::new(rule.path.clone()) {
let path_beneath = PathBeneath::new(fd, rule.access_rules);
landlock_ruleset = landlock_ruleset.add_rule(path_beneath)?;
}
}
let _status = landlock_ruleset.restrict_self();
Ok(())
}
}