mod fs;
use crate::{
config::{FileContents, SandboxConfig},
error::{Error, Result, with_io_context},
sandbox::fs::FsHandle,
};
use std::{
env::home_dir,
ffi::{OsStr, OsString},
fs::File,
io::{self, BufReader, BufWriter},
path::Path,
process::Command,
};
static FAKE_HOME_DIR: &str = "tartarus-fake-home";
#[derive(Debug)]
#[cfg_attr(feature = "config_file", derive(serde::Deserialize, serde::Serialize))]
#[non_exhaustive]
pub enum SandboxType {
#[cfg(target_os = "linux")]
#[cfg_attr(feature = "config_file", serde(rename = "bubblewrap"))]
BubbleWrap,
}
#[derive(Debug)]
pub struct Sandbox {
pub sandbox_type: SandboxType,
pub config: SandboxConfig,
}
impl Sandbox {
pub const fn new(sandbox_type: SandboxType, config: SandboxConfig) -> Self {
Self {
sandbox_type,
config,
}
}
pub const fn configure() -> SandboxConfig {
SandboxConfig::new()
}
pub fn dry_run<'a>(self, process: &OsStr, args: impl Iterator<Item = &'a OsStr>) -> Result<()> {
let cmd = match self.sandbox_type {
SandboxType::BubbleWrap => bubblewrap_cmd(DryRun::Enabled, self.config, process, args)?,
};
print!("{}", cmd.get_program().to_string_lossy());
for arg in cmd.get_args() {
print!(" {}", arg.to_string_lossy());
}
println!();
Ok(())
}
pub fn exec<'a>(self, process: &OsStr, args: impl Iterator<Item = &'a OsStr>) -> Result<()> {
let mut cmd = match self.sandbox_type {
SandboxType::BubbleWrap => {
bubblewrap_cmd(DryRun::Disabled, self.config, process, args)?
}
};
cmd.spawn()
.map_err(|e| {
with_io_context(process.display(), "spawning bubblewrap sandbox for process", e)
})?
.wait()
.map_err(|e| {
with_io_context(process.display(), "executing bubblewrap sandbox for process", e)
})?;
Ok(())
}
}
#[derive(Debug, Clone, Copy)]
enum DryRun {
Enabled,
Disabled,
}
fn bubblewrap_cmd<'a>(
dry_run: DryRun,
config: SandboxConfig,
process: &OsStr,
args: impl Iterator<Item = &'a OsStr>,
) -> Result<Command> {
cfg_select! {
feature = "log" => log::info!("config = {config:?}"),
_ => {}
}
let temp_dir = std::env::temp_dir();
let override_home_dir = temp_dir.join(FAKE_HOME_DIR);
let Some(real_home_dir) = home_dir() else {
return Err(with_io_context(
process.display(),
"executing bubblewrap with home directory for process",
io::ErrorKind::NotFound,
));
};
let fs = FsHandle::new(dry_run);
prepare_fake_home(fs, &real_home_dir, &override_home_dir, &config)?;
let mut cmd = Command::new("setsid");
cmd.args([
"bwrap",
"--ro-bind",
"/",
"/",
"--tmpfs",
])
.arg(temp_dir)
.args([
OsStr::new("--bind"),
override_home_dir.as_os_str(),
override_home_dir.as_os_str(),
]);
if let Some(passthrough_home_dirs) = config.passthrough_home_dirs {
cmd.args(passthrough_home_dirs.into_iter().flat_map(|dir| {
[
OsString::from("--bind"),
real_home_dir.join(&dir).into_os_string(),
override_home_dir.join(&dir).into_os_string(),
]
}));
}
if let Some(writable_dirs) = config.writable_dirs {
cmd.args(writable_dirs.into_iter().flat_map(|dir| {
[
OsString::from("--bind"),
dir.as_os_str().into(),
dir.as_os_str().into(),
]
}));
}
cmd.args([
"--remount-ro",
"/",
"--dev-bind",
"/dev",
"/dev",
"--proc",
"/proc",
])
.args([
OsStr::new("--setenv"),
OsStr::new("HOME"),
override_home_dir.as_os_str(),
]);
if config.allow_network_access {
cmd.args(["--share-net"]);
}
if let Ok(xdg_runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
cmd.args(["--bind", &xdg_runtime_dir, &xdg_runtime_dir]);
}
cmd.arg(process);
cmd.args(args);
Ok(cmd)
}
fn prepare_fake_home(
fs: FsHandle,
real_home_dir: &Path,
override_home_dir: &Path,
config: &SandboxConfig,
) -> Result<()> {
if !config.needs_fake_home() {
return Ok(());
}
fs.create_dir(override_home_dir, "creating override home dir")?;
prepare_passthrough(fs, override_home_dir, config)?;
prepare_overrides(fs, real_home_dir, override_home_dir, config)?;
Ok(())
}
fn prepare_overrides(
fs: FsHandle,
real_home_dir: &Path,
override_home_dir: &Path,
config: &SandboxConfig,
) -> Result<(), Error> {
for override_dir in config.override_home_dirs.iter().flatten() {
fs.validate_relative_path(&override_dir.subpath, "creating override home subdirectory")?;
let override_output_dir = override_home_dir.join(&override_dir.subpath);
fs.create_dir(
&override_output_dir,
format_args!("creating override home dir for {}", override_output_dir.display()),
)?;
let original_input_dir = real_home_dir.join(&override_dir.subpath);
fs.validate_is_dir(&original_input_dir, "creating override home subdirectory")?;
fs.sync_dir(&original_input_dir, &override_output_dir)?;
for override_file in override_dir.overrides.iter().flatten() {
fs.validate_relative_path(&override_file.path, "reading file to modify for override")?;
let source = original_input_dir.join(&override_file.path);
let dest = override_output_dir.join(&override_file.path);
let file = override_file_arg(&source, &dest)?;
override_file.behavior.apply(file)?;
}
}
Ok(())
}
fn prepare_passthrough(
fs: FsHandle,
override_home_dir: &Path,
config: &SandboxConfig,
) -> Result<(), Error> {
for passthrough_dir in config.passthrough_home_dirs.iter().flatten() {
fs.validate_relative_path(passthrough_dir, "mapping as passthrough home dir")?;
let passthrough_dest = override_home_dir.join(passthrough_dir);
fs.create_dir(&passthrough_dest, "creating override dir at")?;
}
Ok(())
}
fn override_file_arg(original_path: &Path, override_path: &Path) -> Result<FileContents, Error> {
let original = File::open(original_path).map(BufReader::new).map_err(|e| {
with_io_context(original_path.display(), "reading file to modify for override", e)
})?;
let output = File::options()
.write(true)
.truncate(true)
.open(override_path)
.map(BufWriter::new)
.map_err(|e| {
with_io_context(
format!("{} with {}", original_path.display(), override_path.display()),
"opening file to override",
e,
)
})?;
Ok(FileContents { original, output })
}