Mark inherited descriptors close-on-exec so none reach the sandbox

This commit is contained in:
2026-11-04 18:12:04 +01:00
parent 14c4f44a1f
commit f9ac595d63
4 changed files with 66 additions and 0 deletions
+6
View File
@@ -35,6 +35,7 @@ pub enum SandboxError {
SeccompUnsupportedArch(String), SeccompUnsupportedArch(String),
PersistentTmpDir(PathBuf, String), PersistentTmpDir(PathBuf, String),
ProcChildrenMissing, ProcChildrenMissing,
InheritedFdsCloexec(std::io::Error),
} }
impl std::fmt::Display for SandboxError { impl std::fmt::Display for SandboxError {
@@ -116,6 +117,10 @@ impl std::fmt::Display for SandboxError {
f, f,
"the kernel does not provide /proc/<pid>/task/<tid>/children (CONFIG_PROC_CHILDREN); agent-sandbox needs it to find the processes it must clean up" "the kernel does not provide /proc/<pid>/task/<tid>/children (CONFIG_PROC_CHILDREN); agent-sandbox needs it to find the processes it must clean up"
), ),
Self::InheritedFdsCloexec(e) => write!(
f,
"cannot keep inherited file descriptors out of the sandbox (close_range with CLOSE_RANGE_CLOEXEC needs Linux 5.11+): {e}"
),
} }
} }
} }
@@ -128,6 +133,7 @@ impl std::error::Error for SandboxError {
Self::Io(e) => Some(e), Self::Io(e) => Some(e),
Self::ConfigRead { source, .. } => Some(source), Self::ConfigRead { source, .. } => Some(source),
Self::ConfigParse { source, .. } => Some(source), Self::ConfigParse { source, .. } => Some(source),
Self::InheritedFdsCloexec(e) => Some(e),
_ => None, _ => None,
} }
} }
+20
View File
@@ -0,0 +1,20 @@
use std::io;
use crate::SandboxError;
const FIRST_FD_AFTER_STDIO: libc::c_uint = 3;
pub fn keep_out_of_sandbox() -> Result<(), SandboxError> {
let result = unsafe {
libc::syscall(
libc::SYS_close_range,
FIRST_FD_AFTER_STDIO,
libc::c_uint::MAX,
libc::CLOSE_RANGE_CLOEXEC,
)
};
if result < 0 {
return Err(SandboxError::InheritedFdsCloexec(io::Error::last_os_error()));
}
Ok(())
}
+2
View File
@@ -3,6 +3,7 @@ pub mod cli;
pub mod config; pub mod config;
mod env; mod env;
mod errors; mod errors;
mod inherited_fds;
mod memfd; mod memfd;
mod persistent_tmp; mod persistent_tmp;
mod preflight; mod preflight;
@@ -99,6 +100,7 @@ fn resolve_run_user_from_proc() -> Option<String> {
} }
pub fn run(config: SandboxConfig) -> Result<ExitStatus, SandboxError> { pub fn run(config: SandboxConfig) -> Result<ExitStatus, SandboxError> {
inherited_fds::keep_out_of_sandbox()?;
preflight::check(&config)?; preflight::check(&config)?;
let forwarded_signals = ForwardedSignals::detect(); let forwarded_signals = ForwardedSignals::detect();
+38
View File
@@ -1,4 +1,6 @@
use std::fs; use std::fs;
use std::os::fd::AsRawFd;
use std::os::unix::process::CommandExt;
use std::process::Command; use std::process::Command;
use tempfile::TempDir; use tempfile::TempDir;
@@ -496,6 +498,42 @@ fn persistent_tmp_leaks_no_descriptor_into_host_tmp() {
); );
} }
#[test]
fn inherited_host_descriptor_does_not_reach_sandbox() {
const PASSED_FD: i32 = 9;
let host_dir = TempDir::new().unwrap();
fs::write(host_dir.path().join("canary"), "host-secret").unwrap();
let host_dir_handle = fs::File::open(host_dir.path()).unwrap();
let host_dir_fd = host_dir_handle.as_raw_fd();
let mut sandbox = Sandbox::new(&[]);
sandbox.args([
"--",
"bash",
"-c",
&format!("cat /proc/self/fd/{PASSED_FD}/canary 2>/dev/null; true"),
]);
unsafe {
sandbox.pre_exec(move || match libc::dup2(host_dir_fd, PASSED_FD) {
-1 => Err(std::io::Error::last_os_error()),
_ => Ok(()),
});
}
let output = sandbox
.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"),
"a descriptor inherited by agent-sandbox reaches the sandboxed command"
);
}
#[test] #[test]
fn persistent_tmp_is_noop_in_blacklist_mode() { fn persistent_tmp_is_noop_in_blacklist_mode() {
let label = format!("e2e-bl-noop-{}-{}", std::process::id(), rand_suffix()); let label = format!("e2e-bl-noop-{}-{}", std::process::id(), rand_suffix());