#![deny(non_ascii_idents)]
#![deny(unsafe_code)]
#![deny(unused_results)]
#![allow(clippy::new_without_default)]
#![warn(trivial_casts, trivial_numeric_casts)]
use libseccomp::*;
use thiserror::Error;
pub mod builtins;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct Rule {
pub syscall: syscalls::Sysno,
pub comparators: Vec<ScmpArgCompare>,
}
impl Rule {
#[must_use]
pub fn new(syscall: syscalls::Sysno) -> Rule {
Rule {
syscall,
comparators: Vec::new(),
}
}
#[must_use]
pub fn and_condition(mut self, comparator: ScmpArgCompare) -> Rule {
self.comparators.push(comparator);
self
}
}
#[derive(Debug, Clone)]
struct LabeledRule(pub &'static str, pub Rule);
pub trait RuleSet {
fn simple_rules(&self) -> Vec<syscalls::Sysno>;
fn conditional_rules(&self) -> HashMap<syscalls::Sysno, Vec<Rule>>;
fn name(&self) -> &'static str;
}
#[must_use]
#[derive(Debug)]
pub struct SafetyContext {
rules: HashMap<syscalls::Sysno, Vec<LabeledRule>>,
}
impl SafetyContext {
pub fn new() -> SafetyContext {
#[cfg(not(target_arch = "x86_64"))]
{
compile_error!("Extrasafe currently only supports the x86_64 architecture. You will likely see other errors about Sysno enum variants not existing; this is why.");
}
SafetyContext {
rules: HashMap::new(),
}
}
#[allow(clippy::needless_pass_by_value)]
fn gather_rules(rules: impl RuleSet) -> Vec<Rule> {
let base_syscalls = rules.simple_rules();
let mut rules = rules.conditional_rules();
for syscall in base_syscalls {
if !rules.contains_key(&syscall) {
let rule = Rule::new(syscall);
rules.entry(syscall)
.or_insert_with(Vec::new)
.push(rule);
}
}
rules.into_values().flatten()
.collect()
}
pub fn enable(mut self, policy: impl RuleSet) -> Result<SafetyContext, ExtraSafeError> {
let policy_name = policy.name();
let new_rules = SafetyContext::gather_rules(policy)
.into_iter()
.map(|rule| LabeledRule(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.rules.get(syscall) {
for labeled_existing_rule in existing_rules {
let existing_rule = &labeled_existing_rule.1;
let new_is_simple = new_rule.comparators.is_empty();
let existing_is_simple = existing_rule.comparators.is_empty();
let same_syscall = new_rule.syscall == existing_rule.syscall;
if same_syscall && new_is_simple && !existing_is_simple {
return Err(ExtraSafeError::ConditionalNoEffectError(
new_rule.syscall,
labeled_existing_rule.0,
labeled_new_rule.0,
));
}
if same_syscall && !new_is_simple && existing_is_simple {
return Err(ExtraSafeError::ConditionalNoEffectError(
new_rule.syscall,
labeled_new_rule.0,
labeled_existing_rule.0,
));
}
}
}
self.rules
.entry(*syscall)
.or_insert_with(Vec::new)
.push(labeled_new_rule);
}
Ok(self)
}
pub fn apply_to_current_thread(self) -> Result<(), ExtraSafeError> {
self.apply(false)
}
pub fn apply_to_all_threads(self) -> Result<(), ExtraSafeError> {
self.apply(true)
}
fn apply(mut self, all_threads: bool) -> Result<(), ExtraSafeError> {
if cfg!(not(target_os = "linux")) {
return Err(ExtraSafeError::UnsupportedOSError);
}
let mut ctx = ScmpFilterContext::new_filter(ScmpAction::Errno(libc::EPERM))?;
if all_threads {
ctx.set_filter_attr(ScmpFilterAttr::CtlTsync, 1)?;
}
else {
ctx.set_filter_attr(ScmpFilterAttr::CtlTsync, 0)?;
}
ctx.add_arch(ScmpArch::Native)?;
self = self.enable(builtins::BasicCapabilities)?;
for LabeledRule(_origin, rule) in self.rules.into_values().flatten() {
if rule.comparators.is_empty() {
ctx.add_rule(ScmpAction::Allow, rule.syscall.id())?;
}
else {
ctx.add_rule_conditional(ScmpAction::Allow, rule.syscall.id(), &rule.comparators)?;
}
}
ctx.load()?;
Ok(())
}
}
#[derive(Debug, Error)]
pub enum ExtraSafeError {
#[error("extrasafe is only usable on Linux.")]
UnsupportedOSError,
#[error("A conditional rule on syscall `{0}` from RuleSet `{1}` would be overridden by a simple rule from RuleSet `{2}`.")]
ConditionalNoEffectError(syscalls::Sysno, &'static str, &'static str),
#[error("A libseccomp error occured. {0:?}")]
SeccompError(#[from] libseccomp::error::SeccompError),
}