Accept SRC:DST remap syntax in --ro/--rw
This commit is contained in:
@@ -17,6 +17,7 @@ ro = [
|
|||||||
"/etc/alsa",
|
"/etc/alsa",
|
||||||
"/run/user/1000/pulse",
|
"/run/user/1000/pulse",
|
||||||
"/run/user/1000/pipewire-0",
|
"/run/user/1000/pipewire-0",
|
||||||
|
# "/host/path:/sandbox/path", # SRC:DST -> mount host SRC at a different target
|
||||||
]
|
]
|
||||||
rw = [
|
rw = [
|
||||||
"~/.config/claude",
|
"~/.config/claude",
|
||||||
|
|||||||
+6
-6
@@ -50,13 +50,13 @@ pub struct Args {
|
|||||||
#[arg(long, overrides_with = "env_filter")]
|
#[arg(long, overrides_with = "env_filter")]
|
||||||
pub no_env_filter: bool,
|
pub no_env_filter: bool,
|
||||||
|
|
||||||
/// Bind an extra path read-write (repeatable)
|
/// Bind an extra path read-write (repeatable). Use SRC:DST to mount at a different target.
|
||||||
#[arg(long = "rw", value_name = "PATH", action = clap::ArgAction::Append)]
|
#[arg(long = "rw", value_name = "SRC[:DST]", action = clap::ArgAction::Append)]
|
||||||
pub extra_rw: Vec<PathBuf>,
|
pub extra_rw: Vec<String>,
|
||||||
|
|
||||||
/// Bind an extra path read-only (repeatable)
|
/// Bind an extra path read-only (repeatable). Use SRC:DST to mount at a different target.
|
||||||
#[arg(long = "ro", value_name = "PATH", action = clap::ArgAction::Append)]
|
#[arg(long = "ro", value_name = "SRC[:DST]", action = clap::ArgAction::Append)]
|
||||||
pub extra_ro: Vec<PathBuf>,
|
pub extra_ro: Vec<String>,
|
||||||
|
|
||||||
/// Print the bwrap command without executing
|
/// Print the bwrap command without executing
|
||||||
#[arg(long, overrides_with = "no_dry_run")]
|
#[arg(long, overrides_with = "no_dry_run")]
|
||||||
|
|||||||
+190
-40
@@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::cli::Args;
|
use crate::cli::Args;
|
||||||
use crate::{EnvEntry, SandboxConfig, SandboxError, SandboxMode};
|
use crate::{BindSpec, EnvEntry, SandboxConfig, SandboxError, SandboxMode};
|
||||||
|
|
||||||
pub fn build(args: Args, file_config: Option<FileConfig>) -> Result<SandboxConfig, SandboxError> {
|
pub fn build(args: Args, file_config: Option<FileConfig>) -> Result<SandboxConfig, SandboxError> {
|
||||||
let (mut globals, mut profile) = match file_config {
|
let (mut globals, mut profile) = match file_config {
|
||||||
@@ -64,8 +64,8 @@ pub fn build(args: Args, file_config: Option<FileConfig>) -> Result<SandboxConfi
|
|||||||
globals.dry_run,
|
globals.dry_run,
|
||||||
),
|
),
|
||||||
chdir: resolve_chdir(args.chdir, profile.chdir, globals.chdir)?,
|
chdir: resolve_chdir(args.chdir, profile.chdir, globals.chdir)?,
|
||||||
extra_rw: merge_paths(args.extra_rw, &profile.rw, &globals.rw)?,
|
extra_rw: merge_bind_specs(args.extra_rw, &profile.rw, &globals.rw)?,
|
||||||
extra_ro: merge_paths(args.extra_ro, &profile.ro, &globals.ro)?,
|
extra_ro: merge_bind_specs(args.extra_ro, &profile.ro, &globals.ro)?,
|
||||||
mask: merge_vecs(args.mask, &profile.mask, &globals.mask),
|
mask: merge_vecs(args.mask, &profile.mask, &globals.mask),
|
||||||
env,
|
env,
|
||||||
unsetenv,
|
unsetenv,
|
||||||
@@ -138,22 +138,55 @@ fn resolve_chdir(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge_paths(
|
fn merge_bind_specs(
|
||||||
cli: Vec<PathBuf>,
|
cli: Vec<String>,
|
||||||
profile: &[PathBuf],
|
profile: &[String],
|
||||||
globals: &[PathBuf],
|
globals: &[String],
|
||||||
) -> Result<Vec<PathBuf>, SandboxError> {
|
) -> Result<Vec<BindSpec>, SandboxError> {
|
||||||
let config_paths = globals.iter().chain(profile).cloned();
|
let config_specs = globals
|
||||||
let cli_paths = cli
|
.iter()
|
||||||
.into_iter()
|
.chain(profile)
|
||||||
.map(prepare_cli_bind_path)
|
.map(|raw| resolve_config_bind_spec(raw));
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
let cli_specs = cli.iter().map(|raw| resolve_cli_bind_spec(raw));
|
||||||
Ok(config_paths.chain(cli_paths).collect())
|
config_specs.chain(cli_specs).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_cli_bind_path(path: PathBuf) -> Result<PathBuf, SandboxError> {
|
fn resolve_cli_bind_spec(raw: &str) -> Result<BindSpec, SandboxError> {
|
||||||
|
let (src, dst) = split_bind_spec(raw)?;
|
||||||
|
let source = prepare_cli_bind_path(Path::new(src))?;
|
||||||
|
let target = match dst {
|
||||||
|
Some(d) => {
|
||||||
|
let p = PathBuf::from(d);
|
||||||
|
require_absolute(&p)?;
|
||||||
|
p
|
||||||
|
}
|
||||||
|
None => source.clone(),
|
||||||
|
};
|
||||||
|
Ok(BindSpec { source, target })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_config_bind_spec(raw: &str) -> Result<BindSpec, SandboxError> {
|
||||||
|
let (src, dst) = split_bind_spec(raw)?;
|
||||||
|
let source = expand_and_require_existing(Path::new(src))?;
|
||||||
|
let target = match dst {
|
||||||
|
Some(d) => expand_and_require_absolute(Path::new(d))?,
|
||||||
|
None => source.clone(),
|
||||||
|
};
|
||||||
|
Ok(BindSpec { source, target })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_bind_spec(raw: &str) -> Result<(&str, Option<&str>), SandboxError> {
|
||||||
|
match raw.split_once(':') {
|
||||||
|
Some((src, dst)) if !src.is_empty() && !dst.is_empty() => Ok((src, Some(dst))),
|
||||||
|
Some(_) => Err(SandboxError::InvalidBindSpec(raw.to_string())),
|
||||||
|
None if raw.is_empty() => Err(SandboxError::InvalidBindSpec(raw.to_string())),
|
||||||
|
None => Ok((raw, None)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_cli_bind_path(path: &Path) -> Result<PathBuf, SandboxError> {
|
||||||
let absolute =
|
let absolute =
|
||||||
std::path::absolute(&path).map_err(|_| SandboxError::PathMissing(path.clone()))?;
|
std::path::absolute(path).map_err(|_| SandboxError::PathMissing(path.to_path_buf()))?;
|
||||||
if !absolute.exists() {
|
if !absolute.exists() {
|
||||||
return Err(SandboxError::PathMissing(absolute));
|
return Err(SandboxError::PathMissing(absolute));
|
||||||
}
|
}
|
||||||
@@ -314,9 +347,9 @@ pub struct Options {
|
|||||||
pub dry_run: Option<bool>,
|
pub dry_run: Option<bool>,
|
||||||
pub chdir: Option<PathBuf>,
|
pub chdir: Option<PathBuf>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub rw: Vec<PathBuf>,
|
pub rw: Vec<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub ro: Vec<PathBuf>,
|
pub ro: Vec<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub mask: Vec<PathBuf>,
|
pub mask: Vec<PathBuf>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -329,12 +362,6 @@ pub struct Options {
|
|||||||
|
|
||||||
impl Options {
|
impl Options {
|
||||||
fn validate_paths(&mut self) -> Result<(), SandboxError> {
|
fn validate_paths(&mut self) -> Result<(), SandboxError> {
|
||||||
for p in &mut self.rw {
|
|
||||||
*p = expand_and_require_existing(p)?;
|
|
||||||
}
|
|
||||||
for p in &mut self.ro {
|
|
||||||
*p = expand_and_require_existing(p)?;
|
|
||||||
}
|
|
||||||
if let Some(ref chdir) = self.chdir {
|
if let Some(ref chdir) = self.chdir {
|
||||||
self.chdir = Some(expand_and_canonicalize(chdir)?);
|
self.chdir = Some(expand_and_canonicalize(chdir)?);
|
||||||
}
|
}
|
||||||
@@ -722,13 +749,13 @@ mod tests {
|
|||||||
fn build_rw_paths_accumulate() {
|
fn build_rw_paths_accumulate() {
|
||||||
let file_config = FileConfig {
|
let file_config = FileConfig {
|
||||||
options: Options {
|
options: Options {
|
||||||
rw: vec![PathBuf::from("/tmp")],
|
rw: vec!["/tmp".into()],
|
||||||
..Options::default()
|
..Options::default()
|
||||||
},
|
},
|
||||||
profile: HashMap::from([(
|
profile: HashMap::from([(
|
||||||
"extra".into(),
|
"extra".into(),
|
||||||
Options {
|
Options {
|
||||||
rw: vec![PathBuf::from("/usr")],
|
rw: vec!["/usr".into()],
|
||||||
..Options::default()
|
..Options::default()
|
||||||
},
|
},
|
||||||
)]),
|
)]),
|
||||||
@@ -736,7 +763,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let args = Args {
|
let args = Args {
|
||||||
profile: Some("extra".into()),
|
profile: Some("extra".into()),
|
||||||
extra_rw: vec![PathBuf::from("/var")],
|
extra_rw: vec!["/var".into()],
|
||||||
..args_with_command()
|
..args_with_command()
|
||||||
};
|
};
|
||||||
let config = build(args, Some(file_config)).unwrap();
|
let config = build(args, Some(file_config)).unwrap();
|
||||||
@@ -885,13 +912,13 @@ mod tests {
|
|||||||
let home = std::env::var("HOME").unwrap();
|
let home = std::env::var("HOME").unwrap();
|
||||||
let file_config = FileConfig {
|
let file_config = FileConfig {
|
||||||
options: Options {
|
options: Options {
|
||||||
rw: vec![PathBuf::from("~/")],
|
rw: vec!["~/".into()],
|
||||||
..Options::default()
|
..Options::default()
|
||||||
},
|
},
|
||||||
..FileConfig::default()
|
..FileConfig::default()
|
||||||
};
|
};
|
||||||
let config = build(args_with_command(), Some(file_config)).unwrap();
|
let config = build(args_with_command(), Some(file_config)).unwrap();
|
||||||
assert_eq!(config.extra_rw, vec![PathBuf::from(&home)]);
|
assert_eq!(config.extra_rw, vec![same_bind(Path::new(&home))]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -902,17 +929,18 @@ mod tests {
|
|||||||
std::fs::write(&target, "x").unwrap();
|
std::fs::write(&target, "x").unwrap();
|
||||||
std::os::unix::fs::symlink(&target, &link).unwrap();
|
std::os::unix::fs::symlink(&target, &link).unwrap();
|
||||||
|
|
||||||
|
let link_str = link.to_str().unwrap().to_string();
|
||||||
let file_config = FileConfig {
|
let file_config = FileConfig {
|
||||||
options: Options {
|
options: Options {
|
||||||
ro: vec![link.clone()],
|
ro: vec![link_str.clone()],
|
||||||
rw: vec![link.clone()],
|
rw: vec![link_str],
|
||||||
..Options::default()
|
..Options::default()
|
||||||
},
|
},
|
||||||
..FileConfig::default()
|
..FileConfig::default()
|
||||||
};
|
};
|
||||||
let config = build(args_with_command(), Some(file_config)).unwrap();
|
let config = build(args_with_command(), Some(file_config)).unwrap();
|
||||||
assert_eq!(config.extra_ro, vec![link.clone()]);
|
assert_eq!(config.extra_ro, vec![same_bind(&link)]);
|
||||||
assert_eq!(config.extra_rw, vec![link]);
|
assert_eq!(config.extra_rw, vec![same_bind(&link)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -923,21 +951,22 @@ mod tests {
|
|||||||
std::fs::write(&target, "x").unwrap();
|
std::fs::write(&target, "x").unwrap();
|
||||||
std::os::unix::fs::symlink(&target, &link).unwrap();
|
std::os::unix::fs::symlink(&target, &link).unwrap();
|
||||||
|
|
||||||
|
let link_str = link.to_str().unwrap().to_string();
|
||||||
let args = Args {
|
let args = Args {
|
||||||
extra_ro: vec![link.clone()],
|
extra_ro: vec![link_str.clone()],
|
||||||
extra_rw: vec![link.clone()],
|
extra_rw: vec![link_str],
|
||||||
..args_with_command()
|
..args_with_command()
|
||||||
};
|
};
|
||||||
let config = build(args, None).unwrap();
|
let config = build(args, None).unwrap();
|
||||||
assert_eq!(config.extra_ro, vec![link.clone()]);
|
assert_eq!(config.extra_ro, vec![same_bind(&link)]);
|
||||||
assert_eq!(config.extra_rw, vec![link]);
|
assert_eq!(config.extra_rw, vec![same_bind(&link)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_relative_config_path_rejected() {
|
fn build_relative_config_path_rejected() {
|
||||||
let file_config = FileConfig {
|
let file_config = FileConfig {
|
||||||
options: Options {
|
options: Options {
|
||||||
rw: vec![PathBuf::from("relative/path")],
|
rw: vec!["relative/path".into()],
|
||||||
..Options::default()
|
..Options::default()
|
||||||
},
|
},
|
||||||
..FileConfig::default()
|
..FileConfig::default()
|
||||||
@@ -948,6 +977,120 @@ mod tests {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_cli_remap_target() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let src = dir.path().join("host");
|
||||||
|
std::fs::create_dir(&src).unwrap();
|
||||||
|
let src_str = src.to_str().unwrap();
|
||||||
|
|
||||||
|
let args = Args {
|
||||||
|
extra_ro: vec![format!("{src_str}:/inside/elsewhere")],
|
||||||
|
..args_with_command()
|
||||||
|
};
|
||||||
|
let config = build(args, None).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
config.extra_ro,
|
||||||
|
vec![BindSpec {
|
||||||
|
source: src,
|
||||||
|
target: PathBuf::from("/inside/elsewhere"),
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_config_remap_target() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let src = dir.path().join("host");
|
||||||
|
std::fs::create_dir(&src).unwrap();
|
||||||
|
let src_str = src.to_str().unwrap();
|
||||||
|
|
||||||
|
let file_config = FileConfig {
|
||||||
|
options: Options {
|
||||||
|
rw: vec![format!("{src_str}:/inside/elsewhere")],
|
||||||
|
..Options::default()
|
||||||
|
},
|
||||||
|
..FileConfig::default()
|
||||||
|
};
|
||||||
|
let config = build(args_with_command(), Some(file_config)).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
config.extra_rw,
|
||||||
|
vec![BindSpec {
|
||||||
|
source: src,
|
||||||
|
target: PathBuf::from("/inside/elsewhere"),
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_config_tilde_expands_in_target() {
|
||||||
|
let home = std::env::var("HOME").unwrap();
|
||||||
|
|
||||||
|
let file_config = FileConfig {
|
||||||
|
options: Options {
|
||||||
|
rw: vec!["~/:~/mounted".into()],
|
||||||
|
..Options::default()
|
||||||
|
},
|
||||||
|
..FileConfig::default()
|
||||||
|
};
|
||||||
|
let config = build(args_with_command(), Some(file_config)).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
config.extra_rw,
|
||||||
|
vec![BindSpec {
|
||||||
|
source: PathBuf::from(&home),
|
||||||
|
target: PathBuf::from(format!("{home}/mounted")),
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_relative_target_rejected() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let src_str = dir.path().to_str().unwrap();
|
||||||
|
|
||||||
|
let args = Args {
|
||||||
|
extra_ro: vec![format!("{src_str}:relative/target")],
|
||||||
|
..args_with_command()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
build(args, None),
|
||||||
|
Err(SandboxError::ConfigPathNotAbsolute(_))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_empty_source_half_rejected() {
|
||||||
|
let args = Args {
|
||||||
|
extra_ro: vec![":/target".into()],
|
||||||
|
..args_with_command()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
build(args, None),
|
||||||
|
Err(SandboxError::InvalidBindSpec(_))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_empty_target_half_rejected() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let src_str = dir.path().to_str().unwrap();
|
||||||
|
|
||||||
|
let args = Args {
|
||||||
|
extra_ro: vec![format!("{src_str}:")],
|
||||||
|
..args_with_command()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
build(args, None),
|
||||||
|
Err(SandboxError::InvalidBindSpec(_))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_chdir_from_config() {
|
fn build_chdir_from_config() {
|
||||||
let file_config = FileConfig {
|
let file_config = FileConfig {
|
||||||
@@ -1281,11 +1424,18 @@ mod tests {
|
|||||||
assert!(matches!(result, Err(SandboxError::NoCommand)));
|
assert!(matches!(result, Err(SandboxError::NoCommand)));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assert_paths(actual: &[PathBuf], expected: &[&str]) {
|
fn assert_paths(actual: &[String], expected: &[&str]) {
|
||||||
let expected: Vec<PathBuf> = expected.iter().map(PathBuf::from).collect();
|
let expected: Vec<String> = expected.iter().map(|s| s.to_string()).collect();
|
||||||
assert_eq!(actual, &expected);
|
assert_eq!(actual, &expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn same_bind(path: &Path) -> BindSpec {
|
||||||
|
BindSpec {
|
||||||
|
source: path.to_path_buf(),
|
||||||
|
target: path.to_path_buf(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn assert_command(cmd: &Option<CommandValue>, expected: &[&str]) {
|
fn assert_command(cmd: &Option<CommandValue>, expected: &[&str]) {
|
||||||
let actual = cmd.clone().unwrap().into_vec();
|
let actual = cmd.clone().unwrap().into_vec();
|
||||||
let expected: Vec<String> = expected.iter().map(|s| s.to_string()).collect();
|
let expected: Vec<String> = expected.iter().map(|s| s.to_string()).collect();
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ pub enum SandboxError {
|
|||||||
UnknownConfigKey(String),
|
UnknownConfigKey(String),
|
||||||
ConfigPathNotAbsolute(PathBuf),
|
ConfigPathNotAbsolute(PathBuf),
|
||||||
InvalidBwrapArg(String),
|
InvalidBwrapArg(String),
|
||||||
|
InvalidBindSpec(String),
|
||||||
NoCommand,
|
NoCommand,
|
||||||
Seccomp(String),
|
Seccomp(String),
|
||||||
SeccompUnsupportedArch(String),
|
SeccompUnsupportedArch(String),
|
||||||
@@ -80,6 +81,9 @@ impl std::fmt::Display for SandboxError {
|
|||||||
Self::InvalidBwrapArg(s) => {
|
Self::InvalidBwrapArg(s) => {
|
||||||
write!(f, "invalid quoting in --bwrap-arg: {s}")
|
write!(f, "invalid quoting in --bwrap-arg: {s}")
|
||||||
}
|
}
|
||||||
|
Self::InvalidBindSpec(s) => {
|
||||||
|
write!(f, "invalid bind spec (expected SRC or SRC:DST): {s:?}")
|
||||||
|
}
|
||||||
Self::NoCommand => write!(
|
Self::NoCommand => write!(
|
||||||
f,
|
f,
|
||||||
"no command to run; specify a command via config, entrypoint, or pass one after --"
|
"no command to run; specify a command via config, entrypoint, or pass one after --"
|
||||||
|
|||||||
+8
-2
@@ -35,14 +35,20 @@ impl EnvEntry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct BindSpec {
|
||||||
|
pub source: PathBuf,
|
||||||
|
pub target: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct SandboxConfig {
|
pub struct SandboxConfig {
|
||||||
pub mode: SandboxMode,
|
pub mode: SandboxMode,
|
||||||
pub hardened: bool,
|
pub hardened: bool,
|
||||||
pub unshare_net: bool,
|
pub unshare_net: bool,
|
||||||
pub seccomp: bool,
|
pub seccomp: bool,
|
||||||
pub env_filter: bool,
|
pub env_filter: bool,
|
||||||
pub extra_rw: Vec<PathBuf>,
|
pub extra_rw: Vec<BindSpec>,
|
||||||
pub extra_ro: Vec<PathBuf>,
|
pub extra_ro: Vec<BindSpec>,
|
||||||
pub mask: Vec<PathBuf>,
|
pub mask: Vec<PathBuf>,
|
||||||
pub env: Vec<EnvEntry>,
|
pub env: Vec<EnvEntry>,
|
||||||
pub unsetenv: Vec<String>,
|
pub unsetenv: Vec<String>,
|
||||||
|
|||||||
+17
-11
@@ -5,7 +5,7 @@ use crate::agents;
|
|||||||
use crate::blacklist;
|
use crate::blacklist;
|
||||||
use crate::env;
|
use crate::env;
|
||||||
use crate::seccomp;
|
use crate::seccomp;
|
||||||
use crate::{EnvEntry, SandboxConfig, SandboxError, SandboxMode};
|
use crate::{BindSpec, EnvEntry, SandboxConfig, SandboxError, SandboxMode};
|
||||||
|
|
||||||
pub fn build_command(config: &SandboxConfig) -> Result<Command, SandboxError> {
|
pub fn build_command(config: &SandboxConfig) -> Result<Command, SandboxError> {
|
||||||
let mut cmd = Command::new("bwrap");
|
let mut cmd = Command::new("bwrap");
|
||||||
@@ -28,12 +28,12 @@ pub fn build_command(config: &SandboxConfig) -> Result<Command, SandboxError> {
|
|||||||
cmd.arg("--bind-try").arg(&path).arg(&path);
|
cmd.arg("--bind-try").arg(&path).arg(&path);
|
||||||
}
|
}
|
||||||
|
|
||||||
for path in &config.extra_ro {
|
for spec in &config.extra_ro {
|
||||||
add_ro_bind(&mut cmd, path)?;
|
add_ro_bind(&mut cmd, spec)?;
|
||||||
}
|
}
|
||||||
add_rw_bind(&mut cmd, &config.chdir)?;
|
add_rw_bind_path(&mut cmd, &config.chdir)?;
|
||||||
for path in &config.extra_rw {
|
for spec in &config.extra_rw {
|
||||||
add_rw_bind(&mut cmd, path)?;
|
add_rw_bind(&mut cmd, spec)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
add_env_policy(&mut cmd, config);
|
add_env_policy(&mut cmd, config);
|
||||||
@@ -220,15 +220,21 @@ fn ro_bind_under_tmpfs(cmd: &mut Command, base: &str, paths: &[&str]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_rw_bind(cmd: &mut Command, path: &Path) -> Result<(), SandboxError> {
|
fn add_rw_bind(cmd: &mut Command, spec: &BindSpec) -> Result<(), SandboxError> {
|
||||||
let source = resolve_bind_source(path)?;
|
let source = resolve_bind_source(&spec.source)?;
|
||||||
cmd.arg("--bind").arg(source).arg(path);
|
cmd.arg("--bind").arg(source).arg(&spec.target);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_ro_bind(cmd: &mut Command, path: &Path) -> Result<(), SandboxError> {
|
fn add_ro_bind(cmd: &mut Command, spec: &BindSpec) -> Result<(), SandboxError> {
|
||||||
|
let source = resolve_bind_source(&spec.source)?;
|
||||||
|
cmd.arg("--ro-bind").arg(source).arg(&spec.target);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_rw_bind_path(cmd: &mut Command, path: &Path) -> Result<(), SandboxError> {
|
||||||
let source = resolve_bind_source(path)?;
|
let source = resolve_bind_source(path)?;
|
||||||
cmd.arg("--ro-bind").arg(source).arg(path);
|
cmd.arg("--bind").arg(source).arg(path);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -210,6 +210,30 @@ fn extra_rw_mount() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ro_mount_with_remapped_target() {
|
||||||
|
let dir = TempDir::new().expect("failed to create temp dir");
|
||||||
|
fs::write(dir.path().join("hello.txt"), "hi").expect("failed to write test file");
|
||||||
|
let src_str = dir.path().to_str().unwrap();
|
||||||
|
let target = "/tmp/agent-sandbox-remap-test";
|
||||||
|
|
||||||
|
let output = sandbox(&["--ro", &format!("{src_str}:{target}")])
|
||||||
|
.args([
|
||||||
|
"--",
|
||||||
|
"bash",
|
||||||
|
"-c",
|
||||||
|
&format!("cat {target}/hello.txt && ls {src_str} 2>&1 | head -1 || echo src_hidden"),
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.expect("agent-sandbox binary failed to execute");
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
assert!(
|
||||||
|
stdout.contains("hi"),
|
||||||
|
"expected file content 'hi' at remapped target, got: {stdout}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rw_refines_ro_parent() {
|
fn rw_refines_ro_parent() {
|
||||||
let parent = TempDir::new().expect("failed to create temp dir");
|
let parent = TempDir::new().expect("failed to create temp dir");
|
||||||
|
|||||||
Reference in New Issue
Block a user