Default to whitelist mode and parallelize tests

Flips the default sandbox mode from blacklist to whitelist and
replaces the global RUST_TEST_THREADS=1 with a targeted RwLock
that only serializes blacklist sandboxes against tests mutating
glob-matching host paths. A new Sandbox newtype acquires the
guard automatically when --blacklist is in args.
This commit is contained in:
2026-04-27 08:18:41 +02:00
parent c77dbc10c3
commit 6e81866226
12 changed files with 158 additions and 81 deletions
-2
View File
@@ -1,2 +0,0 @@
[env]
RUST_TEST_THREADS = "1"
+1 -1
View File
@@ -9,7 +9,7 @@ The config file may set `extra-config = "<absolute path>"` to layer a second fil
## Build and test ## Build and test
- `cargo fmt` and `cargo clippy` must pass before every commit. - `cargo fmt` and `cargo clippy` must pass before every commit.
- `cargo test` runs all integration tests. Tests run serially (configured in `.cargo/config.toml`) because they spawn real bwrap sandboxes that share host paths like `/tmp`. - `cargo test` runs all test cases.
- Never add Co-Authored-By lines to commits. - Never add Co-Authored-By lines to commits.
## Things that will bite you ## Things that will bite you
+2 -2
View File
@@ -10,11 +10,11 @@ use clap::Parser;
about = "Sandbox agentic coding assistants with bubblewrap" about = "Sandbox agentic coding assistants with bubblewrap"
)] )]
pub struct Args { pub struct Args {
/// Blacklist mode: bind / read-only, overlay sensitive paths (default) /// Blacklist mode: bind / read-only, overlay sensitive paths
#[arg(long, conflicts_with = "whitelist")] #[arg(long, conflicts_with = "whitelist")]
pub blacklist: bool, pub blacklist: bool,
/// Whitelist mode: only explicitly listed minimal paths visible /// Whitelist mode: only explicitly listed minimal paths visible (default)
#[arg(long)] #[arg(long)]
pub whitelist: bool, pub whitelist: bool,
+1 -1
View File
@@ -93,7 +93,7 @@ fn merge_mode(
} }
resolve_mode(profile) resolve_mode(profile)
.or_else(|| resolve_mode(globals)) .or_else(|| resolve_mode(globals))
.unwrap_or(SandboxMode::Blacklist) .unwrap_or(SandboxMode::Whitelist)
} }
fn resolve_mode(opts: &Options) -> Option<SandboxMode> { fn resolve_mode(opts: &Options) -> Option<SandboxMode> {
+14 -14
View File
@@ -2,7 +2,7 @@ use crate::common::*;
#[test] #[test]
fn dry_run_prints_and_exits() { fn dry_run_prints_and_exits() {
let output = sandbox(&["--dry-run"]) let output = Sandbox::new(&["--dry-run"])
.args(["--", "bash", "-c", "exit 42"]) .args(["--", "bash", "-c", "exit 42"])
.output() .output()
.expect("agent-sandbox binary failed to execute"); .expect("agent-sandbox binary failed to execute");
@@ -20,7 +20,7 @@ fn dry_run_prints_and_exits() {
#[test] #[test]
fn dry_run_output_is_copy_pasteable_shell() { fn dry_run_output_is_copy_pasteable_shell() {
let dry = sandbox(&["--dry-run"]) let dry = Sandbox::new(&["--dry-run"])
.args(["--", "bash", "-c", "echo $HOME"]) .args(["--", "bash", "-c", "echo $HOME"])
.output() .output()
.expect("agent-sandbox binary failed to execute"); .expect("agent-sandbox binary failed to execute");
@@ -34,7 +34,7 @@ fn dry_run_output_is_copy_pasteable_shell() {
#[test] #[test]
fn empty_home_rejected() { fn empty_home_rejected() {
let output = sandbox(&[]) let output = Sandbox::new(&[])
.env("HOME", "") .env("HOME", "")
.args(["--", "true"]) .args(["--", "true"])
.output() .output()
@@ -53,7 +53,7 @@ fn empty_home_rejected() {
#[test] #[test]
fn config_missing_file_errors() { fn config_missing_file_errors() {
let output = sandbox_withconfig(&["--config", "/nonexistent/config.toml"]) let output = Sandbox::new_with_config(&["--config", "/nonexistent/config.toml"])
.args(["--", "true"]) .args(["--", "true"])
.output() .output()
.expect("failed to execute"); .expect("failed to execute");
@@ -70,7 +70,7 @@ fn config_missing_file_errors() {
fn config_invalid_toml_errors() { fn config_invalid_toml_errors() {
let cfg = ConfigFile::new("not valid {{{{ toml"); let cfg = ConfigFile::new("not valid {{{{ toml");
let output = sandbox_withconfig(&["--config", &cfg]) let output = Sandbox::new_with_config(&["--config", &cfg])
.args(["--", "true"]) .args(["--", "true"])
.output() .output()
.expect("failed to execute"); .expect("failed to execute");
@@ -87,7 +87,7 @@ fn config_invalid_toml_errors() {
fn config_unknown_key_errors() { fn config_unknown_key_errors() {
let cfg = ConfigFile::new("hardened = true\nbogus = \"nope\"\n"); let cfg = ConfigFile::new("hardened = true\nbogus = \"nope\"\n");
let output = sandbox_withconfig(&["--config", &cfg]) let output = Sandbox::new_with_config(&["--config", &cfg])
.args(["--", "true"]) .args(["--", "true"])
.output() .output()
.expect("failed to execute"); .expect("failed to execute");
@@ -102,7 +102,7 @@ fn config_unknown_key_errors() {
#[test] #[test]
fn bwrap_arg_setenv_passes_through() { fn bwrap_arg_setenv_passes_through() {
let output = sandbox(&["--bwrap-arg", "--setenv MYVAR hello"]) let output = Sandbox::new(&["--bwrap-arg", "--setenv MYVAR hello"])
.args(["--", "bash", "-c", "echo $MYVAR"]) .args(["--", "bash", "-c", "echo $MYVAR"])
.output() .output()
.expect("agent-sandbox binary failed to execute"); .expect("agent-sandbox binary failed to execute");
@@ -123,7 +123,7 @@ fn config_entrypoint_appends_passthrough_args() {
"#, "#,
); );
let output = sandbox_withconfig(&["--config", &cfg, "--profile", "test"]) let output = Sandbox::new_with_config(&["--config", &cfg, "--profile", "test"])
.args(["--", "echo entrypoint-works"]) .args(["--", "echo entrypoint-works"])
.output() .output()
.expect("failed to execute"); .expect("failed to execute");
@@ -145,7 +145,7 @@ fn config_entrypoint_falls_back_to_command_defaults() {
"#, "#,
); );
let output = sandbox_withconfig(&["--config", &cfg, "--profile", "test"]) let output = Sandbox::new_with_config(&["--config", &cfg, "--profile", "test"])
.output() .output()
.expect("failed to execute"); .expect("failed to execute");
@@ -165,7 +165,7 @@ fn config_entrypoint_alone_without_command_or_passthrough() {
"#, "#,
); );
let output = sandbox_withconfig(&["--config", &cfg, "--profile", "test"]) let output = Sandbox::new_with_config(&["--config", &cfg, "--profile", "test"])
.output() .output()
.expect("failed to execute"); .expect("failed to execute");
@@ -178,7 +178,7 @@ fn config_entrypoint_alone_without_command_or_passthrough() {
#[test] #[test]
fn cli_entrypoint_appends_passthrough_args() { fn cli_entrypoint_appends_passthrough_args() {
let output = sandbox(&["--entrypoint", "bash"]) let output = Sandbox::new(&["--entrypoint", "bash"])
.args(["--", "-c", "echo cli-entrypoint-works"]) .args(["--", "-c", "echo cli-entrypoint-works"])
.output() .output()
.expect("failed to execute"); .expect("failed to execute");
@@ -198,7 +198,7 @@ fn cli_entrypoint_overrides_config_entrypoint() {
"#, "#,
); );
let output = sandbox_withconfig(&["--config", &cfg, "--entrypoint", "bash"]) let output = Sandbox::new_with_config(&["--config", &cfg, "--entrypoint", "bash"])
.args(["--", "-c", "echo override-works"]) .args(["--", "-c", "echo override-works"])
.output() .output()
.expect("failed to execute"); .expect("failed to execute");
@@ -219,7 +219,7 @@ fn config_command_alone_without_passthrough() {
"#, "#,
); );
let output = sandbox_withconfig(&["--config", &cfg, "--profile", "test"]) let output = Sandbox::new_with_config(&["--config", &cfg, "--profile", "test"])
.output() .output()
.expect("failed to execute"); .expect("failed to execute");
@@ -239,7 +239,7 @@ fn config_command_replaced_by_passthrough() {
"#, "#,
); );
let output = sandbox_withconfig(&["--config", &cfg, "--profile", "test"]) let output = Sandbox::new_with_config(&["--config", &cfg, "--profile", "test"])
.args(["--", "bash", "-c", "echo replaced"]) .args(["--", "bash", "-c", "echo replaced"])
.output() .output()
.expect("failed to execute"); .expect("failed to execute");
+76 -7
View File
@@ -1,18 +1,87 @@
use std::fs; use std::fs;
use std::ops::{Deref, DerefMut};
use std::process::Command; use std::process::Command;
use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tempfile::TempDir; use tempfile::TempDir;
pub fn sandbox_withconfig(extra_args: &[&str]) -> Command { // Blacklist mode globs the host filesystem at sandbox startup to find
let mut cmd = Command::new(env!("CARGO_BIN_EXE_agent-sandbox")); // sensitive paths to mask. A matching host file that vanishes mid-startup
cmd.args(extra_args); // makes bwrap fail in any concurrent blacklist sandbox.
cmd pub struct HostGlobsLock;
impl HostGlobsLock {
/// Acquire shared access. Hold while running a blacklist sandbox.
pub fn for_scan() -> RwLockReadGuard<'static, ()> {
LOCK.read().unwrap()
}
/// Acquire exclusive access. Hold while creating or deleting a host
/// path that may match a blacklist glob (e.g. `/tmp/ssh-*`).
pub fn for_mutation() -> RwLockWriteGuard<'static, ()> {
LOCK.write().unwrap()
}
} }
pub fn sandbox(extra_args: &[&str]) -> Command { static LOCK: RwLock<()> = RwLock::new(());
let mut cmd = sandbox_withconfig(&["--no-config"]);
#[allow(dead_code)]
enum HostGlobsGuard {
Scan(RwLockReadGuard<'static, ()>),
Mutation(RwLockWriteGuard<'static, ()>),
}
pub struct Sandbox {
cmd: Command,
_guard: Option<HostGlobsGuard>,
}
impl Sandbox {
pub fn new(extra_args: &[&str]) -> Self {
Self::build(&["--no-config"], extra_args, scan_guard_for(extra_args))
}
pub fn new_with_config(extra_args: &[&str]) -> Self {
Self::build(&[], extra_args, scan_guard_for(extra_args))
}
pub fn new_for_host_mutation(extra_args: &[&str]) -> Self {
debug_assert!(
extra_args.contains(&"--blacklist"),
"new_for_host_mutation is only meaningful for blacklist sandboxes"
);
Self::build(
&["--no-config"],
extra_args,
Some(HostGlobsGuard::Mutation(HostGlobsLock::for_mutation())),
)
}
fn build(prefix: &[&str], extra_args: &[&str], guard: Option<HostGlobsGuard>) -> Self {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_agent-sandbox"));
cmd.args(prefix);
cmd.args(extra_args); cmd.args(extra_args);
cmd Self { cmd, _guard: guard }
}
}
fn scan_guard_for(extra_args: &[&str]) -> Option<HostGlobsGuard> {
extra_args
.contains(&"--blacklist")
.then(|| HostGlobsGuard::Scan(HostGlobsLock::for_scan()))
}
impl Deref for Sandbox {
type Target = Command;
fn deref(&self) -> &Command {
&self.cmd
}
}
impl DerefMut for Sandbox {
fn deref_mut(&mut self) -> &mut Command {
&mut self.cmd
}
} }
pub struct ConfigFile { pub struct ConfigFile {
+16 -8
View File
@@ -6,7 +6,7 @@ fn printenv_inside(args: &[&str], vars: &[(&str, &str)], query: &[&str]) -> Stri
.map(|v| format!("printenv {v} || echo MISSING:{v}")) .map(|v| format!("printenv {v} || echo MISSING:{v}"))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("; "); .join("; ");
let mut cmd = sandbox(args); let mut cmd = Sandbox::new(args);
for (k, v) in vars { for (k, v) in vars {
cmd.env(k, v); cmd.env(k, v);
} }
@@ -219,7 +219,7 @@ fn whitelist_unsetenv_overrides_kept_var() {
#[test] #[test]
fn blacklist_drops_token_and_secret_vars() { fn blacklist_drops_token_and_secret_vars() {
let stdout = printenv_inside( let stdout = printenv_inside(
&[], &["--blacklist"],
&[ &[
("GH_TOKEN", "gh-secret"), ("GH_TOKEN", "gh-secret"),
("AWS_SECRET_ACCESS_KEY", "aws-secret"), ("AWS_SECRET_ACCESS_KEY", "aws-secret"),
@@ -252,7 +252,7 @@ fn blacklist_drops_token_and_secret_vars() {
#[test] #[test]
fn blacklist_carves_out_vendor_api_keys() { fn blacklist_carves_out_vendor_api_keys() {
let stdout = printenv_inside( let stdout = printenv_inside(
&[], &["--blacklist"],
&[ &[
("ANTHROPIC_API_KEY", "anthropic-key"), ("ANTHROPIC_API_KEY", "anthropic-key"),
("OPENAI_API_KEY", "openai-key"), ("OPENAI_API_KEY", "openai-key"),
@@ -272,7 +272,7 @@ fn blacklist_carves_out_vendor_api_keys() {
#[test] #[test]
fn blacklist_suffix_match_does_not_catch_substring() { fn blacklist_suffix_match_does_not_catch_substring() {
let stdout = printenv_inside( let stdout = printenv_inside(
&[], &["--blacklist"],
&[ &[
("TOKENIZER_PATH", "/opt/tok"), ("TOKENIZER_PATH", "/opt/tok"),
("MY_TOKEN_HOLDER", "holder"), ("MY_TOKEN_HOLDER", "holder"),
@@ -291,14 +291,18 @@ fn blacklist_suffix_match_does_not_catch_substring() {
#[test] #[test]
fn blacklist_keeps_unrelated_host_var() { fn blacklist_keeps_unrelated_host_var() {
let stdout = printenv_inside(&[], &[("MY_NICE_VAR", "hello")], &["MY_NICE_VAR"]); let stdout = printenv_inside(
&["--blacklist"],
&[("MY_NICE_VAR", "hello")],
&["MY_NICE_VAR"],
);
assert!(stdout.contains("hello"), "MY_NICE_VAR stripped: {stdout}"); assert!(stdout.contains("hello"), "MY_NICE_VAR stripped: {stdout}");
} }
#[test] #[test]
fn blacklist_keeps_dbus_vars() { fn blacklist_keeps_dbus_vars() {
let stdout = printenv_inside( let stdout = printenv_inside(
&[], &["--blacklist"],
&[ &[
("DBUS_SESSION_BUS_ADDRESS", "unix:path=/tmp/fake"), ("DBUS_SESSION_BUS_ADDRESS", "unix:path=/tmp/fake"),
("DBUS_SYSTEM_BUS_ADDRESS", "unix:path=/tmp/fake-system"), ("DBUS_SYSTEM_BUS_ADDRESS", "unix:path=/tmp/fake-system"),
@@ -324,7 +328,11 @@ fn no_env_filter_whitelist_keeps_arbitrary_host_var() {
#[test] #[test]
fn no_env_filter_blacklist_keeps_secrets() { fn no_env_filter_blacklist_keeps_secrets() {
let stdout = printenv_inside(&["--no-env-filter"], &[("GH_TOKEN", "kept")], &["GH_TOKEN"]); let stdout = printenv_inside(
&["--blacklist", "--no-env-filter"],
&[("GH_TOKEN", "kept")],
&["GH_TOKEN"],
);
assert!( assert!(
stdout.contains("kept"), stdout.contains("kept"),
"expected --no-env-filter to pass secrets through, got: {stdout}" "expected --no-env-filter to pass secrets through, got: {stdout}"
@@ -347,7 +355,7 @@ fn no_env_filter_still_honors_user_env() {
#[test] #[test]
fn blacklist_env_overrides_builtin_deny() { fn blacklist_env_overrides_builtin_deny() {
let stdout = printenv_inside( let stdout = printenv_inside(
&["--env", "GH_TOKEN=overridden"], &["--blacklist", "--env", "GH_TOKEN=overridden"],
&[("GH_TOKEN", "original")], &[("GH_TOKEN", "original")],
&["GH_TOKEN"], &["GH_TOKEN"],
); );
+10 -10
View File
@@ -2,7 +2,7 @@ use crate::common::*;
#[test] #[test]
fn whitelist_hides_home_contents() { fn whitelist_hides_home_contents() {
let output = sandbox(&["--whitelist"]) let output = Sandbox::new(&["--whitelist"])
.args(["--", "bash", "-c", "ls ~/Documents 2>&1 || echo hidden"]) .args(["--", "bash", "-c", "ls ~/Documents 2>&1 || echo hidden"])
.output() .output()
.expect("agent-sandbox binary failed to execute"); .expect("agent-sandbox binary failed to execute");
@@ -16,7 +16,7 @@ fn whitelist_hides_home_contents() {
#[test] #[test]
fn whitelist_sys_is_readable() { fn whitelist_sys_is_readable() {
let output = sandbox(&["--whitelist"]) let output = Sandbox::new(&["--whitelist"])
.args(["--", "bash", "-c", "cat /sys/class/net/lo/address"]) .args(["--", "bash", "-c", "cat /sys/class/net/lo/address"])
.output() .output()
.expect("agent-sandbox binary failed to execute"); .expect("agent-sandbox binary failed to execute");
@@ -30,7 +30,7 @@ fn whitelist_sys_is_readable() {
#[test] #[test]
fn blacklist_run_is_tmpfs() { fn blacklist_run_is_tmpfs() {
let output = sandbox(&[]) let output = Sandbox::new(&["--blacklist"])
.args([ .args([
"--", "--",
"bash", "bash",
@@ -49,7 +49,7 @@ fn blacklist_run_is_tmpfs() {
#[test] #[test]
fn blacklist_run_dbus_socket_accessible() { fn blacklist_run_dbus_socket_accessible() {
let output = sandbox(&[]) let output = Sandbox::new(&["--blacklist"])
.args([ .args([
"--", "--",
"bash", "bash",
@@ -71,7 +71,7 @@ fn blacklist_runuser_is_tmpfs() {
let run_user = agent_sandbox::require_run_user().expect("failed to determine XDG_RUNTIME_DIR"); let run_user = agent_sandbox::require_run_user().expect("failed to determine XDG_RUNTIME_DIR");
let script = format!("ls -A {} | grep -v '^bus$'", run_user); let script = format!("ls -A {} | grep -v '^bus$'", run_user);
let output = sandbox(&[]) let output = Sandbox::new(&["--blacklist"])
.args(["--", "bash", "-c", &script]) .args(["--", "bash", "-c", &script])
.output() .output()
.expect("agent-sandbox binary failed to execute"); .expect("agent-sandbox binary failed to execute");
@@ -86,7 +86,7 @@ fn blacklist_runuser_is_tmpfs() {
#[test] #[test]
fn blacklist_dev_input_hidden() { fn blacklist_dev_input_hidden() {
let output = sandbox(&[]) let output = Sandbox::new(&["--blacklist"])
.args(["--", "bash", "-c", "ls /dev/input/ 2>/dev/null | wc -l"]) .args(["--", "bash", "-c", "ls /dev/input/ 2>/dev/null | wc -l"])
.output() .output()
.expect("agent-sandbox binary failed to execute"); .expect("agent-sandbox binary failed to execute");
@@ -100,7 +100,7 @@ fn blacklist_dev_input_hidden() {
#[test] #[test]
fn blacklist_root_is_readonly() { fn blacklist_root_is_readonly() {
let output = sandbox(&[]) let output = Sandbox::new(&["--blacklist"])
.args([ .args([
"--", "--",
"bash", "bash",
@@ -124,7 +124,7 @@ fn blacklist_root_is_readonly() {
#[test] #[test]
fn whitelist_root_is_readonly() { fn whitelist_root_is_readonly() {
let output = sandbox(&["--whitelist"]) let output = Sandbox::new(&["--whitelist"])
.args([ .args([
"--", "--",
"bash", "bash",
@@ -148,7 +148,7 @@ fn whitelist_root_is_readonly() {
#[test] #[test]
fn whitelist_mountpoint_parents_are_readonly() { fn whitelist_mountpoint_parents_are_readonly() {
let output = sandbox(&["--whitelist"]) let output = Sandbox::new(&["--whitelist"])
.args([ .args([
"--", "--",
"bash", "bash",
@@ -177,7 +177,7 @@ fn whitelist_mountpoint_parents_are_readonly() {
#[test] #[test]
fn whitelist_tmp_still_writable() { fn whitelist_tmp_still_writable() {
let output = sandbox(&["--whitelist"]) let output = Sandbox::new(&["--whitelist"])
.args([ .args([
"--", "--",
"bash", "bash",
+20 -18
View File
@@ -16,7 +16,7 @@ impl Drop for CleanupFile {
fn cwd_is_writable() { fn cwd_is_writable() {
let _cleanup = CleanupFile("./sandbox_canary"); let _cleanup = CleanupFile("./sandbox_canary");
let output = sandbox(&[]) let output = Sandbox::new(&[])
.args(["--", "bash", "-c", "touch ./sandbox_canary && echo ok"]) .args(["--", "bash", "-c", "touch ./sandbox_canary && echo ok"])
.output() .output()
.expect("agent-sandbox binary failed to execute"); .expect("agent-sandbox binary failed to execute");
@@ -30,7 +30,7 @@ fn cwd_is_writable() {
#[test] #[test]
fn host_fs_is_readonly() { fn host_fs_is_readonly() {
let output = sandbox(&[]) let output = Sandbox::new(&[])
.args(["--", "bash", "-c", "touch /etc/pwned 2>&1 || echo readonly"]) .args(["--", "bash", "-c", "touch /etc/pwned 2>&1 || echo readonly"])
.output() .output()
.expect("agent-sandbox binary failed to execute"); .expect("agent-sandbox binary failed to execute");
@@ -45,7 +45,7 @@ fn host_fs_is_readonly() {
#[test] #[test]
fn ssh_dir_is_hidden() { fn ssh_dir_is_hidden() {
let output = sandbox(&[]) let output = Sandbox::new(&[])
.args(["--", "bash", "-c", "ls ~/.ssh 2>/dev/null | wc -l"]) .args(["--", "bash", "-c", "ls ~/.ssh 2>/dev/null | wc -l"])
.output() .output()
.expect("agent-sandbox binary failed to execute"); .expect("agent-sandbox binary failed to execute");
@@ -60,7 +60,7 @@ fn extra_ro_mount() {
fs::write(dir.path().join("hello.txt"), "hi").expect("failed to write test file"); fs::write(dir.path().join("hello.txt"), "hi").expect("failed to write test file");
let dir_str = dir.path().to_str().unwrap(); let dir_str = dir.path().to_str().unwrap();
let output = sandbox(&["--ro", dir_str]) let output = Sandbox::new(&["--ro", dir_str])
.args([ .args([
"--", "--",
"bash", "bash",
@@ -86,7 +86,7 @@ fn extra_rw_mount() {
let dir = TempDir::new().expect("failed to create temp dir"); let dir = TempDir::new().expect("failed to create temp dir");
let dir_str = dir.path().to_str().unwrap(); let dir_str = dir.path().to_str().unwrap();
let output = sandbox(&["--rw", dir_str]) let output = Sandbox::new(&["--rw", dir_str])
.args([ .args([
"--", "--",
"bash", "bash",
@@ -111,7 +111,7 @@ fn ro_mount_with_remapped_target() {
let src_str = dir.path().to_str().unwrap(); let src_str = dir.path().to_str().unwrap();
let target = "/tmp/agent-sandbox-remap-test"; let target = "/tmp/agent-sandbox-remap-test";
let output = sandbox(&["--ro", &format!("{src_str}:{target}")]) let output = Sandbox::new(&["--ro", &format!("{src_str}:{target}")])
.args([ .args([
"--", "--",
"bash", "bash",
@@ -138,7 +138,7 @@ fn rw_refines_ro_parent() {
let parent_str = parent.path().to_str().unwrap(); let parent_str = parent.path().to_str().unwrap();
let child_str = child.to_str().unwrap(); let child_str = child.to_str().unwrap();
let output = sandbox(&["--ro", parent_str, "--rw", child_str]) let output = Sandbox::new(&["--ro", parent_str, "--rw", child_str])
.args([ .args([
"--", "--",
"bash", "bash",
@@ -164,10 +164,11 @@ fn rw_refines_ro_parent() {
#[test] #[test]
fn blacklist_overlays_survive_tmp_bind() { fn blacklist_overlays_survive_tmp_bind() {
let mut sandbox = Sandbox::new_for_host_mutation(&["--blacklist"]);
fs::write("/tmp/ssh-sandbox-test", "secret").expect("failed to write sentinel"); fs::write("/tmp/ssh-sandbox-test", "secret").expect("failed to write sentinel");
let _cleanup = CleanupFile("/tmp/ssh-sandbox-test"); let _cleanup = CleanupFile("/tmp/ssh-sandbox-test");
let output = sandbox(&[]) let output = sandbox
.args([ .args([
"--", "--",
"bash", "bash",
@@ -190,7 +191,7 @@ fn blacklist_overlays_survive_tmp_bind() {
#[test] #[test]
fn relative_chdir_works() { fn relative_chdir_works() {
let output = sandbox(&["--chdir", "src"]) let output = Sandbox::new(&["--chdir", "src"])
.args(["--", "bash", "-c", "pwd"]) .args(["--", "bash", "-c", "pwd"])
.output() .output()
.expect("agent-sandbox binary failed to execute"); .expect("agent-sandbox binary failed to execute");
@@ -209,7 +210,7 @@ fn relative_chdir_works() {
#[test] #[test]
fn relative_rw_path_works() { fn relative_rw_path_works() {
let output = sandbox(&["--rw", "src"]) let output = Sandbox::new(&["--rw", "src"])
.args(["--", "bash", "-c", "echo ok"]) .args(["--", "bash", "-c", "echo ok"])
.output() .output()
.expect("agent-sandbox binary failed to execute"); .expect("agent-sandbox binary failed to execute");
@@ -223,7 +224,7 @@ fn relative_rw_path_works() {
#[test] #[test]
fn relative_ro_path_works() { fn relative_ro_path_works() {
let output = sandbox(&["--ro", "src"]) let output = Sandbox::new(&["--ro", "src"])
.args(["--", "bash", "-c", "echo ok"]) .args(["--", "bash", "-c", "echo ok"])
.output() .output()
.expect("agent-sandbox binary failed to execute"); .expect("agent-sandbox binary failed to execute");
@@ -237,7 +238,7 @@ fn relative_ro_path_works() {
#[test] #[test]
fn rw_missing_path_errors() { fn rw_missing_path_errors() {
let output = sandbox(&["--rw", "/nonexistent/xyz"]) let output = Sandbox::new(&["--rw", "/nonexistent/xyz"])
.args(["--", "true"]) .args(["--", "true"])
.output() .output()
.expect("agent-sandbox binary failed to execute"); .expect("agent-sandbox binary failed to execute");
@@ -259,7 +260,7 @@ fn mask_hides_directory() {
fs::write(dir.path().join("secret.txt"), "sensitive").expect("failed to write"); fs::write(dir.path().join("secret.txt"), "sensitive").expect("failed to write");
let dir_str = dir.path().canonicalize().unwrap(); let dir_str = dir.path().canonicalize().unwrap();
let output = sandbox(&["--mask", dir_str.to_str().unwrap()]) let output = Sandbox::new(&["--mask", dir_str.to_str().unwrap()])
.args([ .args([
"--", "--",
"bash", "bash",
@@ -283,7 +284,7 @@ fn mask_hides_file() {
fs::write(&file, "sensitive").expect("failed to write"); fs::write(&file, "sensitive").expect("failed to write");
let file_str = file.canonicalize().unwrap(); let file_str = file.canonicalize().unwrap();
let output = sandbox(&["--mask", file_str.to_str().unwrap()]) let output = Sandbox::new(&["--mask", file_str.to_str().unwrap()])
.args([ .args([
"--", "--",
"bash", "bash",
@@ -309,7 +310,7 @@ fn whitelist_ro_symlink_visible_at_link_path() {
std::os::unix::fs::symlink(&target, &link).expect("failed to create symlink"); std::os::unix::fs::symlink(&target, &link).expect("failed to create symlink");
let link_str = link.to_str().unwrap(); let link_str = link.to_str().unwrap();
let output = sandbox(&["--whitelist", "--ro", link_str]) let output = Sandbox::new(&["--whitelist", "--ro", link_str])
.args(["--", "cat", link_str]) .args(["--", "cat", link_str])
.output() .output()
.expect("agent-sandbox binary failed to execute"); .expect("agent-sandbox binary failed to execute");
@@ -328,7 +329,7 @@ fn mask_nonexistent_path_becomes_tmpfs() {
let fake = dir.path().join("does_not_exist"); let fake = dir.path().join("does_not_exist");
let fake_str = fake.to_str().unwrap(); let fake_str = fake.to_str().unwrap();
let output = sandbox(&["--mask", fake_str]) let output = Sandbox::new(&["--mask", fake_str])
.args([ .args([
"--", "--",
"bash", "bash",
@@ -357,7 +358,8 @@ fn blacklist_overlays_survive_absolute_var_run_symlink() {
// like --tmpfs /var/run/dbus trip bwrap's re-rooted symlink resolution. // like --tmpfs /var/run/dbus trip bwrap's re-rooted symlink resolution.
// Arch ships /var/run -> ../run (relative) so we synthesize the absolute // Arch ships /var/run -> ../run (relative) so we synthesize the absolute
// layout inside the sandbox to reproduce on any host. // layout inside the sandbox to reproduce on any host.
let mut bwrap_args = build_bwrap_command(&["--no-seccomp", "--", "true"]); let _guard = HostGlobsLock::for_scan();
let mut bwrap_args = build_bwrap_command(&["--blacklist", "--no-seccomp", "--", "true"]);
inject_absolute_var_run_symlink(&mut bwrap_args); inject_absolute_var_run_symlink(&mut bwrap_args);
let output = Command::new(&bwrap_args[0]) let output = Command::new(&bwrap_args[0])
@@ -373,7 +375,7 @@ fn blacklist_overlays_survive_absolute_var_run_symlink() {
); );
} }
fn build_bwrap_command(sandbox_args: &[&str]) -> Vec<String> { fn build_bwrap_command(sandbox_args: &[&str]) -> Vec<String> {
let output = sandbox(&["--dry-run"]) let output = Sandbox::new(&["--dry-run"])
.args(sandbox_args) .args(sandbox_args)
.output() .output()
.expect("agent-sandbox binary failed to execute"); .expect("agent-sandbox binary failed to execute");
+8 -8
View File
@@ -13,7 +13,7 @@ fn read_sid_from_stat(stat: &str) -> u32 {
} }
fn read_sid_inside_sandbox(extra_args: &[&str]) -> u32 { fn read_sid_inside_sandbox(extra_args: &[&str]) -> u32 {
let output = sandbox(extra_args) let output = Sandbox::new(extra_args)
.args(["--", "bash", "-c", "cat /proc/self/stat"]) .args(["--", "bash", "-c", "cat /proc/self/stat"])
.output() .output()
.expect("agent-sandbox binary failed to execute"); .expect("agent-sandbox binary failed to execute");
@@ -26,7 +26,7 @@ fn read_sid_current_process() -> u32 {
} }
#[test] #[test]
fn unshare_net_blocks_network() { fn unshare_net_blocks_network() {
let output = sandbox(&["--unshare-net"]) let output = Sandbox::new(&["--unshare-net"])
.args([ .args([
"--", "--",
"bash", "bash",
@@ -45,7 +45,7 @@ fn unshare_net_blocks_network() {
#[test] #[test]
fn hardened_pid_namespace() { fn hardened_pid_namespace() {
let output = sandbox(&["--hardened"]) let output = Sandbox::new(&["--hardened"])
.args(["--", "bash", "-c", "ls /proc | grep -cE '^[0-9]+$'"]) .args(["--", "bash", "-c", "ls /proc | grep -cE '^[0-9]+$'"])
.output() .output()
.expect("agent-sandbox binary failed to execute"); .expect("agent-sandbox binary failed to execute");
@@ -65,7 +65,7 @@ fn chdir_override() {
let dir = TempDir::new().expect("failed to create temp dir"); let dir = TempDir::new().expect("failed to create temp dir");
let dir_str = dir.path().to_str().unwrap(); let dir_str = dir.path().to_str().unwrap();
let output = sandbox(&["--chdir", dir_str]) let output = Sandbox::new(&["--chdir", dir_str])
.args(["--", "bash", "-c", "pwd"]) .args(["--", "bash", "-c", "pwd"])
.output() .output()
.expect("agent-sandbox binary failed to execute"); .expect("agent-sandbox binary failed to execute");
@@ -82,7 +82,7 @@ fn chdir_under_hardened_tmp() {
let dir = TempDir::new().expect("failed to create temp dir"); let dir = TempDir::new().expect("failed to create temp dir");
let dir_str = dir.path().to_str().unwrap(); let dir_str = dir.path().to_str().unwrap();
let output = sandbox(&["--hardened", "--chdir", dir_str]) let output = Sandbox::new(&["--hardened", "--chdir", dir_str])
.args(["--", "bash", "-c", "pwd && touch ./ok && echo done"]) .args(["--", "bash", "-c", "pwd && touch ./ok && echo done"])
.output() .output()
.expect("agent-sandbox binary failed to execute"); .expect("agent-sandbox binary failed to execute");
@@ -106,12 +106,12 @@ fn hardened_isolates_sid() {
} }
#[test] #[test]
fn default_mode_shares_session() { fn blacklist_mode_shares_session() {
let inner_sid = read_sid_inside_sandbox(&[]); let inner_sid = read_sid_inside_sandbox(&["--blacklist"]);
let outer_sid = read_sid_current_process(); let outer_sid = read_sid_current_process();
assert_eq!( assert_eq!(
inner_sid, outer_sid, inner_sid, outer_sid,
"default-mode sandbox should share the session ID (got {inner_sid} != {outer_sid})" "blacklist-mode sandbox should share the session ID (got {inner_sid} != {outer_sid})"
); );
} }
+8 -8
View File
@@ -2,7 +2,7 @@ use crate::common::*;
#[test] #[test]
fn seccomp_on_by_default_blocks_unshare() { fn seccomp_on_by_default_blocks_unshare() {
let output = sandbox(&[]) let output = Sandbox::new(&[])
.args(["--", "unshare", "--user", "--map-root-user", "/bin/true"]) .args(["--", "unshare", "--user", "--map-root-user", "/bin/true"])
.output() .output()
.expect("agent-sandbox binary failed to execute"); .expect("agent-sandbox binary failed to execute");
@@ -15,7 +15,7 @@ fn seccomp_on_by_default_blocks_unshare() {
#[test] #[test]
fn seccomp_off_allows_blocked_syscall() { fn seccomp_off_allows_blocked_syscall() {
let output = sandbox(&["--no-seccomp"]) let output = Sandbox::new(&["--no-seccomp"])
.args(["--", "unshare", "--user", "--map-root-user", "/bin/true"]) .args(["--", "unshare", "--user", "--map-root-user", "/bin/true"])
.output() .output()
.expect("agent-sandbox binary failed to execute"); .expect("agent-sandbox binary failed to execute");
@@ -29,7 +29,7 @@ fn seccomp_off_allows_blocked_syscall() {
#[test] #[test]
fn seccomp_dry_run_emits_seccomp_arg() { fn seccomp_dry_run_emits_seccomp_arg() {
let output = sandbox(&["--dry-run"]) let output = Sandbox::new(&["--dry-run"])
.args(["--", "/bin/true"]) .args(["--", "/bin/true"])
.output() .output()
.expect("agent-sandbox binary failed to execute"); .expect("agent-sandbox binary failed to execute");
@@ -43,7 +43,7 @@ fn seccomp_dry_run_emits_seccomp_arg() {
#[test] #[test]
fn seccomp_dry_run_no_seccomp_omits_arg() { fn seccomp_dry_run_no_seccomp_omits_arg() {
let output = sandbox(&["--dry-run", "--no-seccomp"]) let output = Sandbox::new(&["--dry-run", "--no-seccomp"])
.args(["--", "/bin/true"]) .args(["--", "/bin/true"])
.output() .output()
.expect("agent-sandbox binary failed to execute"); .expect("agent-sandbox binary failed to execute");
@@ -57,7 +57,7 @@ fn seccomp_dry_run_no_seccomp_omits_arg() {
#[test] #[test]
fn seccomp_normal_workload_succeeds() { fn seccomp_normal_workload_succeeds() {
let output = sandbox(&[]) let output = Sandbox::new(&[])
.args(["--", "bash", "-c", "ls /etc > /dev/null && date"]) .args(["--", "bash", "-c", "ls /etc > /dev/null && date"])
.output() .output()
.expect("agent-sandbox binary failed to execute"); .expect("agent-sandbox binary failed to execute");
@@ -72,7 +72,7 @@ fn seccomp_normal_workload_succeeds() {
fn seccomp_bash_pthread_fallback_works() { fn seccomp_bash_pthread_fallback_works() {
// Verifies the ENOSYS-not-EPERM choice for clone3 doesn't break libc's // Verifies the ENOSYS-not-EPERM choice for clone3 doesn't break libc's
// clone3 -> clone fallback path that bash uses internally. // clone3 -> clone fallback path that bash uses internally.
let output = sandbox(&[]) let output = Sandbox::new(&[])
.args(["--", "bash", "-c", "true"]) .args(["--", "bash", "-c", "true"])
.output() .output()
.expect("agent-sandbox binary failed to execute"); .expect("agent-sandbox binary failed to execute");
@@ -92,7 +92,7 @@ fn seccomp_blocks_tiocsti() {
// On kernels >= 6.2 with CONFIG_LEGACY_TIOCSTI=n, the kernel blocks TIOCSTI // On kernels >= 6.2 with CONFIG_LEGACY_TIOCSTI=n, the kernel blocks TIOCSTI
// before seccomp sees it. We test with --no-seccomp first to detect that and // before seccomp sees it. We test with --no-seccomp first to detect that and
// skip, so the test only asserts our filter's behaviour. // skip, so the test only asserts our filter's behaviour.
let baseline = sandbox(&["--no-seccomp"]) let baseline = Sandbox::new(&["--no-seccomp"])
.args([ .args([
"--", "--",
"python3", "python3",
@@ -107,7 +107,7 @@ fn seccomp_blocks_tiocsti() {
return; return;
} }
let output = sandbox(&[]) let output = Sandbox::new(&[])
.args([ .args([
"--", "--",
"python3", "python3",
+1 -1
View File
@@ -405,7 +405,7 @@ fn build_cli_command_overrides_config() {
#[test] #[test]
fn build_no_file_config() { fn build_no_file_config() {
let config = build(args_with_command(), None).unwrap(); let config = build(args_with_command(), None).unwrap();
assert!(matches!(config.mode, SandboxMode::Blacklist)); assert!(matches!(config.mode, SandboxMode::Whitelist));
assert!(!config.hardened); assert!(!config.hardened);
} }