Skip to content

Commit

Permalink
Merge a369f99 into 4430e79
Browse files Browse the repository at this point in the history
  • Loading branch information
boustrophedon committed Oct 18, 2023
2 parents 4430e79 + a369f99 commit 188f625
Show file tree
Hide file tree
Showing 20 changed files with 692 additions and 166 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
14 changes: 6 additions & 8 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 All @@ -20,21 +19,20 @@ use super::YesReally;
/// do not provide isolation from each other. You can still access other threads' memory and
/// potentially get them to do operations that are not allowed in the current thread's seccomp
/// context.
#[must_use]
pub struct Threads {
allowed: HashSet<Sysno>,
}

impl Threads {
/// Create a new [`Threads`] ruleset with nothing allowed by default.
#[must_use]
pub fn nothing() -> Threads {
Threads {
allowed: HashSet::new(),
}
}

/// Allow creating new threads and processes.
#[must_use]
pub fn allow_create(mut self) -> Threads {
self.allowed.extend([Sysno::clone, Sysno::clone3]);

Expand All @@ -46,7 +44,6 @@ impl Threads {
/// # Security considerations
/// An attacker with arbitrary code execution and access to a high resolution timer can mount
/// timing attacks (e.g. spectre).
#[must_use]
pub fn allow_sleep(mut self) -> YesReally<Threads> {
self.allowed
.extend([Sysno::clock_nanosleep, Sysno::nanosleep]);
Expand All @@ -64,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 All @@ -89,6 +86,7 @@ impl RuleSet for Threads {
/// `tests/inherit_filters.rs`) but depending on your filter it could still do bad things.
///
/// Note that this also allows the `clone` syscall.
#[must_use]
pub struct ForkAndExec;
impl RuleSet for ForkAndExec {
fn simple_rules(&self) -> Vec<Sysno> {
Expand All @@ -106,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
3 changes: 1 addition & 2 deletions src/builtins/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,18 @@
/// A struct whose purpose is to make you read the documentation for the function you're calling.
/// If you're reading this, go read the documentation for the function that is returning this
/// object.
#[must_use]
pub struct YesReally<T> {
inner: T,
}

impl<T> YesReally<T> {
/// Confirm you really wanted to call the function and return its result.
#[must_use]
pub fn yes_really(self) -> T {
self.inner
}

/// Make a [`YesReally`].
#[must_use]
pub fn new(inner: T) -> YesReally<T> {
YesReally {
inner,
Expand Down
44 changes: 17 additions & 27 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 @@ -63,6 +62,7 @@ const NET_WRITE_SYSCALLS: &[Sysno] = &[Sysno::sendto, Sysno::sendmsg, Sysno::sen
/// care to consider whether you can split up your program (e.g. across separate threads) into a
/// part that opens and writes to files and a part that speaks to the network. This is a good
/// security practice in general.
#[must_use]
pub struct Networking {
/// Syscalls that are allowed
allowed: HashSet<Sysno>,
Expand All @@ -72,7 +72,6 @@ pub struct Networking {

impl Networking {
/// By default, allow no networking syscalls.
#[must_use]
pub fn nothing() -> Networking {
Networking {
allowed: HashSet::new(),
Expand All @@ -82,7 +81,6 @@ impl Networking {

/// Allow a running TCP server to continue running. Does not allow `socket` or `bind`,
/// preventing new sockets from being created.
#[must_use]
pub fn allow_running_tcp_servers(mut self) -> Networking {
self.allowed.extend(NET_IO_SYSCALLS);
self.allowed.extend(NET_READ_SYSCALLS);
Expand All @@ -98,23 +96,22 @@ impl Networking {
/// You probably don't need to use this. In most cases you can just run your server and then
/// use [`allow_running_tcp_servers`](Self::allow_running_tcp_servers). See
/// `examples/network_server.rs` for an example with warp.
#[must_use]
pub fn allow_start_tcp_servers(mut self) -> YesReally<Networking> {
const AF_INET: u64 = libc::AF_INET as u64;
const AF_INET6: u64 = libc::AF_INET6 as u64;
const SOCK_STREAM: u64 = libc::SOCK_STREAM as u64;

// 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 All @@ -131,7 +128,6 @@ impl Networking {

/// Allow a running UDP socket to continue running. Does not allow `socket` or `bind`,
/// preventing new sockets from being created.
#[must_use]
pub fn allow_running_udp_sockets(mut self) -> Networking {
self.allowed.extend(NET_IO_SYSCALLS);
self.allowed.extend(NET_READ_SYSCALLS);
Expand All @@ -146,23 +142,22 @@ impl Networking {
///
/// You probably don't need to use this. In most cases you can just run your server and then
/// use [`allow_running_udp_sockets`](Self::allow_running_udp_sockets).
#[must_use]
pub fn allow_start_udp_servers(mut self) -> YesReally<Networking> {
const AF_INET: u64 = libc::AF_INET as u64;
const AF_INET6: u64 = libc::AF_INET6 as u64;
const SOCK_DGRAM: u64 = libc::SOCK_DGRAM as u64;

// 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 @@ -191,23 +186,22 @@ impl Networking {
///
/// In some cases you can create the socket ahead of time, but that isn't possible with e.g.
/// reqwest, so we allow socket but not bind here.
#[must_use]
pub fn allow_start_tcp_clients(mut self) -> Networking {
const AF_INET: u64 = libc::AF_INET as u64;
const AF_INET6: u64 = libc::AF_INET6 as u64;
const SOCK_STREAM: u64 = libc::SOCK_STREAM as u64;

// 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 All @@ -225,7 +219,6 @@ impl Networking {
///
/// This is technically the same as
/// [`allow_running_tcp_servers`](Self::allow_running_tcp_servers).
#[must_use]
pub fn allow_running_tcp_clients(mut self) -> Networking {
self.allowed.extend(NET_IO_SYSCALLS);
self.allowed.extend(NET_READ_SYSCALLS);
Expand All @@ -240,23 +233,22 @@ impl Networking {
///
/// You probably don't need to use this. In most cases you can just run your server and then
/// use [`allow_running_unix_servers`](Self::allow_running_unix_servers).
#[must_use]
pub fn allow_start_unix_servers(mut self) -> YesReally<Networking> {
const AF_UNIX: u64 = libc::AF_UNIX as u64;
const SOCK_STREAM: u64 = libc::SOCK_STREAM as u64;
const SOCK_DGRAM: u64 = libc::SOCK_DGRAM as u64;

// 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 All @@ -271,7 +263,6 @@ impl Networking {

/// Allow a running Unix server to continue running. Does not allow `socket` or `bind`,
/// preventing new sockets from being created.
#[must_use]
pub fn allow_running_unix_servers(mut self) -> Networking {
self.allowed.extend(NET_IO_SYSCALLS);
self.allowed.extend(NET_READ_SYSCALLS);
Expand All @@ -285,7 +276,6 @@ impl Networking {
///
/// This is technically the same as
/// [`allow_running_unix_servers`](Self::allow_running_unix_servers).
#[must_use]
pub fn allow_running_unix_clients(mut self) -> Networking {
self.allowed.extend(NET_IO_SYSCALLS);
self.allowed.extend(NET_READ_SYSCALLS);
Expand Down
Loading

0 comments on commit 188f625

Please sign in to comment.