1#[cfg(target_os = "linux")]
67mod sandbox_linux;
68#[cfg(target_os = "macos")]
69mod sandbox_macos;
70#[cfg(target_os = "netbsd")]
71mod sandbox_netbsd;
72#[cfg(any(target_os = "illumos", target_os = "solaris"))]
73mod sandbox_sunos;
74
75use crate::action::{ActionType, FSType};
76use crate::config::Config;
77use crate::{Interrupted, RunContext};
78use anyhow::{Context, Result, bail};
79use rayon::prelude::*;
80use std::fs;
81use std::io::Write;
82use std::os::unix::process::CommandExt;
83use std::path::{Path, PathBuf};
84use std::process::{Child, Command, ExitStatus, Output, Stdio};
85use std::sync::Arc;
86use std::sync::atomic::{AtomicBool, Ordering};
87use std::sync::mpsc::RecvTimeoutError;
88use std::time::{Duration, Instant};
89use tracing::{debug, info, info_span, warn};
90
91pub(crate) const SHUTDOWN_POLL_INTERVAL: Duration = Duration::from_millis(100);
95
96pub(crate) const KILL_PROCESSES_MAX_RETRIES: u32 = 5;
99pub(crate) const KILL_PROCESSES_INITIAL_DELAY_MS: u64 = 64;
100
101pub fn wait_with_shutdown(child: &mut Child, shutdown: &AtomicBool) -> Result<ExitStatus> {
106 loop {
107 if shutdown.load(Ordering::SeqCst) {
108 let _ = child.kill();
109 let _ = child.wait();
110 bail!("Interrupted by shutdown");
111 }
112 match child.try_wait()? {
113 Some(status) => return Ok(status),
114 None => std::thread::sleep(SHUTDOWN_POLL_INTERVAL),
115 }
116 }
117}
118
119pub fn wait_output_with_shutdown(child: Child, shutdown: &AtomicBool) -> Result<Output> {
130 let pid = child.id();
131 let (tx, rx) = std::sync::mpsc::channel();
132
133 std::thread::spawn(move || {
134 let _ = tx.send(child.wait_with_output());
135 });
136
137 loop {
138 if shutdown.load(Ordering::SeqCst) {
139 unsafe {
140 libc::kill(pid as i32, libc::SIGKILL);
141 }
142 let _ = rx.recv();
143 bail!("Interrupted by shutdown");
144 }
145 match rx.recv_timeout(SHUTDOWN_POLL_INTERVAL) {
146 Ok(result) => return result.map_err(Into::into),
147 Err(RecvTimeoutError::Timeout) => continue,
148 Err(RecvTimeoutError::Disconnected) => {
149 bail!("wait thread disconnected unexpectedly");
150 }
151 }
152 }
153}
154
155#[derive(Clone, Debug, Default)]
157pub struct Sandbox {
158 config: Config,
159}
160
161impl Sandbox {
162 pub fn new(config: &Config) -> Sandbox {
171 Sandbox {
172 config: config.clone(),
173 }
174 }
175
176 pub fn enabled(&self) -> bool {
181 self.config.sandboxes().is_some()
182 }
183
184 fn basedir(&self) -> Option<&PathBuf> {
185 self.config.sandboxes().as_ref().map(|s| &s.basedir)
186 }
187
188 pub fn path(&self, id: usize) -> PathBuf {
192 let sandbox = &self.config.sandboxes().as_ref().unwrap();
193 let mut p = PathBuf::from(&sandbox.basedir);
194 p.push(id.to_string());
195 p
196 }
197
198 pub fn command(&self, id: usize, cmd: &Path) -> Command {
203 let mut c = if self.enabled() {
204 let mut c = Command::new("/usr/sbin/chroot");
205 c.arg(self.path(id)).arg(cmd);
206 c
207 } else {
208 Command::new(cmd)
209 };
210 self.apply_environment(&mut c);
211 c
212 }
213
214 fn apply_environment(&self, cmd: &mut Command) {
222 let Some(env) = self.config.environment() else {
223 return;
224 };
225 if env.clear {
226 cmd.env_clear();
227 for name in &env.inherit {
228 if let Ok(value) = std::env::var(name) {
229 cmd.env(name, value);
230 }
231 }
232 }
233 for (name, value) in &env.set {
234 cmd.env(name, value);
235 }
236 }
237
238 pub fn kill_processes_by_id(&self, id: usize) {
243 if !self.enabled() {
244 return;
245 }
246 let sandbox = self.path(id);
247 if sandbox.exists() {
248 let span = info_span!("kill_processes", sandbox_id = id);
249 let _guard = span.enter();
250 self.kill_processes(&sandbox);
251 }
252 }
253
254 fn mountpath(&self, id: usize, mnt: &PathBuf) -> PathBuf {
259 let mut p = self.path(id);
264 match mnt.strip_prefix("/") {
265 Ok(s) => p.push(s),
266 Err(_) => p.push(mnt),
267 };
268 p
269 }
270
271 fn verify_path_in_sandbox(&self, id: usize, path: &Path) -> Result<()> {
277 let sandbox_root = self.path(id);
278 let canonical_sandbox = sandbox_root.canonicalize().unwrap_or(sandbox_root.clone());
282
283 let canonical_path = if path.exists() {
285 path.canonicalize()?
286 } else {
287 if let Some(parent) = path.parent() {
289 if parent.exists() {
290 let canonical_parent = parent.canonicalize()?;
291 if !canonical_parent.starts_with(&canonical_sandbox) {
292 bail!(
293 "Path escapes sandbox: {} is not within {}",
294 path.display(),
295 sandbox_root.display()
296 );
297 }
298 }
299 }
300 return Ok(());
301 };
302
303 if !canonical_path.starts_with(&canonical_sandbox) {
304 bail!(
305 "Path escapes sandbox: {} resolves to {} which is not within {}",
306 path.display(),
307 canonical_path.display(),
308 canonical_sandbox.display()
309 );
310 }
311 Ok(())
312 }
313
314 fn bobmarker(&self, id: usize) -> PathBuf {
322 self.path(id).join(".bob")
323 }
324
325 fn completedpath(&self, id: usize) -> PathBuf {
326 self.bobmarker(id).join("completed")
327 }
328
329 fn create_marker(&self, id: usize) -> Result<()> {
330 let path = self.bobmarker(id);
331 fs::create_dir(&path).with_context(|| format!("Failed to create {}", path.display()))?;
332 Ok(())
333 }
334
335 fn mark_complete(&self, id: usize) -> Result<()> {
336 let path = self.completedpath(id);
337 fs::create_dir(&path).with_context(|| format!("Failed to create {}", path.display()))?;
338 Ok(())
339 }
340
341 fn is_bob_sandbox(&self, id: usize) -> bool {
342 self.bobmarker(id).exists()
343 }
344
345 fn is_sandbox_complete(&self, id: usize) -> bool {
346 self.completedpath(id).exists()
347 }
348
349 fn discover_sandboxes(&self) -> Result<Vec<usize>> {
354 let Some(basedir) = self.basedir() else {
355 return Ok(vec![]);
356 };
357 if !basedir.exists() {
358 return Ok(vec![]);
359 }
360 let mut ids = Vec::new();
361 let entries = fs::read_dir(basedir)
362 .with_context(|| format!("Failed to read {}", basedir.display()))?;
363 for entry in entries {
364 let entry = entry?;
365 let path = entry.path();
366 if !path.is_dir() {
367 continue;
368 }
369 let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
370 continue;
371 };
372 let Ok(id) = name.parse::<usize>() else {
373 continue;
374 };
375 if self.is_bob_sandbox(id) {
376 ids.push(id);
377 }
378 }
379 ids.sort();
380 Ok(ids)
381 }
382
383 pub fn create(&self, id: usize) -> Result<()> {
388 let sandbox = self.path(id);
389 if sandbox.exists() {
390 if self.is_sandbox_complete(id) {
391 return Ok(());
392 }
393 bail!(
394 "Sandbox exists but is incomplete: {}.\n\
395 Run 'bob util sandbox destroy' first.",
396 sandbox.display()
397 );
398 }
399 fs::create_dir_all(&sandbox)
400 .with_context(|| format!("Failed to create {}", sandbox.display()))?;
401 self.create_marker(id)?;
402 self.perform_actions(id)?;
403 self.mark_complete(id)?;
404 Ok(())
405 }
406
407 pub fn execute(
416 &self,
417 id: usize,
418 script: &Path,
419 envs: Vec<(String, String)>,
420 stdin_data: Option<&str>,
421 protected: bool,
422 ) -> Result<Child> {
423 use std::io::Write;
424
425 let mut cmd = self.command(id, script);
426 cmd.current_dir("/");
427
428 for (key, val) in envs {
429 cmd.env(key, val);
430 }
431
432 if stdin_data.is_some() {
433 cmd.stdin(Stdio::piped());
434 }
435
436 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
437
438 if protected {
439 cmd.process_group(0);
440 }
441
442 let mut child = cmd.spawn()?;
443
444 if let Some(data) = stdin_data {
445 if let Some(mut stdin) = child.stdin.take() {
446 stdin.write_all(data.as_bytes())?;
447 }
448 }
449
450 Ok(child)
451 }
452
453 pub fn execute_script(
457 &self,
458 id: usize,
459 content: &str,
460 envs: Vec<(String, String)>,
461 ) -> Result<Child> {
462 use std::io::Write;
463
464 let mut cmd = self.command(id, Path::new("/bin/sh"));
465 cmd.current_dir("/").arg("-s");
466
467 for (key, val) in envs {
468 cmd.env(key, val);
469 }
470
471 let mut child = cmd
472 .stdin(Stdio::piped())
473 .stdout(Stdio::piped())
474 .stderr(Stdio::piped())
475 .spawn()?;
476
477 if let Some(mut stdin) = child.stdin.take() {
478 stdin.write_all(content.as_bytes())?;
479 }
480
481 Ok(child)
482 }
483
484 pub fn execute_command<I, S>(
488 &self,
489 id: usize,
490 cmd: &Path,
491 args: I,
492 envs: Vec<(String, String)>,
493 ) -> Result<Child>
494 where
495 I: IntoIterator<Item = S>,
496 S: AsRef<std::ffi::OsStr>,
497 {
498 let mut command = self.command(id, cmd);
499 command.args(args);
500 for (key, val) in envs {
501 command.env(key, val);
502 }
503 command
504 .stdin(Stdio::null())
505 .stdout(Stdio::piped())
506 .stderr(Stdio::piped())
507 .spawn()
508 .map_err(Into::into)
509 }
510
511 pub fn run_pre_build(
517 &self,
518 id: usize,
519 config: &Config,
520 envs: Vec<(String, String)>,
521 ) -> Result<bool> {
522 if let Some(script) = config.script("pre-build") {
523 info!(script = %script.display(), "Running pre-build script");
524 let child = self.execute(id, script, envs, None, false)?;
525 let output = child.wait_with_output()?;
526 if output.status.success() {
527 info!(script = %script.display(), result = "success", "Finished running pre-build script");
528 } else {
529 let stderr = String::from_utf8_lossy(&output.stderr);
530 let stdout = String::from_utf8_lossy(&output.stdout);
531 warn!(
532 script = %script.display(),
533 result = "failed",
534 stdout = %stdout.trim(),
535 stderr = %stderr.trim(),
536 "Finished running pre-build script"
537 );
538 return Ok(false);
539 }
540 }
541 Ok(true)
542 }
543
544 pub fn run_post_build(
553 &self,
554 id: usize,
555 config: &Config,
556 envs: Vec<(String, String)>,
557 ) -> Result<bool> {
558 if let Some(script) = config.script("post-build") {
559 info!(script = %script.display(), "Running post-build script");
560 let child = self.execute(id, script, envs, None, true)?;
562 let output = child.wait_with_output()?;
563 if output.status.success() {
564 info!(script = %script.display(), result = "success", "Finished running post-build script");
565 } else {
566 let stderr = String::from_utf8_lossy(&output.stderr);
567 let stdout = String::from_utf8_lossy(&output.stdout);
568 warn!(
569 script = %script.display(),
570 result = "failed",
571 stdout = %stdout.trim(),
572 stderr = %stderr.trim(),
573 "Finished running post-build script"
574 );
575 return Ok(false);
576 }
577 }
578 Ok(true)
579 }
580
581 pub fn destroy(&self, id: usize) -> anyhow::Result<()> {
585 let sandbox = self.path(id);
586 if !sandbox.exists() {
587 return Ok(());
588 }
589 let completeddir = self.completedpath(id);
595 if completeddir.exists() {
596 self.remove_empty_hierarchy(&completeddir)?;
597 }
598 self.reverse_actions(id)?;
599 self.kill_processes(&sandbox);
605 if sandbox.exists() {
611 let bobmarker = self.bobmarker(id);
612 let entries = fs::read_dir(&sandbox)
613 .with_context(|| format!("Failed to read {}", sandbox.display()))?;
614 for entry in entries {
615 let entry = entry?;
616 let path = entry.path();
617 if path == bobmarker {
618 continue;
619 }
620 self.remove_empty_hierarchy(&path)?;
621 }
622 if bobmarker.exists() {
628 self.remove_empty_hierarchy(&bobmarker)?;
629 }
630 self.remove_empty_hierarchy(&sandbox)?;
631 }
632 Ok(())
633 }
634
635 pub fn create_all(&self, count: usize) -> Result<()> {
639 if count == 1 {
640 print!("Creating sandbox...");
641 } else {
642 print!("Creating {} sandboxes...", count);
643 }
644 let _ = std::io::stdout().flush();
645 let start = Instant::now();
646 let results: Vec<(usize, Result<()>)> = (0..count)
647 .into_par_iter()
648 .map(|i| (i, self.create(i)))
649 .collect();
650 let mut first_error: Option<anyhow::Error> = None;
651 let mut sandbox_ids: Vec<usize> = Vec::new();
652 for (i, result) in results {
653 sandbox_ids.push(i);
654 if let Err(e) = result {
655 if first_error.is_none() {
656 first_error = Some(e.context(format!("sandbox {}", i)));
657 }
658 }
659 }
660 if let Some(e) = first_error {
661 println!();
662 for i in &sandbox_ids {
663 if let Err(destroy_err) = self.destroy(*i) {
664 eprintln!("Warning: failed to destroy sandbox {}: {}", i, destroy_err);
665 }
666 }
667 return Err(e);
668 }
669 println!(" done ({:.1}s)", start.elapsed().as_secs_f32());
670 Ok(())
671 }
672
673 pub fn destroy_all(&self) -> Result<()> {
679 let sandboxes = self.discover_sandboxes()?;
680 if sandboxes.is_empty() {
681 return Ok(());
682 }
683 let envs = self.config.script_env(None);
684 for &id in &sandboxes {
685 if self.path(id).exists() {
686 match self.run_post_build(id, &self.config, envs.clone()) {
687 Ok(true) => {}
688 Ok(false) => {
689 warn!("post-build script failed for sandbox {}", id)
690 }
691 Err(e) => {
692 warn!(error = %e, sandbox = id, "post-build script error")
693 }
694 }
695 }
696 }
697 if sandboxes.len() == 1 {
698 print!("Destroying sandbox...");
699 } else {
700 print!("Destroying {} sandboxes...", sandboxes.len());
701 }
702 let _ = std::io::stdout().flush();
703 let start = Instant::now();
704 let results: Vec<(usize, Result<()>)> = sandboxes
705 .into_par_iter()
706 .map(|i| (i, self.destroy(i)))
707 .collect();
708 let mut failed = 0;
709 for (i, result) in results {
710 if let Err(e) = result {
711 if failed == 0 {
712 println!();
713 }
714 eprintln!("sandbox {}: {:#}", i, e);
715 failed += 1;
716 }
717 }
718 if failed == 0 {
719 println!(" done ({:.1}s)", start.elapsed().as_secs_f32());
720 Ok(())
721 } else {
722 Err(anyhow::anyhow!(
723 "Failed to destroy {} sandbox{}.\n\
724 Remove unexpected files, then run 'bob util sandbox destroy'.",
725 failed,
726 if failed == 1 { "" } else { "es" }
727 ))
728 }
729 }
730
731 pub fn list_all(&self) -> Result<()> {
735 for id in self.discover_sandboxes()? {
736 let sandbox = self.path(id);
737 if self.is_sandbox_complete(id) {
738 println!("{}", sandbox.display())
739 } else {
740 println!("{} (incomplete)", sandbox.display())
741 }
742 }
743 Ok(())
744 }
745
746 pub fn count_existing(&self) -> Result<usize> {
750 Ok(self.discover_sandboxes()?.len())
751 }
752
753 fn remove_empty_dirs(&self, id: usize, mountpoint: &Path) {
758 for p in mountpoint.ancestors() {
759 if !p.starts_with(self.path(id)) {
763 break;
764 }
765 if !p.exists() {
769 continue;
770 }
771 if fs::remove_dir(p).is_err() {
776 break;
777 }
778 }
779 }
780
781 #[allow(clippy::only_used_in_recursion)]
786 fn remove_empty_hierarchy(&self, path: &Path) -> Result<()> {
787 let meta = fs::symlink_metadata(path)?;
789
790 if meta.is_symlink() {
791 fs::remove_file(path).map_err(|e| {
793 anyhow::anyhow!("Failed to remove symlink {}: {}", path.display(), e)
794 })?;
795 return Ok(());
796 }
797
798 if !meta.is_dir() {
799 bail!(
801 "Cannot remove sandbox: non-directory exists at {}",
802 path.display()
803 );
804 }
805
806 let entries =
808 fs::read_dir(path).with_context(|| format!("Failed to read {}", path.display()))?;
809 for entry in entries {
810 let entry = entry?;
811 self.remove_empty_hierarchy(&entry.path())?;
812 }
813
814 fs::remove_dir(path).map_err(|e| {
816 anyhow::anyhow!(
817 "Failed to remove directory {}: {}. Directory may not be empty.",
818 path.display(),
819 e
820 )
821 })
822 }
823
824 fn perform_actions(&self, id: usize) -> Result<()> {
829 let Some(sandbox) = &self.config.sandboxes() else {
830 bail!("Internal error: trying to perform actions when sandboxes disabled.");
831 };
832 for action in sandbox.actions.iter() {
833 action.validate()?;
834 let action_type = action.action_type()?;
835
836 let src = action.src().or(action.dest());
838 let dest = action
839 .dest()
840 .or(action.src())
841 .map(|d| self.mountpath(id, d));
842 if let Some(ref dest_path) = dest {
843 self.verify_path_in_sandbox(id, dest_path)?;
844 }
845
846 let mut opts = vec![];
847 if let Some(o) = action.opts() {
848 for opt in o.split(' ').collect::<Vec<&str>>() {
849 opts.push(opt);
850 }
851 }
852
853 let status = match action_type {
854 ActionType::Mount => {
855 let fs_type = action.fs_type()?;
856 let src =
857 src.ok_or_else(|| anyhow::anyhow!("mount action requires src or dest"))?;
858 let dest = dest.ok_or_else(|| anyhow::anyhow!("mount action requires dest"))?;
859 if action.ifexists() && !src.exists() {
860 debug!(
861 sandbox = id,
862 action = "mount",
863 fs = ?fs_type,
864 src = %src.display(),
865 "Skipped (source does not exist)"
866 );
867 continue;
868 }
869 debug!(
870 sandbox = id,
871 action = "mount",
872 fs = ?fs_type,
873 src = %src.display(),
874 dest = %dest.display(),
875 "Mounting"
876 );
877 match fs_type {
878 FSType::Bind => self.mount_bindfs(src, &dest, &opts)?,
879 FSType::Dev => self.mount_devfs(src, &dest, &opts)?,
880 FSType::Fd => self.mount_fdfs(src, &dest, &opts)?,
881 FSType::Nfs => self.mount_nfs(src, &dest, &opts)?,
882 FSType::Proc => self.mount_procfs(src, &dest, &opts)?,
883 FSType::Tmp => self.mount_tmpfs(src, &dest, &opts)?,
884 }
885 }
886 ActionType::Copy => {
887 let src =
888 src.ok_or_else(|| anyhow::anyhow!("copy action requires src or dest"))?;
889 let dest = dest.ok_or_else(|| anyhow::anyhow!("copy action requires dest"))?;
890 debug!(
891 sandbox = id,
892 action = "copy",
893 src = %src.display(),
894 dest = %dest.display(),
895 "Copying"
896 );
897 if let Some(parent) = dest.parent() {
898 fs::create_dir_all(parent)
899 .with_context(|| format!("Failed to create {}", parent.display()))?;
900 }
901 copy_dir::copy_dir(src, &dest).with_context(|| {
902 format!("Failed to copy {} to {}", src.display(), dest.display())
903 })?;
904 None
905 }
906 ActionType::Cmd => {
907 if let Some(create_cmd) = action.create_cmd() {
908 debug!(
909 sandbox = id,
910 action = "cmd",
911 cmd = create_cmd,
912 chroot = action.chroot(),
913 "Running create command"
914 );
915 self.run_action_cmd(id, create_cmd, action.chroot())?
916 } else {
917 None
918 }
919 }
920 ActionType::Symlink => {
921 let src = action
922 .src()
923 .ok_or_else(|| anyhow::anyhow!("symlink action requires src"))?;
924 let dest = action
925 .dest()
926 .ok_or_else(|| anyhow::anyhow!("symlink action requires dest"))?;
927 let dest_path = self.mountpath(id, dest);
928 debug!(
929 sandbox = id,
930 action = "symlink",
931 src = %src.display(),
932 dest = %dest_path.display(),
933 "Creating symlink"
934 );
935 if let Some(parent) = dest_path.parent() {
936 if !parent.exists() {
937 fs::create_dir_all(parent).with_context(|| {
938 format!("Failed to create {}", parent.display())
939 })?;
940 }
941 }
942 std::os::unix::fs::symlink(src, &dest_path).with_context(|| {
943 format!(
944 "Failed to create symlink {} -> {}",
945 dest_path.display(),
946 src.display()
947 )
948 })?;
949 None
950 }
951 };
952 if let Some(s) = status {
953 if !s.success() {
954 bail!("Sandbox action failed");
955 }
956 }
957 }
958 Ok(())
959 }
960
961 fn run_action_cmd(
974 &self,
975 id: usize,
976 cmd: &str,
977 chroot: bool,
978 ) -> Result<Option<std::process::ExitStatus>> {
979 let sandbox_path = self.path(id);
980 let output = if chroot {
981 let mut c = Command::new("/usr/sbin/chroot");
982 self.apply_environment(&mut c);
983 c.arg(&sandbox_path)
984 .arg("/bin/sh")
985 .arg("-c")
986 .arg(cmd)
987 .stdin(Stdio::null())
988 .stdout(Stdio::piped())
989 .stderr(Stdio::piped())
990 .process_group(0);
991 c.output()?
992 } else {
993 let mut c = Command::new("/bin/sh");
994 self.apply_environment(&mut c);
995 c.arg("-c")
996 .arg(cmd)
997 .env("bob_sandbox_path", &sandbox_path)
998 .current_dir(&sandbox_path)
999 .stdin(Stdio::null())
1000 .stdout(Stdio::piped())
1001 .stderr(Stdio::piped())
1002 .process_group(0);
1003 c.output()?
1004 };
1005
1006 if !output.status.success() {
1007 let stdout = String::from_utf8_lossy(&output.stdout);
1008 let stderr = String::from_utf8_lossy(&output.stderr);
1009 if !stdout.is_empty() {
1010 warn!(cmd, stdout = %stdout.trim(), "Action command output");
1011 }
1012 if !stderr.is_empty() {
1013 warn!(cmd, stderr = %stderr.trim(), "Action command error");
1014 }
1015 }
1016
1017 Ok(Some(output.status))
1018 }
1019
1020 fn reverse_actions(&self, id: usize) -> anyhow::Result<()> {
1021 let Some(sandbox) = &self.config.sandboxes() else {
1022 bail!("Internal error: trying to reverse actions when sandboxes disabled.");
1023 };
1024 for action in sandbox.actions.iter().rev() {
1025 let action_type = action.action_type()?;
1026 let dest = action
1028 .dest()
1029 .or(action.src())
1030 .map(|d| self.mountpath(id, d));
1031
1032 match action_type {
1033 ActionType::Cmd => {
1034 if let Some(destroy_cmd) = action.destroy_cmd() {
1035 debug!(
1036 sandbox = id,
1037 action = "cmd",
1038 cmd = destroy_cmd,
1039 chroot = action.chroot(),
1040 "Running destroy command"
1041 );
1042 let status = self.run_action_cmd(id, destroy_cmd, action.chroot())?;
1043 if let Some(s) = status {
1044 if !s.success() {
1045 bail!("Failed to run destroy command: exit code {:?}", s.code());
1046 }
1047 }
1048 }
1049 }
1050 ActionType::Copy => {
1051 let Some(mntdest) = dest else { continue };
1052 if !mntdest.exists() {
1053 self.remove_empty_dirs(id, &mntdest);
1054 continue;
1055 }
1056 if fs::remove_dir(&mntdest).is_ok() {
1057 continue;
1058 }
1059 debug!(
1065 sandbox = id,
1066 action = "copy",
1067 dest = %mntdest.display(),
1068 "Removing copied directory"
1069 );
1070 self.verify_path_in_sandbox(id, &mntdest)?;
1071 self.remove_dir_recursive(&mntdest)?;
1072 self.remove_empty_dirs(id, &mntdest);
1073 }
1074 ActionType::Symlink => {
1075 let Some(mntdest) = dest else { continue };
1076 if mntdest.is_symlink() {
1077 debug!(
1078 sandbox = id,
1079 action = "symlink",
1080 dest = %mntdest.display(),
1081 "Removing symlink"
1082 );
1083 fs::remove_file(&mntdest)?;
1084 }
1085 self.remove_empty_dirs(id, &mntdest);
1086 }
1087 ActionType::Mount => {
1088 let Some(mntdest) = dest else { continue };
1089 let fs_type = action.fs_type()?;
1090
1091 let src = action.src().or(action.dest());
1092 if let Some(src) = src {
1093 if action.ifexists() && !src.exists() {
1094 continue;
1095 }
1096 }
1097
1098 if !mntdest.exists() {
1104 self.remove_empty_dirs(id, &mntdest);
1105 continue;
1106 }
1107
1108 if fs::remove_dir(&mntdest).is_ok() {
1114 continue;
1115 }
1116
1117 self.kill_processes_for_path(&mntdest);
1122
1123 debug!(
1129 sandbox = id,
1130 action = "mount",
1131 fs = ?fs_type,
1132 dest = %mntdest.display(),
1133 "Unmounting"
1134 );
1135 let status = match fs_type {
1136 FSType::Bind => self.unmount_bindfs(&mntdest)?,
1137 FSType::Dev => self.unmount_devfs(&mntdest)?,
1138 FSType::Fd => self.unmount_fdfs(&mntdest)?,
1139 FSType::Nfs => self.unmount_nfs(&mntdest)?,
1140 FSType::Proc => self.unmount_procfs(&mntdest)?,
1141 FSType::Tmp => self.unmount_tmpfs(&mntdest)?,
1142 };
1143 if let Some(s) = status {
1144 if !s.success() {
1145 bail!("Failed to unmount {}", mntdest.display());
1146 }
1147 }
1148 self.remove_empty_dirs(id, &mntdest);
1149 }
1150 }
1151 }
1152 Ok(())
1153 }
1154
1155 #[allow(clippy::only_used_in_recursion)]
1165 fn remove_dir_recursive(&self, path: &Path) -> Result<()> {
1166 let meta = fs::symlink_metadata(path)
1168 .with_context(|| format!("Failed to stat {}", path.display()))?;
1169 if meta.is_symlink() {
1170 fs::remove_file(path)
1172 .with_context(|| format!("Failed to remove {}", path.display()))?;
1173 return Ok(());
1174 }
1175 if !meta.is_dir() {
1176 fs::remove_file(path)
1177 .with_context(|| format!("Failed to remove {}", path.display()))?;
1178 return Ok(());
1179 }
1180 let entries =
1181 fs::read_dir(path).with_context(|| format!("Failed to read {}", path.display()))?;
1182 for entry in entries {
1183 let entry = entry?;
1184 let entry_path = entry.path();
1185 let file_type = entry.file_type()?;
1187 if file_type.is_symlink() {
1188 fs::remove_file(&entry_path)
1190 .with_context(|| format!("Failed to remove {}", entry_path.display()))?;
1191 } else if file_type.is_dir() {
1192 self.remove_dir_recursive(&entry_path)?;
1193 } else {
1194 fs::remove_file(&entry_path)
1195 .with_context(|| format!("Failed to remove {}", entry_path.display()))?;
1196 }
1197 }
1198 fs::remove_dir(path).with_context(|| format!("Failed to remove {}", path.display()))?;
1199 Ok(())
1200 }
1201}
1202
1203#[derive(Debug)]
1211pub struct SandboxScope {
1212 sandbox: Sandbox,
1213 count: usize,
1214 ctx: RunContext,
1215}
1216
1217impl SandboxScope {
1218 pub fn new(sandbox: Sandbox, ctx: RunContext) -> Self {
1224 Self {
1225 sandbox,
1226 count: 0,
1227 ctx,
1228 }
1229 }
1230
1231 pub fn ensure(&mut self, n: usize) -> Result<()> {
1241 if !self.sandbox.enabled() || n <= self.count {
1242 return Ok(());
1243 }
1244 let to_create = n - self.count;
1245 if to_create == 1 {
1246 print!("Creating sandbox...");
1247 } else {
1248 print!("Creating {} sandboxes...", to_create);
1249 }
1250 let _ = std::io::stdout().flush();
1251 let start = Instant::now();
1252 let results: Vec<(usize, Result<()>)> = (self.count..n)
1253 .into_par_iter()
1254 .map(|i| (i, self.sandbox.create(i)))
1255 .collect();
1256
1257 if self.ctx.shutdown.load(Ordering::SeqCst) {
1259 for (i, result) in &results {
1260 if result.is_ok() {
1261 let _ = self.sandbox.destroy(*i);
1262 }
1263 }
1264 return Err(Interrupted.into());
1265 }
1266
1267 let mut first_error: Option<anyhow::Error> = None;
1268 let mut sandbox_ids: Vec<usize> = Vec::new();
1269 for (i, result) in results {
1270 sandbox_ids.push(i);
1271 if let Err(e) = result {
1272 if first_error.is_none() {
1273 first_error = Some(e.context(format!("sandbox {}", i)));
1274 }
1275 }
1276 }
1277 if let Some(e) = first_error {
1278 println!();
1279 for i in &sandbox_ids {
1280 if let Err(destroy_err) = self.sandbox.destroy(*i) {
1281 eprintln!("Warning: failed to destroy sandbox {}: {}", i, destroy_err);
1282 }
1283 }
1284 return Err(e);
1285 }
1286 println!(" done ({:.1}s)", start.elapsed().as_secs_f32());
1287 self.count = n;
1288 Ok(())
1289 }
1290
1291 pub fn sandbox(&self) -> &Sandbox {
1293 &self.sandbox
1294 }
1295
1296 pub fn enabled(&self) -> bool {
1298 self.sandbox.enabled()
1299 }
1300
1301 pub fn shutdown(&self) -> &Arc<AtomicBool> {
1303 &self.ctx.shutdown
1304 }
1305}
1306
1307impl Drop for SandboxScope {
1308 fn drop(&mut self) {
1309 if self.sandbox.enabled() && self.count > 0 {
1310 if let Err(e) = self.sandbox.destroy_all() {
1311 eprintln!("Warning: failed to destroy sandboxes: {}", e);
1312 }
1313 }
1314 }
1315}