Add mask option to hide paths/files from sandbox
This commit is contained in:
@@ -54,6 +54,10 @@ pub struct Args {
|
||||
#[arg(long)]
|
||||
pub no_config: bool,
|
||||
|
||||
/// Hide a path inside the sandbox with a tmpfs overlay (repeatable)
|
||||
#[arg(long = "mask", value_name = "PATH", action = clap::ArgAction::Append)]
|
||||
pub mask: Vec<PathBuf>,
|
||||
|
||||
/// Command and arguments to run inside the sandbox
|
||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||
pub command_and_args: Vec<OsString>,
|
||||
|
||||
@@ -37,6 +37,7 @@ pub fn build(args: Args, file_config: Option<FileConfig>) -> Result<SandboxConfi
|
||||
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)?,
|
||||
mask: merge_mask(args.mask, &profile.mask, &globals.mask),
|
||||
command,
|
||||
command_args,
|
||||
})
|
||||
@@ -98,6 +99,10 @@ fn merge_paths(
|
||||
Ok(config_paths.chain(cli_paths).collect())
|
||||
}
|
||||
|
||||
fn merge_mask(cli: Vec<PathBuf>, profile: &[PathBuf], globals: &[PathBuf]) -> Vec<PathBuf> {
|
||||
globals.iter().chain(profile).cloned().chain(cli).collect()
|
||||
}
|
||||
|
||||
fn resolve_command(
|
||||
mut positional: Vec<OsString>,
|
||||
profile_cmd: Option<CommandValue>,
|
||||
@@ -180,6 +185,8 @@ pub struct Options {
|
||||
pub rw: Vec<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub ro: Vec<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub mask: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl Options {
|
||||
@@ -193,10 +200,19 @@ impl Options {
|
||||
if let Some(ref chdir) = self.chdir {
|
||||
self.chdir = Some(expand_and_canonicalize(chdir)?);
|
||||
}
|
||||
for p in &mut self.mask {
|
||||
*p = expand_and_require_absolute(p)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn expand_and_require_absolute(path: &Path) -> Result<PathBuf, SandboxError> {
|
||||
let expanded = expand_tilde(path)?;
|
||||
require_absolute(&expanded)?;
|
||||
Ok(expanded)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum CommandValue {
|
||||
@@ -531,6 +547,72 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_tilde_expansion_in_config_paths() {
|
||||
let home = std::env::var("HOME").unwrap();
|
||||
let file_config = FileConfig {
|
||||
options: Options {
|
||||
rw: vec![PathBuf::from("~/")],
|
||||
..Options::default()
|
||||
},
|
||||
..FileConfig::default()
|
||||
};
|
||||
let config = build(args_with_command(), Some(file_config)).unwrap();
|
||||
assert_eq!(config.extra_rw, vec![PathBuf::from(&home)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_relative_config_path_rejected() {
|
||||
let file_config = FileConfig {
|
||||
options: Options {
|
||||
rw: vec![PathBuf::from("relative/path")],
|
||||
..Options::default()
|
||||
},
|
||||
..FileConfig::default()
|
||||
};
|
||||
assert!(matches!(
|
||||
build(args_with_command(), Some(file_config)),
|
||||
Err(SandboxError::ConfigPathNotAbsolute(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_chdir_from_config() {
|
||||
let file_config = FileConfig {
|
||||
options: Options {
|
||||
chdir: Some(PathBuf::from("/tmp")),
|
||||
..Options::default()
|
||||
},
|
||||
..FileConfig::default()
|
||||
};
|
||||
let config = build(args_with_command(), Some(file_config)).unwrap();
|
||||
assert_eq!(config.chdir, std::fs::canonicalize("/tmp").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_mask_accumulates() {
|
||||
let file_config = FileConfig {
|
||||
options: Options {
|
||||
mask: vec![PathBuf::from("/tmp/a")],
|
||||
..Options::default()
|
||||
},
|
||||
profile: HashMap::from([(
|
||||
"extra".into(),
|
||||
Options {
|
||||
mask: vec![PathBuf::from("/tmp/b")],
|
||||
..Options::default()
|
||||
},
|
||||
)]),
|
||||
};
|
||||
let args = Args {
|
||||
profile: Some("extra".into()),
|
||||
mask: vec![PathBuf::from("/tmp/c")],
|
||||
..args_with_command()
|
||||
};
|
||||
let config = build(args, Some(file_config)).unwrap();
|
||||
assert_eq!(config.mask.len(), 3);
|
||||
}
|
||||
|
||||
fn assert_paths(actual: &[PathBuf], expected: &[&str]) {
|
||||
let expected: Vec<PathBuf> = expected.iter().map(PathBuf::from).collect();
|
||||
assert_eq!(actual, &expected);
|
||||
|
||||
@@ -25,6 +25,7 @@ pub struct SandboxConfig {
|
||||
pub no_net: bool,
|
||||
pub extra_rw: Vec<PathBuf>,
|
||||
pub extra_ro: Vec<PathBuf>,
|
||||
pub mask: Vec<PathBuf>,
|
||||
pub command: PathBuf,
|
||||
pub command_args: Vec<OsString>,
|
||||
pub chdir: PathBuf,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use crate::agents;
|
||||
@@ -39,6 +39,8 @@ pub fn build_command(config: &SandboxConfig) -> Result<Command, SandboxError> {
|
||||
cmd.arg("--die-with-parent");
|
||||
cmd.arg("--chdir").arg(&config.chdir);
|
||||
|
||||
apply_masks(&mut cmd, &config.mask);
|
||||
|
||||
cmd.arg("--")
|
||||
.arg(&config.command)
|
||||
.args(&config.command_args);
|
||||
@@ -46,6 +48,16 @@ pub fn build_command(config: &SandboxConfig) -> Result<Command, SandboxError> {
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
fn apply_masks(cmd: &mut Command, masks: &[PathBuf]) {
|
||||
for path in masks {
|
||||
if path.is_file() {
|
||||
cmd.arg("--ro-bind").arg("/dev/null").arg(path);
|
||||
} else {
|
||||
cmd.arg("--tmpfs").arg(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_blacklist_mode(cmd: &mut Command) -> Result<(), SandboxError> {
|
||||
let ctx = blacklist::resolve_path_context()?;
|
||||
cmd.args(["--ro-bind", "/", "/"]);
|
||||
|
||||
Reference in New Issue
Block a user