Skip to content

Commit

Permalink
Switch seccomp backend from libseccomp to seccompiler
Browse files Browse the repository at this point in the history
Using seccompiler has three main advantages:
- The entire codebase is in Rust
- We get musl support for free
- You don't need to have libseccomp installed on the target machine

The biggest disadvantage is that we lose support for various platforms
that libseccomp supports but libseccompiler doesn't. However, since
seccompiler supports aarch64 and x86_64 and 99% of software that would
benefit from using seccomp runs on those platforms, I think it's a
worthwile tradeoff.

The other disadvantage is that seccompiler is not quite as mature as
libseccomp and doesn't support all the features libseccomp does. I'm not
currently using those features, several of which are aimed at container
runtimes, so I don't see an issue for now.
  • Loading branch information
boustrophedon committed Oct 18, 2023
1 parent 3b0f21e commit a369f99
Show file tree
Hide file tree
Showing 18 changed files with 681 additions and 131 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
unreleased
----------

- Switch seccomp backend to seccompiler
- This adds several new structs that act as wrappers around the underlying seccompiler structs
- Macros are defined in extrasafe now to replace the ones provided by
libseccomp for comparing and filtering syscall arguments
- Add `#[must_use]` attributes to several structs

0.2.0
-----
- reexport syscalls dependency
Expand Down
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ keywords = ["security", "seccomp", "syscall"]
categories = ["os::linux-apis"]

[dependencies]
libseccomp = "^0.3"
seccompiler = { version = "^0.4", default-features = false }
libc = "^0.2"
syscalls = { version = "^0.6", default-features = false }

