use std::process::Command;
fn sandlock_bin() -> Command {
let cmd = Command::new(env!("CARGO_BIN_EXE_sandlock"));
cmd
}
#[test]
fn test_check_command() {
let output = sandlock_bin()
.args(["check"])
.output()
.expect("failed to run sandlock check");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Landlock"), "Should mention Landlock");
}
#[test]
fn test_run_echo() {
let output = sandlock_bin()
.args(["run", "-r", "/usr", "-r", "/lib", "-r", "/lib64", "-r", "/bin", "-r", "/etc", "--", "echo", "test123"])
.output()
.expect("failed to run sandlock");
assert!(output.status.success(), "Exit status: {:?}, stderr: {}", output.status, String::from_utf8_lossy(&output.stderr));
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("test123"));
}
#[test]
fn test_run_exit_code() {
let output = sandlock_bin()
.args(["run", "-r", "/usr", "-r", "/lib", "-r", "/lib64", "-r", "/bin", "--", "sh", "-c", "exit 42"])
.output()
.expect("failed to run");
assert_eq!(output.status.code(), Some(42));
}
#[test]
fn test_run_denied_path() {
let output = sandlock_bin()
.args(["run", "-r", "/usr", "-r", "/lib", "-r", "/lib64", "-r", "/bin", "--", "cat", "/etc/group"])
.output()
.expect("failed to run");
assert!(!output.status.success(), "Should fail without /etc readable");
}
#[test]
fn test_run_hostname_virtualized() {
let output = sandlock_bin()
.args(["run", "--name", "mybox", "-r", "/usr", "-r", "/lib", "-r", "/lib64", "-r", "/bin", "--", "cat", "/etc/hostname"])
.output()
.expect("failed to run");
assert!(output.status.success(), "virtualized /etc/hostname should be readable: stderr={}", String::from_utf8_lossy(&output.stderr));
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(stdout.trim(), "mybox", "expected virtual hostname, got {:?}", stdout.trim());
}
#[test]
fn test_profile_list_empty() {
let output = sandlock_bin()
.args(["profile", "list"])
.output()
.expect("failed to run");
assert!(output.status.success());
}
#[test]
fn test_no_args_shows_help() {
let output = sandlock_bin()
.output()
.expect("failed to run");
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("Usage") || stderr.contains("sandlock"));
}
#[test]
fn test_cpu_cores_flag_accepted() {
let output = sandlock_bin()
.args(["run", "--help"])
.output()
.expect("failed to run sandlock");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("--cpu-cores"), "help should mention --cpu-cores");
}
#[test]
fn test_status_fd_flag_accepted() {
let bin = env!("CARGO_BIN_EXE_sandlock");
let output = std::process::Command::new(bin)
.args(["run", "--help"])
.output()
.expect("failed to run sandlock");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("--status-fd"), "help should mention --status-fd");
}
#[test]
fn test_time_start_fakes_year() {
let output = sandlock_bin()
.args([
"run",
"-r", "/usr",
"-r", "/lib",
"-r", "/lib64",
"-r", "/bin",
"-r", "/etc",
"--time-start", "2000-06-15T00:00:00Z",
"--",
"date", "+%Y",
])
.output()
.expect("failed to run sandlock with --time-start");
assert!(
output.status.success(),
"sandlock exited with failure: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.trim() == "2000",
"Expected year 2000, got: {:?}",
stdout.trim()
);
}
#[test]
fn test_no_supervisor_echo() {
let output = sandlock_bin()
.args(["run", "--no-supervisor", "-r", "/usr", "-r", "/lib", "-r", "/lib64", "-r", "/bin", "-r", "/etc", "--", "echo", "no-supervisor-test"])
.output()
.expect("failed to run sandlock --no-supervisor");
assert!(output.status.success(), "Exit status: {:?}, stderr: {}", output.status, String::from_utf8_lossy(&output.stderr));
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("no-supervisor-test"));
}
#[test]
fn test_no_supervisor_blocks_denied_path() {
let output = sandlock_bin()
.args(["run", "--no-supervisor", "-r", "/usr", "-r", "/lib", "-r", "/lib64", "-r", "/bin", "--", "cat", "/etc/hostname"])
.output()
.expect("failed to run");
assert!(!output.status.success(), "Should fail without /etc readable");
}
#[test]
fn test_no_supervisor_rejects_fs_deny() {
let output = sandlock_bin()
.args(["run", "--no-supervisor", "--fs-deny", "/etc/hostname", "-r", "/usr", "--", "echo", "hi"])
.output()
.expect("failed to run");
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("--fs-deny"), "stderr: {}", stderr);
}
#[test]
fn test_no_supervisor_rejects_net_deny() {
let output = sandlock_bin()
.args(["run", "--no-supervisor", "--net-deny", "10.0.0.0/8", "--", "/bin/true"])
.output()
.expect("failed to run");
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("--net-deny"), "stderr: {}", stderr);
}
#[test]
fn test_net_allow_and_net_deny_are_mutually_exclusive() {
let output = sandlock_bin()
.args(["run", "--net-allow", "github.com:443", "--net-deny", "10.0.0.0/8", "--", "/bin/true"])
.output()
.expect("failed to run");
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("mutually exclusive"), "stderr: {}", stderr);
}
#[test]
fn test_no_supervisor_rejects_incompatible_flags() {
let output = sandlock_bin()
.args(["run", "--no-supervisor", "--max-memory", "100M", "-r", "/usr", "--", "echo", "hi"])
.output()
.expect("failed to run");
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("--no-supervisor is incompatible with"), "stderr: {}", stderr);
}
#[test]
fn test_no_supervisor_writable_path() {
let output = sandlock_bin()
.args(["run", "--no-supervisor", "-r", "/usr", "-r", "/lib", "-r", "/lib64", "-r", "/bin", "-w", "/tmp", "--",
"sh", "-c", "echo no-supervisor-write > /tmp/sandlock-no-supervisor-test && cat /tmp/sandlock-no-supervisor-test"])
.output()
.expect("failed to run");
assert!(output.status.success(), "Exit status: {:?}, stderr: {}", output.status, String::from_utf8_lossy(&output.stderr));
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("no-supervisor-write"));
let _ = std::fs::remove_file("/tmp/sandlock-no-supervisor-test");
}
#[test]
fn test_no_supervisor_nested_sandbox() {
let sandlock_path = env!("CARGO_BIN_EXE_sandlock");
let sandlock_dir = std::path::Path::new(sandlock_path).parent().unwrap().to_str().unwrap();
let output = sandlock_bin()
.args(["run", "--no-supervisor",
"-r", "/usr", "-r", "/lib", "-r", "/lib64", "-r", "/bin", "-r", "/etc",
"-r", "/proc", "-r", "/dev", "-w", "/tmp",
"-r", sandlock_dir,
"--", sandlock_path, "run",
"-r", "/usr", "-r", "/lib", "-r", "/lib64", "-r", "/bin", "-r", "/etc",
"--", "echo", "nested-works"])
.output()
.expect("failed to run nested sandbox");
assert!(output.status.success(), "Exit status: {:?}, stderr: {}", output.status, String::from_utf8_lossy(&output.stderr));
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("nested-works"));
}
#[test]
fn test_no_supervisor_exit_code() {
let output = sandlock_bin()
.args(["run", "--no-supervisor", "-r", "/usr", "-r", "/lib", "-r", "/lib64", "-r", "/bin", "--", "sh", "-c", "exit 42"])
.output()
.expect("failed to run");
assert_eq!(output.status.code(), Some(42));
}
#[test]
fn test_cow_commit_runs_on_cli_exit() {
let workdir = tempfile::tempdir().expect("tempdir");
let sentinel = workdir.path().join("sentinel.txt");
assert!(!sentinel.exists(), "precondition: sentinel should not exist");
let cmd = format!("echo committed > {}", sentinel.display());
let output = sandlock_bin()
.args([
"run",
"-r", "/usr", "-r", "/lib", "-r", "/lib64", "-r", "/bin", "-r", "/etc",
"-w", workdir.path().to_str().unwrap(),
"--workdir", workdir.path().to_str().unwrap(),
"--", "sh", "-c", &cmd,
])
.output()
.expect("failed to run sandlock");
assert!(
output.status.success(),
"sandlock exit={:?}, stderr: {}",
output.status.code(),
String::from_utf8_lossy(&output.stderr),
);
assert!(
sentinel.exists(),
"COW commit did not run on CLI exit: {} missing. \
Was process::exit called instead of returning the exit code?",
sentinel.display(),
);
let contents = std::fs::read_to_string(&sentinel).unwrap_or_default();
assert_eq!(contents.trim(), "committed");
}
#[test]
fn test_uid_mapping_fakes_root() {
let output = sandlock_bin()
.args([
"run",
"--uid", "0",
"-r", "/usr", "-r", "/lib", "-r", "/lib64", "-r", "/bin", "-r", "/etc",
"--", "id", "-u",
])
.output()
.expect("failed to run sandlock");
assert!(
output.status.success(),
"sandlock --uid 0 failed: stderr={}",
String::from_utf8_lossy(&output.stderr),
);
assert_eq!(
String::from_utf8_lossy(&output.stdout).trim(),
"0",
"expected UID 0 inside sandbox; got stdout={:?}",
String::from_utf8_lossy(&output.stdout),
);
}
#[test]
fn test_uid_mapping_arbitrary_uid() {
let output = sandlock_bin()
.args([
"run",
"--uid", "1234",
"-r", "/usr", "-r", "/lib", "-r", "/lib64", "-r", "/bin", "-r", "/etc",
"--", "id", "-u",
])
.output()
.expect("failed to run sandlock");
assert!(
output.status.success(),
"sandlock --uid 1234 failed: stderr={}",
String::from_utf8_lossy(&output.stderr),
);
assert_eq!(
String::from_utf8_lossy(&output.stdout).trim(),
"1234",
);
}