1use itertools::Itertools;
2use std::{
3 collections::HashMap,
4 fs::File,
5 os::fd::AsRawFd,
6 path::{Component, Path, PathBuf},
7};
8use sys_mount::{FilesystemType, Mount, MountFlags, Unmount, UnmountDrop, UnmountFlags};
9#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
11pub struct MountTarget {
12 pub target: PathBuf,
13 pub fstype: Option<String>,
14 pub flags: MountFlags,
15 pub data: Option<String>,
16}
17
18impl Default for MountTarget {
19 fn default() -> Self {
20 Self {
21 target: Default::default(),
22 fstype: Default::default(),
23 flags: MountFlags::empty(),
24 data: Default::default(),
25 }
26 }
27}
28
29impl MountTarget {
30 pub fn new(
32 target: PathBuf,
33 fstype: Option<String>,
34 flags: MountFlags,
35 data: Option<String>,
36 ) -> Self {
37 Self {
38 target,
39 fstype,
40 flags,
41 data,
42 }
43 }
44
45 #[tracing::instrument]
46 pub fn mount(&self, source: &PathBuf, root: &Path) -> std::io::Result<UnmountDrop<Mount>> {
47 let target = self.target.strip_prefix("/").unwrap_or(&self.target);
49 tracing::info!(?root, "Mounting {source:?} to {target:?}");
50 let target = root.join(target);
51 std::fs::create_dir_all(&target)?;
52
53 let mut mount = Mount::builder().flags(self.flags);
61 if let Some(fstype) = &self.fstype {
62 mount = mount.fstype(FilesystemType::Manual(fstype));
63 }
64
65 if let Some(data) = &self.data {
66 mount = mount.data(data);
67 }
68
69 let mount = mount.mount_autodrop(source, &target, UnmountFlags::empty())?;
70 Ok(mount)
71 }
72
73 pub fn umount(&self, root: &Path) -> std::io::Result<()> {
74 let target = self.target.strip_prefix("/").unwrap_or(&self.target);
76 let target = root.join(target);
77
78 nix::mount::umount(&target)?;
79 Ok(())
80 }
81}
82
83#[derive(Default)]
86pub struct MountTable {
87 inner: HashMap<PathBuf, MountTarget>,
90 mounts: Vec<UnmountDrop<Mount>>,
91}
92
93impl MountTable {
94 pub fn new() -> Self {
95 Self {
96 inner: HashMap::new(),
97 mounts: Vec::new(),
98 }
99 }
100 pub fn set_table(&mut self, table: HashMap<PathBuf, MountTarget>) {
102 self.inner = table;
103 }
104
105 pub fn add_mount(&mut self, mount: MountTarget, source: PathBuf) {
107 self.inner.insert(source, mount);
108 }
109
110 pub fn add_sysmount(&mut self, mount: UnmountDrop<Mount>) {
111 self.mounts.push(mount);
112 }
113
114 fn sort_mounts(&self) -> impl Iterator<Item = (&PathBuf, &MountTarget)> {
118 self.inner.iter().sorted_by(|(_, a), (_, b)| {
119 match (a.target.components().count(), b.target.components().count()) {
120 (1, _) if a.target.components().next() == Some(Component::RootDir) => {
121 std::cmp::Ordering::Less
122 } (_, 1) if b.target.components().next() == Some(Component::RootDir) => {
124 std::cmp::Ordering::Greater
125 } (x, y) if x == y => a.target.cmp(&b.target),
127 (x, y) => x.cmp(&y),
128 }
129 })
130 }
131
132 pub fn mount_chroot(&mut self, root: &Path) -> std::io::Result<()> {
134 self.mounts = self
141 .sort_mounts()
142 .map(|(source, mount)| {
143 tracing::trace!(?mount, ?source, "Mounting");
144 mount.mount(source, root)
145 })
146 .collect::<std::io::Result<_>>()?;
147 Ok(())
148 }
149
150 pub fn umount_chroot(&mut self) -> std::io::Result<()> {
151 self.mounts.drain(..).rev().try_for_each(|mount| {
152 tracing::trace!("Unmounting {:?}", mount.target_path());
153 mount.unmount(UnmountFlags::DETACH)
155 })
156 }
157}
158
159pub struct Container {
165 pub root: PathBuf,
166 pub mount_table: MountTable,
167 _initialized: bool,
168 chroot: bool,
169 sysroot: File,
170 pwd: File,
171}
172
173impl Container {
174 #[inline(always)]
179 pub fn chroot(&mut self) -> std::io::Result<()> {
180 if !self._initialized {
181 self.mount()?;
186 }
187
188 nix::unistd::chroot(&self.root)?;
189 self.chroot = true;
190 nix::unistd::chdir("/")?;
191 Ok(())
192 }
193
194 #[inline(always)]
204 pub fn exit_chroot(&mut self) -> std::io::Result<()> {
205 nix::unistd::fchdir(self.sysroot.as_raw_fd())?;
206 nix::unistd::chroot(".")?;
207 self.chroot = false;
208
209 nix::unistd::fchdir(self.pwd.as_raw_fd())?;
211 Ok(())
212 }
213
214 pub fn new(chrootpath: PathBuf) -> Self {
219 let pwd = std::fs::File::open("/proc/self/cwd").unwrap();
220 let sysroot = std::fs::File::open("/").unwrap();
221
222 let mut container = Self {
223 pwd,
224 root: chrootpath,
225 mount_table: MountTable::new(),
226 sysroot,
227 _initialized: false,
228 chroot: false,
229 };
230
231 container.setup_minimal_mounts();
232
233 container
234 }
235
236 #[inline(always)]
238 pub fn run<F, T>(&mut self, f: F) -> std::io::Result<T>
239 where
240 F: FnOnce() -> T,
241 {
242 if !self._initialized {
244 self.mount()?;
245 }
246 if !self.chroot {
247 self.chroot()?;
248 }
249 tracing::trace!("Running function inside container");
250 let ret = f();
251 if self.chroot {
252 self.exit_chroot()?;
253 }
254 if self._initialized {
255 self.umount()?;
256 }
257 Ok(ret)
258 }
259
260 pub fn mount(&mut self) -> std::io::Result<()> {
262 self.mount_table.mount_chroot(&self.root)?;
263 self._initialized = true;
264 Ok(())
265 }
266
267 pub fn umount(&mut self) -> std::io::Result<()> {
269 self.mount_table.umount_chroot()?;
270 self._initialized = false;
271 Ok(())
272 }
273
274 pub fn host_bind_mount(&mut self) -> &mut Self {
277 self.bind_mount(PathBuf::from("/"), PathBuf::from("/run/host"));
278 self
279 }
280
281 pub fn bind_mount(&mut self, source: PathBuf, target: PathBuf) {
283 self.mount_table.add_mount(
284 MountTarget {
285 target,
286 flags: MountFlags::BIND,
287 ..MountTarget::default()
288 },
289 source,
290 );
291 }
292
293 pub fn add_mount(&mut self, mount: MountTarget, source: PathBuf) {
297 self.mount_table.add_mount(mount, source);
298 }
299
300 fn setup_minimal_mounts(&mut self) {
301 self.mount_table.add_mount(
302 MountTarget {
303 target: "proc".into(),
304 fstype: Some("proc".to_string()),
305 ..MountTarget::default()
306 },
307 PathBuf::from("/proc"),
308 );
309
310 self.mount_table.add_mount(
311 MountTarget {
312 target: "sys".into(),
313 fstype: Some("sysfs".to_string()),
314 ..MountTarget::default()
315 },
316 PathBuf::from("/sys"),
317 );
318
319 self.bind_mount("/dev".into(), "dev".into());
320 self.bind_mount("/dev/pts".into(), "dev/pts".into());
321 }
322}
323
324impl Drop for Container {
325 fn drop(&mut self) {
326 tracing::trace!("Dropping container, images will be unmounted");
327 if self.chroot {
328 self.exit_chroot().unwrap();
329 }
330 if self._initialized {
331 self.umount().unwrap();
332 }
333 }
334}
335
336#[cfg(test)]
338mod tests {
340 use super::*;
341 use std::path::PathBuf;
342 #[ignore = "This test requires root"]
343 #[test]
344 fn test_container() {
345 std::fs::create_dir_all("/tmp/tiffin").unwrap();
346 let mut container = Container::new(PathBuf::from("/tmp/tiffin"));
347 container
348 .run(|| std::fs::create_dir_all("/tmp/tiffin/test").unwrap())
349 .unwrap();
350 }
351}