bob/sandbox/
sandbox_linux.rs1use crate::sandbox::Sandbox;
18use anyhow::Context;
19use std::fs;
20use std::os::unix::process::CommandExt;
21use std::path::Path;
22use std::process::{Command, ExitStatus, Stdio};
23use tracing::{debug, info, warn};
24
25impl Sandbox {
26 pub fn mount_bindfs(
27 &self,
28 src: &Path,
29 dest: &Path,
30 opts: &[&str],
31 ) -> anyhow::Result<Option<ExitStatus>> {
32 fs::create_dir_all(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
33 let cmd = "/bin/mount";
34 let mut mount_opts = vec!["bind"];
36 mount_opts.extend(opts.iter().copied());
37 let opts_str = mount_opts.join(",");
38 Ok(Some(
39 Command::new(cmd)
40 .arg("-o")
41 .arg(&opts_str)
42 .arg(src)
43 .arg(dest)
44 .process_group(0)
45 .status()
46 .context(format!("Unable to execute {}", cmd))?,
47 ))
48 }
49
50 pub fn mount_devfs(
51 &self,
52 _src: &Path,
53 dest: &Path,
54 opts: &[&str],
55 ) -> anyhow::Result<Option<ExitStatus>> {
56 fs::create_dir_all(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
57 let cmd = "/bin/mount";
58 Ok(Some(
59 Command::new(cmd)
60 .arg("-t")
61 .arg("devtmpfs")
62 .args(opts)
63 .arg("devtmpfs")
64 .arg(dest)
65 .process_group(0)
66 .status()
67 .context(format!("Unable to execute {}", cmd))?,
68 ))
69 }
70
71 pub fn mount_fdfs(
72 &self,
73 _src: &Path,
74 dest: &Path,
75 opts: &[&str],
76 ) -> anyhow::Result<Option<ExitStatus>> {
77 fs::create_dir_all(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
78 let cmd = "/bin/mount";
79 let mut mount_opts = vec!["bind"];
81 mount_opts.extend(opts.iter().copied());
82 let opts_str = mount_opts.join(",");
83 Ok(Some(
84 Command::new(cmd)
85 .arg("-o")
86 .arg(&opts_str)
87 .arg("/dev/fd")
88 .arg(dest)
89 .process_group(0)
90 .status()
91 .context(format!("Unable to execute {}", cmd))?,
92 ))
93 }
94
95 pub fn mount_nfs(
96 &self,
97 src: &Path,
98 dest: &Path,
99 opts: &[&str],
100 ) -> anyhow::Result<Option<ExitStatus>> {
101 fs::create_dir_all(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
102 let cmd = "/bin/mount";
103 Ok(Some(
104 Command::new(cmd)
105 .arg("-t")
106 .arg("nfs")
107 .args(opts)
108 .arg(src)
109 .arg(dest)
110 .process_group(0)
111 .status()
112 .context(format!("Unable to execute {}", cmd))?,
113 ))
114 }
115
116 pub fn mount_procfs(
117 &self,
118 _src: &Path,
119 dest: &Path,
120 opts: &[&str],
121 ) -> anyhow::Result<Option<ExitStatus>> {
122 fs::create_dir_all(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
123 let cmd = "/bin/mount";
124 Ok(Some(
125 Command::new(cmd)
126 .arg("-t")
127 .arg("proc")
128 .args(opts)
129 .arg("proc")
130 .arg(dest)
131 .process_group(0)
132 .status()
133 .context(format!("Unable to execute {}", cmd))?,
134 ))
135 }
136
137 pub fn mount_tmpfs(
138 &self,
139 _src: &Path,
140 dest: &Path,
141 opts: &[&str],
142 ) -> anyhow::Result<Option<ExitStatus>> {
143 fs::create_dir_all(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
144 let cmd = "/bin/mount";
145 let mut args = vec!["-t", "tmpfs"];
146 let mut mount_opts: Vec<String> = vec![];
148 for opt in opts {
149 if opt.starts_with("size=") || opt.starts_with("mode=") {
150 mount_opts.push(opt.to_string());
151 }
152 }
153 if !mount_opts.is_empty() {
154 args.push("-o");
155 }
156 let opts_str = mount_opts.join(",");
157 Ok(Some(
158 Command::new(cmd)
159 .args(&args)
160 .arg(if !mount_opts.is_empty() {
161 &opts_str
162 } else {
163 ""
164 })
165 .arg("tmpfs")
166 .arg(dest)
167 .process_group(0)
168 .status()
169 .context(format!("Unable to execute {}", cmd))?,
170 ))
171 }
172
173 fn unmount_common(&self, dest: &Path) -> anyhow::Result<Option<ExitStatus>> {
174 let cmd = "/bin/umount";
175 Ok(Some(
179 Command::new(cmd)
180 .arg(dest)
181 .stdout(Stdio::null())
182 .stderr(Stdio::null())
183 .process_group(0)
184 .status()
185 .context(format!("Unable to execute {}", cmd))?,
186 ))
187 }
188
189 pub fn unmount_bindfs(&self, dest: &Path) -> anyhow::Result<Option<ExitStatus>> {
190 self.unmount_common(dest)
191 }
192
193 pub fn unmount_devfs(&self, dest: &Path) -> anyhow::Result<Option<ExitStatus>> {
194 self.unmount_common(dest)
195 }
196
197 pub fn unmount_fdfs(&self, dest: &Path) -> anyhow::Result<Option<ExitStatus>> {
198 self.unmount_common(dest)
199 }
200
201 pub fn unmount_nfs(&self, dest: &Path) -> anyhow::Result<Option<ExitStatus>> {
202 self.unmount_common(dest)
203 }
204
205 pub fn unmount_procfs(&self, dest: &Path) -> anyhow::Result<Option<ExitStatus>> {
206 self.unmount_common(dest)
207 }
208
209 pub fn unmount_tmpfs(&self, dest: &Path) -> anyhow::Result<Option<ExitStatus>> {
210 self.unmount_common(dest)
211 }
212
213 pub fn kill_processes_for_path(&self, path: &Path) {
220 for iteration in 0..super::KILL_PROCESSES_MAX_RETRIES {
221 let output = Command::new("fuser")
222 .arg("-m")
223 .arg(path)
224 .stdout(Stdio::piped())
225 .stderr(Stdio::null())
226 .process_group(0)
227 .output();
228
229 let Ok(out) = output else { return };
230
231 let stdout = String::from_utf8_lossy(&out.stdout);
232 if stdout.split_whitespace().next().is_none() {
233 return;
234 }
235
236 debug!(path = %path.display(), "Killing processes for mount");
237
238 let _ = Command::new("fuser")
239 .arg("-km")
240 .arg(path)
241 .stdout(Stdio::null())
242 .stderr(Stdio::null())
243 .process_group(0)
244 .status();
245
246 let delay_ms = super::KILL_PROCESSES_INITIAL_DELAY_MS << iteration;
247 std::thread::sleep(std::time::Duration::from_millis(delay_ms));
248 }
249 }
250
251 pub fn kill_processes(&self, sandbox: &Path) {
257 for iteration in 0..super::KILL_PROCESSES_MAX_RETRIES {
258 let mut killed: Vec<i32> = Vec::new();
259
260 if let Ok(procs) = procfs::process::all_processes() {
262 for proc in procs.flatten() {
263 if Self::process_uses_path(&proc, sandbox).is_some() {
264 killed.push(proc.pid);
265 unsafe {
266 libc::kill(proc.pid, libc::SIGKILL);
267 }
268 }
269 }
270 }
271
272 if killed.is_empty() {
273 debug!(retries = iteration, "No processes found in sandbox");
274 return;
275 }
276
277 let pids: Vec<String> = killed.iter().map(|p| p.to_string()).collect();
278 info!(pids = %pids.join(" "), "Killed processes using sandbox");
279
280 let delay_ms = super::KILL_PROCESSES_INITIAL_DELAY_MS << iteration;
282 std::thread::sleep(std::time::Duration::from_millis(delay_ms));
283 }
284 let proc_info = Self::get_process_info(sandbox);
286 warn!(
287 max_retries = super::KILL_PROCESSES_MAX_RETRIES,
288 remaining = %proc_info,
289 "Gave up killing processes after max retries"
290 );
291 }
292
293 fn get_process_info(sandbox: &Path) -> String {
295 let mut info = Vec::new();
296 if let Ok(procs) = procfs::process::all_processes() {
297 for proc in procs.flatten() {
298 if Self::process_uses_path(&proc, sandbox).is_some() {
299 let cmdline = proc
300 .cmdline()
301 .map(|c| c.join(" "))
302 .unwrap_or_else(|_| String::from("?"));
303 info.push(format!("pid={} cmd='{}'", proc.pid, cmdline));
304 }
305 }
306 }
307 if info.is_empty() {
308 String::from("(none)")
309 } else {
310 info.join(", ")
311 }
312 }
313
314 fn process_uses_path(proc: &procfs::process::Process, dir: &Path) -> Option<String> {
317 if let Ok(cwd) = proc.cwd() {
319 if cwd.starts_with(dir) {
320 return Some(format!("cwd={}", cwd.display()));
321 }
322 }
323
324 if let Ok(root) = proc.root() {
326 if root.starts_with(dir) {
327 return Some(format!("root={}", root.display()));
328 }
329 }
330
331 if let Ok(fds) = proc.fd() {
333 for fd in fds.flatten() {
334 if let procfs::process::FDTarget::Path(path) = fd.target {
335 if path.starts_with(dir) {
336 return Some(format!("fd={}", path.display()));
337 }
338 }
339 }
340 }
341
342 None
343 }
344}