Accept SRC:DST remap syntax in --ro/--rw

This commit is contained in:
2026-04-22 21:51:00 +02:00
parent 06bb638737
commit 305ac9d927
7 changed files with 250 additions and 59 deletions
+6 -6
View File
@@ -50,13 +50,13 @@ pub struct Args {
#[arg(long, overrides_with = "env_filter")]
pub no_env_filter: bool,
/// Bind an extra path read-write (repeatable)
#[arg(long = "rw", value_name = "PATH", action = clap::ArgAction::Append)]
pub extra_rw: Vec<PathBuf>,
/// Bind an extra path read-write (repeatable). Use SRC:DST to mount at a different target.
#[arg(long = "rw", value_name = "SRC[:DST]", action = clap::ArgAction::Append)]
pub extra_rw: Vec<String>,
/// Bind an extra path read-only (repeatable)
#[arg(long = "ro", value_name = "PATH", action = clap::ArgAction::Append)]
pub extra_ro: Vec<PathBuf>,
/// Bind an extra path read-only (repeatable). Use SRC:DST to mount at a different target.
#[arg(long = "ro", value_name = "SRC[:DST]", action = clap::ArgAction::Append)]
pub extra_ro: Vec<String>,
/// Print the bwrap command without executing
#[arg(long, overrides_with = "no_dry_run")]
+190 -40
View File
@@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
use serde::Deserialize;
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> {
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,
),
chdir: resolve_chdir(args.chdir, profile.chdir, globals.chdir)?,
extra_rw: merge_paths(args.extra_rw, &profile.rw, &globals.rw)?,
extra_ro: merge_paths(args.extra_ro, &profile.ro, &globals.ro)?,
extra_rw: merge_bind_specs(args.extra_rw, &profile.rw, &globals.rw)?,
extra_ro: merge_bind_specs(args.extra_ro, &profile.ro, &globals.ro)?,
mask: merge_vecs(args.mask, &profile.mask, &globals.mask),
env,
unsetenv,
@@ -138,22 +138,55 @@ fn resolve_chdir(
}
}
fn merge_paths(
cli: Vec<PathBuf>,
profile: &[PathBuf],
globals: &[PathBuf],
) -> Result<Vec<PathBuf>, SandboxError> {
let config_paths = globals.iter().chain(profile).cloned();
let cli_paths = cli
.into_iter()
.map(prepare_cli_bind_path)
.collect::<Result<Vec<_>, _>>()?;
Ok(config_paths.chain(cli_paths).collect())
fn merge_bind_specs(
cli: Vec<String>,
profile: &[String],
globals: &[String],
) -> Result<Vec<BindSpec>, SandboxError> {
let config_specs = globals
.iter()
.chain(profile)
.map(|raw| resolve_config_bind_spec(raw));
let cli_specs = cli.iter().map(|raw| resolve_cli_bind_spec(raw));
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 =
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() {
return Err(SandboxError::PathMissing(absolute));
}
@@ -314,9 +347,9 @@ pub struct Options {
pub dry_run: Option<bool>,
pub chdir: Option<PathBuf>,
#[serde(default)]
pub rw: Vec<PathBuf>,
pub rw: Vec<String>,
#[serde(default)]
pub ro: Vec<PathBuf>,
pub ro: Vec<String>,
#[serde(default)]
pub mask: Vec<PathBuf>,
#[serde(default)]
@@ -329,12 +362,6 @@ pub struct Options {
impl Options {
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 {
self.chdir = Some(expand_and_canonicalize(chdir)?);
}
@@ -722,13 +749,13 @@ mod tests {
fn build_rw_paths_accumulate() {
let file_config = FileConfig {
options: Options {
rw: vec![PathBuf::from("/tmp")],
rw: vec!["/tmp".into()],
..Options::default()
},
profile: HashMap::from([(
"extra".into(),
Options {
rw: vec![PathBuf::from("/usr")],
rw: vec!["/usr".into()],
..Options::default()
},
)]),
@@ -736,7 +763,7 @@ mod tests {
};
let args = Args {
profile: Some("extra".into()),
extra_rw: vec![PathBuf::from("/var")],
extra_rw: vec!["/var".into()],
..args_with_command()
};
let config = build(args, Some(file_config)).unwrap();
@@ -885,13 +912,13 @@ mod tests {
let home = std::env::var("HOME").unwrap();
let file_config = FileConfig {
options: Options {
rw: vec![PathBuf::from("~/")],
rw: vec!["~/".into()],
..Options::default()
},
..FileConfig::default()
};
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]
@@ -902,17 +929,18 @@ mod tests {
std::fs::write(&target, "x").unwrap();
std::os::unix::fs::symlink(&target, &link).unwrap();
let link_str = link.to_str().unwrap().to_string();
let file_config = FileConfig {
options: Options {
ro: vec![link.clone()],
rw: vec![link.clone()],
ro: vec![link_str.clone()],
rw: vec![link_str],
..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]);
assert_eq!(config.extra_ro, vec![same_bind(&link)]);
assert_eq!(config.extra_rw, vec![same_bind(&link)]);
}
#[test]
@@ -923,21 +951,22 @@ mod tests {
std::fs::write(&target, "x").unwrap();
std::os::unix::fs::symlink(&target, &link).unwrap();
let link_str = link.to_str().unwrap().to_string();
let args = Args {
extra_ro: vec![link.clone()],
extra_rw: vec![link.clone()],
extra_ro: vec![link_str.clone()],
extra_rw: vec![link_str],
..args_with_command()
};
let config = build(args, None).unwrap();
assert_eq!(config.extra_ro, vec![link.clone()]);
assert_eq!(config.extra_rw, vec![link]);
assert_eq!(config.extra_ro, vec![same_bind(&link)]);
assert_eq!(config.extra_rw, vec![same_bind(&link)]);
}
#[test]
fn build_relative_config_path_rejected() {
let file_config = FileConfig {
options: Options {
rw: vec![PathBuf::from("relative/path")],
rw: vec!["relative/path".into()],
..Options::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]
fn build_chdir_from_config() {
let file_config = FileConfig {
@@ -1281,11 +1424,18 @@ mod tests {
assert!(matches!(result, Err(SandboxError::NoCommand)));
}
fn assert_paths(actual: &[PathBuf], expected: &[&str]) {
let expected: Vec<PathBuf> = expected.iter().map(PathBuf::from).collect();
fn assert_paths(actual: &[String], expected: &[&str]) {
let expected: Vec<String> = expected.iter().map(|s| s.to_string()).collect();
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]) {
let actual = cmd.clone().unwrap().into_vec();
let expected: Vec<String> = expected.iter().map(|s| s.to_string()).collect();
+4
View File
@@ -27,6 +27,7 @@ pub enum SandboxError {
UnknownConfigKey(String),
ConfigPathNotAbsolute(PathBuf),
InvalidBwrapArg(String),
InvalidBindSpec(String),
NoCommand,
Seccomp(String),
SeccompUnsupportedArch(String),
@@ -80,6 +81,9 @@ impl std::fmt::Display for SandboxError {
Self::InvalidBwrapArg(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!(
f,
"no command to run; specify a command via config, entrypoint, or pass one after --"
+8 -2
View File
@@ -35,14 +35,20 @@ impl EnvEntry {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BindSpec {
pub source: PathBuf,
pub target: PathBuf,
}
pub struct SandboxConfig {
pub mode: SandboxMode,
pub hardened: bool,
pub unshare_net: bool,
pub seccomp: bool,
pub env_filter: bool,
pub extra_rw: Vec<PathBuf>,
pub extra_ro: Vec<PathBuf>,
pub extra_rw: Vec<BindSpec>,
pub extra_ro: Vec<BindSpec>,
pub mask: Vec<PathBuf>,
pub env: Vec<EnvEntry>,
pub unsetenv: Vec<String>,
+17 -11
View File
@@ -5,7 +5,7 @@ use crate::agents;
use crate::blacklist;
use crate::env;
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> {
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);
}
for path in &config.extra_ro {
add_ro_bind(&mut cmd, path)?;
for spec in &config.extra_ro {
add_ro_bind(&mut cmd, spec)?;
}
add_rw_bind(&mut cmd, &config.chdir)?;
for path in &config.extra_rw {
add_rw_bind(&mut cmd, path)?;
add_rw_bind_path(&mut cmd, &config.chdir)?;
for spec in &config.extra_rw {
add_rw_bind(&mut cmd, spec)?;
}
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> {
let source = resolve_bind_source(path)?;
cmd.arg("--bind").arg(source).arg(path);
fn add_rw_bind(cmd: &mut Command, spec: &BindSpec) -> Result<(), SandboxError> {
let source = resolve_bind_source(&spec.source)?;
cmd.arg("--bind").arg(source).arg(&spec.target);
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)?;
cmd.arg("--ro-bind").arg(source).arg(path);
cmd.arg("--bind").arg(source).arg(path);
Ok(())
}