Organize test code better

This commit is contained in:
2026-04-25 15:10:42 +02:00
parent 0ea83b2af0
commit 7f9b21ef4f
16 changed files with 2852 additions and 2830 deletions
+5
View File
@@ -2,6 +2,7 @@
name = "agent-sandbox" name = "agent-sandbox"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
autotests = false
[lib] [lib]
name = "agent_sandbox" name = "agent_sandbox"
@@ -11,6 +12,10 @@ path = "src/lib.rs"
name = "agent-sandbox" name = "agent-sandbox"
path = "src/main.rs" path = "src/main.rs"
[[test]]
name = "e2e"
path = "tests/e2e/main.rs"
[dependencies] [dependencies]
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
glob = "0.3" glob = "0.3"
+2 -1239
View File
File diff suppressed because it is too large Load Diff
+2 -39
View File
@@ -164,42 +164,5 @@ const BLACKLIST_DROP_SUFFIXES: &[&str] = &[
]; ];
#[cfg(test)] #[cfg(test)]
mod tests { #[path = "../tests/unit/env.rs"]
use super::*; mod tests;
#[test]
fn keepenv_emits_setenv_for_present_key() {
let parent = vec![("XDG_RUNTIME_DIR".into(), "/run/user/1000".into())];
let args = keepenv_args(&["XDG_RUNTIME_DIR".into()], &parent);
assert_eq!(args, vec!["--setenv", "XDG_RUNTIME_DIR", "/run/user/1000"]);
}
#[test]
fn keepenv_skips_absent_keys() {
let parent = vec![("HOME".into(), "/home/me".into())];
let args = keepenv_args(&["XDG_RUNTIME_DIR".into()], &parent);
assert!(args.is_empty());
}
#[test]
fn keepenv_preserves_caller_key_order() {
let parent = vec![
("B".into(), "2".into()),
("A".into(), "1".into()),
("C".into(), "3".into()),
];
let args = keepenv_args(&["A".into(), "B".into(), "C".into()], &parent);
assert_eq!(
args,
vec![
"--setenv", "A", "1", "--setenv", "B", "2", "--setenv", "C", "3"
]
);
}
#[test]
fn keepenv_empty_keys_yields_nothing() {
let parent = vec![("A".into(), "1".into())];
assert!(keepenv_args(&[], &parent).is_empty());
}
}
+2 -78
View File
@@ -164,81 +164,5 @@ fn serialize(program: &[sock_filter]) -> Vec<u8> {
} }
#[cfg(test)] #[cfg(test)]
mod tests { #[path = "../tests/unit/seccomp.rs"]
use super::*; mod tests;
#[test]
fn builds_on_supported_arch() {
let bytes = build_program_bytes().expect("seccomp program should build");
assert!(!bytes.is_empty(), "serialized BPF program is empty");
assert_eq!(bytes.len() % 8, 0, "BPF byte stream must be 8-byte aligned");
}
#[test]
fn allowlist_contains_essential_syscalls() {
for needed in &[
"read",
"write",
"openat",
"close",
"execve",
"exit_group",
"mmap",
"brk",
"clone",
] {
assert!(
ALLOWED_SYSCALLS.contains(needed),
"allowlist missing essential syscall: {needed}"
);
}
}
#[test]
fn allowlist_excludes_dangerous_syscalls() {
for denied in &[
"bpf",
"perf_event_open",
"userfaultfd",
"kexec_load",
"kexec_file_load",
"init_module",
"finit_module",
"delete_module",
"mount",
"umount",
"umount2",
"unshare",
"setns",
"pivot_root",
"ptrace",
"process_vm_readv",
"process_vm_writev",
"keyctl",
"personality",
"clone3",
"io_uring_setup",
"io_uring_register",
"io_uring_enter",
"fanotify_init",
"fanotify_mark",
"open_by_handle_at",
"name_to_handle_at",
"fsopen",
"fsconfig",
"fsmount",
"fspick",
"open_tree",
"move_mount",
"mount_setattr",
"reboot",
"swapon",
"swapoff",
] {
assert!(
!ALLOWED_SYSCALLS.contains(denied),
"allowlist must not contain dangerous syscall: {denied}"
);
}
}
}
+252
View File
@@ -0,0 +1,252 @@
use crate::common::*;
#[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 dry_run_output_is_copy_pasteable_shell() {
let dry = sandbox(&["--dry-run"])
.args(["--", "bash", "-c", "echo $HOME"])
.output()
.expect("agent-sandbox binary failed to execute");
let dry_cmd = String::from_utf8_lossy(&dry.stdout).trim().to_string();
let args = shlex::split(&dry_cmd).expect("dry-run output is not valid shell");
assert_eq!(args[0], "bwrap");
assert_eq!(args[args.len() - 1], "echo $HOME");
assert_eq!(args[args.len() - 2], "-c");
}
#[test]
fn empty_home_rejected() {
let output = sandbox(&[])
.env("HOME", "")
.args(["--", "true"])
.output()
.expect("agent-sandbox binary failed to execute");
assert!(
!output.status.success(),
"expected failure with empty HOME, but got success"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.to_lowercase().contains("home"),
"expected error mentioning HOME, got: {stderr}"
);
}
#[test]
fn config_missing_file_errors() {
let output = sandbox_withconfig(&["--config", "/nonexistent/config.toml"])
.args(["--", "true"])
.output()
.expect("failed to execute");
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("/nonexistent/config.toml"),
"expected config path in error, got: {stderr}"
);
}
#[test]
fn config_invalid_toml_errors() {
let cfg = ConfigFile::new("not valid {{{{ toml");
let output = sandbox_withconfig(&["--config", &cfg])
.args(["--", "true"])
.output()
.expect("failed to execute");
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("cannot parse"),
"expected parse error, got: {stderr}"
);
}
#[test]
fn config_unknown_key_errors() {
let cfg = ConfigFile::new("hardened = true\nbogus = \"nope\"\n");
let output = sandbox_withconfig(&["--config", &cfg])
.args(["--", "true"])
.output()
.expect("failed to execute");
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("unknown config key"),
"expected unknown key error, got: {stderr}"
);
}
#[test]
fn bwrap_arg_setenv_passes_through() {
let output = sandbox(&["--bwrap-arg", "--setenv MYVAR hello"])
.args(["--", "bash", "-c", "echo $MYVAR"])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
assert_eq!(
stdout, "hello",
"expected --bwrap-arg to pass --setenv through to bwrap, got: {stdout}"
);
}
#[test]
fn config_entrypoint_appends_passthrough_args() {
let cfg = ConfigFile::new(
r#"
[profile.test]
entrypoint = ["bash", "-c"]
"#,
);
let output = sandbox_withconfig(&["--config", &cfg, "--profile", "test"])
.args(["--", "echo entrypoint-works"])
.output()
.expect("failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
assert_eq!(
stdout, "entrypoint-works",
"expected passthrough args appended to entrypoint, got: {stdout}"
);
}
#[test]
fn config_entrypoint_falls_back_to_command_defaults() {
let cfg = ConfigFile::new(
r#"
[profile.test]
entrypoint = ["bash", "-c"]
command = ["echo default-args"]
"#,
);
let output = sandbox_withconfig(&["--config", &cfg, "--profile", "test"])
.output()
.expect("failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
assert_eq!(
stdout, "default-args",
"expected command defaults when no passthrough args, got: {stdout}"
);
}
#[test]
fn config_entrypoint_alone_without_command_or_passthrough() {
let cfg = ConfigFile::new(
r#"
[profile.test]
entrypoint = ["bash", "-c", "echo entrypoint-only"]
"#,
);
let output = sandbox_withconfig(&["--config", &cfg, "--profile", "test"])
.output()
.expect("failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
assert_eq!(
stdout, "entrypoint-only",
"expected entrypoint to run on its own, got: {stdout}"
);
}
#[test]
fn cli_entrypoint_appends_passthrough_args() {
let output = sandbox(&["--entrypoint", "bash"])
.args(["--", "-c", "echo cli-entrypoint-works"])
.output()
.expect("failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
assert_eq!(
stdout, "cli-entrypoint-works",
"expected --entrypoint to receive trailing args, got: {stdout}"
);
}
#[test]
fn cli_entrypoint_overrides_config_entrypoint() {
let cfg = ConfigFile::new(
r#"
entrypoint = ["/bin/false"]
"#,
);
let output = sandbox_withconfig(&["--config", &cfg, "--entrypoint", "bash"])
.args(["--", "-c", "echo override-works"])
.output()
.expect("failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
assert_eq!(
stdout, "override-works",
"expected CLI --entrypoint to override config entrypoint, got: {stdout}"
);
}
#[test]
fn config_command_alone_without_passthrough() {
let cfg = ConfigFile::new(
r#"
[profile.test]
command = ["bash", "-c", "echo command-only"]
"#,
);
let output = sandbox_withconfig(&["--config", &cfg, "--profile", "test"])
.output()
.expect("failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
assert_eq!(
stdout, "command-only",
"expected config command to run on its own, got: {stdout}"
);
}
#[test]
fn config_command_replaced_by_passthrough() {
let cfg = ConfigFile::new(
r#"
[profile.test]
command = ["bash", "-c", "echo should-not-see-this"]
"#,
);
let output = sandbox_withconfig(&["--config", &cfg, "--profile", "test"])
.args(["--", "bash", "-c", "echo replaced"])
.output()
.expect("failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
assert_eq!(
stdout, "replaced",
"expected passthrough to replace config command, got: {stdout}"
);
}
+40
View File
@@ -0,0 +1,40 @@
use std::fs;
use std::process::Command;
use tempfile::TempDir;
pub fn sandbox_withconfig(extra_args: &[&str]) -> Command {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_agent-sandbox"));
cmd.args(extra_args);
cmd
}
pub fn sandbox(extra_args: &[&str]) -> Command {
let mut cmd = sandbox_withconfig(&["--no-config"]);
cmd.args(extra_args);
cmd
}
pub struct ConfigFile {
_dir: TempDir,
path: String,
}
impl ConfigFile {
pub fn new(content: &str) -> Self {
let dir = TempDir::new().unwrap();
let path = dir.path().join("config.toml");
fs::write(&path, content).expect("failed to write config");
Self {
_dir: dir,
path: path.to_str().unwrap().to_string(),
}
}
}
impl std::ops::Deref for ConfigFile {
type Target = str;
fn deref(&self) -> &str {
&self.path
}
}
+359
View File
@@ -0,0 +1,359 @@
use crate::common::*;
fn printenv_inside(args: &[&str], vars: &[(&str, &str)], query: &[&str]) -> String {
let script = query
.iter()
.map(|v| format!("printenv {v} || echo MISSING:{v}"))
.collect::<Vec<_>>()
.join("; ");
let mut cmd = sandbox(args);
for (k, v) in vars {
cmd.env(k, v);
}
let output = cmd
.args(["--", "bash", "-c", &script])
.output()
.expect("agent-sandbox binary failed to execute");
String::from_utf8_lossy(&output.stdout).into_owned()
}
#[test]
fn whitelist_keeps_identity_and_terminal_vars() {
let stdout = printenv_inside(
&["--whitelist"],
&[("TERM", "xterm-test"), ("LANG", "C.UTF-8")],
&["HOME", "PATH", "TERM", "LANG"],
);
assert!(!stdout.contains("MISSING:HOME"), "HOME stripped: {stdout}");
assert!(!stdout.contains("MISSING:PATH"), "PATH stripped: {stdout}");
assert!(stdout.contains("xterm-test"), "TERM stripped: {stdout}");
assert!(stdout.contains("C.UTF-8"), "LANG stripped: {stdout}");
}
#[test]
fn whitelist_strips_arbitrary_host_var() {
let stdout = printenv_inside(
&["--whitelist"],
&[("SOME_RANDOM_NOISE_VAR", "leak")],
&["SOME_RANDOM_NOISE_VAR"],
);
assert!(
stdout.contains("MISSING:SOME_RANDOM_NOISE_VAR"),
"expected arbitrary host var to be stripped, got: {stdout}"
);
assert!(!stdout.contains("leak"));
}
#[test]
fn whitelist_keeps_vendor_prefixes() {
let stdout = printenv_inside(
&["--whitelist"],
&[
("CLAUDE_FOO", "claude-val"),
("ANTHROPIC_MODEL", "anthropic-val"),
("OPENAI_API_KEY", "openai-val"),
("CODEX_FOO", "codex-val"),
("GEMINI_API_KEY", "gemini-val"),
("OTEL_SERVICE_NAME", "otel-val"),
],
&[
"CLAUDE_FOO",
"ANTHROPIC_MODEL",
"OPENAI_API_KEY",
"CODEX_FOO",
"GEMINI_API_KEY",
"OTEL_SERVICE_NAME",
],
);
for expected in [
"claude-val",
"anthropic-val",
"openai-val",
"codex-val",
"gemini-val",
"otel-val",
] {
assert!(
stdout.contains(expected),
"expected {expected} in output, got: {stdout}"
);
}
assert!(!stdout.contains("MISSING:"), "unexpected strip: {stdout}");
}
#[test]
fn whitelist_keeps_lc_prefix() {
let stdout = printenv_inside(
&["--whitelist"],
&[("LC_TIME", "en_US.UTF-8")],
&["LC_TIME"],
);
assert!(stdout.contains("en_US.UTF-8"), "LC_TIME missing: {stdout}");
}
#[test]
fn whitelist_keeps_non_gui_xdg_vars() {
let stdout = printenv_inside(
&["--whitelist"],
&[
("XDG_CONFIG_HOME", "/cfg"),
("XDG_DATA_HOME", "/data"),
("XDG_CACHE_HOME", "/cache"),
("XDG_STATE_HOME", "/state"),
("XDG_CONFIG_DIRS", "/etc/xdg"),
("XDG_DATA_DIRS", "/usr/share"),
],
&[
"XDG_CONFIG_HOME",
"XDG_DATA_HOME",
"XDG_CACHE_HOME",
"XDG_STATE_HOME",
"XDG_CONFIG_DIRS",
"XDG_DATA_DIRS",
],
);
assert!(
!stdout.contains("MISSING:"),
"XDG non-GUI stripped: {stdout}"
);
}
#[test]
fn whitelist_strips_gui_xdg_vars() {
let stdout = printenv_inside(
&["--whitelist"],
&[
("XDG_RUNTIME_DIR", "/run/user/1000"),
("XDG_SESSION_ID", "1"),
("XDG_CURRENT_DESKTOP", "KDE"),
("XDG_SEAT", "seat0"),
],
&[
"XDG_RUNTIME_DIR",
"XDG_SESSION_ID",
"XDG_CURRENT_DESKTOP",
"XDG_SEAT",
],
);
for var in [
"XDG_RUNTIME_DIR",
"XDG_SESSION_ID",
"XDG_CURRENT_DESKTOP",
"XDG_SEAT",
] {
assert!(
stdout.contains(&format!("MISSING:{var}")),
"expected {var} stripped, got: {stdout}"
);
}
}
#[test]
fn whitelist_strips_dbus_vars() {
let stdout = printenv_inside(
&["--whitelist"],
&[
("DBUS_SESSION_BUS_ADDRESS", "unix:path=/foo"),
("DBUS_SYSTEM_BUS_ADDRESS", "unix:path=/bar"),
],
&["DBUS_SESSION_BUS_ADDRESS", "DBUS_SYSTEM_BUS_ADDRESS"],
);
assert!(
stdout.contains("MISSING:DBUS_SESSION_BUS_ADDRESS"),
"expected DBUS_SESSION stripped: {stdout}"
);
assert!(
stdout.contains("MISSING:DBUS_SYSTEM_BUS_ADDRESS"),
"expected DBUS_SYSTEM stripped: {stdout}"
);
}
#[test]
fn whitelist_env_sets_user_var() {
let stdout = printenv_inside(
&["--whitelist", "--env", "USER_INJECTED=forced"],
&[],
&["USER_INJECTED"],
);
assert!(stdout.contains("forced"), "env not applied: {stdout}");
}
#[test]
fn whitelist_env_keep_passes_through_host_var() {
let stdout = printenv_inside(
&["--whitelist", "--env", "PASSED_THROUGH"],
&[("PASSED_THROUGH", "from-host")],
&["PASSED_THROUGH"],
);
assert!(
stdout.contains("from-host"),
"expected --env KEY to pass host value through: {stdout}"
);
}
#[test]
fn whitelist_env_keep_absent_host_var_is_skipped() {
let stdout = printenv_inside(
&["--whitelist", "--env", "NEVER_SET_ON_HOST"],
&[],
&["NEVER_SET_ON_HOST"],
);
assert!(
stdout.contains("MISSING:NEVER_SET_ON_HOST"),
"expected absent keep-var to remain unset: {stdout}"
);
}
#[test]
fn whitelist_unsetenv_overrides_kept_var() {
let stdout = printenv_inside(
&["--whitelist", "--unsetenv", "TERM"],
&[("TERM", "xterm-test")],
&["TERM"],
);
assert!(
stdout.contains("MISSING:TERM"),
"expected --unsetenv to strip kept var: {stdout}"
);
}
#[test]
fn blacklist_drops_token_and_secret_vars() {
let stdout = printenv_inside(
&[],
&[
("GH_TOKEN", "gh-secret"),
("AWS_SECRET_ACCESS_KEY", "aws-secret"),
("MY_PASSWORD", "pw"),
("FOO_API_KEY", "fookey"),
],
&[
"GH_TOKEN",
"AWS_SECRET_ACCESS_KEY",
"MY_PASSWORD",
"FOO_API_KEY",
],
);
for var in [
"GH_TOKEN",
"AWS_SECRET_ACCESS_KEY",
"MY_PASSWORD",
"FOO_API_KEY",
] {
assert!(
stdout.contains(&format!("MISSING:{var}")),
"expected {var} stripped in blacklist mode, got: {stdout}"
);
}
for leaked in ["gh-secret", "aws-secret", "pw", "fookey"] {
assert!(!stdout.contains(leaked), "{leaked} leaked: {stdout}");
}
}
#[test]
fn blacklist_carves_out_vendor_api_keys() {
let stdout = printenv_inside(
&[],
&[
("ANTHROPIC_API_KEY", "anthropic-key"),
("OPENAI_API_KEY", "openai-key"),
("GEMINI_API_KEY", "gemini-key"),
],
&["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY"],
);
for expected in ["anthropic-key", "openai-key", "gemini-key"] {
assert!(
stdout.contains(expected),
"expected {expected} to survive carve-out, got: {stdout}"
);
}
assert!(!stdout.contains("MISSING:"), "carve-out failed: {stdout}");
}
#[test]
fn blacklist_suffix_match_does_not_catch_substring() {
let stdout = printenv_inside(
&[],
&[
("TOKENIZER_PATH", "/opt/tok"),
("MY_TOKEN_HOLDER", "holder"),
],
&["TOKENIZER_PATH", "MY_TOKEN_HOLDER"],
);
assert!(
stdout.contains("/opt/tok"),
"TOKENIZER_PATH stripped: {stdout}"
);
assert!(
stdout.contains("holder"),
"MY_TOKEN_HOLDER stripped: {stdout}"
);
}
#[test]
fn blacklist_keeps_unrelated_host_var() {
let stdout = printenv_inside(&[], &[("MY_NICE_VAR", "hello")], &["MY_NICE_VAR"]);
assert!(stdout.contains("hello"), "MY_NICE_VAR stripped: {stdout}");
}
#[test]
fn blacklist_keeps_dbus_vars() {
let stdout = printenv_inside(
&[],
&[
("DBUS_SESSION_BUS_ADDRESS", "unix:path=/tmp/fake"),
("DBUS_SYSTEM_BUS_ADDRESS", "unix:path=/tmp/fake-system"),
],
&["DBUS_SESSION_BUS_ADDRESS", "DBUS_SYSTEM_BUS_ADDRESS"],
);
assert!(stdout.contains("unix:path=/tmp/fake"));
assert!(stdout.contains("unix:path=/tmp/fake-system"));
}
#[test]
fn no_env_filter_whitelist_keeps_arbitrary_host_var() {
let stdout = printenv_inside(
&["--whitelist", "--no-env-filter"],
&[("SOME_RANDOM_NOISE_VAR", "kept")],
&["SOME_RANDOM_NOISE_VAR"],
);
assert!(
stdout.contains("kept"),
"expected --no-env-filter to pass host var through, got: {stdout}"
);
}
#[test]
fn no_env_filter_blacklist_keeps_secrets() {
let stdout = printenv_inside(&["--no-env-filter"], &[("GH_TOKEN", "kept")], &["GH_TOKEN"]);
assert!(
stdout.contains("kept"),
"expected --no-env-filter to pass secrets through, got: {stdout}"
);
}
#[test]
fn no_env_filter_still_honors_user_env() {
let stdout = printenv_inside(
&["--no-env-filter", "--env", "FORCED=yes"],
&[],
&["FORCED"],
);
assert!(
stdout.contains("yes"),
"expected user --env to still work with --no-env-filter, got: {stdout}"
);
}
#[test]
fn blacklist_env_overrides_builtin_deny() {
let stdout = printenv_inside(
&["--env", "GH_TOKEN=overridden"],
&[("GH_TOKEN", "original")],
&["GH_TOKEN"],
);
assert!(
stdout.contains("overridden"),
"expected --env to override deny, got: {stdout}"
);
assert!(!stdout.contains("original"));
}
+8
View File
@@ -0,0 +1,8 @@
mod common;
mod cli;
mod env;
mod modes;
mod mounts;
mod namespaces;
mod seccomp;
+200
View File
@@ -0,0 +1,200 @@
use crate::common::*;
#[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 whitelist_sys_is_readable() {
let output = sandbox(&["--whitelist"])
.args(["--", "bash", "-c", "cat /sys/class/net/lo/address"])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
assert_eq!(
stdout, "00:00:00:00:00:00",
"expected loopback address from /sys, got: {stdout}"
);
}
#[test]
fn blacklist_run_is_tmpfs() {
let output = sandbox(&[])
.args([
"--",
"bash",
"-c",
"touch /run/test_canary 2>&1 && echo WRITABLE || echo BLOCKED",
])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("WRITABLE"),
"expected /run to be a writable tmpfs in blacklist mode, got: {stdout}"
);
}
#[test]
fn blacklist_run_dbus_socket_accessible() {
let output = sandbox(&[])
.args([
"--",
"bash",
"-c",
"test -e /run/dbus/system_bus_socket && echo EXISTS || echo MISSING",
])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
assert_eq!(
stdout, "EXISTS",
"expected /run/dbus/system_bus_socket to be accessible in blacklist mode"
);
}
#[test]
fn blacklist_runuser_is_tmpfs() {
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 output = sandbox(&[])
.args(["--", "bash", "-c", &script])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
assert!(
stdout.is_empty(),
"expected only 'bus' (or empty) in {}, got unexpected entries: {stdout}",
run_user
);
}
#[test]
fn blacklist_dev_input_hidden() {
let output = sandbox(&[])
.args(["--", "bash", "-c", "ls /dev/input/ 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 /dev/input/ to be empty in blacklist mode, got {stdout} entries"
);
}
#[test]
fn blacklist_root_is_readonly() {
let output = sandbox(&[])
.args([
"--",
"bash",
"-c",
"touch /rootfile 2>&1 && echo WRITABLE || echo READONLY; \
mkdir /newdir 2>&1 && echo MKDIR_OK || echo MKDIR_FAIL",
])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("READONLY"),
"expected root to be read-only in blacklist mode, got: {stdout}"
);
assert!(
stdout.contains("MKDIR_FAIL"),
"expected mkdir at root to fail in blacklist mode, got: {stdout}"
);
}
#[test]
fn whitelist_root_is_readonly() {
let output = sandbox(&["--whitelist"])
.args([
"--",
"bash",
"-c",
"touch /rootfile 2>&1 && echo WRITABLE || echo READONLY; \
mkdir /newdir 2>&1 && echo MKDIR_OK || echo MKDIR_FAIL",
])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("READONLY"),
"expected root to be read-only in whitelist mode, got: {stdout}"
);
assert!(
stdout.contains("MKDIR_FAIL"),
"expected mkdir at root to fail in whitelist mode, got: {stdout}"
);
}
#[test]
fn whitelist_mountpoint_parents_are_readonly() {
let output = sandbox(&["--whitelist"])
.args([
"--",
"bash",
"-c",
"echo pwned > /home/testfile 2>&1 && echo HOME_WRITABLE || echo HOME_READONLY; \
touch /etc/newfile 2>&1 && echo ETC_WRITABLE || echo ETC_READONLY; \
touch /var/newfile 2>&1 && echo VAR_WRITABLE || echo VAR_READONLY",
])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("HOME_READONLY"),
"expected /home to be read-only in whitelist mode, got: {stdout}"
);
assert!(
stdout.contains("ETC_READONLY"),
"expected /etc to be read-only in whitelist mode, got: {stdout}"
);
assert!(
stdout.contains("VAR_READONLY"),
"expected /var to be read-only in whitelist mode, got: {stdout}"
);
}
#[test]
fn whitelist_tmp_still_writable() {
let output = sandbox(&["--whitelist"])
.args([
"--",
"bash",
"-c",
"touch /tmp/ok 2>&1 && echo TMP_WRITABLE || echo TMP_READONLY; \
touch /var/tmp/ok 2>&1 && echo VARTMP_WRITABLE || echo VARTMP_READONLY",
])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("TMP_WRITABLE"),
"expected /tmp to remain writable in whitelist mode, got: {stdout}"
);
assert!(
stdout.contains("VARTMP_WRITABLE"),
"expected /var/tmp to remain writable in whitelist mode, got: {stdout}"
);
}
+392
View File
@@ -0,0 +1,392 @@
use std::fs;
use std::process::Command;
use tempfile::TempDir;
use crate::common::*;
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 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 ro_mount_with_remapped_target() {
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 src_str = dir.path().to_str().unwrap();
let target = "/tmp/agent-sandbox-remap-test";
let output = sandbox(&["--ro", &format!("{src_str}:{target}")])
.args([
"--",
"bash",
"-c",
&format!("cat {target}/hello.txt && ls {src_str} 2>&1 | head -1 || echo src_hidden"),
])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("hi"),
"expected file content 'hi' at remapped target, got: {stdout}"
);
}
#[test]
fn rw_refines_ro_parent() {
let parent = TempDir::new().expect("failed to create temp dir");
let child = parent.path().join("sub");
fs::create_dir(&child).expect("failed to create sub dir");
fs::write(parent.path().join("top.txt"), "top").expect("write");
fs::write(child.join("inner.txt"), "inner").expect("write");
let parent_str = parent.path().to_str().unwrap();
let child_str = child.to_str().unwrap();
let output = sandbox(&["--ro", parent_str, "--rw", child_str])
.args([
"--",
"bash",
"-c",
&format!(
"touch {parent_str}/top_new 2>&1 || echo parent_ro; \
touch {child_str}/child_new && echo child_rw"
),
])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("parent_ro"),
"parent should be read-only, got: {stdout}"
);
assert!(
stdout.contains("child_rw"),
"child should be writable, got: {stdout}"
);
}
#[test]
fn blacklist_overlays_survive_tmp_bind() {
fs::write("/tmp/ssh-sandbox-test", "secret").expect("failed to write sentinel");
let _cleanup = CleanupFile("/tmp/ssh-sandbox-test");
let output = sandbox(&[])
.args([
"--",
"bash",
"-c",
"cat /tmp/ssh-sandbox-test 2>/dev/null && echo LEAKED || echo HIDDEN",
])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("HIDDEN"),
"expected /tmp/ssh-* to be hidden in blacklist mode, got: {stdout}"
);
assert!(
!stdout.contains("LEAKED"),
"/tmp/ssh-sandbox-test was readable inside the sandbox"
);
}
#[test]
fn relative_chdir_works() {
let output = sandbox(&["--chdir", "src"])
.args(["--", "bash", "-c", "pwd"])
.output()
.expect("agent-sandbox binary failed to execute");
assert!(
output.status.success(),
"relative --chdir should work, stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
assert!(
stdout.ends_with("/src"),
"expected cwd ending in /src, got: {stdout}"
);
}
#[test]
fn relative_rw_path_works() {
let output = sandbox(&["--rw", "src"])
.args(["--", "bash", "-c", "echo ok"])
.output()
.expect("agent-sandbox binary failed to execute");
assert!(
output.status.success(),
"relative --rw should work, stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn relative_ro_path_works() {
let output = sandbox(&["--ro", "src"])
.args(["--", "bash", "-c", "echo ok"])
.output()
.expect("agent-sandbox binary failed to execute");
assert!(
output.status.success(),
"relative --ro should work, stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[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}"
);
}
#[test]
fn mask_hides_directory() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("secret.txt"), "sensitive").expect("failed to write");
let dir_str = dir.path().canonicalize().unwrap();
let output = sandbox(&["--mask", dir_str.to_str().unwrap()])
.args([
"--",
"bash",
"-c",
&format!("ls {} 2>/dev/null | wc -l", dir_str.display()),
])
.output()
.expect("failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
assert_eq!(
stdout, "0",
"expected masked directory to be empty, got {stdout} entries"
);
}
#[test]
fn mask_hides_file() {
let dir = TempDir::new().unwrap();
let file = dir.path().join("secret.txt");
fs::write(&file, "sensitive").expect("failed to write");
let file_str = file.canonicalize().unwrap();
let output = sandbox(&["--mask", file_str.to_str().unwrap()])
.args([
"--",
"bash",
"-c",
&format!("cat {} 2>/dev/null || echo HIDDEN", file_str.display()),
])
.output()
.expect("failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("sensitive"),
"expected masked file contents to be hidden, got: {stdout}"
);
}
#[test]
fn whitelist_ro_symlink_visible_at_link_path() {
let dir = TempDir::new().unwrap();
let target = dir.path().join("target.txt");
let link = dir.path().join("link.txt");
fs::write(&target, "hello from target").expect("failed to write target");
std::os::unix::fs::symlink(&target, &link).expect("failed to create symlink");
let link_str = link.to_str().unwrap();
let output = sandbox(&["--whitelist", "--ro", link_str])
.args(["--", "cat", link_str])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("hello from target"),
"expected symlink path to be readable inside sandbox, got stdout: {stdout}, stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn mask_nonexistent_path_becomes_tmpfs() {
let dir = TempDir::new().unwrap();
let fake = dir.path().join("does_not_exist");
let fake_str = fake.to_str().unwrap();
let output = sandbox(&["--mask", fake_str])
.args([
"--",
"bash",
"-c",
&format!(
"test -d {fake_str} && touch {fake_str}/canary && echo WRITABLE || echo MISSING"
),
])
.output()
.expect("failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("WRITABLE"),
"expected nonexistent mask to create a writable tmpfs, got: {stdout}"
);
assert!(
!fake.join("canary").exists(),
"tmpfs writes should not leak to host"
);
}
#[test]
fn blacklist_overlays_survive_absolute_var_run_symlink() {
// On Debian/Ubuntu, /var/run -> /run is an absolute symlink; overlays
// like --tmpfs /var/run/dbus trip bwrap's re-rooted symlink resolution.
// Arch ships /var/run -> ../run (relative) so we synthesize the absolute
// layout inside the sandbox to reproduce on any host.
let mut bwrap_args = build_bwrap_command(&["--no-seccomp", "--", "true"]);
inject_absolute_var_run_symlink(&mut bwrap_args);
let output = Command::new(&bwrap_args[0])
.args(&bwrap_args[1..])
.output()
.expect("failed to invoke bwrap directly");
assert!(
output.status.success(),
"bwrap failed — an overlay target traverses an absolute /var/run symlink.\n\
stderr: {}",
String::from_utf8_lossy(&output.stderr),
);
}
fn build_bwrap_command(sandbox_args: &[&str]) -> Vec<String> {
let output = sandbox(&["--dry-run"])
.args(sandbox_args)
.output()
.expect("agent-sandbox binary failed to execute");
let cmd = String::from_utf8_lossy(&output.stdout);
let parsed = shlex::split(cmd.trim()).expect("dry-run output is not valid shell");
assert_eq!(parsed[0], "bwrap");
parsed
}
fn inject_absolute_var_run_symlink(bwrap_args: &mut Vec<String>) {
assert_eq!(bwrap_args[1], "--ro-bind");
assert_eq!(bwrap_args[2], "/");
assert_eq!(bwrap_args[3], "/");
let flags = ["--tmpfs", "/var", "--symlink", "/run", "/var/run"].map(String::from);
bwrap_args.splice(4..4, flags);
}
+117
View File
@@ -0,0 +1,117 @@
use std::fs;
use tempfile::TempDir;
use crate::common::*;
fn read_sid_from_stat(stat: &str) -> u32 {
stat.split_whitespace()
.nth(5)
.expect("missing field 6 in /proc/self/stat")
.parse()
.expect("failed to parse session ID")
}
fn read_sid_inside_sandbox(extra_args: &[&str]) -> u32 {
let output = sandbox(extra_args)
.args(["--", "bash", "-c", "cat /proc/self/stat"])
.output()
.expect("agent-sandbox binary failed to execute");
read_sid_from_stat(&String::from_utf8_lossy(&output.stdout))
}
fn read_sid_current_process() -> u32 {
let stat = fs::read_to_string("/proc/self/stat").expect("failed to read /proc/self/stat");
read_sid_from_stat(&stat)
}
#[test]
fn unshare_net_blocks_network() {
let output = sandbox(&["--unshare-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 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 hardened_isolates_sid() {
let inner_sid = read_sid_inside_sandbox(&["--hardened"]);
let outer_sid = read_sid_current_process();
assert_ne!(
inner_sid, outer_sid,
"sandboxed process should have a different session ID (got {inner_sid} == {outer_sid})"
);
}
#[test]
fn default_mode_shares_session() {
let inner_sid = read_sid_inside_sandbox(&[]);
let outer_sid = read_sid_current_process();
assert_eq!(
inner_sid, outer_sid,
"default-mode sandbox should share the session ID (got {inner_sid} != {outer_sid})"
);
}
+124
View File
@@ -0,0 +1,124 @@
use crate::common::*;
#[test]
fn seccomp_on_by_default_blocks_unshare() {
let output = sandbox(&[])
.args(["--", "unshare", "--user", "--map-root-user", "/bin/true"])
.output()
.expect("agent-sandbox binary failed to execute");
assert!(
!output.status.success(),
"expected unshare(2) to be blocked by default seccomp filter, but it succeeded"
);
}
#[test]
fn seccomp_off_allows_blocked_syscall() {
let output = sandbox(&["--no-seccomp"])
.args(["--", "unshare", "--user", "--map-root-user", "/bin/true"])
.output()
.expect("agent-sandbox binary failed to execute");
assert!(
output.status.success(),
"expected unshare(2) to succeed without seccomp, stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn seccomp_dry_run_emits_seccomp_arg() {
let output = sandbox(&["--dry-run"])
.args(["--", "/bin/true"])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("--seccomp"),
"expected --seccomp in dry-run output, got: {stdout}"
);
}
#[test]
fn seccomp_dry_run_no_seccomp_omits_arg() {
let output = sandbox(&["--dry-run", "--no-seccomp"])
.args(["--", "/bin/true"])
.output()
.expect("agent-sandbox binary failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("--seccomp"),
"expected no --seccomp in dry-run output with --no-seccomp, got: {stdout}"
);
}
#[test]
fn seccomp_normal_workload_succeeds() {
let output = sandbox(&[])
.args(["--", "bash", "-c", "ls /etc > /dev/null && date"])
.output()
.expect("agent-sandbox binary failed to execute");
assert!(
output.status.success(),
"expected normal workload to succeed under default seccomp, stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn seccomp_bash_pthread_fallback_works() {
// Verifies the ENOSYS-not-EPERM choice for clone3 doesn't break libc's
// clone3 -> clone fallback path that bash uses internally.
let output = sandbox(&[])
.args(["--", "bash", "-c", "true"])
.output()
.expect("agent-sandbox binary failed to execute");
assert!(
output.status.success(),
"expected bash to succeed under default seccomp (clone3 fallback), stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn seccomp_blocks_tiocsti() {
// TIOCSTI (0x5412) injects keystrokes into the terminal input queue.
// Without --new-session, this is the primary defense against CVE-2017-5226.
//
// 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
// skip, so the test only asserts our filter's behaviour.
let baseline = sandbox(&["--no-seccomp"])
.args([
"--",
"python3",
"-c",
"import fcntl; fcntl.ioctl(0, 0x5412, b'x')",
])
.output()
.expect("agent-sandbox binary failed to execute");
if !baseline.status.success() {
// Kernel already blocks TIOCSTI; seccomp filter is untestable here.
return;
}
let output = sandbox(&[])
.args([
"--",
"python3",
"-c",
"import fcntl; fcntl.ioctl(0, 0x5412, b'x')",
])
.output()
.expect("agent-sandbox binary failed to execute");
assert!(
!output.status.success(),
"expected TIOCSTI to be blocked by seccomp filter"
);
}
-1474
View File
File diff suppressed because it is too large Load Diff
+1236
View File
File diff suppressed because it is too large Load Diff
+37
View File
@@ -0,0 +1,37 @@
use super::*;
#[test]
fn keepenv_emits_setenv_for_present_key() {
let parent = vec![("XDG_RUNTIME_DIR".into(), "/run/user/1000".into())];
let args = keepenv_args(&["XDG_RUNTIME_DIR".into()], &parent);
assert_eq!(args, vec!["--setenv", "XDG_RUNTIME_DIR", "/run/user/1000"]);
}
#[test]
fn keepenv_skips_absent_keys() {
let parent = vec![("HOME".into(), "/home/me".into())];
let args = keepenv_args(&["XDG_RUNTIME_DIR".into()], &parent);
assert!(args.is_empty());
}
#[test]
fn keepenv_preserves_caller_key_order() {
let parent = vec![
("B".into(), "2".into()),
("A".into(), "1".into()),
("C".into(), "3".into()),
];
let args = keepenv_args(&["A".into(), "B".into(), "C".into()], &parent);
assert_eq!(
args,
vec![
"--setenv", "A", "1", "--setenv", "B", "2", "--setenv", "C", "3"
]
);
}
#[test]
fn keepenv_empty_keys_yields_nothing() {
let parent = vec![("A".into(), "1".into())];
assert!(keepenv_args(&[], &parent).is_empty());
}
+76
View File
@@ -0,0 +1,76 @@
use super::*;
#[test]
fn builds_on_supported_arch() {
let bytes = build_program_bytes().expect("seccomp program should build");
assert!(!bytes.is_empty(), "serialized BPF program is empty");
assert_eq!(bytes.len() % 8, 0, "BPF byte stream must be 8-byte aligned");
}
#[test]
fn allowlist_contains_essential_syscalls() {
for needed in &[
"read",
"write",
"openat",
"close",
"execve",
"exit_group",
"mmap",
"brk",
"clone",
] {
assert!(
ALLOWED_SYSCALLS.contains(needed),
"allowlist missing essential syscall: {needed}"
);
}
}
#[test]
fn allowlist_excludes_dangerous_syscalls() {
for denied in &[
"bpf",
"perf_event_open",
"userfaultfd",
"kexec_load",
"kexec_file_load",
"init_module",
"finit_module",
"delete_module",
"mount",
"umount",
"umount2",
"unshare",
"setns",
"pivot_root",
"ptrace",
"process_vm_readv",
"process_vm_writev",
"keyctl",
"personality",
"clone3",
"io_uring_setup",
"io_uring_register",
"io_uring_enter",
"fanotify_init",
"fanotify_mark",
"open_by_handle_at",
"name_to_handle_at",
"fsopen",
"fsconfig",
"fsmount",
"fspick",
"open_tree",
"move_mount",
"mount_setattr",
"reboot",
"swapon",
"swapoff",
] {
assert!(
!ALLOWED_SYSCALLS.contains(denied),
"allowlist must not contain dangerous syscall: {denied}"
);
}
}