#![cfg(target_os = "linux")]
use std::path::Path;
use super::policy::SandboxPolicy;
pub const BWRAP_PATH: &str = "/usr/bin/bwrap";
#[must_use]
pub fn is_available() -> bool {
if Path::new(BWRAP_PATH).exists() {
return true;
}
std::env::var_os("PATH")
.is_some_and(|paths| std::env::split_paths(&paths).any(|dir| dir.join("bwrap").is_file()))
}
fn bwrap_program() -> String {
if Path::new(BWRAP_PATH).exists() {
BWRAP_PATH.to_string()
} else {
"bwrap".to_string()
}
}
#[must_use]
pub fn build_bwrap_command(
policy: &SandboxPolicy,
cwd: &Path,
program: &str,
args: &[String],
) -> Vec<String> {
let mut cmd: Vec<String> = Vec::with_capacity(16 + args.len());
cmd.push(bwrap_program());
cmd.push("--ro-bind".to_string());
cmd.push("/".to_string());
cmd.push("/".to_string());
for writable in policy.get_writable_roots(cwd) {
if !writable.root.exists() {
continue;
}
let root = writable.root.to_string_lossy().to_string();
cmd.push("--bind".to_string());
cmd.push(root.clone());
cmd.push(root);
for subpath in &writable.read_only_subpaths {
if !subpath.exists() {
continue;
}
let subpath = subpath.to_string_lossy().to_string();
cmd.push("--ro-bind".to_string());
cmd.push(subpath.clone());
cmd.push(subpath);
}
}
cmd.push("--chdir".to_string());
cmd.push(cwd.to_string_lossy().to_string());
cmd.push("--unshare-all".to_string());
if policy.has_network_access() {
cmd.push("--share-net".to_string());
}
cmd.push("--die-with-parent".to_string());
cmd.push("--".to_string());
cmd.push(program.to_string());
cmd.extend(args.iter().cloned());
cmd
}
#[must_use]
pub fn detect_denial(exit_code: i32, stderr: &str) -> bool {
if exit_code == 0 {
return false;
}
let lower = stderr.to_ascii_lowercase();
lower.contains("read-only file system")
|| lower.contains("bwrap:")
|| (lower.contains("permission denied") && lower.contains("bwrap"))
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn shell_args() -> Vec<String> {
vec!["-c".to_string(), "echo hi".to_string()]
}
#[test]
fn is_available_does_not_panic() {
let _ = is_available();
}
#[test]
fn read_only_policy_has_no_writable_binds() {
let cmd = build_bwrap_command(
&SandboxPolicy::ReadOnly,
Path::new("/home/user/project"),
"sh",
&shell_args(),
);
assert!(cmd.iter().any(|a| a == "--ro-bind"));
assert!(!cmd.iter().any(|a| a == "--bind"), "{cmd:?}");
assert!(cmd.iter().any(|a| a == "--unshare-all"));
assert!(!cmd.iter().any(|a| a == "--share-net"));
assert_eq!(cmd[cmd.len() - 1], "echo hi");
assert_eq!(cmd[cmd.len() - 3], "sh");
}
#[test]
fn workspace_write_binds_existing_cwd_and_respects_network() {
let cwd = std::env::temp_dir().join("zagens-bwrap-test-cwd");
let _ = std::fs::create_dir_all(&cwd);
let policy = SandboxPolicy::workspace_with_roots(vec![PathBuf::from("/nonexistent")], true);
let cmd = build_bwrap_command(&policy, &cwd, "sh", &shell_args());
assert!(cmd.iter().any(|a| a == "--bind"), "{cmd:?}");
assert!(cmd.iter().any(|a| a == "--share-net"));
assert!(!cmd.iter().any(|a| a == "/nonexistent"), "{cmd:?}");
let chdir_idx = cmd.iter().position(|a| a == "--chdir").expect("--chdir");
assert!(cmd[chdir_idx + 1].contains("zagens-bwrap-test-cwd"));
}
#[test]
fn denial_detection_matches_erofs() {
assert!(detect_denial(1, "sh: touch: Read-only file system"));
assert!(detect_denial(1, "bwrap: cannot mount"));
assert!(!detect_denial(0, "Read-only file system"));
assert!(!detect_denial(1, "normal failure"));
}
}