231 lines
6.3 KiB
Rust
231 lines
6.3 KiB
Rust
use std::fs;
|
|
use std::process::Command;
|
|
|
|
use tempfile::TempDir;
|
|
|
|
fn sandbox(extra_args: &[&str]) -> Command {
|
|
let mut cmd = Command::new(env!("CARGO_BIN_EXE_agent-sandbox"));
|
|
cmd.args(extra_args);
|
|
cmd
|
|
}
|
|
|
|
struct CleanupFile(&'static str);
|
|
|
|
impl Drop for CleanupFile {
|
|
fn drop(&mut self) {
|
|
let _ = fs::remove_file(self.0);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn cwd_is_writable() {
|
|
let _cleanup = CleanupFile("./sandbox_canary");
|
|
|
|
let output = sandbox(&[])
|
|
.args(["--", "bash", "-c", "touch ./sandbox_canary && echo ok"])
|
|
.output()
|
|
.expect("agent-sandbox binary failed to execute");
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
assert!(
|
|
stdout.contains("ok"),
|
|
"expected 'ok' in stdout, got: {stdout}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn host_fs_is_readonly() {
|
|
let output = sandbox(&[])
|
|
.args(["--", "bash", "-c", "touch /etc/pwned 2>&1 || echo readonly"])
|
|
.output()
|
|
.expect("agent-sandbox binary failed to execute");
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
assert!(
|
|
stdout.contains("readonly"),
|
|
"expected 'readonly' in stdout, got: {stdout}"
|
|
);
|
|
assert!(!std::path::Path::new("/etc/pwned").exists());
|
|
}
|
|
|
|
#[test]
|
|
fn ssh_dir_is_hidden() {
|
|
let output = sandbox(&[])
|
|
.args(["--", "bash", "-c", "ls ~/.ssh 2>/dev/null | wc -l"])
|
|
.output()
|
|
.expect("agent-sandbox binary failed to execute");
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
assert_eq!(stdout, "0", "expected empty ~/.ssh, got {stdout} entries");
|
|
}
|
|
|
|
#[test]
|
|
fn no_net_blocks_network() {
|
|
let output = sandbox(&["--no-net"])
|
|
.args([
|
|
"--",
|
|
"bash",
|
|
"-c",
|
|
"curl -s --max-time 2 http://1.1.1.1 2>&1; echo $?",
|
|
])
|
|
.output()
|
|
.expect("agent-sandbox binary failed to execute");
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
assert!(
|
|
!stdout.trim().ends_with("0"),
|
|
"expected curl to fail, got: {stdout}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn hardened_pid_namespace() {
|
|
let output = sandbox(&["--hardened"])
|
|
.args(["--", "bash", "-c", "ls /proc | grep -cE '^[0-9]+$'"])
|
|
.output()
|
|
.expect("agent-sandbox binary failed to execute");
|
|
|
|
let count: u32 = String::from_utf8_lossy(&output.stdout)
|
|
.trim()
|
|
.parse()
|
|
.unwrap_or(999);
|
|
assert!(
|
|
count < 10,
|
|
"expected isolated PID namespace with few PIDs, got {count}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn whitelist_hides_home_contents() {
|
|
let output = sandbox(&["--whitelist"])
|
|
.args(["--", "bash", "-c", "ls ~/Documents 2>&1 || echo hidden"])
|
|
.output()
|
|
.expect("agent-sandbox binary failed to execute");
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
assert!(
|
|
stdout.contains("hidden"),
|
|
"expected ~/Documents to be hidden, got: {stdout}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn extra_ro_mount() {
|
|
let dir = TempDir::new().expect("failed to create temp dir");
|
|
fs::write(dir.path().join("hello.txt"), "hi").expect("failed to write test file");
|
|
let dir_str = dir.path().to_str().unwrap();
|
|
|
|
let output = sandbox(&["--ro", dir_str])
|
|
.args([
|
|
"--",
|
|
"bash",
|
|
"-c",
|
|
&format!("cat {dir_str}/hello.txt && touch {dir_str}/new 2>&1 || echo readonly"),
|
|
])
|
|
.output()
|
|
.expect("agent-sandbox binary failed to execute");
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
assert!(
|
|
stdout.contains("hi"),
|
|
"expected file content 'hi', got: {stdout}"
|
|
);
|
|
assert!(
|
|
stdout.contains("readonly"),
|
|
"expected ro mount to block writes, got: {stdout}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn extra_rw_mount() {
|
|
let dir = TempDir::new().expect("failed to create temp dir");
|
|
let dir_str = dir.path().to_str().unwrap();
|
|
|
|
let output = sandbox(&["--rw", dir_str])
|
|
.args([
|
|
"--",
|
|
"bash",
|
|
"-c",
|
|
&format!("touch {dir_str}/canary && echo ok"),
|
|
])
|
|
.output()
|
|
.expect("agent-sandbox binary failed to execute");
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
assert!(stdout.contains("ok"), "expected 'ok', got: {stdout}");
|
|
assert!(
|
|
dir.path().join("canary").exists(),
|
|
"canary file should exist on host after rw mount"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn chdir_override() {
|
|
let dir = TempDir::new().expect("failed to create temp dir");
|
|
let dir_str = dir.path().to_str().unwrap();
|
|
|
|
let output = sandbox(&["--chdir", dir_str])
|
|
.args(["--", "bash", "-c", "pwd"])
|
|
.output()
|
|
.expect("agent-sandbox binary failed to execute");
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
assert_eq!(
|
|
stdout, dir_str,
|
|
"expected cwd to be {dir_str}, got: {stdout}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn chdir_under_hardened_tmp() {
|
|
let dir = TempDir::new().expect("failed to create temp dir");
|
|
let dir_str = dir.path().to_str().unwrap();
|
|
|
|
let output = sandbox(&["--hardened", "--chdir", dir_str])
|
|
.args(["--", "bash", "-c", "pwd && touch ./ok && echo done"])
|
|
.output()
|
|
.expect("agent-sandbox binary failed to execute");
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
assert!(
|
|
stdout.contains("done"),
|
|
"expected chdir under /tmp to work with --hardened, got: {stdout}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dry_run_prints_and_exits() {
|
|
let output = sandbox(&["--dry-run"])
|
|
.args(["--", "bash", "-c", "exit 42"])
|
|
.output()
|
|
.expect("agent-sandbox binary failed to execute");
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
assert!(
|
|
stdout.contains("bwrap"),
|
|
"expected bwrap command in dry-run output, got: {stdout}"
|
|
);
|
|
assert!(
|
|
output.status.success(),
|
|
"dry-run should exit 0, not 42 from the inner command"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rw_missing_path_errors() {
|
|
let output = sandbox(&["--rw", "/nonexistent/xyz"])
|
|
.args(["--", "true"])
|
|
.output()
|
|
.expect("agent-sandbox binary failed to execute");
|
|
|
|
assert!(
|
|
!output.status.success(),
|
|
"expected non-zero exit for missing --rw path"
|
|
);
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
assert!(
|
|
stderr.contains("/nonexistent/xyz"),
|
|
"expected path in error message, got: {stderr}"
|
|
);
|
|
}
|