use sandlock_core::policy::BranchAction;
#[allow(unused_imports)]
use sandlock_core::{Policy, Sandbox};
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
fn helper_binary() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../tests/rootfs-helper")
.canonicalize()
.expect("rootfs-helper not found — build.rs should have compiled it")
}
fn minimal_exec_policy(rootfs: &PathBuf) -> sandlock_core::PolicyBuilder {
Policy::builder()
.chroot(rootfs)
.fs_read("/usr")
.fs_read("/bin")
.fs_read("/proc")
.fs_read("/dev")
}
fn temp_dir(name: &str) -> PathBuf {
let dir =
std::env::temp_dir().join(format!("sandlock-test-chroot-{}-{}", name, std::process::id()));
let _ = fs::create_dir_all(&dir);
dir
}
fn build_test_rootfs(name: &str) -> PathBuf {
let rootfs = temp_dir(name);
let helper = helper_binary();
for dir in &["usr/bin", "usr/sbin", "etc", "proc", "dev", "tmp"] {
let _ = fs::create_dir_all(rootfs.join(dir));
}
let _ = fs::set_permissions(rootfs.join("tmp"), fs::Permissions::from_mode(0o1777));
let dest = rootfs.join("usr/bin/rootfs-helper");
fs::hard_link(&helper, &dest)
.or_else(|_| fs::copy(&helper, &dest).map(|_| ()))
.expect("failed to install rootfs-helper into rootfs");
for cmd in &["sh", "cat", "echo", "ls", "pwd", "readlink", "true", "write"] {
let link = rootfs.join(format!("usr/bin/{}", cmd));
let _ = fs::remove_file(&link);
std::os::unix::fs::symlink("rootfs-helper", &link)
.expect("failed to create busybox symlink");
}
let _ = std::os::unix::fs::symlink("usr/bin", rootfs.join("bin"));
let _ = std::os::unix::fs::symlink("usr/sbin", rootfs.join("sbin"));
rootfs
}
fn cleanup_rootfs(rootfs: &PathBuf) {
let _ = fs::remove_dir_all(rootfs);
}
#[tokio::test]
async fn test_chroot_ls_root() {
let rootfs = build_test_rootfs("ls-root");
let policy = Policy::builder()
.chroot(&rootfs)
.fs_read("/usr")
.fs_read("/bin")
.fs_read("/etc")
.fs_read("/proc")
.fs_read("/dev")
.fs_read("/tmp")
.build()
.unwrap();
let result = Sandbox::run(&policy, &["rootfs-helper", "ls", "/"]).await;
match result {
Ok(r) => {
assert!(
r.success(),
"ls / should succeed, stderr: {}",
r.stderr_str().unwrap_or("")
);
let stdout = r.stdout_str().unwrap_or("");
assert!(stdout.contains("usr"), "should list usr, got: {}", stdout);
assert!(stdout.contains("tmp"), "should list tmp, got: {}", stdout);
assert!(stdout.contains("bin"), "should list bin, got: {}", stdout);
assert!(stdout.contains("etc"), "should list etc, got: {}", stdout);
}
Err(e) => eprintln!("Chroot test skipped: {}", e),
}
cleanup_rootfs(&rootfs);
}
#[tokio::test]
async fn test_chroot_no_escape() {
let rootfs = build_test_rootfs("no-escape");
let sentinel = "sandlock-chroot-sentinel";
fs::write(rootfs.join("etc/sentinel"), sentinel).unwrap();
let policy = Policy::builder()
.chroot(&rootfs)
.fs_read("/usr")
.fs_read("/bin")
.fs_read("/etc")
.fs_read("/proc")
.fs_read("/dev")
.build()
.unwrap();
let result = Sandbox::run(&policy, &["rootfs-helper", "cat", "/../../etc/sentinel"]).await;
match result {
Ok(r) => {
assert!(
r.success(),
"cat should succeed, stderr: {}",
r.stderr_str().unwrap_or("")
);
let stdout = r.stdout_str().unwrap_or("");
assert_eq!(
stdout.trim(),
sentinel,
"should read chroot sentinel, got: {}",
stdout
);
}
Err(e) => eprintln!("Chroot test skipped: {}", e),
}
cleanup_rootfs(&rootfs);
}
#[tokio::test]
async fn test_chroot_getcwd() {
let rootfs = build_test_rootfs("getcwd");
let policy = Policy::builder()
.chroot(&rootfs)
.fs_read("/usr")
.fs_read("/bin")
.fs_read("/etc")
.fs_read("/proc")
.fs_read("/dev")
.build()
.unwrap();
let result = Sandbox::run(&policy, &["rootfs-helper", "pwd"]).await;
match result {
Ok(r) => {
assert!(
r.success(),
"pwd should succeed, stderr: {}",
r.stderr_str().unwrap_or("")
);
let stdout = r.stdout_str().unwrap_or("").trim().to_string();
assert_eq!(stdout, "/", "pwd should return /, got: {}", stdout);
}
Err(e) => eprintln!("Chroot test skipped: {}", e),
}
cleanup_rootfs(&rootfs);
}
#[tokio::test]
async fn test_chroot_write_file() {
let rootfs = build_test_rootfs("write-file");
let policy = Policy::builder()
.chroot(&rootfs)
.fs_read("/usr")
.fs_read("/bin")
.fs_read("/etc")
.fs_read("/proc")
.fs_read("/dev")
.fs_write("/tmp")
.build()
.unwrap();
let result = Sandbox::run(
&policy,
&["rootfs-helper", "sh", "-c", "echo hello > /tmp/test.txt && cat /tmp/test.txt"],
)
.await;
match result {
Ok(r) => {
assert!(
r.success(),
"should succeed, stderr: {}",
r.stderr_str().unwrap_or("")
);
let stdout = r.stdout_str().unwrap_or("").trim().to_string();
assert_eq!(stdout, "hello", "cat should output hello, got: {}", stdout);
let real_path = rootfs.join("tmp/test.txt");
assert!(
real_path.exists(),
"test.txt should exist at {}",
real_path.display()
);
}
Err(e) => eprintln!("Chroot test skipped: {}", e),
}
cleanup_rootfs(&rootfs);
}
#[tokio::test]
async fn test_chroot_cow_directory_open_stays_in_rootfs() {
let rootfs = build_test_rootfs("cow-dir-open");
let tmp_dir = rootfs.join("tmp");
fs::write(rootfs.join("tmp/rootfs-only.txt"), "rootfs").unwrap();
let host_marker = std::env::temp_dir().join(format!(
"sandlock-host-marker-{}",
std::process::id()
));
fs::write(&host_marker, "host").unwrap();
let policy = Policy::builder()
.chroot(&rootfs)
.fs_read("/usr")
.fs_read("/bin")
.fs_read("/etc")
.fs_read("/proc")
.fs_read("/dev")
.fs_read("/tmp")
.workdir(&tmp_dir)
.on_exit(BranchAction::Abort)
.build()
.unwrap();
let result = Sandbox::run(&policy, &["rootfs-helper", "ls", "/tmp"]).await;
match result {
Ok(r) => {
assert!(
r.success(),
"ls /tmp should succeed, stderr: {}",
r.stderr_str().unwrap_or("")
);
let stdout = r.stdout_str().unwrap_or("");
assert!(
stdout.contains("rootfs-only.txt"),
"expected to see rootfs file in /tmp, got: {}",
stdout
);
assert!(
!stdout.contains(host_marker.file_name().unwrap().to_string_lossy().as_ref()),
"host /tmp leaked into chroot listing: {}",
stdout
);
}
Err(e) => eprintln!("Chroot test skipped: {}", e),
}
let _ = fs::remove_file(&host_marker);
cleanup_rootfs(&rootfs);
}
#[tokio::test]
async fn test_chroot_with_cow() {
let rootfs = build_test_rootfs("cow");
let tmp_dir = rootfs.join("tmp");
let policy = Policy::builder()
.chroot(&rootfs)
.fs_read("/usr")
.fs_read("/bin")
.fs_read("/etc")
.fs_read("/proc")
.fs_read("/dev")
.fs_write("/tmp")
.workdir(&tmp_dir)
.on_exit(BranchAction::Abort)
.build()
.unwrap();
let result = Sandbox::run(
&policy,
&["rootfs-helper", "sh", "-c", "echo cow-test > /tmp/cow.txt"],
)
.await;
match result {
Ok(r) => {
assert!(
r.success(),
"should succeed, stderr: {}",
r.stderr_str().unwrap_or("")
);
let cow_file = tmp_dir.join("cow.txt");
assert!(
!cow_file.exists(),
"cow.txt should not exist after abort, but found at {}",
cow_file.display()
);
}
Err(e) => eprintln!("Chroot test skipped: {}", e),
}
cleanup_rootfs(&rootfs);
}
#[tokio::test]
async fn test_chroot_proc_self_root() {
let rootfs = build_test_rootfs("proc-self-root");
let policy = Policy::builder()
.chroot(&rootfs)
.fs_read("/usr")
.fs_read("/bin")
.fs_read("/etc")
.fs_read("/proc")
.fs_read("/dev")
.build()
.unwrap();
let result = Sandbox::run(&policy, &["rootfs-helper", "readlink", "/proc/self/root"]).await;
match result {
Ok(r) => {
assert!(
r.success(),
"readlink should succeed, stderr: {}",
r.stderr_str().unwrap_or("")
);
let stdout = r.stdout_str().unwrap_or("").trim().to_string();
assert_eq!(
stdout, "/",
"readlink /proc/self/root should return /, got: {}",
stdout
);
}
Err(e) => eprintln!("Chroot test skipped: {}", e),
}
cleanup_rootfs(&rootfs);
}
#[tokio::test]
async fn test_chroot_write_denied_without_fs_write() {
let rootfs = build_test_rootfs("write-denied");
let policy = minimal_exec_policy(&rootfs)
.fs_read("/etc")
.fs_read("/tmp")
.build()
.unwrap();
let result = Sandbox::run(
&policy,
&["rootfs-helper", "sh", "-c", "echo denied > /tmp/should-fail.txt"],
)
.await;
match result {
Ok(r) => {
assert!(
!r.success(),
"write should fail without fs_write, but got exit=0"
);
}
Err(e) => eprintln!("Chroot test skipped: {}", e),
}
cleanup_rootfs(&rootfs);
}
#[tokio::test]
async fn test_chroot_exec_with_root_readable() {
let rootfs = build_test_rootfs("exec-root-readable");
let policy = minimal_exec_policy(&rootfs)
.fs_read("/etc")
.fs_read("/")
.build()
.unwrap();
let result = Sandbox::run(&policy, &["/bin/rootfs-helper", "echo", "chroot-exec-ok"]).await;
match result {
Ok(r) => {
assert!(
r.success(),
"/bin/rootfs-helper should succeed with fs_read(\"/\"), exit={:?} stderr: {} stdout: {}",
r.code(), r.stderr_str().unwrap_or(""), r.stdout_str().unwrap_or("")
);
let stdout = r.stdout_str().unwrap_or("");
assert!(
stdout.contains("chroot-exec-ok"),
"should print chroot-exec-ok, got: {}",
stdout
);
}
Err(e) => eprintln!("Chroot test skipped: {}", e),
}
cleanup_rootfs(&rootfs);
}
#[tokio::test]
async fn test_chroot_fs_deny_blocks_virtual_path() {
let rootfs = build_test_rootfs("fs-deny");
fs::write(rootfs.join("etc/hostname"), "sandlock-test-host").unwrap();
let policy = minimal_exec_policy(&rootfs)
.fs_read("/etc")
.fs_deny("/etc/hostname")
.build()
.unwrap();
let result = Sandbox::run(&policy, &["rootfs-helper", "cat", "/etc/hostname"]).await;
match result {
Ok(r) => {
assert!(
!r.success(),
"cat /etc/hostname should fail when fs_deny overrides fs_read, exit={:?} stdout={}",
r.code(),
r.stdout_str().unwrap_or("")
);
}
Err(e) => eprintln!("Chroot test skipped: {}", e),
}
cleanup_rootfs(&rootfs);
}
#[tokio::test]
async fn test_chroot_read_denied_without_fs_read() {
let rootfs = build_test_rootfs("read-denied");
fs::write(rootfs.join("etc/hostname"), "sandlock-test-host").unwrap();
let policy = minimal_exec_policy(&rootfs)
.build()
.unwrap();
let result = Sandbox::run(&policy, &["rootfs-helper", "cat", "/etc/hostname"]).await;
match result {
Ok(r) => {
assert!(
!r.success(),
"cat /etc/hostname should fail without fs_read(\"/etc\"), exit={:?} stdout={}",
r.code(),
r.stdout_str().unwrap_or("")
);
}
Err(e) => eprintln!("Chroot test skipped: {}", e),
}
cleanup_rootfs(&rootfs);
}
#[tokio::test]
async fn test_fs_mount_read_write() {
let rootfs = build_test_rootfs("fs-mount-rw");
fs::create_dir_all(rootfs.join("work")).unwrap();
let work_dir = temp_dir("fs-mount-work");
fs::write(work_dir.join("input.txt"), "hello mount").unwrap();
let policy = minimal_exec_policy(&rootfs)
.fs_read("/tmp")
.fs_write("/tmp")
.fs_read("/work")
.fs_write("/work")
.fs_mount("/work", &work_dir)
.build()
.unwrap();
let result = Sandbox::run(&policy, &["rootfs-helper", "cat", "/work/input.txt"]).await;
match result {
Ok(r) => {
assert!(
r.success(),
"cat /work/input.txt failed: exit={:?} stderr={}",
r.code(),
r.stderr_str().unwrap_or("")
);
assert_eq!(
r.stdout_str().unwrap_or("").trim(),
"hello mount",
"expected 'hello mount', got: {}",
r.stdout_str().unwrap_or("")
);
}
Err(e) => eprintln!("fs_mount test skipped: {}", e),
}
let _ = fs::remove_dir_all(&rootfs);
let _ = fs::remove_dir_all(&work_dir);
}