use crate::contract::seccomp_evidence::{SeccompActionKind, SeccompArch, SeccompEvidence};
use seccompiler::{BpfProgram, SeccompAction, SeccompFilter, TargetArch};
use std::collections::{BTreeMap, BTreeSet};
pub const SECCOMPILER_VERSION: &str = "=0.5.0";
const POLICY_DIGEST_DOMAIN: &str = "bvisor.seccomp-policy.v1";
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Syscall {
name: &'static str,
nr: i64,
}
impl Syscall {
#[must_use]
const fn new(name: &'static str, nr: i64) -> Self {
Self { name, nr }
}
#[must_use]
pub fn name(self) -> &'static str {
self.name
}
#[must_use]
pub fn number(self) -> i64 {
self.nr
}
#[cfg(test)]
#[must_use]
pub(crate) fn for_test(name: &'static str, nr: i64) -> Self {
Self::new(name, nr)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum DefaultAction {
Errno(u32),
KillProcess,
}
impl DefaultAction {
fn to_seccomp(self) -> SeccompAction {
match self {
Self::Errno(e) => SeccompAction::Errno(e),
Self::KillProcess => SeccompAction::KillProcess,
}
}
fn to_kind(self) -> SeccompActionKind {
match self {
Self::Errno(e) => SeccompActionKind::Errno(e),
Self::KillProcess => SeccompActionKind::KillProcess,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum SeccompCompileError {
MissingMandatoryBase {
syscall: &'static str,
},
EmptyAllowlist,
Assembler(String),
CanonicalEncoding(String),
DenylistDeniesMandatoryBase {
syscall: &'static str,
},
EmptyDenylist,
}
impl std::fmt::Display for SeccompCompileError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingMandatoryBase { syscall } => write!(
f,
"seccomp policy omits mandatory base syscall {syscall}: a launcher filter must \
always allow execve/execveat/write/exit_group"
),
Self::EmptyAllowlist => {
write!(
f,
"seccomp policy has an empty allowlist (denies even the mandatory base)"
)
}
Self::Assembler(e) => write!(f, "seccompiler rejected the filter: {e}"),
Self::CanonicalEncoding(e) => {
write!(f, "could not canonically encode the seccomp policy: {e}")
}
Self::DenylistDeniesMandatoryBase { syscall } => write!(
f,
"seccomp denylist names mandatory base syscall {syscall} in its deny set: a \
launcher filter must NEVER deny execve/execveat/write/exit_group"
),
Self::EmptyDenylist => write!(
f,
"seccomp denylist has an empty deny set (a default-allow filter that denies \
nothing is a permit-all no-op, not a confinement layer)"
),
}
}
}
impl std::error::Error for SeccompCompileError {}
#[derive(Clone, Debug, PartialEq, Eq)]
enum Mode {
Allowlist {
default_action: DefaultAction,
allow: BTreeSet<Syscall>,
},
Denylist {
deny_action: DefaultAction,
deny: BTreeSet<Syscall>,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SeccompPolicy {
mode: Mode,
}
impl SeccompPolicy {
#[must_use]
pub fn mandatory_base() -> [Syscall; 4] {
[
Syscall::new("execve", libc::SYS_execve),
Syscall::new("execveat", libc::SYS_execveat),
Syscall::new("write", libc::SYS_write),
Syscall::new("exit_group", libc::SYS_exit_group),
]
}
#[must_use]
pub fn task_creation_syscalls() -> Vec<Syscall> {
let mut v = vec![
Syscall::new("clone", libc::SYS_clone),
Syscall::new("clone3", libc::SYS_clone3),
];
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
{
v.push(Syscall::new("fork", libc::SYS_fork));
v.push(Syscall::new("vfork", libc::SYS_vfork));
}
v
}
#[must_use]
pub fn socket_syscall() -> Syscall {
Syscall::new("socket", libc::SYS_socket)
}
#[must_use]
pub fn launcher_base(default_action: DefaultAction) -> Self {
let allow = Self::mandatory_base().into_iter().collect();
Self {
mode: Mode::Allowlist {
default_action,
allow,
},
}
}
#[must_use]
pub fn denylist(
deny_action: DefaultAction,
deny_syscalls: impl IntoIterator<Item = Syscall>,
) -> Self {
Self {
mode: Mode::Denylist {
deny_action,
deny: deny_syscalls.into_iter().collect(),
},
}
}
#[must_use]
pub fn deny_new_tasks(deny_action: DefaultAction) -> Self {
Self::denylist(deny_action, Self::task_creation_syscalls())
}
#[must_use]
pub fn network_deny_did(deny_action: DefaultAction) -> Self {
Self::denylist(deny_action, [Self::socket_syscall()])
}
#[must_use]
pub fn allow(mut self, syscall: Syscall) -> Self {
if let Mode::Allowlist { allow, .. } = &mut self.mode {
allow.insert(syscall);
}
self
}
#[must_use]
pub fn default_action(&self) -> DefaultAction {
match &self.mode {
Mode::Allowlist { default_action, .. } => *default_action,
Mode::Denylist { deny_action, .. } => *deny_action,
}
}
#[must_use]
pub fn is_denylist(&self) -> bool {
matches!(self.mode, Mode::Denylist { .. })
}
#[cfg(test)]
#[must_use]
pub(crate) fn empty_for_test(default_action: DefaultAction) -> Self {
Self {
mode: Mode::Allowlist {
default_action,
allow: BTreeSet::new(),
},
}
}
#[cfg(test)]
#[must_use]
pub(crate) fn without_for_test(mut self, name: &str) -> Self {
if let Mode::Allowlist { allow, .. } = &mut self.mode {
allow.retain(|s| s.name != name);
}
self
}
#[cfg(test)]
#[must_use]
pub(crate) fn denylist_with_base_for_test(
deny_action: DefaultAction,
base: &'static str,
) -> Self {
let nr = Self::mandatory_base()
.into_iter()
.find(|s| s.name == base)
.map_or(libc::SYS_execve, Syscall::number);
Self::denylist(deny_action, [Syscall::for_test(base, nr)])
}
pub fn allowlist(&self) -> impl Iterator<Item = Syscall> + '_ {
let set: &BTreeSet<Syscall> = match &self.mode {
Mode::Allowlist { allow, .. } => allow,
Mode::Denylist { deny, .. } => deny,
};
set.iter().copied()
}
pub fn policy_digest(&self) -> Result<[u8; 32], SeccompCompileError> {
#[derive(serde::Serialize)]
struct PolicyDigestInput<'a> {
domain: &'a str,
mode: u8,
default_action: (u8, u32),
syscalls: Vec<&'a str>,
}
let (mode_tag, action, set) = match &self.mode {
Mode::Allowlist {
default_action,
allow,
} => (0u8, *default_action, allow),
Mode::Denylist { deny_action, deny } => (1u8, *deny_action, deny),
};
let mut syscalls: Vec<&str> = set.iter().map(|s| s.name).collect();
syscalls.sort_unstable();
syscalls.dedup();
let input = PolicyDigestInput {
domain: POLICY_DIGEST_DOMAIN,
mode: mode_tag,
default_action: action.to_kind().wire_tag(),
syscalls,
};
let bytes = batpak::canonical::to_bytes(&input)
.map_err(|e| SeccompCompileError::CanonicalEncoding(e.to_string()))?;
Ok(batpak::event::hash::compute_hash(&bytes))
}
pub fn compile(&self, arch: SeccompArch) -> Result<CompiledFilter, SeccompCompileError> {
let (rules, named_action, default_action, deny_terminal): (
BTreeMap<i64, Vec<seccompiler::SeccompRule>>,
SeccompAction,
SeccompAction,
DefaultAction,
) = match &self.mode {
Mode::Allowlist {
default_action,
allow,
} => {
if allow.is_empty() {
return Err(SeccompCompileError::EmptyAllowlist);
}
let allowed_names: BTreeSet<&str> = allow.iter().map(|s| s.name).collect();
for base in Self::mandatory_base() {
if !allowed_names.contains(base.name) {
return Err(SeccompCompileError::MissingMandatoryBase {
syscall: base.name,
});
}
}
let rules = allow.iter().map(|s| (s.nr, Vec::new())).collect();
(
rules,
SeccompAction::Allow,
default_action.to_seccomp(),
*default_action,
)
}
Mode::Denylist { deny_action, deny } => {
if deny.is_empty() {
return Err(SeccompCompileError::EmptyDenylist);
}
let denied_names: BTreeSet<&str> = deny.iter().map(|s| s.name).collect();
for base in Self::mandatory_base() {
if denied_names.contains(base.name) {
return Err(SeccompCompileError::DenylistDeniesMandatoryBase {
syscall: base.name,
});
}
}
let rules = deny.iter().map(|s| (s.nr, Vec::new())).collect();
(
rules,
deny_action.to_seccomp(),
SeccompAction::Allow,
*deny_action,
)
}
};
let filter = SeccompFilter::new(rules, default_action, named_action, to_target_arch(arch))
.map_err(|e| SeccompCompileError::Assembler(e.to_string()))?;
let program: BpfProgram = filter.try_into().map_err(|e: seccompiler::BackendError| {
SeccompCompileError::Assembler(e.to_string())
})?;
let policy_digest = self.policy_digest()?;
let bpf_bytes = canonical_bpf_bytes(&program);
let bpf_digest = batpak::event::hash::compute_hash(&bpf_bytes);
let mut action_profile = vec![SeccompActionKind::Allow, deny_terminal.to_kind()];
action_profile.sort_unstable_by_key(|a| a.wire_tag());
action_profile.dedup();
let evidence = SeccompEvidence {
policy_digest,
bpf_digest,
target_arch: arch,
seccompiler_version: SECCOMPILER_VERSION.to_string(),
action_profile,
observed_installed_mode: None,
};
Ok(CompiledFilter { program, evidence })
}
}
#[derive(Clone, Debug)]
pub struct CompiledFilter {
program: BpfProgram,
evidence: SeccompEvidence,
}
impl CompiledFilter {
#[must_use]
pub fn program(&self) -> &BpfProgram {
&self.program
}
#[must_use]
pub fn evidence(&self) -> &SeccompEvidence {
&self.evidence
}
#[must_use]
pub fn bpf_bytes(&self) -> Vec<u8> {
canonical_bpf_bytes(&self.program)
}
}
#[must_use]
pub fn seccomp_filter_available() -> bool {
std::fs::read_to_string("/proc/sys/kernel/seccomp/actions_avail")
.map(|s| !s.trim().is_empty())
.unwrap_or(false)
}
fn to_target_arch(arch: SeccompArch) -> TargetArch {
match arch {
SeccompArch::X86_64 => TargetArch::x86_64,
SeccompArch::Aarch64 => TargetArch::aarch64,
SeccompArch::Riscv64 => TargetArch::riscv64,
}
}
fn canonical_bpf_bytes(program: &BpfProgram) -> Vec<u8> {
let mut bytes = Vec::with_capacity(program.len() * 8);
for insn in program {
bytes.extend_from_slice(&insn.code.to_le_bytes());
bytes.push(insn.jt);
bytes.push(insn.jf);
bytes.extend_from_slice(&insn.k.to_le_bytes());
}
bytes
}
#[cfg(test)]
#[path = "seccomp_tests.rs"]
mod tests;