bob/sandbox/
sandbox_linux.rs

1/*
2 * Copyright (c) 2025 Jonathan Perkin <jonathan@perkin.org.uk>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
16
17use crate::sandbox::Sandbox;
18use anyhow::Context;
19use std::fs;
20use std::path::Path;
21use std::process::{Command, ExitStatus, Stdio};
22
23impl Sandbox {
24    pub fn mount_bindfs(
25        &self,
26        src: &Path,
27        dest: &Path,
28        opts: &[&str],
29    ) -> anyhow::Result<Option<ExitStatus>> {
30        fs::create_dir_all(dest)?;
31        let cmd = "/bin/mount";
32        // Build mount options: start with "bind", add any user-specified opts
33        let mut mount_opts = vec!["bind"];
34        mount_opts.extend(opts.iter().copied());
35        let opts_str = mount_opts.join(",");
36        Ok(Some(
37            Command::new(cmd)
38                .arg("-o")
39                .arg(&opts_str)
40                .arg(src)
41                .arg(dest)
42                .status()
43                .context(format!("Unable to execute {}", cmd))?,
44        ))
45    }
46
47    pub fn mount_devfs(
48        &self,
49        _src: &Path,
50        dest: &Path,
51        opts: &[&str],
52    ) -> anyhow::Result<Option<ExitStatus>> {
53        fs::create_dir_all(dest)?;
54        let cmd = "/bin/mount";
55        Ok(Some(
56            Command::new(cmd)
57                .arg("-t")
58                .arg("devtmpfs")
59                .args(opts)
60                .arg("devtmpfs")
61                .arg(dest)
62                .status()
63                .context(format!("Unable to execute {}", cmd))?,
64        ))
65    }
66
67    pub fn mount_fdfs(
68        &self,
69        _src: &Path,
70        dest: &Path,
71        opts: &[&str],
72    ) -> anyhow::Result<Option<ExitStatus>> {
73        fs::create_dir_all(dest)?;
74        let cmd = "/bin/mount";
75        // Build mount options: start with "bind", add any user-specified opts
76        let mut mount_opts = vec!["bind"];
77        mount_opts.extend(opts.iter().copied());
78        let opts_str = mount_opts.join(",");
79        Ok(Some(
80            Command::new(cmd)
81                .arg("-o")
82                .arg(&opts_str)
83                .arg("/dev/fd")
84                .arg(dest)
85                .status()
86                .context(format!("Unable to execute {}", cmd))?,
87        ))
88    }
89
90    pub fn mount_nfs(
91        &self,
92        src: &Path,
93        dest: &Path,
94        opts: &[&str],
95    ) -> anyhow::Result<Option<ExitStatus>> {
96        fs::create_dir_all(dest)?;
97        let cmd = "/bin/mount";
98        Ok(Some(
99            Command::new(cmd)
100                .arg("-t")
101                .arg("nfs")
102                .args(opts)
103                .arg(src)
104                .arg(dest)
105                .status()
106                .context(format!("Unable to execute {}", cmd))?,
107        ))
108    }
109
110    pub fn mount_procfs(
111        &self,
112        _src: &Path,
113        dest: &Path,
114        opts: &[&str],
115    ) -> anyhow::Result<Option<ExitStatus>> {
116        fs::create_dir_all(dest)?;
117        let cmd = "/bin/mount";
118        Ok(Some(
119            Command::new(cmd)
120                .arg("-t")
121                .arg("proc")
122                .args(opts)
123                .arg("proc")
124                .arg(dest)
125                .status()
126                .context(format!("Unable to execute {}", cmd))?,
127        ))
128    }
129
130    pub fn mount_tmpfs(
131        &self,
132        _src: &Path,
133        dest: &Path,
134        opts: &[&str],
135    ) -> anyhow::Result<Option<ExitStatus>> {
136        fs::create_dir_all(dest)?;
137        let cmd = "/bin/mount";
138        let mut args = vec!["-t", "tmpfs"];
139        // Convert opts to mount -o style if they look like size options
140        let mut mount_opts: Vec<String> = vec![];
141        for opt in opts {
142            if opt.starts_with("size=") || opt.starts_with("mode=") {
143                mount_opts.push(opt.to_string());
144            }
145        }
146        if !mount_opts.is_empty() {
147            args.push("-o");
148        }
149        let opts_str = mount_opts.join(",");
150        Ok(Some(
151            Command::new(cmd)
152                .args(&args)
153                .arg(if !mount_opts.is_empty() { &opts_str } else { "" })
154                .arg("tmpfs")
155                .arg(dest)
156                .status()
157                .context(format!("Unable to execute {}", cmd))?,
158        ))
159    }
160
161    fn unmount_common(
162        &self,
163        dest: &Path,
164    ) -> anyhow::Result<Option<ExitStatus>> {
165        let cmd = "/bin/umount";
166        Ok(Some(
167            Command::new(cmd)
168                .arg(dest)
169                .stdout(Stdio::null())
170                .stderr(Stdio::null())
171                .status()
172                .context(format!("Unable to execute {}", cmd))?,
173        ))
174    }
175
176    pub fn unmount_bindfs(
177        &self,
178        dest: &Path,
179    ) -> anyhow::Result<Option<ExitStatus>> {
180        self.unmount_common(dest)
181    }
182
183    pub fn unmount_devfs(
184        &self,
185        dest: &Path,
186    ) -> anyhow::Result<Option<ExitStatus>> {
187        self.unmount_common(dest)
188    }
189
190    pub fn unmount_fdfs(
191        &self,
192        dest: &Path,
193    ) -> anyhow::Result<Option<ExitStatus>> {
194        self.unmount_common(dest)
195    }
196
197    pub fn unmount_nfs(
198        &self,
199        dest: &Path,
200    ) -> anyhow::Result<Option<ExitStatus>> {
201        self.unmount_common(dest)
202    }
203
204    pub fn unmount_procfs(
205        &self,
206        dest: &Path,
207    ) -> anyhow::Result<Option<ExitStatus>> {
208        self.unmount_common(dest)
209    }
210
211    pub fn unmount_tmpfs(
212        &self,
213        dest: &Path,
214    ) -> anyhow::Result<Option<ExitStatus>> {
215        self.unmount_common(dest)
216    }
217
218    /// Kill all processes using files within a sandbox path.
219    pub fn kill_processes(&self, sandbox: &Path) {
220        // Use fuser -km to kill all processes using the mount point recursively
221        let _ = Command::new("fuser")
222            .arg("-k")
223            .arg(sandbox)
224            .stdout(Stdio::null())
225            .stderr(Stdio::null())
226            .status();
227
228        // Give processes a moment to die
229        std::thread::sleep(std::time::Duration::from_millis(500));
230    }
231}