extrasafe 0.1.2

Make your code extrasafe by preventing it from calling unneeded syscalls.
Documentation
#![deny(non_ascii_idents)]
#![deny(unsafe_code)]
#![deny(unused_results)]
#![allow(clippy::new_without_default)]
// Denied in CI
//#![warn(missing_docs)]
#![warn(trivial_casts, trivial_numeric_casts)]

//! extrasafe is a library that makes it easy to improve your program's security by selectively
//! allowing the syscalls it can perform via the Linux kernel's seccomp facilities.
//!
//! See the [`SafetyContext`] struct's documentation and the tests/ and examples/ directories for
//! more information on how to use it.

use libseccomp::*;
use thiserror::Error;

pub mod builtins;

use std::collections::HashMap;

#[derive(Debug, Clone)]
/// A seccomp rule.
pub struct Rule {
    /// The syscall being filtered
    pub syscall: syscalls::Sysno,
    // Yes, technically this is not the correct usage of "comparators" but it's fine.
    /// Comparisons applied to the syscall's args. The Rule allows the syscall if all comparators
    /// evaluate to true.
    pub comparators: Vec<ScmpArgCompare>,
}

impl Rule {
    /// Constructs a new Rule that unconditionally allows the given syscall.
    #[must_use]
    pub fn new(syscall: syscalls::Sysno) -> Rule {
        Rule {
            syscall,
            comparators: Vec::new(),
        }
    }

    /// Adds a condition to the Rule which must evaluate to true in order for the syscall to be
    /// allowed.
    #[must_use]
    pub fn and_condition(mut self, comparator: ScmpArgCompare) -> Rule {
        self.comparators.push(comparator);

        self
    }
}

#[derive(Debug, Clone)]
/// A [`Rule`] labeled with the profile it originated from. Internal-only.
struct LabeledRule(pub &'static str, pub Rule);

/// A [`RuleSet`] is a collection of seccomp Rules that enable a functionality.
pub trait RuleSet {
    /// A simple rule is one that just allows the syscall without restriction.
    fn simple_rules(&self) -> Vec<syscalls::Sysno>;

    /// A conditional rule is a rule that uses a condition to restrict the syscall, e.g. only
    /// specific flags as parameters.
    fn conditional_rules(&self) -> HashMap<syscalls::Sysno, Vec<Rule>>;

    /// The name of the profile.
    fn name(&self) -> &'static str;
}

#[must_use]
#[derive(Debug)]
/// A struct representing a set of rules to be loaded into a seccomp filter and applied to the
/// current thread, or all threads in the current process.
///
/// Create with [`new()`](Self::new). Add [`RuleSet`]s with [`enable()`](Self::enable), and then use [`apply_to_current_thread()`](Self::apply_to_current_thread)
/// to apply the filters to the current thread, or [`apply_to_all_threads()`](Self::apply_to_all_threads) to apply the filter to
/// all threads in the process.
pub struct SafetyContext {
    /// May either be a single simple rule or multiple conditional rules, but not both.
    rules: HashMap<syscalls::Sysno, Vec<LabeledRule>>,
}

impl SafetyContext {
    /// Create a new [`SafetyContext`]. The seccomp filters will not be loaded until either
    /// [`apply_to_current_thread`](Self::apply_to_current_thread) or
    /// [`apply_to_all_threads`](Self::apply_to_all_threads) is called.
    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(),
        }
    }

    /// Gather unconditional and conditional rules to be provided to the seccomp context.
    #[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()
    }

    /// Enable the simple and conditional rules provided by the [`RuleSet`].
    ///
    /// # Errors
    /// Will return [`ExtraSafeError::ConditionalNoEffectError`] if a conditional rule is enabled at
    /// the same time as a simple rule for a syscall, which would override the conditional rule.
    pub fn enable(mut self, policy: impl RuleSet) -> Result<SafetyContext, ExtraSafeError> {
        // Note that we can't do this check in each individual gather_rules because different
        // policies may enable the same syscall.

        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)
    }

    /// Load the [`SafetyContext`]'s rules into a seccomp filter and apply the filter to the current
    /// thread.
    ///
    /// # Errors
    /// May return [`ExtraSafeError::SeccompError`].
    pub fn apply_to_current_thread(self) -> Result<(), ExtraSafeError> {
        self.apply(false)
    }

    /// Load the [`SafetyContext`]'s rules into a seccomp filter and apply the filter to all threads in
    /// this process.
    ///
    /// # Errors
    /// May return [`ExtraSafeError::SeccompError`].
    pub fn apply_to_all_threads(self) -> Result<(), ExtraSafeError> {
        self.apply(true)
    }

    fn apply(mut self, all_threads: bool) -> Result<(), ExtraSafeError> {
        // This guard will not currently ever be hit because libseccomp-rs will fail to build
        // before we get here. If we ever move off of it or if libseccomp-rs decides to do a
        // no-op build on non-linux platform, having this guard here means end users will still
        // have to explicitly acknowledge that extrasafe isn't running on that platform.
        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 {
            // this is the default but we set it just to be sure.
            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)]
/// The error type produced by [`SafetyContext`]
pub enum ExtraSafeError {
    #[error("extrasafe is only usable on Linux.")]
    /// Error created when trying to apply filters on non-Linux operating systems. Should never
    /// occur.
    UnsupportedOSError,
    #[error("A conditional rule on syscall `{0}` from RuleSet `{1}` would be overridden by a simple rule from RuleSet `{2}`.")]
    /// Error created when a simple rule would override a conditional rule.
    ConditionalNoEffectError(syscalls::Sysno, &'static str, &'static str),
    #[error("A libseccomp error occured. {0:?}")]
    /// An error from the underlying seccomp library.
    SeccompError(#[from] libseccomp::error::SeccompError),
}