184 lines
4.8 KiB
Rust
184 lines
4.8 KiB
Rust
//! Bubblewrap sandbox (user namespaces for Linux/macOS)
|
|
|
|
use crate::security::traits::Sandbox;
|
|
use std::process::Command;
|
|
|
|
/// Bubblewrap sandbox backend
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct BubblewrapSandbox;
|
|
|
|
impl BubblewrapSandbox {
|
|
pub fn new() -> std::io::Result<Self> {
|
|
if Self::is_installed() {
|
|
Ok(Self)
|
|
} else {
|
|
Err(std::io::Error::new(
|
|
std::io::ErrorKind::NotFound,
|
|
"Bubblewrap not found",
|
|
))
|
|
}
|
|
}
|
|
|
|
pub fn probe() -> std::io::Result<Self> {
|
|
Self::new()
|
|
}
|
|
|
|
fn is_installed() -> bool {
|
|
Command::new("bwrap")
|
|
.arg("--version")
|
|
.output()
|
|
.map(|o| o.status.success())
|
|
.unwrap_or(false)
|
|
}
|
|
}
|
|
|
|
impl Sandbox for BubblewrapSandbox {
|
|
fn wrap_command(&self, cmd: &mut Command) -> std::io::Result<()> {
|
|
let program = cmd.get_program().to_string_lossy().to_string();
|
|
let args: Vec<String> = cmd
|
|
.get_args()
|
|
.map(|s| s.to_string_lossy().to_string())
|
|
.collect();
|
|
|
|
let mut bwrap_cmd = Command::new("bwrap");
|
|
bwrap_cmd.args([
|
|
"--ro-bind",
|
|
"/usr",
|
|
"/usr",
|
|
"--dev",
|
|
"/dev",
|
|
"--proc",
|
|
"/proc",
|
|
"--bind",
|
|
"/tmp",
|
|
"/tmp",
|
|
"--unshare-all",
|
|
"--die-with-parent",
|
|
]);
|
|
bwrap_cmd.arg(&program);
|
|
bwrap_cmd.args(&args);
|
|
|
|
*cmd = bwrap_cmd;
|
|
Ok(())
|
|
}
|
|
|
|
fn is_available(&self) -> bool {
|
|
Self::is_installed()
|
|
}
|
|
|
|
fn name(&self) -> &str {
|
|
"bubblewrap"
|
|
}
|
|
|
|
fn description(&self) -> &str {
|
|
"User namespace sandbox (requires bwrap)"
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn bubblewrap_sandbox_name() {
|
|
let sandbox = BubblewrapSandbox;
|
|
assert_eq!(sandbox.name(), "bubblewrap");
|
|
}
|
|
|
|
#[test]
|
|
fn bubblewrap_is_available_only_if_installed() {
|
|
// Result depends on whether bwrap is installed
|
|
let sandbox = BubblewrapSandbox;
|
|
let _available = sandbox.is_available();
|
|
|
|
// Either way, the name should still work
|
|
assert_eq!(sandbox.name(), "bubblewrap");
|
|
}
|
|
|
|
// ── §1.1 Sandbox isolation flag tests ──────────────────────
|
|
|
|
#[test]
|
|
fn bubblewrap_wrap_command_includes_isolation_flags() {
|
|
let sandbox = BubblewrapSandbox;
|
|
let mut cmd = Command::new("echo");
|
|
cmd.arg("hello");
|
|
sandbox.wrap_command(&mut cmd).unwrap();
|
|
|
|
assert_eq!(
|
|
cmd.get_program().to_string_lossy(),
|
|
"bwrap",
|
|
"wrapped command should use bwrap as program"
|
|
);
|
|
|
|
let args: Vec<String> = cmd
|
|
.get_args()
|
|
.map(|s| s.to_string_lossy().to_string())
|
|
.collect();
|
|
|
|
assert!(
|
|
args.contains(&"--unshare-all".to_string()),
|
|
"must include --unshare-all for namespace isolation"
|
|
);
|
|
assert!(
|
|
args.contains(&"--die-with-parent".to_string()),
|
|
"must include --die-with-parent to prevent orphan processes"
|
|
);
|
|
assert!(
|
|
!args.contains(&"--share-net".to_string()),
|
|
"must NOT include --share-net (network should be blocked)"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn bubblewrap_wrap_command_preserves_original_command() {
|
|
let sandbox = BubblewrapSandbox;
|
|
let mut cmd = Command::new("ls");
|
|
cmd.arg("-la");
|
|
cmd.arg("/tmp");
|
|
sandbox.wrap_command(&mut cmd).unwrap();
|
|
|
|
let args: Vec<String> = cmd
|
|
.get_args()
|
|
.map(|s| s.to_string_lossy().to_string())
|
|
.collect();
|
|
|
|
assert!(
|
|
args.contains(&"ls".to_string()),
|
|
"original program must be passed as argument"
|
|
);
|
|
assert!(
|
|
args.contains(&"-la".to_string()),
|
|
"original args must be preserved"
|
|
);
|
|
assert!(
|
|
args.contains(&"/tmp".to_string()),
|
|
"original args must be preserved"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn bubblewrap_wrap_command_binds_required_paths() {
|
|
let sandbox = BubblewrapSandbox;
|
|
let mut cmd = Command::new("echo");
|
|
sandbox.wrap_command(&mut cmd).unwrap();
|
|
|
|
let args: Vec<String> = cmd
|
|
.get_args()
|
|
.map(|s| s.to_string_lossy().to_string())
|
|
.collect();
|
|
|
|
assert!(
|
|
args.contains(&"--ro-bind".to_string()),
|
|
"must include read-only bind for /usr"
|
|
);
|
|
assert!(
|
|
args.contains(&"--dev".to_string()),
|
|
"must include /dev mount"
|
|
);
|
|
assert!(
|
|
args.contains(&"--proc".to_string()),
|
|
"must include /proc mount"
|
|
);
|
|
}
|
|
}
|