From 7c9375cd94caf02af2045f855e74b935aaabaad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krist=C3=B3f=20T=C3=B3th?= Date: Thu, 23 Apr 2026 18:47:27 +0200 Subject: [PATCH] Canonicalize blacklist overlay paths to skirt /var/run symlink --- src/blacklist.rs | 24 ++++++++++++++---------- tests/integration.rs | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/src/blacklist.rs b/src/blacklist.rs index 277594a..546adeb 100644 --- a/src/blacklist.rs +++ b/src/blacklist.rs @@ -14,8 +14,8 @@ pub struct BlacklistOverlays { } pub fn resolve_overlays(ctx: &PathContext) -> Result { - let mut tmpfs_dirs = Vec::new(); - let mut null_bind_files = Vec::new(); + let mut tmpfs_dirs: Vec = Vec::new(); + let mut null_bind_files: Vec = Vec::new(); for raw in SENSITIVE_PATHS { let expanded = expand_path(raw, ctx); @@ -23,9 +23,13 @@ pub fn resolve_overlays(ctx: &PathContext) -> Result tmpfs_dirs.push(path), - PathKind::File => { - if !is_under_tmpfs_dir(&path, &tmpfs_dirs) { + PathKind::Dir(path) => { + if !tmpfs_dirs.contains(&path) { + tmpfs_dirs.push(path); + } + } + PathKind::File(path) => { + if !is_under_tmpfs_dir(&path, &tmpfs_dirs) && !null_bind_files.contains(&path) { null_bind_files.push(path); } } @@ -47,15 +51,15 @@ pub fn resolve_path_context() -> Result { } enum PathKind { - Dir, - File, + Dir(PathBuf), + File(PathBuf), Missing, } fn classify_path(path: &Path) -> PathKind { - match fs::metadata(path) { - Ok(m) if m.is_dir() => PathKind::Dir, - Ok(_) => PathKind::File, + match fs::canonicalize(path) { + Ok(path) if path.is_dir() => PathKind::Dir(path), + Ok(path) => PathKind::File(path), Err(_) => PathKind::Missing, } } diff --git a/tests/integration.rs b/tests/integration.rs index 7d5e94b..e2ad390 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1393,6 +1393,47 @@ fn seccomp_bash_pthread_fallback_works() { ); } +#[test] +fn blacklist_overlays_survive_absolute_var_run_symlink() { + // On Debian/Ubuntu, /var/run -> /run is an absolute symlink; overlays + // like --tmpfs /var/run/dbus trip bwrap's re-rooted symlink resolution. + // Arch ships /var/run -> ../run (relative) so we synthesize the absolute + // layout inside the sandbox to reproduce on any host. + let mut bwrap_args = build_bwrap_command(&["--no-seccomp", "--", "true"]); + inject_absolute_var_run_symlink(&mut bwrap_args); + + let output = Command::new(&bwrap_args[0]) + .args(&bwrap_args[1..]) + .output() + .expect("failed to invoke bwrap directly"); + + assert!( + output.status.success(), + "bwrap failed — an overlay target traverses an absolute /var/run symlink.\n\ + stderr: {}", + String::from_utf8_lossy(&output.stderr), + ); +} + +fn build_bwrap_command(sandbox_args: &[&str]) -> Vec { + let output = sandbox(&["--dry-run"]) + .args(sandbox_args) + .output() + .expect("agent-sandbox binary failed to execute"); + let cmd = String::from_utf8_lossy(&output.stdout); + let parsed = shlex::split(cmd.trim()).expect("dry-run output is not valid shell"); + assert_eq!(parsed[0], "bwrap"); + parsed +} + +fn inject_absolute_var_run_symlink(bwrap_args: &mut Vec) { + assert_eq!(bwrap_args[1], "--ro-bind"); + assert_eq!(bwrap_args[2], "/"); + assert_eq!(bwrap_args[3], "/"); + let flags = ["--tmpfs", "/var", "--symlink", "/run", "/var/run"].map(String::from); + bwrap_args.splice(4..4, flags); +} + #[test] fn seccomp_blocks_tiocsti() { // TIOCSTI (0x5412) injects keystrokes into the terminal input queue.