Allow host runtimes to bypass the native builder sandbox

This commit is contained in:
2026-11-12 09:32:30 +00:00
parent 0b79bb4227
commit a0bb71b175
11 changed files with 197 additions and 84 deletions
+7 -2
View File
@@ -29,9 +29,14 @@ It also owns a registry and kache cache.
These directories live under `<dir>/host/builds/`.
Cargo fetches dependencies with network access.
The command then runs offline inside bubblewrap namespaces.
By default, the command then runs offline inside bubblewrap namespaces.
The host-only `sausage run --no-builder-sandbox WORLD` switch runs Cargo directly instead.
Tests use `RuntimeOptions.no_builder_sandbox` and select it with `SAUSAGE_TEST_NO_BUILDER_SANDBOX=1`.
The switch is not a world config setting.
Direct mode uses no user, mount, network, or PID namespaces.
It retains the source copy, working directory, private registry, kache settings, timeout, output, and artifact publication.
The source copy and target directory permit writes.
In the default sandboxed mode, the source copy and target directory permit writes.
The private kache cache also permits writes.
The registry and Nix store remain read-only.
Host home directories and the host network are absent.
+64 -44
View File
@@ -47,15 +47,23 @@ pub struct Builder {
root: Arc<dyn Layer>,
directory: PathBuf,
lock: Mutex<()>,
no_sandbox: bool,
}
impl Builder {
pub fn new(root: Arc<dyn Layer>, host: &Path) -> Result<Self> {
Self::new_with_options(root, host, false)
}
pub fn new_with_options(root: Arc<dyn Layer>, host: &Path, no_sandbox: bool) -> Result<Self> {
Ok(Self {
root,
directory: std::fs::canonicalize(host)?.join("builds"),
lock: Mutex::new(()),
no_sandbox,
})
}
fn command(&self) -> Command {
Command::new(if self.no_sandbox { "cargo" } else { "bwrap" })
}
pub async fn execute(&self, request: Request, tx: &mpsc::Sender<Event>) -> Result<()> {
let _guard = self.lock.lock().await;
let action = request
@@ -221,60 +229,72 @@ impl Builder {
if action == "run" && run_path.exists() {
std::fs::remove_file(&run_path)?;
}
let mut cmd = Command::new("bwrap");
cmd.args([
"--unshare-user",
"--unshare-pid",
"--unshare-net",
"--die-with-parent",
"--ro-bind",
"/nix/store",
"/nix/store",
])
.arg("--bind")
.arg(&temporary)
.arg("/tmp")
.arg("--ro-bind")
.arg(executable("sh")?)
.arg("/bin/sh");
for path in [&source, &target_mount, &cache] {
cmd.arg("--bind").arg(path).arg(path);
let mut cmd = self.command();
if !self.no_sandbox {
cmd.args([
"--unshare-user",
"--unshare-pid",
"--unshare-net",
"--die-with-parent",
"--ro-bind",
"/nix/store",
"/nix/store",
])
.arg("--bind")
.arg(&temporary)
.arg("/tmp")
.arg("--ro-bind")
.arg(executable("sh")?)
.arg("/bin/sh");
for path in [&source, &target_mount, &cache] {
cmd.arg("--bind").arg(path).arg(path);
}
for path in [&registry, &tools] {
cmd.arg("--ro-bind").arg(path).arg(path);
}
cmd.args([
"--proc",
"/proc",
"--dev",
"/dev",
"--remount-ro",
"/",
"--chdir",
])
.arg(&cwd)
.arg("--clearenv");
} else {
cmd.current_dir(&cwd);
}
for path in [&registry, &tools] {
cmd.arg("--ro-bind").arg(path).arg(path);
}
cmd.args([
"--proc",
"/proc",
"--dev",
"/dev",
"--remount-ro",
"/",
"--chdir",
])
.arg(&cwd)
.args(["--clearenv", "--setenv", "PATH"])
.arg(std::env::var("PATH")?)
.args(["--setenv", "HOME", "/nonexistent", "--setenv", "CARGO_HOME"])
.arg(&registry)
.args(["--setenv", "CARGO_TARGET_DIR"])
.arg(&target)
.args(["--setenv", "KACHE_CACHE_DIR"])
.arg(&cache)
.args(["--setenv", "SAUSAGE_CWD_INIT"])
.arg(tools.join("init.o"))
.args(["--setenv", "SAUSAGE_RUN_PATH"])
.arg(&run_path);
let mut environment = vec![
("PATH", std::env::var_os("PATH").context("PATH missing")?),
("HOME", "/nonexistent".into()),
("CARGO_HOME", registry.clone().into_os_string()),
("CARGO_TARGET_DIR", target.clone().into_os_string()),
("KACHE_CACHE_DIR", cache.clone().into_os_string()),
("SAUSAGE_CWD_INIT", tools.join("init.o").into_os_string()),
("SAUSAGE_RUN_PATH", run_path.clone().into_os_string()),
];
for name in [
"CC_wasm32_wasip2",
"AR_wasm32_wasip2",
"CFLAGS_wasm32_wasip2",
] {
if let Ok(value) = std::env::var(name) {
environment.push((name, value.into()));
}
}
for (name, value) in environment {
if self.no_sandbox {
cmd.env(name, value);
} else {
cmd.args(["--setenv", name]).arg(value);
}
}
cmd.args(["cargo", "--offline", "--config"])
if !self.no_sandbox {
cmd.arg("cargo");
}
cmd.args(["--offline", "--config"])
.arg(format!("build.rustc-wrapper={:?}", kache))
.arg("--config")
.arg(format!(
+11
View File
@@ -1,5 +1,16 @@
//! Focused tests for source discovery, argument forwarding, eviction, and process cancellation.
use super::*;
#[test]
fn native_command_defaults_to_bwrap_and_can_bypass_it() -> Result<()> {
let temp = tempfile::tempdir()?;
let world = crate::world::World::open(temp.path())?;
assert!(!crate::process::RuntimeOptions::default().no_builder_sandbox);
let default = Builder::new(world.root.clone(), &world.host_dir)?;
assert_eq!(default.command().as_std().get_program(), "bwrap");
let direct = Builder::new_with_options(world.root, &world.host_dir, true)?;
assert_eq!(direct.command().as_std().get_program(), "cargo");
Ok(())
}
#[tokio::test]
async fn source_links_survive_round_trip() -> Result<()> {
let temp = tempfile::tempdir()?;