#[cfg(target_os = "linux")]
use super::policy::WritableRoot;
#[cfg(target_os = "linux")]
use std::collections::BTreeSet;
#[cfg(target_os = "linux")]
use std::path::{Path, PathBuf};
#[cfg(target_os = "linux")]
pub const BWRAP_PATH: &str = "/usr/bin/bwrap";
#[cfg(target_os = "linux")]
pub fn is_available() -> bool {
is_executable(std::path::Path::new(BWRAP_PATH))
}
#[cfg(target_os = "linux")]
fn is_executable(path: &std::path::Path) -> bool {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(path)
.is_ok_and(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
}
#[cfg(not(target_os = "linux"))]
pub fn is_available() -> bool {
false
}
#[cfg(target_os = "linux")]
pub fn build_bwrap_command(
cwd: &std::path::Path,
program: &str,
args: &[String],
writable_roots: &[WritableRoot],
network_access: bool,
) -> Vec<String> {
let (writable_mounts, read_only_mounts) = safe_mounts(writable_roots);
let mut cmd: Vec<String> =
Vec::with_capacity(10 + args.len() + 3 * (writable_mounts.len() + read_only_mounts.len()));
cmd.push(BWRAP_PATH.to_string());
cmd.push("--unshare-all".to_string());
if network_access {
cmd.push("--share-net".to_string());
}
cmd.push("--ro-bind".to_string());
cmd.push("/".to_string());
cmd.push("/".to_string());
for root in writable_mounts {
let root = root.to_string_lossy().into_owned();
cmd.push("--bind".to_string());
cmd.push(root.clone());
cmd.push(root);
}
for root in read_only_mounts {
let root = root.to_string_lossy().into_owned();
cmd.push("--ro-bind".to_string());
cmd.push(root.clone());
cmd.push(root);
}
let cwd_str = cwd.to_string_lossy().to_string();
cmd.push("--chdir".to_string());
cmd.push(cwd_str);
cmd.push("--".to_string());
cmd.push(program.to_string());
cmd.extend(args.iter().cloned());
cmd
}
#[cfg(target_os = "linux")]
fn safe_mounts(writable_roots: &[WritableRoot]) -> (Vec<PathBuf>, Vec<PathBuf>) {
let mut writable = BTreeSet::new();
let mut read_only = BTreeSet::new();
for root in writable_roots {
let Some(canonical_root) = safe_existing_directory(&root.root) else {
continue;
};
writable.insert(canonical_root.clone());
for exception in &root.read_only_subpaths {
let Some(canonical_exception) = existing_directory(exception) else {
continue;
};
if canonical_exception.starts_with(&canonical_root) {
read_only.insert(canonical_exception);
}
}
}
(
writable.into_iter().collect(),
read_only.into_iter().collect(),
)
}
#[cfg(target_os = "linux")]
fn safe_existing_directory(path: &Path) -> Option<PathBuf> {
let canonical = existing_directory(path)?;
(canonical != Path::new("/")).then_some(canonical)
}
#[cfg(target_os = "linux")]
fn existing_directory(path: &Path) -> Option<PathBuf> {
let canonical = path.canonicalize().ok()?;
canonical.is_dir().then_some(canonical)
}
#[cfg(target_os = "linux")]
pub fn detect_denial(exit_code: i32, stderr: &str) -> bool {
exit_code != 0
&& (stderr
.lines()
.any(|line| line.trim_start().starts_with("bwrap:"))
|| stderr.contains("Read-only file system"))
}
#[cfg(not(target_os = "linux"))]
pub fn detect_denial(_exit_code: i32, _stderr: &str) -> bool {
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_available_does_not_panic() {
let _ = is_available();
}
#[test]
#[cfg(target_os = "linux")]
fn test_build_bwrap_command_structure() {
let dir = tempfile::tempdir().expect("tempdir");
let cwd = dir.path();
let cmd = build_bwrap_command(
cwd,
"sh",
&["-c".to_string(), "echo hi".to_string()],
&[WritableRoot::new(cwd.to_path_buf())],
false,
);
assert_eq!(cmd[0], "/usr/bin/bwrap");
assert!(cmd.contains(&"--ro-bind".to_string()));
assert!(cmd.contains(&"--chdir".to_string()));
assert!(cmd.contains(&"--unshare-all".to_string()));
assert!(!cmd.contains(&"--share-net".to_string()));
assert_eq!(cmd[cmd.len() - 1], "echo hi");
assert_eq!(cmd[cmd.len() - 2], "-c");
assert_eq!(cmd[cmd.len() - 3], "sh");
}
#[test]
#[cfg(target_os = "linux")]
fn read_only_command_does_not_remount_the_working_directory_writable() {
let dir = tempfile::tempdir().expect("tempdir");
let cwd = dir.path();
let cmd = build_bwrap_command(cwd, "true", &[], &[], false);
assert!(!cmd.iter().any(|arg| arg == "--bind"));
assert!(!cmd.iter().any(|arg| arg == "--share-net"));
assert!(
cmd.windows(2)
.any(|args| args[0] == "--chdir" && args[1] == cwd.to_string_lossy())
);
}
#[test]
#[cfg(target_os = "linux")]
fn workspace_write_mounts_every_safe_root_and_protects_read_only_descendants() {
let dir = tempfile::tempdir().expect("tempdir");
let workspace = dir.path().join("workspace");
let extra = dir.path().join("extra");
let protected = workspace.join(".codewhale");
std::fs::create_dir_all(&protected).expect("protected directory");
std::fs::create_dir_all(&extra).expect("extra directory");
let roots = vec![
WritableRoot::with_exceptions(workspace.clone(), vec![protected.clone()]),
WritableRoot::new(extra.clone()),
WritableRoot::new(dir.path().join("missing")),
WritableRoot::new(PathBuf::from("/")),
];
let cmd = build_bwrap_command(&workspace, "true", &[], &roots, true);
for root in [&workspace, &extra] {
let canonical = root.canonicalize().expect("canonical root");
assert!(has_mount(&cmd, "--bind", &canonical));
}
assert!(has_mount(
&cmd,
"--ro-bind",
&protected.canonicalize().expect("canonical protected path")
));
assert!(!has_mount(&cmd, "--bind", Path::new("/")));
assert!(!cmd.iter().any(|arg| arg.ends_with("/missing")));
let unshare = cmd
.iter()
.position(|arg| arg == "--unshare-all")
.expect("unshare all");
let share = cmd
.iter()
.position(|arg| arg == "--share-net")
.expect("share net");
assert!(share > unshare);
}
#[cfg(target_os = "linux")]
fn has_mount(command: &[String], flag: &str, path: &Path) -> bool {
let path = path.to_string_lossy();
command.windows(3).any(|args| {
args[0] == flag
&& args[1].as_str() == path.as_ref()
&& args[2].as_str() == path.as_ref()
})
}
#[test]
#[cfg(target_os = "linux")]
fn executable_probe_requires_a_regular_executable_file() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("bwrap");
std::fs::write(&path, b"fixture").expect("write fixture");
assert!(!is_executable(&path));
let mut permissions = std::fs::metadata(&path).expect("metadata").permissions();
permissions.set_mode(0o755);
std::fs::set_permissions(&path, permissions).expect("set executable bit");
assert!(is_executable(&path));
assert!(!is_executable(dir.path()));
}
#[test]
fn denial_detection_requires_a_failed_sandbox_signal() {
assert!(!detect_denial(0, "bwrap: ignored on success"));
#[cfg(target_os = "linux")]
{
assert!(detect_denial(1, "bwrap: Creating new namespace failed"));
assert!(detect_denial(1, "Read-only file system"));
assert!(!detect_denial(1, "child output mentions bwrap: casually"));
assert!(!detect_denial(1, "Permission denied"));
assert!(!detect_denial(1, "Operation not permitted"));
assert!(!detect_denial(1, "ordinary command failure"));
}
}
}