Skip to content

Commit

Permalink
feat: sandbox trampoline implementation (#935)
Browse files Browse the repository at this point in the history
  • Loading branch information
wolfv authored Dec 9, 2024
1 parent ddb05ce commit c74c657
Show file tree
Hide file tree
Showing 8 changed files with 291 additions and 0 deletions.
25 changes: 25 additions & 0 deletions crates/rattler_sandbox/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "rattler_sandbox"
version = "0.1.0"
categories.workspace = true
homepage.workspace = true
repository.workspace = true
license.workspace = true
edition.workspace = true
readme.workspace = true

[dependencies]
clap = { workspace = true, features = ["derive"] }
fs-err = { workspace = true }
tokio = { workspace = true, optional = true, features = ["process"]}

[target.'cfg(any(target_os = "macos", all(target_os = "linux", target_arch = "x86_64"), all(target_os = "linux", target_arch = "aarch64")))'.dependencies]
birdcage = "0.8.1"

[dev-dependencies]
libtest-mimic = "0.8.1"

[[test]]
name = "integration_test"
path = "tests/integration_test.rs"
harness = false
31 changes: 31 additions & 0 deletions crates/rattler_sandbox/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// A shim for the sandbox that is used on non-supported platforms
#[cfg(not(any(
all(target_os = "linux", target_arch = "x86_64"),
all(target_os = "linux", target_arch = "aarch64"),
all(target_os = "macos", target_arch = "x86_64"),
all(target_os = "macos", target_arch = "aarch64"),
)))]
mod sandbox_shim;
#[cfg(not(any(
all(target_os = "linux", target_arch = "x86_64"),
all(target_os = "linux", target_arch = "aarch64"),
all(target_os = "macos", target_arch = "x86_64"),
all(target_os = "macos", target_arch = "aarch64"),
)))]
pub use sandbox_shim::*;

/// The actual implementation of the sandbox that is used on supported platforms
#[cfg(any(
all(target_os = "linux", target_arch = "x86_64"),
all(target_os = "linux", target_arch = "aarch64"),
all(target_os = "macos", target_arch = "x86_64"),
all(target_os = "macos", target_arch = "aarch64"),
))]
pub mod sandbox;
#[cfg(any(
all(target_os = "linux", target_arch = "x86_64"),
all(target_os = "linux", target_arch = "aarch64"),
all(target_os = "macos", target_arch = "x86_64"),
all(target_os = "macos", target_arch = "aarch64"),
))]
pub use sandbox::*;
75 changes: 75 additions & 0 deletions crates/rattler_sandbox/src/sandbox/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use birdcage::process::Command;
use birdcage::{Birdcage, Sandbox};
use clap::Parser;

pub mod sandbox_impl;
#[cfg(feature = "tokio")]
pub mod tokio;

pub use sandbox_impl::{sandboxed_command, Exception};

#[derive(clap::Parser)]
struct Opts {
#[clap(long)]
fs_exec_and_read: Option<Vec<String>>,

#[clap(long)]
fs_write_and_read: Option<Vec<String>>,

#[clap(long)]
fs_read: Option<Vec<String>>,

#[clap(long)]
network: bool,

#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
}

// This function checks if the current executable should execute as a sandboxed process
fn init() {
let mut args = std::env::args().collect::<Vec<String>>();
// Remove the first `__sandbox_trampoline__` argument
args.remove(1);
let opts = Opts::parse_from(args.iter());
// Allow access to our test executable.
let mut sandbox = Birdcage::new();

if let Some(fs_exec_and_read) = opts.fs_exec_and_read {
for path in fs_exec_and_read {
let _ = sandbox.add_exception(birdcage::Exception::ExecuteAndRead(path.into()));
}
}

if let Some(fs_read) = opts.fs_read {
for path in fs_read {
let _ = sandbox.add_exception(birdcage::Exception::Read(path.into()));
}
}

if let Some(fs_write_and_read) = opts.fs_write_and_read {
for path in fs_write_and_read {
let _ = sandbox.add_exception(birdcage::Exception::WriteAndRead(path.into()));
}
}
if let Some((exe, args)) = opts.args.split_first() {
// Initialize the sandbox; by default everything is prohibited.
let mut command = Command::new(exe);
command.args(args);

let mut child = sandbox.spawn(command).unwrap();

let status = child.wait().unwrap();
std::process::exit(status.code().unwrap());
} else {
panic!("No executable provided");
}
}

pub fn init_sandbox() {
// TODO ideally we check that we are single threaded, but birdcage will also check it later on
if std::env::args().any(|arg| arg == "__sandbox_trampoline__") {
// This is a sandboxed process, so we initialize the sandbox
init();
}
}
78 changes: 78 additions & 0 deletions crates/rattler_sandbox/src/sandbox/sandbox_impl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//! We expose some sandbox items that then call itself through the trampoline
use std::process::Command;

