Accept SRC:DST remap syntax in --ro/--rw
This commit is contained in:
+6
-6
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user