1mod fs;
2
3use crate::{
4 config::{FileContents, SandboxConfig},
5 error::{Error, Result, with_io_context},
6 sandbox::fs::FsHandle,
7};
8use std::{
9 env::home_dir,
10 ffi::{OsStr, OsString},
11 fs::File,
12 io::{self, BufReader, BufWriter},
13 path::Path,
14 process::Command,
15};
16
17static FAKE_HOME_DIR: &str = "tartarus-fake-home";
18
19#[derive(Debug)]
20#[cfg_attr(feature = "config_file", derive(serde::Deserialize, serde::Serialize))]
21#[non_exhaustive]
22pub enum SandboxType {
23 #[cfg(target_os = "linux")]
24 #[cfg_attr(feature = "config_file", serde(rename = "bubblewrap"))]
25 BubbleWrap,
26}
27
28#[derive(Debug)]
29pub struct Sandbox {
30 pub sandbox_type: SandboxType,
31 pub config: SandboxConfig,
32}
33
34impl Sandbox {
35 pub const fn new(sandbox_type: SandboxType, config: SandboxConfig) -> Self {
36 Self {
37 sandbox_type,
38 config,
39 }
40 }
41
42 pub const fn configure() -> SandboxConfig {
43 SandboxConfig::new()
44 }
45
46 pub fn dry_run<'a>(self, process: &OsStr, args: impl Iterator<Item = &'a OsStr>) -> Result<()> {
52 let cmd = match self.sandbox_type {
53 SandboxType::BubbleWrap => bubblewrap_cmd(DryRun::Enabled, self.config, process, args)?,
54 };
55
56 print!("{}", cmd.get_program().to_string_lossy());
57
58 for arg in cmd.get_args() {
59 print!(" {}", arg.to_string_lossy());
60 }
61 println!();
62
63 Ok(())
64 }
65
66 pub fn exec<'a>(self, process: &OsStr, args: impl Iterator<Item = &'a OsStr>) -> Result<()> {
72 let mut cmd = match self.sandbox_type {
73 SandboxType::BubbleWrap => {
74 bubblewrap_cmd(DryRun::Disabled, self.config, process, args)?
75 }
76 };
77
78 cmd.spawn()
79 .map_err(|e| {
80 with_io_context(process.display(), "spawning bubblewrap sandbox for process", e)
81 })?
82 .wait()
83 .map_err(|e| {
84 with_io_context(process.display(), "executing bubblewrap sandbox for process", e)
85 })?;
86
87 Ok(())
88 }
89}
90
91#[derive(Debug, Clone, Copy)]
92enum DryRun {
93 Enabled,
94 Disabled,
95}
96
97fn bubblewrap_cmd<'a>(
103 dry_run: DryRun,
104 config: SandboxConfig,
105 process: &OsStr,
106 args: impl Iterator<Item = &'a OsStr>,
107) -> Result<Command> {
108 cfg_select! {
109 feature = "log" => log::info!("config = {config:?}"),
110 _ => {}
111 }
112
113 let temp_dir = std::env::temp_dir();
114 let override_home_dir = temp_dir.join(FAKE_HOME_DIR);
115
116 let Some(real_home_dir) = home_dir() else {
117 return Err(with_io_context(
118 process.display(),
119 "executing bubblewrap with home directory for process",
120 io::ErrorKind::NotFound,
121 ));
122 };
123
124 let fs = FsHandle::new(dry_run);
125
126 prepare_fake_home(fs, &real_home_dir, &override_home_dir, &config)?;
127
128 let mut cmd = Command::new("setsid");
129 cmd.args([
130 "bwrap",
132 "--ro-bind",
134 "/",
135 "/",
136 "--tmpfs",
139 ])
140 .arg(temp_dir)
141 .args([
142 OsStr::new("--bind"),
144 override_home_dir.as_os_str(),
145 override_home_dir.as_os_str(),
146 ]);
147
148 if let Some(passthrough_home_dirs) = config.passthrough_home_dirs {
149 cmd.args(passthrough_home_dirs.into_iter().flat_map(|dir| {
150 [
151 OsString::from("--bind"),
152 real_home_dir.join(&dir).into_os_string(),
153 override_home_dir.join(&dir).into_os_string(),
154 ]
155 }));
156 }
157
158 if let Some(writable_dirs) = config.writable_dirs {
159 cmd.args(writable_dirs.into_iter().flat_map(|dir| {
160 [
161 OsString::from("--bind"),
162 dir.as_os_str().into(),
163 dir.as_os_str().into(),
164 ]
165 }));
166 }
167
168 cmd.args([
169 "--remount-ro",
171 "/",
172 "--dev-bind",
174 "/dev",
175 "/dev",
176 "--proc",
178 "/proc",
179 ])
180 .args([
181 OsStr::new("--setenv"),
183 OsStr::new("HOME"),
184 override_home_dir.as_os_str(),
185 ]);
186
187 if config.allow_network_access {
189 cmd.args(["--share-net"]);
190 }
191
192 if let Ok(xdg_runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
194 cmd.args(["--bind", &xdg_runtime_dir, &xdg_runtime_dir]);
195 }
196
197 cmd.arg(process);
199 cmd.args(args);
200
201 Ok(cmd)
202}
203
204fn prepare_fake_home(
206 fs: FsHandle,
207 real_home_dir: &Path,
208 override_home_dir: &Path,
209 config: &SandboxConfig,
210) -> Result<()> {
211 if !config.needs_fake_home() {
212 return Ok(());
213 }
214
215 fs.create_dir(override_home_dir, "creating override home dir")?;
216
217 prepare_passthrough(fs, override_home_dir, config)?;
218 prepare_overrides(fs, real_home_dir, override_home_dir, config)?;
219
220 Ok(())
221}
222
223fn prepare_overrides(
240 fs: FsHandle,
241 real_home_dir: &Path,
242 override_home_dir: &Path,
243 config: &SandboxConfig,
244) -> Result<(), Error> {
245 for override_dir in config.override_home_dirs.iter().flatten() {
246 fs.validate_relative_path(&override_dir.subpath, "creating override home subdirectory")?;
247
248 let override_output_dir = override_home_dir.join(&override_dir.subpath);
249
250 fs.create_dir(
251 &override_output_dir,
252 format_args!("creating override home dir for {}", override_output_dir.display()),
253 )?;
254
255 let original_input_dir = real_home_dir.join(&override_dir.subpath);
256 fs.validate_is_dir(&original_input_dir, "creating override home subdirectory")?;
257
258 fs.sync_dir(&original_input_dir, &override_output_dir)?;
259
260 for override_file in override_dir.overrides.iter().flatten() {
261 fs.validate_relative_path(&override_file.path, "reading file to modify for override")?;
262 let source = original_input_dir.join(&override_file.path);
263 let dest = override_output_dir.join(&override_file.path);
264
265 let file = override_file_arg(&source, &dest)?;
266
267 override_file.behavior.apply(file)?;
268 }
269 }
270
271 Ok(())
272}
273
274fn prepare_passthrough(
277 fs: FsHandle,
278 override_home_dir: &Path,
279 config: &SandboxConfig,
280) -> Result<(), Error> {
281 for passthrough_dir in config.passthrough_home_dirs.iter().flatten() {
282 fs.validate_relative_path(passthrough_dir, "mapping as passthrough home dir")?;
283
284 let passthrough_dest = override_home_dir.join(passthrough_dir);
285 fs.create_dir(&passthrough_dest, "creating override dir at")?;
286 }
287
288 Ok(())
289}
290
291fn override_file_arg(original_path: &Path, override_path: &Path) -> Result<FileContents, Error> {
292 let original = File::open(original_path).map(BufReader::new).map_err(|e| {
293 with_io_context(original_path.display(), "reading file to modify for override", e)
294 })?;
295
296 let output = File::options()
297 .write(true)
298 .truncate(true)
299 .open(override_path)
300 .map(BufWriter::new)
301 .map_err(|e| {
302 with_io_context(
303 format!("{} with {}", original_path.display(), override_path.display()),
304 "opening file to override",
305 e,
306 )
307 })?;
308
309 Ok(FileContents { original, output })
310}