use std::process::Stdio;
use super::{run_capped, Backend, RunSpec, Sandbox, SandboxOutcome};
use crate::error::{Error, Result};
pub struct LinuxSandbox;
impl Sandbox for LinuxSandbox {
async fn run(&self, spec: RunSpec<'_>) -> Result<SandboxOutcome> {
if !unshare_works() {
return run_capped(Backend::PortableFloor, spec, |_cmd| {}).await;
}
let wrapped = unshare_argv(spec.argv, spec.allow_network);
let wspec = RunSpec {
argv: &wrapped,
workdir: spec.workdir,
limits: spec.limits,
allow_network: spec.allow_network,
};
let outcome = run_capped(Backend::LinuxNamespaces, wspec, |_cmd| {}).await?;
match wrapper_failure(&outcome) {
Some(reason) => Err(Error::Sandbox { reason }),
None => Ok(outcome),
}
}
fn backend(&self) -> Backend {
if unshare_works() {
Backend::LinuxNamespaces
} else {
Backend::PortableFloor
}
}
}
fn unshare_works() -> bool {
static OK: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*OK.get_or_init(|| {
std::process::Command::new("unshare")
.args([
"--user",
"--map-root-user",
"--mount",
"--pid",
"--fork",
"--net",
"--",
"true",
])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
})
}
fn wrapper_failure(outcome: &SandboxOutcome) -> Option<String> {
let stderr = outcome.stderr.trim();
(!outcome.success() && stderr.starts_with("unshare:"))
.then(|| format!("the namespace wrapper failed, the command never ran: {stderr}"))
}
pub(crate) fn unshare_argv(inner: &[String], allow_network: bool) -> Vec<String> {
let mut v: Vec<String> = vec![
"unshare".into(),
"--user".into(),
"--map-root-user".into(),
"--mount".into(),
"--pid".into(),
"--fork".into(),
];
if !allow_network {
v.push("--net".into());
}
v.push("--".into());
v.extend(inner.iter().cloned());
v
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn denies_network_with_a_new_net_namespace() {
let argv = unshare_argv(&["echo".into(), "hi".into()], false);
assert!(
argv.contains(&"--net".into()),
"net namespace must isolate network by default"
);
assert!(argv
.windows(2)
.any(|w| w == ["--".to_string(), "echo".to_string()]));
}
#[test]
fn allows_network_when_asked() {
let argv = unshare_argv(&["echo".into()], true);
assert!(
!argv.contains(&"--net".into()),
"no net namespace when network is allowed"
);
}
#[test]
fn the_reported_backend_is_the_one_the_host_can_actually_run() {
let expected = if unshare_works() {
Backend::LinuxNamespaces
} else {
Backend::PortableFloor
};
assert_eq!(LinuxSandbox.backend(), expected);
}
#[tokio::test]
async fn degrades_to_the_floor_when_the_wrapper_does_not_work() {
if unshare_works() {
return; }
let dir = tempfile::tempdir().unwrap();
let argv = vec!["sh".into(), "-c".into(), "echo hi".into()];
let out = LinuxSandbox
.run(RunSpec {
argv: &argv,
workdir: dir.path(),
limits: &crate::sandbox::SandboxLimits::default(),
allow_network: false,
})
.await
.unwrap();
assert!(out.success(), "a degraded run must still run, got {out:?}");
assert_eq!(out.backend, Backend::PortableFloor);
assert!(out.stdout.contains("hi"));
}
#[test]
fn a_wrapper_failure_is_not_a_verification_failure() {
let fail = |stderr: &str, code: Option<i32>| SandboxOutcome {
backend: Backend::LinuxNamespaces,
argv: vec!["unshare".into()],
exit_code: code,
cap_hit: None,
stdout: String::new(),
stderr: stderr.into(),
};
assert!(wrapper_failure(&fail(
"unshare: write failed /proc/self/uid_map: Operation not permitted\n",
Some(1)
))
.is_some());
assert!(wrapper_failure(&fail("error[E0308]: mismatched types", Some(1))).is_none());
assert!(wrapper_failure(&fail("", Some(0))).is_none());
}
}