//! Kernel-level sandbox assertions (spec §15, acceptance-blocking): these //! tests spawn REAL containers and probe the controls from inside — //! asserting kernel behavior, not configuration strings. use std::process::Command; use cm_sandbox::{DockerDriver, SandboxDriver, SandboxSpec}; const IMAGE: &str = "clawmates/agent-base:dev"; /// Builds the agent image once if missing (subsequent runs hit the cache). fn ensure_image() { let exists = Command::new("docker") .args(["image", "inspect", IMAGE]) .output() .expect("docker available") .status .success(); if exists { return; } let root = env!("CARGO_MANIFEST_DIR"); let status = Command::new("docker") .args([ "build", "-t", IMAGE, "-f", &format!("{root}/../../images/agent-base/Dockerfile"), &format!("{root}/../../images/agent-base"), ]) .status() .expect("docker build runs"); assert!(status.success(), "agent-base image build failed"); } async fn spawn(name_suffix: &str) -> (DockerDriver, cm_sandbox::SandboxHandle) { ensure_image(); let driver = DockerDriver::connect().expect("docker daemon reachable"); let spec = SandboxSpec { name: format!("clawmates-test-sbx-{name_suffix}-{}", std::process::id()), image: IMAGE.into(), memory_bytes: 256 * 1024 * 1024, nano_cpus: 1_000_000_000, pids_limit: 128, egress: false, }; // Clean leftovers from interrupted runs, then provision fresh. let _ = driver.destroy_by_name(&spec.name).await; let handle = driver.provision(&spec).await.expect("sandbox provisions"); (driver, handle) } #[tokio::test] async fn runs_as_uid_10001_with_no_capabilities() { let (driver, handle) = spawn("uid").await; let uid = driver.exec(&handle, &["id", "-u"]).await.unwrap(); assert_eq!(uid.stdout.trim(), "10001", "stderr: {}", uid.stderr); // CapEff all-zero proves cap-drop ALL took effect in the kernel. let caps = driver .exec(&handle, &["grep", "CapEff", "/proc/self/status"]) .await .unwrap(); let value = caps.stdout.split_whitespace().last().unwrap_or(""); assert_eq!( u64::from_str_radix(value, 16).unwrap(), 0, "effective capabilities must be empty, got {value}" ); driver.destroy(&handle).await.unwrap(); } #[tokio::test] async fn rootfs_is_read_only_with_writable_tmp() { let (driver, handle) = spawn("rootfs").await; let write_root = driver .exec(&handle, &["touch", "/etc/owned"]) .await .unwrap(); assert_ne!(write_root.exit_code, 0, "rootfs must reject writes"); assert!( write_root.stderr.contains("Read-only file system"), "got: {}", write_root.stderr ); let write_tmp = driver .exec(&handle, &["touch", "/tmp/scratch"]) .await .unwrap(); assert_eq!(write_tmp.exit_code, 0, "tmpfs /tmp must be writable"); let write_home = driver .exec(&handle, &["touch", "/home/agent/file"]) .await .unwrap(); assert_eq!(write_home.exit_code, 0, "workdir must be writable"); driver.destroy(&handle).await.unwrap(); } #[tokio::test] async fn seccomp_denies_namespace_and_tracing_syscalls() { let (driver, handle) = spawn("seccomp").await; // unshare(2) is denied by the profile even though no capability is // required for a user-namespace unshare. let unshare = driver .exec(&handle, &["unshare", "--user", "true"]) .await .unwrap(); assert_ne!(unshare.exit_code, 0, "unshare must be denied"); driver.destroy(&handle).await.unwrap(); } #[tokio::test] async fn sandbox_has_no_network_path_at_all() { let (driver, handle) = spawn("egress").await; // network=none: no traffic-carrying interface exists. The kernel // auto-creates inert DOWN tunnel devices (tunl0, gre0, sit0, ...) in // every namespace; what must be absent is any ethernet/veth device. let interfaces = driver .exec(&handle, &["cat", "/proc/net/dev"]) .await .unwrap(); let carriers: Vec<&str> = interfaces .stdout .lines() .skip(2) .map(str::trim_start) .filter(|line| { line.starts_with("eth") || line.starts_with("en") || line.starts_with("veth") }) .collect(); assert!(carriers.is_empty(), "unexpected interfaces: {carriers:?}"); // And an actual connect attempt goes nowhere. let connect = driver .exec( &handle, &[ "timeout", "3", "bash", "-c", "echo probe > /dev/tcp/1.1.1.1/443", ], ) .await .unwrap(); assert_ne!(connect.exit_code, 0, "egress connect must fail"); driver.destroy(&handle).await.unwrap(); } #[tokio::test] async fn no_setuid_binaries_exist_and_privileges_cannot_grow() { let (driver, handle) = spawn("setuid").await; let setuid = driver .exec( &handle, &[ "find", "/usr", "/bin", "/sbin", "-perm", "/6000", "-type", "f", ], ) .await .unwrap(); assert_eq!(setuid.stdout.trim(), "", "setuid binaries found"); // NoNewPrivs flag is set on every process (no-new-privileges). let nnp = driver .exec(&handle, &["grep", "NoNewPrivs", "/proc/self/status"]) .await .unwrap(); assert!(nnp.stdout.trim().ends_with('1'), "got: {}", nnp.stdout); driver.destroy(&handle).await.unwrap(); } #[tokio::test] async fn destroy_removes_the_container_and_health_reflects_it() { let (driver, handle) = spawn("lifecycle").await; assert!(driver.health(&handle).await.unwrap()); driver.destroy(&handle).await.unwrap(); assert!(!driver.health(&handle).await.unwrap()); }