Bind persistent tmp dirs with --bind-fd so the sandbox inherits no host descriptor

This commit is contained in:
2026-11-04 18:10:43 +01:00
parent b5937b1a84
commit 14c4f44a1f
3 changed files with 65 additions and 16 deletions
+31 -12
View File
@@ -3,15 +3,14 @@ use std::io;
use std::os::fd::{AsRawFd, OwnedFd};
use std::os::unix::fs::{DirBuilderExt, MetadataExt, OpenOptionsExt};
use std::path::PathBuf;
use std::process::Command;
use crate::SandboxConfig;
use crate::errors::SandboxError;
pub struct PersistentTmpDirs {
pub tmp: PathBuf,
pub var_tmp: PathBuf,
// Held to keep `/proc/self/fd/<n>` bind sources valid through `exec()`.
_keep_fds: Vec<OwnedFd>,
tmp: BindSource,
var_tmp: BindSource,
}
impl PersistentTmpDirs {
@@ -25,20 +24,40 @@ impl PersistentTmpDirs {
if config.dry_run {
return Ok(Some(Self {
tmp: tmp.host_path,
var_tmp: var_tmp.host_path,
_keep_fds: Vec::new(),
tmp: BindSource::HostPath(tmp.host_path),
var_tmp: BindSource::HostPath(var_tmp.host_path),
}));
}
let tmp_fd = tmp.open()?;
let var_tmp_fd = var_tmp.open()?;
Ok(Some(Self {
tmp: format!("/proc/self/fd/{}", tmp_fd.as_raw_fd()).into(),
var_tmp: format!("/proc/self/fd/{}", var_tmp_fd.as_raw_fd()).into(),
_keep_fds: vec![tmp_fd, var_tmp_fd],
tmp: BindSource::OpenDir(tmp.open()?),
var_tmp: BindSource::OpenDir(var_tmp.open()?),
}))
}
pub fn add_binds(&self, cmd: &mut Command) {
self.tmp.add_bind(cmd, "/tmp");
self.var_tmp.add_bind(cmd, "/var/tmp");
}
}
enum BindSource {
HostPath(PathBuf),
/// bwrap closes a `--bind-fd` descriptor once mounted, so the sandboxed
/// command never inherits this handle on the host filesystem.
OpenDir(OwnedFd),
}
impl BindSource {
fn add_bind(&self, cmd: &mut Command, target: &str) {
match self {
Self::HostPath(path) => cmd.arg("--bind").arg(path).arg(target),
Self::OpenDir(fd) => cmd
.arg("--bind-fd")
.arg(fd.as_raw_fd().to_string())
.arg(target),
};
}
}
pub struct PersistentTmpDir {
+1 -4
View File
@@ -250,10 +250,7 @@ fn add_whitelist_mode(
cmd.arg("--tmpfs").arg(&cache_dir);
match persistent {
Some(p) => {
cmd.arg("--bind").arg(&p.tmp).arg("/tmp");
cmd.arg("--bind").arg(&p.var_tmp).arg("/var/tmp");
}
Some(p) => p.add_binds(cmd),
None => {
cmd.args(["--tmpfs", "/tmp"]);
cmd.args(["--tmpfs", "/var/tmp"]);
+33
View File
@@ -463,6 +463,39 @@ fn persistent_tmp_survives_across_invocations() {
);
}
#[test]
fn persistent_tmp_leaks_no_descriptor_into_host_tmp() {
let label = format!("e2e-fd-escape-{}-{}", std::process::id(), rand_suffix());
let _cleanup_tmp = CleanupPath::new(format!("/tmp/agent-sandbox-{label}"));
let _cleanup_var = CleanupPath::new(format!("/var/tmp/agent-sandbox-{label}"));
let canary_name = format!("agent-sandbox-fd-canary-{label}");
let canary_host = format!("/tmp/{canary_name}");
let _cleanup_canary = CleanupPath::new(&canary_host);
fs::write(&canary_host, "host-secret").unwrap();
let output = Sandbox::new(&["--persistent-tmp", "--persistent-key", &label])
.args([
"--",
"bash",
"-c",
&format!(
"for fd in /proc/self/fd/*; do cat \"$fd/../{canary_name}\" 2>/dev/null; done; true"
),
])
.output()
.expect("agent-sandbox binary failed to execute");
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
!String::from_utf8_lossy(&output.stdout).contains("host-secret"),
"an inherited descriptor reaches host /tmp"
);
}
#[test]
fn persistent_tmp_is_noop_in_blacklist_mode() {
let label = format!("e2e-bl-noop-{}-{}", std::process::id(), rand_suffix());