Bind symlinked rw/ro paths at the user-written destination
Canonicalizing rw/ro paths in the config layer resolved symlinks before the sandbox was built, so a symlinked entry only appeared at its target's location -- never at the path the user wrote. Stop canonicalizing rw/ro at the config layer and instead resolve only the source side of the bind in sandbox.rs.
This commit is contained in:
@@ -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::<Result<Vec<_>, _>>()?;
|
||||
Ok(config_paths.chain(cli_paths).collect())
|
||||
}
|
||||
|
||||
fn prepare_cli_bind_path(path: PathBuf) -> Result<PathBuf, SandboxError> {
|
||||
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<String>) -> Result<Vec<String>, 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<PathBuf, SandboxError> {
|
||||
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<PathBuf, SandboxError> {
|
||||
let expanded = expand_tilde(path)?;
|
||||
require_absolute(&expanded)?;
|
||||
@@ -332,6 +349,8 @@ fn expand_and_canonicalize(path: &Path) -> Result<PathBuf, SandboxError> {
|
||||
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 {
|
||||
|
||||
@@ -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<PathBuf, SandboxError> {
|
||||
std::fs::canonicalize(path).map_err(|_| SandboxError::PathMissing(path.to_path_buf()))
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user