diff --git a/src/config.rs b/src/config.rs index 98733bc..39d68fb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -95,11 +95,20 @@ fn merge_paths( let config_paths = globals.iter().chain(profile).cloned(); let cli_paths = cli .into_iter() - .map(|p| std::fs::canonicalize(&p).map_err(|_| SandboxError::PathMissing(p))) + .map(prepare_cli_bind_path) .collect::, _>>()?; Ok(config_paths.chain(cli_paths).collect()) } +fn prepare_cli_bind_path(path: PathBuf) -> Result { + let absolute = + std::path::absolute(&path).map_err(|_| SandboxError::PathMissing(path.clone()))?; + if !absolute.exists() { + return Err(SandboxError::PathMissing(absolute)); + } + Ok(absolute) +} + fn split_bwrap_args(raw: Vec) -> Result, SandboxError> { let mut out = Vec::new(); for value in &raw { @@ -228,10 +237,10 @@ pub struct Options { impl Options { fn validate_paths(&mut self) -> Result<(), SandboxError> { for p in &mut self.rw { - *p = expand_and_canonicalize(p)?; + *p = expand_and_require_existing(p)?; } for p in &mut self.ro { - *p = expand_and_canonicalize(p)?; + *p = expand_and_require_existing(p)?; } if let Some(ref chdir) = self.chdir { self.chdir = Some(expand_and_canonicalize(chdir)?); @@ -243,6 +252,14 @@ impl Options { } } +fn expand_and_require_existing(path: &Path) -> Result { + let expanded = expand_and_require_absolute(path)?; + if !expanded.exists() { + return Err(SandboxError::PathMissing(expanded)); + } + Ok(expanded) +} + fn expand_and_require_absolute(path: &Path) -> Result { let expanded = expand_tilde(path)?; require_absolute(&expanded)?; @@ -332,6 +349,8 @@ fn expand_and_canonicalize(path: &Path) -> Result { mod tests { use std::sync::LazyLock; + use tempfile::TempDir; + use super::*; const FULL_CONFIG_TOML: &str = r#" @@ -615,6 +634,45 @@ mod tests { assert_eq!(config.extra_rw, vec![PathBuf::from(&home)]); } + #[test] + fn build_preserves_symlinks_in_config_paths() { + let dir = TempDir::new().unwrap(); + let target = dir.path().join("target"); + let link = dir.path().join("link"); + std::fs::write(&target, "x").unwrap(); + std::os::unix::fs::symlink(&target, &link).unwrap(); + + let file_config = FileConfig { + options: Options { + ro: vec![link.clone()], + rw: vec![link.clone()], + ..Options::default() + }, + ..FileConfig::default() + }; + let config = build(args_with_command(), Some(file_config)).unwrap(); + assert_eq!(config.extra_ro, vec![link.clone()]); + assert_eq!(config.extra_rw, vec![link]); + } + + #[test] + fn build_preserves_symlinks_in_cli_paths() { + let dir = TempDir::new().unwrap(); + let target = dir.path().join("target"); + let link = dir.path().join("link"); + std::fs::write(&target, "x").unwrap(); + std::os::unix::fs::symlink(&target, &link).unwrap(); + + let args = Args { + extra_ro: vec![link.clone()], + extra_rw: vec![link.clone()], + ..args_with_command() + }; + let config = build(args, None).unwrap(); + assert_eq!(config.extra_ro, vec![link.clone()]); + assert_eq!(config.extra_rw, vec![link]); + } + #[test] fn build_relative_config_path_rejected() { let file_config = FileConfig { diff --git a/src/sandbox.rs b/src/sandbox.rs index aa053cb..3c0e2e7 100644 --- a/src/sandbox.rs +++ b/src/sandbox.rs @@ -180,17 +180,17 @@ fn ro_bind_under_tmpfs(cmd: &mut Command, base: &str, paths: &[&str]) { } fn add_rw_bind(cmd: &mut Command, path: &Path) -> Result<(), SandboxError> { - if !path.exists() { - return Err(SandboxError::PathMissing(path.to_path_buf())); - } - cmd.arg("--bind").arg(path).arg(path); + let source = resolve_bind_source(path)?; + cmd.arg("--bind").arg(source).arg(path); Ok(()) } fn add_ro_bind(cmd: &mut Command, path: &Path) -> Result<(), SandboxError> { - if !path.exists() { - return Err(SandboxError::PathMissing(path.to_path_buf())); - } - cmd.arg("--ro-bind").arg(path).arg(path); + let source = resolve_bind_source(path)?; + cmd.arg("--ro-bind").arg(source).arg(path); Ok(()) } + +fn resolve_bind_source(path: &Path) -> Result { + std::fs::canonicalize(path).map_err(|_| SandboxError::PathMissing(path.to_path_buf())) +} diff --git a/tests/integration.rs b/tests/integration.rs index 04f755f..73f10b5 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -796,6 +796,28 @@ fn config_command_replaced_by_passthrough() { ); } +#[test] +fn whitelist_ro_symlink_visible_at_link_path() { + let dir = TempDir::new().unwrap(); + let target = dir.path().join("target.txt"); + let link = dir.path().join("link.txt"); + fs::write(&target, "hello from target").expect("failed to write target"); + std::os::unix::fs::symlink(&target, &link).expect("failed to create symlink"); + let link_str = link.to_str().unwrap(); + + let output = sandbox(&["--whitelist", "--ro", link_str]) + .args(["--", "cat", link_str]) + .output() + .expect("agent-sandbox binary failed to execute"); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("hello from target"), + "expected symlink path to be readable inside sandbox, got stdout: {stdout}, stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); +} + #[test] fn mask_nonexistent_path_becomes_tmpfs() { let dir = TempDir::new().unwrap();