use std::collections::{HashSet, HashMap};
use std::fs::File;
use std::os::unix::io::AsRawFd;
#[cfg(feature = "landlock")]
use std::path::{Path, PathBuf};
use syscalls::Sysno;
#[cfg(feature = "landlock")]
use crate::LandlockRule;
#[cfg(feature = "landlock")]
use crate::landlock::{access, AccessFs, BitFlags};
use crate::{RuleSet, SeccompRule};
use super::YesReally;
pub(crate) const IO_READ_SYSCALLS: &[Sysno] = &[Sysno::read, Sysno::readv, Sysno::preadv, Sysno::preadv2, Sysno::pread64, Sysno::lseek];
pub(crate) const IO_WRITE_SYSCALLS: &[Sysno] = &[Sysno::write, Sysno::writev, Sysno::pwritev, Sysno::pwritev2, Sysno::pwrite64,
Sysno::fsync, Sysno::fdatasync, Sysno::lseek];
pub(crate) const IO_OPEN_SYSCALLS: &[Sysno] = &[Sysno::open, Sysno::openat, Sysno::openat2];
pub(crate) const IO_IOCTL_SYSCALLS: &[Sysno] = &[Sysno::ioctl, Sysno::fcntl];
pub(crate) const IO_METADATA_SYSCALLS: &[Sysno] = &[Sysno::stat, Sysno::fstat, Sysno::newfstatat,
Sysno::lstat, Sysno::statx,
Sysno::getdents, Sysno::getdents64,
Sysno::getcwd];
pub(crate) const IO_CLOSE_SYSCALLS: &[Sysno] = &[Sysno::close, Sysno::close_range];
pub(crate) const IO_UNLINK_SYSCALLS: &[Sysno] = &[Sysno::unlink, Sysno::unlinkat];
#[must_use]
pub struct SystemIO {
allowed: HashSet<Sysno>,
custom: HashMap<Sysno, Vec<SeccompRule>>,
#[cfg(feature = "landlock")]
landlock_rules: HashMap<PathBuf, LandlockRule>,
}
impl SystemIO {
pub fn nothing() -> SystemIO {
SystemIO {
allowed: HashSet::new(),
custom: HashMap::new(),
#[cfg(feature = "landlock")]
landlock_rules: HashMap::new()
}
}
pub fn everything() -> SystemIO {
SystemIO::nothing()
.allow_read()
.allow_write()
.allow_open().yes_really()
.allow_metadata()
.allow_unlink()
.allow_close()
}
pub fn allow_read(mut self) -> SystemIO {
self.allowed.extend(IO_READ_SYSCALLS);
self
}
pub fn allow_write(mut self) -> SystemIO {
self.allowed.extend(IO_WRITE_SYSCALLS);
self
}
pub fn allow_unlink(mut self) -> SystemIO {
self.allowed.extend(IO_UNLINK_SYSCALLS);
self
}
pub fn allow_open(mut self) -> YesReally<SystemIO> {
self.allowed.extend(IO_OPEN_SYSCALLS);
YesReally::new(self)
}
pub fn allow_open_readonly(mut self) -> SystemIO {
const O_WRONLY: u64 = libc::O_WRONLY as u64;
const O_RDWR: u64 = libc::O_RDWR as u64;
const O_APPEND: u64 = libc::O_APPEND as u64;
const O_CREAT: u64 = libc::O_CREAT as u64;
const O_EXCL: u64 = libc::O_EXCL as u64;
const WRITECREATE: u64 = O_WRONLY | O_RDWR | O_APPEND | O_CREAT | O_EXCL;
let rule = SeccompRule::new(Sysno::open)
.and_condition(seccomp_arg_filter!(arg1 & WRITECREATE == 0));
self.custom.entry(Sysno::open)
.or_insert_with(Vec::new)
.push(rule);
let rule = SeccompRule::new(Sysno::openat)
.and_condition(seccomp_arg_filter!(arg2 & WRITECREATE == 0));
self.custom.entry(Sysno::openat)
.or_insert_with(Vec::new)
.push(rule);
self
}
pub fn allow_metadata(mut self) -> SystemIO {
self.allowed.extend(IO_METADATA_SYSCALLS);
self
}
pub fn allow_ioctl(mut self) -> SystemIO {
self.allowed.extend(IO_IOCTL_SYSCALLS);
self
}
pub fn allow_close(mut self) -> SystemIO {
self.allowed.extend(IO_CLOSE_SYSCALLS);
self
}
pub fn allow_stdin(mut self) -> SystemIO {
let rule = SeccompRule::new(Sysno::read)
.and_condition(seccomp_arg_filter!(arg0 == 0));
self.custom.entry(Sysno::read)
.or_insert_with(Vec::new)
.push(rule);
self
}
pub fn allow_stdout(mut self) -> SystemIO {
let rule = SeccompRule::new(Sysno::write)
.and_condition(seccomp_arg_filter!(arg0 == 1));
self.custom.entry(Sysno::write)
.or_insert_with(Vec::new)
.push(rule);
self
}
pub fn allow_stderr(mut self) -> SystemIO {
let rule = SeccompRule::new(Sysno::write)
.and_condition(seccomp_arg_filter!(arg0 == 2));
self.custom.entry(Sysno::write)
.or_insert_with(Vec::new)
.push(rule);
self
}
#[allow(clippy::missing_panics_doc)]
pub fn allow_file_read(mut self, file: &File) -> SystemIO {
let fd = file.as_raw_fd().try_into().expect("provided fd was negative");
for &syscall in IO_READ_SYSCALLS {
let rule = SeccompRule::new(syscall)
.and_condition(seccomp_arg_filter!(arg0 == fd));
self.custom.entry(syscall)
.or_insert_with(Vec::new)
.push(rule);
}
for &syscall in IO_METADATA_SYSCALLS {
let rule = SeccompRule::new(syscall)
.and_condition(seccomp_arg_filter!(arg0 == fd));
self.custom.entry(syscall)
.or_insert_with(Vec::new)
.push(rule);
}
self
}
#[allow(clippy::missing_panics_doc)]
pub fn allow_file_write(mut self, file: &File) -> SystemIO {
let fd = file.as_raw_fd().try_into().expect("provided fd was negative");
let rule = SeccompRule::new(Sysno::write)
.and_condition(seccomp_arg_filter!(arg0 == fd));
self.custom.entry(Sysno::write)
.or_insert_with(Vec::new)
.push(rule);
self
}
}
impl RuleSet for SystemIO {
fn simple_rules(&self) -> Vec<syscalls::Sysno> {
self.allowed.iter().copied().collect()
}
fn conditional_rules(&self) -> HashMap<syscalls::Sysno, Vec<SeccompRule>> {
self.custom.clone()
}
#[cfg(feature = "landlock")]
fn landlock_rules(&self) -> Vec<LandlockRule> {
self.landlock_rules.values().cloned().collect()
}
fn name(&self) -> &'static str {
"SystemIO"
}
}
#[cfg(feature = "landlock")]
impl SystemIO {
fn insert_flags<P: AsRef<Path>>(&mut self, path: P, new_flags: BitFlags<AccessFs>) {
let path = path.as_ref().to_path_buf();
let _flag = self.landlock_rules.entry(path.clone())
.and_modify(|existing_flags| existing_flags.access_rules.insert(new_flags))
.or_insert_with(|| LandlockRule::new(&path, new_flags));
}
pub fn allow_read_path<P: AsRef<Path>>(mut self, path: P) -> SystemIO {
let new_flags = access::read_path();
self.insert_flags(path, new_flags);
self.allow_close()
.allow_read()
.allow_metadata()
.allow_open().yes_really()
}
pub fn allow_write_file<P: AsRef<Path>>(mut self, path: P) -> SystemIO {
let new_flags = access::write_file();
self.insert_flags(path, new_flags);
self.allow_close()
.allow_write()
.allow_metadata()
.allow_open().yes_really()
}
pub fn allow_create_in_dir<P: AsRef<Path>>(mut self, path: P) -> SystemIO {
let new_flags = access::create_file() | access::write_file();
self.insert_flags(path, new_flags);
self.allowed.extend(&[Sysno::creat]);
self.allow_open().yes_really()
}
pub fn allow_list_dir<P: AsRef<Path>>(mut self, path: P) -> SystemIO {
let new_flags = access::list_dir();
self.insert_flags(path, new_flags);
self.allow_metadata()
.allow_close()
.allow_ioctl()
.allow_open().yes_really()
}
pub fn allow_create_dir<P: AsRef<Path>>(mut self, path: P) -> SystemIO {
let new_flags = access::create_dir();
self.insert_flags(path, new_flags);
self.allowed.extend(&[Sysno::mkdir, Sysno::mkdirat]);
self
}
pub fn allow_remove_file<P: AsRef<Path>>(mut self, path: P) -> SystemIO {
let new_flags = access::delete_file();
self.insert_flags(path, new_flags);
self.allowed.extend(&[Sysno::unlink, Sysno::unlinkat]);
self
}
pub fn allow_remove_dir<P: AsRef<Path>>(mut self, path: P) -> SystemIO {
let new_flags = access::delete_dir();
self.insert_flags(path, new_flags);
self.allowed.extend(&[Sysno::rmdir, Sysno::unlinkat]);
self
}
}
#[cfg(feature = "landlock")]
impl SystemIO {
pub fn allow_ssl_files(mut self) -> SystemIO {
let new_flags = access::read_path() | access::list_dir();
for path in &["/etc/ssl/certs", "/etc/ca-certificates"] {
self.insert_flags(path, new_flags);
}
self.insert_flags("/etc/localtime", access::read_path());
self.allow_close()
.allow_read()
.allow_metadata()
.allow_open().yes_really()
}
pub fn allow_dns_files(mut self) -> SystemIO {
let new_flags = access::read_path();
for path in &["/etc/resolv.conf", "/etc/hosts", "/etc/host.conf", "/etc/nsswitch.conf", "/etc/gai.conf"] {
self.insert_flags(path, new_flags);
}
self.allow_close()
.allow_read()
.allow_metadata()
.allow_open().yes_really()
}
}