#![cfg(target_os = "linux")]
use std::path::Path;
use std::process::Stdio;
use sandbox_run::{Command, ProtectHome, ProtectSystem, Sandbox, landlock_available};
fn can_read(path: &Path, sandbox: sandbox_run::SandboxPolicy) -> bool {
Command::new("cat")
.arg(path)
.sandbox(sandbox)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("spawn cat")
.success()
}
fn can_write(path: &Path, sandbox: sandbox_run::SandboxPolicy) -> bool {
Command::new("sh")
.arg("-c")
.arg(format!("echo x > {}", path.display()))
.sandbox(sandbox)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("spawn sh")
.success()
}
#[test]
fn inaccessible_paths_denies_reads() {
if !landlock_available() {
eprintln!("skipping: Landlock not enforced on this kernel");
return;
}
let dir = tempfile::tempdir().unwrap();
let secret = dir.path().join("secret.txt");
std::fs::write(&secret, "topsecret").unwrap();
let policy = Sandbox::new()
.protect_system(ProtectSystem::No)
.inaccessible_paths([dir.path()])
.no_new_privileges(true)
.build()
.unwrap();
assert!(!can_read(&secret, policy), "inaccessible_paths must deny the read");
let open = Sandbox::new()
.protect_system(ProtectSystem::No)
.read_write_paths([dir.path()])
.no_new_privileges(true)
.build()
.unwrap();
assert!(can_read(&secret, open), "sanity: file is readable when granted");
}
#[test]
fn carve_in_under_denied_subtree_is_reachable() {
if !landlock_available() {
eprintln!("skipping: Landlock not enforced on this kernel");
return;
}
let home = tempfile::tempdir().unwrap();
let data = home.path().join("service/data");
std::fs::create_dir_all(&data).unwrap();
let allowed = data.join("ok.txt");
std::fs::write(&allowed, "x").unwrap();
let secret = home.path().join("secret.txt");
std::fs::write(&secret, "topsecret").unwrap();
let policy = Sandbox::new()
.protect_system(ProtectSystem::No)
.inaccessible_paths([home.path()])
.read_write_paths([data.as_path()])
.no_new_privileges(true)
.build()
.unwrap();
let again = Sandbox::new()
.protect_system(ProtectSystem::No)
.inaccessible_paths([home.path()])
.read_write_paths([data.as_path()])
.no_new_privileges(true)
.build()
.unwrap();
assert!(can_read(&allowed, policy), "carved-in data dir must stay readable");
assert!(
can_write(&allowed, again),
"carved-in data dir must stay writable"
);
let policy2 = Sandbox::new()
.protect_system(ProtectSystem::No)
.inaccessible_paths([home.path()])
.read_write_paths([data.as_path()])
.no_new_privileges(true)
.build()
.unwrap();
assert!(
!can_read(&secret, policy2),
"sibling under the denied tree must stay hidden"
);
}
#[test]
fn read_only_paths_deny_writes() {
if !landlock_available() {
eprintln!("skipping: Landlock not enforced on this kernel");
return;
}
let home = tempfile::tempdir().unwrap();
let ro = home.path().join("store");
std::fs::create_dir_all(&ro).unwrap();
let file = ro.join("index.json");
std::fs::write(&file, "{}").unwrap();
let read_policy = Sandbox::new()
.protect_system(ProtectSystem::No)
.inaccessible_paths([home.path()])
.read_only_paths([ro.as_path()])
.no_new_privileges(true)
.build()
.unwrap();
assert!(can_read(&file, read_policy), "read_only_paths must allow reads");
let write_policy = Sandbox::new()
.protect_system(ProtectSystem::No)
.inaccessible_paths([home.path()])
.read_only_paths([ro.as_path()])
.no_new_privileges(true)
.build()
.unwrap();
assert!(
!can_write(&file, write_policy),
"read_only_paths must deny writes"
);
}
#[test]
fn read_only_carve_in_under_denied_tree_is_executable() {
if !landlock_available() {
eprintln!("skipping: Landlock not enforced on this kernel");
return;
}
let home = tempfile::tempdir().unwrap();
let bin_dir = home.path().join("bin");
std::fs::create_dir_all(&bin_dir).unwrap();
let exe = bin_dir.join("true");
std::fs::copy("/bin/true", &exe).unwrap();
let run = |sandbox: sandbox_run::SandboxPolicy| {
Command::new(&exe)
.sandbox(sandbox)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
};
let hidden = Sandbox::new()
.protect_system(ProtectSystem::No)
.inaccessible_paths([home.path()])
.no_new_privileges(true)
.build()
.unwrap();
assert!(
!run(hidden),
"binary under a denied tree must not be executable without a carve-in"
);
let carved = Sandbox::new()
.protect_system(ProtectSystem::No)
.inaccessible_paths([home.path()])
.read_only_paths([exe.as_path()])
.no_new_privileges(true)
.build()
.unwrap();
assert!(run(carved), "read-only carve-in must remain executable");
}
#[test]
fn protect_home_yes_denies_home_content() {
if !landlock_available() {
eprintln!("skipping: Landlock not enforced on this kernel");
return;
}
let Some(home) = std::env::var_os("HOME").map(std::path::PathBuf::from) else {
eprintln!("skipping: HOME not set");
return;
};
if !home.starts_with("/home") && !home.starts_with("/root") {
eprintln!("skipping: HOME ({}) is outside the protected trees", home.display());
return;
}
let secret = home.join(format!(".sandbox_run_test_secret_{}", std::process::id()));
std::fs::write(&secret, "topsecret").unwrap();
let policy = Sandbox::new()
.protect_system(ProtectSystem::Strict)
.protect_home(ProtectHome::Yes)
.no_new_privileges(true)
.build()
.unwrap();
let denied = !can_read(&secret, policy);
let _ = std::fs::remove_file(&secret);
assert!(denied, "ProtectHome::Yes must hide files under $HOME");
}