/// Add exceptions to the sandbox
pub enum Exception {
ExecuteAndRead(String),
Read(String),
ReadAndWrite(String),
Networking,
}

/// Create a `Command` that will run the current executable with the given exceptions
pub fn sandboxed_command(exe: &str, exceptions: &[Exception]) -> Command {
let self_exe = std::env::current_exe().unwrap();
let mut cmd = Command::new(self_exe);
cmd.arg("__sandbox_trampoline__");

for exception in exceptions {
match exception {
Exception::ExecuteAndRead(path) => {
cmd.arg("--fs-exec-and-read").arg(path);
}
Exception::Read(path) => {
cmd.arg("--fs-read").arg(path);
}
Exception::ReadAndWrite(path) => {
cmd.arg("--fs-write-and-read").arg(path);
}
Exception::Networking => {
cmd.arg("--network");
}
}
}

cmd.arg(exe);

cmd
}

#[cfg(test)]
mod tests {
use std::ffi::OsStr;

use super::*;

#[test]
fn test_sandboxed_command() {
let cmd = sandboxed_command(
"test",
&[
Exception::ExecuteAndRead("/bin".into()),
Exception::Read("/etc".into()),
Exception::ReadAndWrite("/tmp".into()),
Exception::Networking,
],
);

let args = cmd.get_args();

// args to string to compare
let args: Vec<&OsStr> = args.collect();

assert_eq!(
args,
vec![
"__sandbox_trampoline__",
"--fs-exec-and-read",
"/bin",
"--fs-read",
"/etc",
"--fs-write-and-read",
"/tmp",
"--network",
"test",
]
);
}
}
31 changes: 31 additions & 0 deletions crates/rattler_sandbox/src/sandbox/tokio.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//! We expose some sandbox items that then call itself through the trampoline
use crate::sandbox::Exception;
use tokio::process::Command;

/// Create a `Command` that will run the current executable with the given exceptions
pub fn sandboxed_command(exe: &str, exceptions: &[Exception]) -> Command {
let self_exe = std::env::current_exe().unwrap();
let mut cmd = Command::new(self_exe);
cmd.arg("__sandbox_trampoline__");

for exception in exceptions {
match exception {
Exception::ExecuteAndRead(path) => {
cmd.arg("--fs-exec-and-read").arg(path);
}
Exception::Read(path) => {
cmd.arg("--fs-read").arg(path);
}
Exception::ReadAndWrite(path) => {
cmd.arg("--fs-write-and-read").arg(path);
}
Exception::Networking => {
cmd.arg("--network");
}
}
}

cmd.arg(exe);

cmd
}
3 changes: 3 additions & 0 deletions crates/rattler_sandbox/src/sandbox_shim.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub fn init_sandbox() {
panic!("Sandbox is not supported on this platform");
}
28 changes: 28 additions & 0 deletions crates/rattler_sandbox/tests/integration_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#[cfg(any(
target_os = "macos",
all(target_os = "linux", target_arch = "x86_64"),
all(target_os = "linux", target_arch = "aarch64"),
))]
mod tests_impl;

#[cfg(any(
target_os = "macos",
all(target_os = "linux", target_arch = "x86_64"),
all(target_os = "linux", target_arch = "aarch64"),
))]
fn main() {
rattler_sandbox::init_sandbox();
let args = libtest_mimic::Arguments::from_args();
let tests = tests_impl::tests();
libtest_mimic::run(&args, tests).exit();
}

#[cfg(not(any(
target_os = "macos",
all(target_os = "linux", target_arch = "x86_64"),
all(target_os = "linux", target_arch = "aarch64"),
)))]
fn main() {
eprintln!("This platform is not supported by the sandbox");
std::process::exit(0);
}
20 changes: 20 additions & 0 deletions crates/rattler_sandbox/tests/tests_impl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#![cfg(any(
target_os = "macos",
all(target_os = "linux", target_arch = "x86_64"),
all(target_os = "linux", target_arch = "aarch64"),
))]

use libtest_mimic::{Failed, Trial};
use rattler_sandbox::sandboxed_command;

fn test_cannot_ls() -> Result<(), Failed> {
let mut cmd = sandboxed_command("ls", &[]);
cmd.arg("/");
let output = cmd.output().unwrap();
assert!(!output.status.success());
Ok(())
}

pub fn tests() -> Vec<Trial> {
vec![Trial::test("test_cannot_ls", test_cannot_ls)]
}

0 comments on commit c74c657

Please sign in to comment.