Skip to content

Commit

Permalink
Add mount namespaces to linux sandbox
Browse files Browse the repository at this point in the history
This patch adds optional mount namespaces to the linux sandbox to
allow for filesystem isolation on systems without landlock support.

Filesystem isolation now requires either landlock OR namespace creation
to be successful in order for the sandbox creation to be successful.

Landlock will be layered on top of the mount namespace if both are
available.

While landlock automatically resolves symlink access, mount namespaces
do not. So to allow access to `/usr/lib` through `/lib`, it is now
necessary to allow both `/lib` AND `/usr/lib`.
  • Loading branch information
cd-work committed Sep 25, 2023
1 parent a57b5ea commit 4c48482
Show file tree
Hide file tree
Showing 6 changed files with 363 additions and 18 deletions.
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ name = "fs"
path = "tests/fs.rs"
harness = false

[[test]]
name = "fs_without_landlock"
path = "tests/fs_without_landlock.rs"
harness = false

[[test]]
name = "full_env"
path = "tests/full_env.rs"
Expand Down
77 changes: 68 additions & 9 deletions src/linux/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
//! This module implements sandboxing on Linux based on the Landlock LSM,
//! namespaces, and seccomp.

use std::collections::HashMap;
use std::path::PathBuf;

use landlock::{
make_bitflags, Access, AccessFs, Compatible, PathBeneath, PathFd, Ruleset, RulesetAttr,
RulesetCreated, RulesetCreatedAttr, RulesetStatus, ABI as LANDLOCK_ABI,
Expand All @@ -20,12 +23,34 @@ const ABI: LANDLOCK_ABI = LANDLOCK_ABI::V1;

/// Linux sandboxing.
pub struct LinuxSandbox {
bind_mounts: HashMap<PathBuf, libc::c_ulong>,
env_exceptions: Vec<String>,
landlock: RulesetCreated,
allow_networking: bool,
full_env: bool,
}

impl LinuxSandbox {
/// Add or modify a bind mount.
///
/// This will add a new bind mount with the specified permission if it does
/// not exist already.
///
/// If the bind mount already exists, it will *ADD* the additional
/// permissions.
fn update_bind_mount(&mut self, path: PathBuf, write: bool, execute: bool) {
let flags = self.bind_mounts.entry(path).or_insert(libc::MS_RDONLY | libc::MS_NOEXEC);

if write {
*flags &= !libc::MS_RDONLY;
}

if execute {
*flags &= !libc::MS_NOEXEC;
}
}
}

impl Sandbox for LinuxSandbox {
fn new() -> Result<Self> {
// Setup landlock filtering.
Expand All @@ -35,14 +60,38 @@ impl Sandbox for LinuxSandbox {
.create()?;
landlock.as_mut().set_no_new_privs(true);

Ok(Self { landlock, env_exceptions: Vec::new(), allow_networking: false, full_env: false })
Ok(Self {
landlock,
allow_networking: false,
full_env: false,
env_exceptions: Default::default(),
bind_mounts: Default::default(),
})
}

fn add_exception(&mut self, exception: Exception) -> Result<&mut Self> {
let (path, access) = match exception {
Exception::Read(path) => (path, make_bitflags!(AccessFs::{ ReadFile | ReadDir })),
Exception::Write(path) => (path, AccessFs::from_write(ABI)),
Exception::ExecuteAndRead(path) => (path, AccessFs::from_read(ABI)),
let (path_fd, access) = match exception {
Exception::Read(path) => {
let path_fd = PathFd::new(&path)?;

self.update_bind_mount(path, false, false);

(path_fd, make_bitflags!(AccessFs::{ ReadFile | ReadDir }))
},
Exception::Write(path) => {
let path_fd = PathFd::new(&path)?;

self.update_bind_mount(path, true, false);

(path_fd, AccessFs::from_write(ABI))
},
Exception::ExecuteAndRead(path) => {
let path_fd = PathFd::new(&path)?;

self.update_bind_mount(path, false, true);

(path_fd, AccessFs::from_read(ABI))
},
Exception::Environment(key) => {
self.env_exceptions.push(key);
return Ok(self);
Expand All @@ -57,7 +106,7 @@ impl Sandbox for LinuxSandbox {
},
};

let rule = PathBeneath::new(PathFd::new(path)?, access);
let rule = PathBeneath::new(path_fd, access);

self.landlock.as_mut().add_rule(rule)?;

Expand All @@ -71,18 +120,28 @@ impl Sandbox for LinuxSandbox {
}

// Setup namespaces.
let namespace_result = namespaces::create_namespaces(!self.allow_networking);
let namespace_result =
namespaces::create_namespaces(self.allow_networking, self.bind_mounts);

// Setup seccomp network filter.
if !self.allow_networking {
let seccomp_result = NetworkFilter::apply();

// Propagate failure if neither seccomp nor namespaces could isolate networking.
namespace_result.or(seccomp_result)?;
if let (Err(_), Err(err)) = (&namespace_result, seccomp_result) {
return Err(err);
}
}

// Apply landlock rules.
let status = self.landlock.restrict_self()?;
let landlock_result = self.landlock.restrict_self();

// Ensure either landlock or namespaces are enforced.
let status = match (landlock_result, namespace_result) {
(Ok(status), _) => status,
(Err(_), Ok(_)) => return Ok(()),
(Err(err), _) => return Err(err.into()),
};

// Ensure all restrictions were properly applied.
if status.no_new_privs && status.ruleset == RulesetStatus::FullyEnforced {
Expand Down
Loading

0 comments on commit 4c48482

Please sign in to comment.