Optionally back /tmp and /var/tmp with stable host directories
This commit is contained in:
+12
@@ -66,6 +66,18 @@ pub struct Args {
|
||||
#[arg(long, overrides_with = "dry_run")]
|
||||
pub no_dry_run: bool,
|
||||
|
||||
/// Back /tmp and /var/tmp with persistent host directories
|
||||
#[arg(long, overrides_with = "no_persistent_tmp")]
|
||||
pub persistent_tmp: bool,
|
||||
|
||||
/// Disable persistent /tmp (overrides config-file `persistent-tmp = true`)
|
||||
#[arg(long, overrides_with = "persistent_tmp")]
|
||||
pub no_persistent_tmp: bool,
|
||||
|
||||
/// Override the derived session key with an explicit label
|
||||
#[arg(long = "persistent-key", value_name = "LABEL")]
|
||||
pub persistent_key: Option<String>,
|
||||
|
||||
/// Working directory inside the sandbox (default: current directory)
|
||||
#[arg(long, value_name = "PATH")]
|
||||
pub chdir: Option<PathBuf>,
|
||||
|
||||
+93
-27
@@ -5,6 +5,7 @@ use std::path::{Path, PathBuf};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::cli::Args;
|
||||
use crate::session_key::SessionKey;
|
||||
use crate::{BindSpec, EnvEntry, SandboxConfig, SandboxError, SandboxMode};
|
||||
|
||||
pub fn build(args: Args, file_config: Option<FileConfig>) -> Result<SandboxConfig, SandboxError> {
|
||||
@@ -13,72 +14,120 @@ pub fn build(args: Args, file_config: Option<FileConfig>) -> Result<SandboxConfi
|
||||
let profile = c.resolve_profile(args.profile.as_deref())?;
|
||||
(c.options, profile)
|
||||
}
|
||||
None => (Options::default(), Options::default()),
|
||||
None => (
|
||||
Options::default(),
|
||||
ResolvedProfile {
|
||||
name: args.profile.clone(),
|
||||
options: Options::default(),
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
validate_mode(&globals)?;
|
||||
validate_mode(&profile)?;
|
||||
validate_mode(&profile.options)?;
|
||||
globals.validate_paths()?;
|
||||
profile.validate_paths()?;
|
||||
profile.options.validate_paths()?;
|
||||
|
||||
let (command, command_args) =
|
||||
resolve_command(args.entrypoint, args.command_and_args, &profile, &globals)?;
|
||||
let mode = merge_mode(args.blacklist, args.whitelist, &profile.options, &globals);
|
||||
let chdir = resolve_chdir(
|
||||
args.chdir.clone(),
|
||||
profile.options.chdir.clone(),
|
||||
globals.chdir.clone(),
|
||||
)?;
|
||||
let persistent_tmp_key = resolve_persistent_tmp_key(&args, &profile, &globals, &mode, &chdir);
|
||||
|
||||
let (command, command_args) = resolve_command(
|
||||
args.entrypoint,
|
||||
args.command_and_args,
|
||||
&profile.options,
|
||||
&globals,
|
||||
)?;
|
||||
let command = resolve_binary(&command)
|
||||
.ok_or_else(|| SandboxError::CommandNotFound(PathBuf::from(&command)))?;
|
||||
|
||||
let env = dedupe_env_last_wins(parse_env_entries(&merge_vecs(
|
||||
args.env,
|
||||
&profile.env,
|
||||
&profile.options.env,
|
||||
&globals.env,
|
||||
))?);
|
||||
let unsetenv = merge_vecs(args.unsetenv, &profile.unsetenv, &globals.unsetenv);
|
||||
let unsetenv = merge_vecs(args.unsetenv, &profile.options.unsetenv, &globals.unsetenv);
|
||||
reject_env_key_conflicts(&env, &unsetenv)?;
|
||||
|
||||
Ok(SandboxConfig {
|
||||
mode: merge_mode(args.blacklist, args.whitelist, &profile, &globals),
|
||||
mode,
|
||||
hardened: merge_flag(
|
||||
merge_flag_pair(args.hardened, args.no_hardened),
|
||||
profile.hardened,
|
||||
profile.options.hardened,
|
||||
globals.hardened,
|
||||
),
|
||||
unshare_net: merge_flag(
|
||||
merge_flag_pair(args.unshare_net, args.share_net),
|
||||
profile.unshare_net,
|
||||
profile.options.unshare_net,
|
||||
globals.unshare_net,
|
||||
),
|
||||
seccomp: merge_flag_with_default(
|
||||
merge_flag_pair(args.seccomp, args.no_seccomp),
|
||||
profile.seccomp,
|
||||
profile.options.seccomp,
|
||||
globals.seccomp,
|
||||
true,
|
||||
),
|
||||
env_filter: merge_flag_with_default(
|
||||
merge_flag_pair(args.env_filter, args.no_env_filter),
|
||||
profile.env_filter,
|
||||
profile.options.env_filter,
|
||||
globals.env_filter,
|
||||
true,
|
||||
),
|
||||
dry_run: merge_flag(
|
||||
merge_flag_pair(args.dry_run, args.no_dry_run),
|
||||
profile.dry_run,
|
||||
profile.options.dry_run,
|
||||
globals.dry_run,
|
||||
),
|
||||
chdir: resolve_chdir(args.chdir, profile.chdir, globals.chdir)?,
|
||||
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),
|
||||
chdir,
|
||||
extra_rw: merge_bind_specs(args.extra_rw, &profile.options.rw, &globals.rw)?,
|
||||
extra_ro: merge_bind_specs(args.extra_ro, &profile.options.ro, &globals.ro)?,
|
||||
mask: merge_vecs(args.mask, &profile.options.mask, &globals.mask),
|
||||
env,
|
||||
unsetenv,
|
||||
bwrap_args: split_bwrap_args(merge_vecs(
|
||||
args.bwrap_args,
|
||||
&profile.bwrap_args,
|
||||
&profile.options.bwrap_args,
|
||||
&globals.bwrap_args,
|
||||
))?,
|
||||
command,
|
||||
command_args,
|
||||
persistent_tmp_key,
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_persistent_tmp_key(
|
||||
args: &Args,
|
||||
profile: &ResolvedProfile,
|
||||
globals: &Options,
|
||||
mode: &SandboxMode,
|
||||
chdir: &Path,
|
||||
) -> Option<String> {
|
||||
if matches!(mode, SandboxMode::Blacklist) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let enabled = merge_flag(
|
||||
merge_flag_pair(args.persistent_tmp, args.no_persistent_tmp),
|
||||
profile.options.persistent_tmp,
|
||||
globals.persistent_tmp,
|
||||
);
|
||||
if !enabled {
|
||||
return None;
|
||||
}
|
||||
let label = args
|
||||
.persistent_key
|
||||
.clone()
|
||||
.or_else(|| profile.options.persistent_key.clone())
|
||||
.or_else(|| globals.persistent_key.clone());
|
||||
Some(
|
||||
label.unwrap_or_else(|| SessionKey::default().derive(profile.name.as_deref(), mode, chdir)),
|
||||
)
|
||||
}
|
||||
|
||||
fn merge_mode(
|
||||
cli_blacklist: bool,
|
||||
cli_whitelist: bool,
|
||||
@@ -297,6 +346,12 @@ pub struct FileConfig {
|
||||
_unknown: HashMap<String, toml::Value>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ResolvedProfile {
|
||||
pub name: Option<String>,
|
||||
pub options: Options,
|
||||
}
|
||||
|
||||
impl FileConfig {
|
||||
pub fn load(path: &Path) -> Result<Self, SandboxError> {
|
||||
Self::load_file(path)?.load_extra()
|
||||
@@ -364,15 +419,22 @@ impl FileConfig {
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn resolve_profile(&self, selected: Option<&str>) -> Result<Options, SandboxError> {
|
||||
match selected.or(self.options.profile.as_deref()) {
|
||||
Some(leaf) => self.resolve_chain(leaf),
|
||||
None => Ok(Options::default()),
|
||||
}
|
||||
fn resolve_profile(&self, selected: Option<&str>) -> Result<ResolvedProfile, SandboxError> {
|
||||
let Some(name) = selected
|
||||
.map(String::from)
|
||||
.or_else(|| self.options.profile.clone())
|
||||
else {
|
||||
return Ok(ResolvedProfile::default());
|
||||
};
|
||||
let options = self.resolve_chain(&name)?;
|
||||
Ok(ResolvedProfile {
|
||||
name: Some(name),
|
||||
options,
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_chain(&self, leaf: &str) -> Result<Options, SandboxError> {
|
||||
let chain = self.collect_chain(leaf)?;
|
||||
fn resolve_chain(&self, name: &str) -> Result<Options, SandboxError> {
|
||||
let chain = self.collect_chain(name)?;
|
||||
|
||||
let merged = chain
|
||||
.into_iter()
|
||||
@@ -382,10 +444,10 @@ impl FileConfig {
|
||||
Ok(merged)
|
||||
}
|
||||
|
||||
fn collect_chain(&self, leaf: &str) -> Result<Vec<Options>, SandboxError> {
|
||||
fn collect_chain(&self, name: &str) -> Result<Vec<Options>, SandboxError> {
|
||||
let mut visited: Vec<String> = Vec::new();
|
||||
let mut chain: Vec<Options> = Vec::new();
|
||||
let mut current = leaf.to_string();
|
||||
let mut current = name.to_string();
|
||||
|
||||
loop {
|
||||
if visited.iter().any(|seen| seen == ¤t) {
|
||||
@@ -424,6 +486,8 @@ pub struct Options {
|
||||
pub entrypoint: Option<CommandValue>,
|
||||
pub command: Option<CommandValue>,
|
||||
pub dry_run: Option<bool>,
|
||||
pub persistent_tmp: Option<bool>,
|
||||
pub persistent_key: Option<String>,
|
||||
pub chdir: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub rw: Vec<String>,
|
||||
@@ -453,6 +517,8 @@ impl Options {
|
||||
entrypoint: extra.entrypoint.or(self.entrypoint),
|
||||
command: extra.command.or(self.command),
|
||||
dry_run: extra.dry_run.or(self.dry_run),
|
||||
persistent_tmp: extra.persistent_tmp.or(self.persistent_tmp),
|
||||
persistent_key: extra.persistent_key.or(self.persistent_key),
|
||||
chdir: extra.chdir.or(self.chdir),
|
||||
rw: append(self.rw, extra.rw),
|
||||
ro: append(self.ro, extra.ro),
|
||||
|
||||
@@ -33,6 +33,7 @@ pub enum SandboxError {
|
||||
NoCommand,
|
||||
Seccomp(String),
|
||||
SeccompUnsupportedArch(String),
|
||||
PersistentTmpDir(PathBuf, String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SandboxError {
|
||||
@@ -107,6 +108,9 @@ impl std::fmt::Display for SandboxError {
|
||||
f,
|
||||
"seccomp filtering is not supported on this architecture: {arch} (use --no-seccomp to disable)"
|
||||
),
|
||||
Self::PersistentTmpDir(path, reason) => {
|
||||
write!(f, "persistent-tmp directory '{}': {reason}", path.display())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-11
@@ -4,17 +4,20 @@ pub mod cli;
|
||||
pub mod config;
|
||||
mod env;
|
||||
mod errors;
|
||||
mod persistent_tmp;
|
||||
mod preflight;
|
||||
mod sandbox;
|
||||
mod seccomp;
|
||||
mod session_key;
|
||||
|
||||
pub use errors::SandboxError;
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::fs;
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::sandbox::Bwrap;
|
||||
|
||||
pub enum SandboxMode {
|
||||
Blacklist,
|
||||
Whitelist,
|
||||
@@ -57,6 +60,7 @@ pub struct SandboxConfig {
|
||||
pub command_args: Vec<OsString>,
|
||||
pub chdir: PathBuf,
|
||||
pub dry_run: bool,
|
||||
pub persistent_tmp_key: Option<String>,
|
||||
}
|
||||
|
||||
pub fn require_home() -> Result<String, SandboxError> {
|
||||
@@ -87,19 +91,12 @@ fn resolve_run_user_from_proc() -> Option<String> {
|
||||
pub fn run(config: SandboxConfig) -> Result<(), SandboxError> {
|
||||
preflight::check(&config)?;
|
||||
|
||||
let mut cmd = sandbox::build_command(&config)?;
|
||||
let bwrap = Bwrap::build(&config)?;
|
||||
|
||||
if config.dry_run {
|
||||
println!("{}", shell_quote_command(&cmd));
|
||||
println!("{}", bwrap.shell_quoted());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(SandboxError::Io(cmd.exec()))
|
||||
}
|
||||
|
||||
fn shell_quote_command(cmd: &std::process::Command) -> String {
|
||||
let prog = cmd.get_program().to_string_lossy();
|
||||
let args = cmd.get_args().map(|a| a.to_string_lossy());
|
||||
let all: Vec<_> = std::iter::once(prog).chain(args).collect();
|
||||
shlex::try_join(all.iter().map(|s| s.as_ref())).unwrap()
|
||||
Err(SandboxError::Io(bwrap.exec()))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
use std::fs::{DirBuilder, File, OpenOptions};
|
||||
use std::io;
|
||||
use std::os::fd::{AsRawFd, OwnedFd};
|
||||
use std::os::unix::fs::{DirBuilderExt, MetadataExt, OpenOptionsExt};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::SandboxConfig;
|
||||
use crate::errors::SandboxError;
|
||||
|
||||
pub struct PersistentTmpDirs {
|
||||
pub tmp: PathBuf,
|
||||
pub var_tmp: PathBuf,
|
||||
// Held to keep `/proc/self/fd/<n>` bind sources valid through `exec()`.
|
||||
_keep_fds: Vec<OwnedFd>,
|
||||
}
|
||||
|
||||
impl PersistentTmpDirs {
|
||||
pub fn resolve(config: &SandboxConfig) -> Result<Option<Self>, SandboxError> {
|
||||
let Some(key) = &config.persistent_tmp_key else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let tmp = PersistentTmpDir::new(format!("/tmp/agent-sandbox-{key}"));
|
||||
let var_tmp = PersistentTmpDir::new(format!("/var/tmp/agent-sandbox-{key}"));
|
||||
|
||||
if config.dry_run {
|
||||
return Ok(Some(Self {
|
||||
tmp: tmp.host_path,
|
||||
var_tmp: var_tmp.host_path,
|
||||
_keep_fds: Vec::new(),
|
||||
}));
|
||||
}
|
||||
|
||||
let tmp_fd = tmp.open()?;
|
||||
let var_tmp_fd = var_tmp.open()?;
|
||||
Ok(Some(Self {
|
||||
tmp: format!("/proc/self/fd/{}", tmp_fd.as_raw_fd()).into(),
|
||||
var_tmp: format!("/proc/self/fd/{}", var_tmp_fd.as_raw_fd()).into(),
|
||||
_keep_fds: vec![tmp_fd, var_tmp_fd],
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PersistentTmpDir {
|
||||
pub host_path: PathBuf,
|
||||
}
|
||||
|
||||
impl PersistentTmpDir {
|
||||
pub fn new(path: impl Into<PathBuf>) -> Self {
|
||||
Self {
|
||||
host_path: path.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open(&self) -> Result<OwnedFd, SandboxError> {
|
||||
self.create_if_missing()?;
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.custom_flags(libc::O_NOFOLLOW | libc::O_DIRECTORY)
|
||||
.open(&self.host_path)
|
||||
.map_err(|e| self.error(format!("open: {e}")))?;
|
||||
self.verify_owner(&file)?;
|
||||
let fd: OwnedFd = file.into();
|
||||
clear_cloexec(&fd).map_err(|e| self.error(format!("clear FD_CLOEXEC: {e}")))?;
|
||||
Ok(fd)
|
||||
}
|
||||
|
||||
fn create_if_missing(&self) -> Result<(), SandboxError> {
|
||||
match DirBuilder::new().mode(0o700).create(&self.host_path) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Ok(()),
|
||||
Err(e) => Err(self.error(format!("create: {e}"))),
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_owner(&self, file: &File) -> Result<(), SandboxError> {
|
||||
let meta = file
|
||||
.metadata()
|
||||
.map_err(|e| self.error(format!("stat: {e}")))?;
|
||||
let euid = unsafe { libc::geteuid() };
|
||||
if meta.uid() != euid {
|
||||
return Err(self.error(format!("owned by uid {} (expected {euid})", meta.uid())));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn error(&self, reason: String) -> SandboxError {
|
||||
SandboxError::PersistentTmpDir(self.host_path.clone(), reason)
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_cloexec(fd: &OwnedFd) -> io::Result<()> {
|
||||
let raw = fd.as_raw_fd();
|
||||
let flags = unsafe { libc::fcntl(raw, libc::F_GETFD) };
|
||||
if flags < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
if unsafe { libc::fcntl(raw, libc::F_SETFD, flags & !libc::FD_CLOEXEC) } < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use std::os::unix::fs::{PermissionsExt, symlink};
|
||||
use std::process::Command;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn creates_dir_on_first_open_with_mode_0700() {
|
||||
let scratch = TempDir::new().unwrap();
|
||||
let dir = PersistentTmpDir::new(scratch.path().join("new"));
|
||||
let _fd = dir.open().unwrap();
|
||||
let mode = fs::metadata(&dir.host_path).unwrap().permissions().mode() & 0o777;
|
||||
assert_eq!(mode, 0o700);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn second_open_preserves_contents() {
|
||||
let scratch = TempDir::new().unwrap();
|
||||
let dir = PersistentTmpDir::new(scratch.path().join("reuse"));
|
||||
let _fd1 = dir.open().unwrap();
|
||||
let marker = dir.host_path.join("marker");
|
||||
fs::write(&marker, "x").unwrap();
|
||||
let _fd2 = dir.open().unwrap();
|
||||
assert!(marker.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_regular_file_at_path() {
|
||||
let scratch = TempDir::new().unwrap();
|
||||
let path = scratch.path().join("plain-file");
|
||||
fs::write(&path, "x").unwrap();
|
||||
assert!(matches!(
|
||||
PersistentTmpDir::new(path).open(),
|
||||
Err(SandboxError::PersistentTmpDir(_, _))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_symlink_at_path() {
|
||||
let scratch = TempDir::new().unwrap();
|
||||
let real = scratch.path().join("real");
|
||||
fs::create_dir(&real).unwrap();
|
||||
let link = scratch.path().join("link");
|
||||
symlink(&real, &link).unwrap();
|
||||
assert!(matches!(
|
||||
PersistentTmpDir::new(link).open(),
|
||||
Err(SandboxError::PersistentTmpDir(_, _))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fd_survives_fork_and_exec() {
|
||||
let scratch = TempDir::new().unwrap();
|
||||
let dir = PersistentTmpDir::new(scratch.path().join("fd"));
|
||||
let fd = dir.open().unwrap();
|
||||
let raw = fd.as_raw_fd();
|
||||
let output = Command::new("bash")
|
||||
.arg("-c")
|
||||
.arg(format!("test -e /proc/self/fd/{raw}"))
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"fd {raw} should be inherited across exec; stderr={}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
}
|
||||
+55
-9
@@ -1,13 +1,38 @@
|
||||
use std::io;
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use crate::agents;
|
||||
use crate::blacklist;
|
||||
use crate::env;
|
||||
use crate::persistent_tmp::PersistentTmpDirs;
|
||||
use crate::seccomp;
|
||||
use crate::{BindSpec, EnvEntry, SandboxConfig, SandboxError, SandboxMode};
|
||||
|
||||
pub fn build_command(config: &SandboxConfig) -> Result<Command, SandboxError> {
|
||||
pub struct Bwrap {
|
||||
command: Command,
|
||||
_persistent: Option<PersistentTmpDirs>,
|
||||
}
|
||||
|
||||
impl Bwrap {
|
||||
pub fn build(config: &SandboxConfig) -> Result<Self, SandboxError> {
|
||||
build_command(config)
|
||||
}
|
||||
|
||||
pub fn exec(mut self) -> io::Error {
|
||||
self.command.exec()
|
||||
}
|
||||
|
||||
pub fn shell_quoted(&self) -> String {
|
||||
let prog = self.command.get_program().to_string_lossy();
|
||||
let args = self.command.get_args().map(|a| a.to_string_lossy());
|
||||
let all: Vec<_> = std::iter::once(prog).chain(args).collect();
|
||||
shlex::try_join(all.iter().map(|s| s.as_ref())).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn build_command(config: &SandboxConfig) -> Result<Bwrap, SandboxError> {
|
||||
let mut cmd = Command::new("bwrap");
|
||||
let hardened = config.hardened || matches!(config.mode, SandboxMode::Whitelist);
|
||||
|
||||
@@ -18,10 +43,17 @@ pub fn build_command(config: &SandboxConfig) -> Result<Command, SandboxError> {
|
||||
cmd.arg("--unshare-net");
|
||||
}
|
||||
|
||||
match config.mode {
|
||||
SandboxMode::Blacklist => add_blacklist_mode(&mut cmd)?,
|
||||
SandboxMode::Whitelist => add_whitelist_mode(&mut cmd)?,
|
||||
}
|
||||
let persistent = match config.mode {
|
||||
SandboxMode::Blacklist => {
|
||||
add_blacklist_mode(&mut cmd)?;
|
||||
None
|
||||
}
|
||||
SandboxMode::Whitelist => {
|
||||
let persistent = PersistentTmpDirs::resolve(config)?;
|
||||
add_whitelist_mode(&mut cmd, persistent.as_ref())?;
|
||||
persistent
|
||||
}
|
||||
};
|
||||
|
||||
for path in agents::agent_rw_paths() {
|
||||
cmd.arg("--bind-try").arg(&path).arg(&path);
|
||||
@@ -54,7 +86,10 @@ pub fn build_command(config: &SandboxConfig) -> Result<Command, SandboxError> {
|
||||
.arg(&config.command)
|
||||
.args(&config.command_args);
|
||||
|
||||
Ok(cmd)
|
||||
Ok(Bwrap {
|
||||
command: cmd,
|
||||
_persistent: persistent,
|
||||
})
|
||||
}
|
||||
|
||||
fn add_env_policy(cmd: &mut Command, config: &SandboxConfig) {
|
||||
@@ -140,7 +175,10 @@ fn add_blacklist_mode(cmd: &mut Command) -> Result<(), SandboxError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_whitelist_mode(cmd: &mut Command) -> Result<(), SandboxError> {
|
||||
fn add_whitelist_mode(
|
||||
cmd: &mut Command,
|
||||
persistent: Option<&PersistentTmpDirs>,
|
||||
) -> Result<(), SandboxError> {
|
||||
let home = crate::require_home()?;
|
||||
|
||||
cmd.args(["--ro-bind", "/usr", "/usr"]);
|
||||
@@ -178,8 +216,16 @@ fn add_whitelist_mode(cmd: &mut Command) -> Result<(), SandboxError> {
|
||||
let cache_dir = format!("{home}/.cache");
|
||||
cmd.arg("--tmpfs").arg(&cache_dir);
|
||||
|
||||
cmd.args(["--tmpfs", "/tmp"]);
|
||||
cmd.args(["--tmpfs", "/var/tmp"]);
|
||||
match persistent {
|
||||
Some(p) => {
|
||||
cmd.arg("--bind").arg(&p.tmp).arg("/tmp");
|
||||
cmd.arg("--bind").arg(&p.var_tmp).arg("/var/tmp");
|
||||
}
|
||||
None => {
|
||||
cmd.args(["--tmpfs", "/tmp"]);
|
||||
cmd.args(["--tmpfs", "/var/tmp"]);
|
||||
}
|
||||
}
|
||||
cmd.args(["--dev", "/dev"]);
|
||||
cmd.args(["--tmpfs", "/dev/shm"]);
|
||||
cmd.args(["--tmpfs", "/run"]);
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
use std::path::Path;
|
||||
|
||||
use crate::SandboxMode;
|
||||
|
||||
pub struct SessionKey {
|
||||
pub default_profile: &'static str,
|
||||
pub hex_len: usize,
|
||||
}
|
||||
|
||||
impl Default for SessionKey {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
default_profile: "<default>",
|
||||
hex_len: 12,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SessionKey {
|
||||
pub fn derive(
|
||||
&self,
|
||||
profile: Option<&str>,
|
||||
mode: &SandboxMode,
|
||||
canonical_cwd: &Path,
|
||||
) -> String {
|
||||
let profile = profile.unwrap_or(self.default_profile);
|
||||
let mode = match mode {
|
||||
SandboxMode::Blacklist => "blacklist",
|
||||
SandboxMode::Whitelist => "whitelist",
|
||||
};
|
||||
let cwd = canonical_cwd.to_string_lossy();
|
||||
|
||||
let mut hasher = blake3::Hasher::new();
|
||||
hasher.update(profile.as_bytes());
|
||||
hasher.update(b"\0");
|
||||
hasher.update(mode.as_bytes());
|
||||
hasher.update(b"\0");
|
||||
hasher.update(cwd.as_bytes());
|
||||
|
||||
let hex = hasher.finalize().to_hex();
|
||||
hex[..self.hex_len].to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn missing_profile_uses_default_label() {
|
||||
let sk = SessionKey::default();
|
||||
let cwd = Path::new("/x");
|
||||
assert_eq!(
|
||||
sk.derive(None, &SandboxMode::Whitelist, cwd),
|
||||
sk.derive(Some(sk.default_profile), &SandboxMode::Whitelist, cwd),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_respects_configured_hex_len() {
|
||||
let sk = SessionKey {
|
||||
hex_len: 8,
|
||||
..SessionKey::default()
|
||||
};
|
||||
let key = sk.derive(Some("foo"), &SandboxMode::Whitelist, Path::new("/x"));
|
||||
assert_eq!(key.len(), 8);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user