Expand All @@ -23,5 +23,5 @@ tempfile = "^3"
tokio = "^1.15"
hyper = { version = "^0.14", features = ["http1", "server", "runtime", "tcp"] }
warp = "^0.3"
reqwest = "^0.11"
reqwest = { version = "^0.11", default-features = false, features = ["rustls-tls"] }
rusqlite = "^0.26"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ fn main() {

You've used safe and unsafe Rust: now your code can be extrasafe.

extrasafe is a wrapper around [libseccomp](https://libseccomp.readthedocs.io/en/latest/), which uses [the Linux kernel's seccomp](https://www.kernel.org/doc/html/latest/userspace-api/seccomp_filter.html) syscall-filtering functionality to prevent your program from calling syscalls you don't need. Seccomp is used by systemd, Chrome, application sandboxes like bubblewrap and firejail, and container runtimes. Seccomp by itself is not a complete sandboxing system.
extrasafe is a wrapper around [the Linux kernel's seccomp](https://www.kernel.org/doc/html/latest/userspace-api/seccomp_filter.html) syscall-filtering functionality to prevent your program from calling syscalls you don't need. Seccomp is used by systemd, Chrome, application sandboxes like bubblewrap and firejail, and container runtimes. Seccomp by itself is not a complete sandboxing system.

The goal of extrasafe is to make it easy to add extra security to your own programs without having to rely on external configuration by the person running the software.

Expand Down
9 changes: 4 additions & 5 deletions design.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,13 @@ This is the entry point. You create a SafetyContext, in which you gather rules v
- RuleSet
A trait that provides a collection of simple and conditional rules. You can think of this as a facet of a security policy, like allowing IO, network, or clock access. There are implementors provided by extrasafe and you can also define your own.
- SeccompRule
A syscall and an optional number of conditions on the syscall's arguments. You can make comparisons on the arguments of the syscall, but the conditions can't dereference pointers so you can't do e.g. string comparisons on file paths.
A syscall and an optional number of conditions (`SeccompArgumentFilter`s) on the syscall's arguments. You can make comparisons on the arguments of the syscall, but the conditions can't dereference pointers so you can't do e.g. string comparisons on file paths.

The comparisons are such that when the comparison returns **true**, the syscall is allowed.

A single SeccompRule may contain multiple conditions, which are anded together by libseccomp, that is, they all must be true for the syscall to be allowed. If multiple rules are loaded, the syscall is allowed if any of the rules allow it.
A single SeccompRule may contain multiple conditions, which are anded together by seccompiler, that is, they all must be true for the syscall to be allowed. If multiple rules are loaded for a single syscall, the syscall is allowed if any of the rules allow it.


One thing to note is that libseccomp will silently override conditional rules on a syscall if an unconditional one is added (in the context of extrasafe, where all filters are allow-based). extrasafe catches this and provides an error with the conflicting syscall and the names of the RuleSets that provided the rules.
That is, the argument filters within a SeccompRule are and-ed together, but the SeccompRules themselves are or-ed together.

# Typical usage

Expand All @@ -30,7 +29,7 @@ DNS requires accessing /etc/resolv.conf and ssl typically requires opening a bun

### Network calls

TODO: tcp sockets and accept, tcp clients opening connections
TODO: see tests/examples, describe tcp sockets and accept, tcp clients opening connections

### Threads vs processes and IPC
TODO, threads don't give as much isolation as processes due to sharing namespace, fds, environment variables, etc.
Expand Down
5 changes: 2 additions & 3 deletions examples/user-guide.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@ fn simple_example() {
}

fn custom_ruleset() {
use extrasafe::{SeccompRule, RuleSet};
use extrasafe::*;
use extrasafe::syscalls::Sysno;
use libseccomp::scmp_cmp;

use std::collections::HashMap;

Expand All @@ -42,7 +41,7 @@ fn custom_ruleset() {

let rule = SeccompRule::new(Sysno::socket)
.and_condition(
scmp_cmp!($arg0 & SOCK_STREAM == SOCK_STREAM));
seccomp_arg_filter!(arg0 & SOCK_STREAM == SOCK_STREAM));
HashMap::from([
(Sysno::socket, vec![rule,])
])
Expand Down
9 changes: 4 additions & 5 deletions src/builtins/danger_zone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

use std::collections::{HashMap, HashSet};

//use libseccomp::scmp_cmp;
use syscalls::Sysno;

use crate::{SeccompRule, RuleSet};
Expand Down Expand Up @@ -62,13 +61,13 @@ impl RuleSet for Threads {
// let mut rules = HashMap::new();

// let clone = SeccompRule::new(Sysno::clone)
// .and_condition(scmp_cmp!($arg2 & CLONE_THREAD == CLONE_THREAD));
// .and_condition(seccomp_arg_filter!(arg2 & CLONE_THREAD == CLONE_THREAD));
// rules.entry(Sysno::clone)
// .or_insert_with(Vec::new)
// .push(clone);

// let clone3 = SeccompRule::new(Sysno::clone3)
// .and_condition(scmp_cmp!($arg2 & CLONE_THREAD == CLONE_THREAD));
// .and_condition(seccomp_arg_filter!(arg2 & CLONE_THREAD == CLONE_THREAD));
// rules.entry(Sysno::clone3)
// .or_insert_with(Vec::new)
// .push(clone3);
Expand Down Expand Up @@ -105,13 +104,13 @@ impl RuleSet for ForkAndExec {
// let mut custom = HashMap::new();
//
// let clone = SeccompRule::new(Sysno::clone)
// .and_condition(scmp_cmp!($arg2 & CLONE_PARENT == CLONE_PARENT));
// .and_condition(seccomp_arg_filter!(arg2 & CLONE_PARENT == CLONE_PARENT));
// custom.entry(Sysno::clone)
// .or_insert_with(Vec::new)
// .push(clone);

// let clone3 = SeccompRule::new(Sysno::clone3)
// .and_condition(scmp_cmp!($arg2 & CLONE_PARENT == CLONE_PARENT));
// .and_condition(seccomp_arg_filter!(arg2 & CLONE_PARENT == CLONE_PARENT));
// custom.entry(Sysno::clone3)
// .or_insert_with(Vec::new)
// .push(clone3);
Expand Down
33 changes: 16 additions & 17 deletions src/builtins/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

use std::collections::{HashMap, HashSet};

use libseccomp::scmp_cmp;
use syscalls::Sysno;

use super::YesReally;
Expand Down Expand Up @@ -104,15 +103,15 @@ impl Networking {

// IPv4
let rule = SeccompRule::new(Sysno::socket)
.and_condition(scmp_cmp!($arg0 & AF_INET == AF_INET))
.and_condition(scmp_cmp!($arg1 & SOCK_STREAM == SOCK_STREAM));
.and_condition(seccomp_arg_filter!(arg0 & AF_INET == AF_INET))
.and_condition(seccomp_arg_filter!(arg1 & SOCK_STREAM == SOCK_STREAM));
self.custom.entry(Sysno::socket)
.or_insert_with(Vec::new)
.push(rule);
// IPv6
let rule = SeccompRule::new(Sysno::socket)
.and_condition(scmp_cmp!($arg0 & AF_INET6 == AF_INET6))
.and_condition(scmp_cmp!($arg1 & SOCK_STREAM == SOCK_STREAM));
.and_condition(seccomp_arg_filter!(arg0 & AF_INET6 == AF_INET6))
.and_condition(seccomp_arg_filter!(arg1 & SOCK_STREAM == SOCK_STREAM));
self.custom.entry(Sysno::socket)
.or_insert_with(Vec::new)
.push(rule);
Expand Down Expand Up @@ -150,15 +149,15 @@ impl Networking {

// IPv4
let rule = SeccompRule::new(Sysno::socket)
.and_condition(scmp_cmp!($arg0 & AF_INET == AF_INET))
.and_condition(scmp_cmp!($arg1 & SOCK_DGRAM == SOCK_DGRAM));
.and_condition(seccomp_arg_filter!(arg0 & AF_INET == AF_INET))
.and_condition(seccomp_arg_filter!(arg1 & SOCK_DGRAM == SOCK_DGRAM));
self.custom.entry(Sysno::socket)
.or_insert_with(Vec::new)
.push(rule);
// IPv6
let rule = SeccompRule::new(Sysno::socket)
.and_condition(scmp_cmp!($arg0 & AF_INET6 == AF_INET6))
.and_condition(scmp_cmp!($arg1 & SOCK_DGRAM == SOCK_DGRAM));
.and_condition(seccomp_arg_filter!(arg0 & AF_INET6 == AF_INET6))
.and_condition(seccomp_arg_filter!(arg1 & SOCK_DGRAM == SOCK_DGRAM));
self.custom.entry(Sysno::socket)
.or_insert_with(Vec::new)
.push(rule);
Expand Down Expand Up @@ -194,15 +193,15 @@ impl Networking {

// IPv4
let rule = SeccompRule::new(Sysno::socket)
.and_condition(scmp_cmp!($arg0 & AF_INET == AF_INET))
.and_condition(scmp_cmp!($arg1 & SOCK_STREAM == SOCK_STREAM));
.and_condition(seccomp_arg_filter!(arg0 & AF_INET == AF_INET))
.and_condition(seccomp_arg_filter!(arg1 & SOCK_STREAM == SOCK_STREAM));
self.custom.entry(Sysno::socket)
.or_insert_with(Vec::new)
.push(rule);
// IPv6
let rule = SeccompRule::new(Sysno::socket)
.and_condition(scmp_cmp!($arg0 & AF_INET6 == AF_INET6))
.and_condition(scmp_cmp!($arg1 & SOCK_STREAM == SOCK_STREAM));
.and_condition(seccomp_arg_filter!(arg0 & AF_INET6 == AF_INET6))
.and_condition(seccomp_arg_filter!(arg1 & SOCK_STREAM == SOCK_STREAM));
self.custom.entry(Sysno::socket)
.or_insert_with(Vec::new)
.push(rule);
Expand Down Expand Up @@ -241,15 +240,15 @@ impl Networking {

// We allow both stream and dgram unix sockets
let rule = SeccompRule::new(Sysno::socket)
.and_condition(scmp_cmp!($arg0 & AF_UNIX == AF_UNIX))
.and_condition(scmp_cmp!($arg1 & SOCK_STREAM == SOCK_STREAM));
.and_condition(seccomp_arg_filter!(arg0 & AF_UNIX == AF_UNIX))
.and_condition(seccomp_arg_filter!(arg1 & SOCK_STREAM == SOCK_STREAM));
self.custom.entry(Sysno::socket)
.or_insert_with(Vec::new)
.push(rule);
// DGRAM
let rule = SeccompRule::new(Sysno::socket)
.and_condition(scmp_cmp!($arg0 & AF_UNIX == AF_UNIX))
.and_condition(scmp_cmp!($arg1 & SOCK_DGRAM == SOCK_DGRAM));
.and_condition(seccomp_arg_filter!(arg0 & AF_UNIX == AF_UNIX))
.and_condition(seccomp_arg_filter!(arg1 & SOCK_DGRAM == SOCK_DGRAM));
self.custom.entry(Sysno::socket)
.or_insert_with(Vec::new)
.push(rule);
Expand Down
21 changes: 10 additions & 11 deletions src/builtins/systemio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ use std::collections::{HashSet, HashMap};
use std::fs::File;
use std::os::unix::io::AsRawFd;

use libseccomp::*;
use syscalls::Sysno;

use crate::{RuleSet, SeccompRule};
Expand Down Expand Up @@ -111,13 +110,13 @@ impl SystemIO {

// flags are the second argument for open but the third for openat
let rule = SeccompRule::new(Sysno::open)
.and_condition(scmp_cmp!($arg1 & WRITECREATE == 0));
.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(scmp_cmp!($arg2 & WRITECREATE == 0));
.and_condition(seccomp_arg_filter!(arg2 & WRITECREATE == 0));
self.custom.entry(Sysno::openat)
.or_insert_with(Vec::new)
.push(rule);
Expand Down Expand Up @@ -149,7 +148,7 @@ impl SystemIO {
/// Allow reading from stdin
pub fn allow_stdin(mut self) -> SystemIO {
let rule = SeccompRule::new(Sysno::read)
.and_condition(scmp_cmp!($arg0 == 0));
.and_condition(seccomp_arg_filter!(arg0 == 0));
self.custom.entry(Sysno::read)
.or_insert_with(Vec::new)
.push(rule);
Expand All @@ -160,7 +159,7 @@ impl SystemIO {
/// Allow writing to stdout
pub fn allow_stdout(mut self) -> SystemIO {
let rule = SeccompRule::new(Sysno::write)
.and_condition(scmp_cmp!($arg0 == 1));
.and_condition(seccomp_arg_filter!(arg0 == 1));
self.custom.entry(Sysno::write)
.or_insert_with(Vec::new)
.push(rule);
Expand All @@ -171,7 +170,7 @@ impl SystemIO {
/// Allow writing to stderr
pub fn allow_stderr(mut self) -> SystemIO {
let rule = SeccompRule::new(Sysno::write)
.and_condition(scmp_cmp!($arg0 == 2));
.and_condition(seccomp_arg_filter!(arg0 == 2));
self.custom.entry(Sysno::write)
.or_insert_with(Vec::new)
.push(rule);
Expand All @@ -188,17 +187,17 @@ impl SystemIO {
/// it's possible that the fd will be reused and therefore may be read from.
#[allow(clippy::missing_panics_doc)]
pub fn allow_file_read(mut self, file: &File) -> SystemIO {
let fd = file.as_raw_fd();
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(scmp_cmp!($arg0 == fd.try_into().expect("fd provided was negative")));
.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(scmp_cmp!($arg0 == fd.try_into().expect("fd provided was negative")));
.and_condition(seccomp_arg_filter!(arg0 == fd));
self.custom.entry(syscall)
.or_insert_with(Vec::new)
.push(rule);
Expand All @@ -216,9 +215,9 @@ impl SystemIO {
/// it's possible that the fd will be reused and therefore may be written to.
#[allow(clippy::missing_panics_doc)]
pub fn allow_file_write(mut self, file: &File) -> SystemIO {
let fd = file.as_raw_fd();
let fd = file.as_raw_fd().try_into().expect("provided fd was negative");
let rule = SeccompRule::new(Sysno::write)
.and_condition(scmp_cmp!($arg0 == fd.try_into().expect("fd provided was negative")));
.and_condition(seccomp_arg_filter!(arg0 == fd));
self.custom.entry(Sysno::write)
.or_insert_with(Vec::new)
.push(rule);
Expand Down
Loading

0 comments on commit a369f99

Please sign in to comment.