Skip to main content

bootc_internal_mount/
tempmount.rs

1use std::os::fd::AsFd;
2use std::path::Path;
3
4use anyhow::{Context, Result};
5
6use camino::Utf8Path;
7use cap_std_ext::cap_std::{ambient_authority, fs::Dir};
8use fn_error_context::context;
9use rustix::mount::{MountFlags, MoveMountFlags, UnmountFlags, move_mount, unmount};
10
11/// RAII guard that synchronously unmounts a path on drop, flushing all writes.
12///
13/// Prefer this over `MNT_DETACH` when the mounted filesystem has received
14/// writes (e.g. FAT ESP) and you need them flushed before the guard drops.
15#[derive(Debug)]
16pub struct MountGuard(std::path::PathBuf);
17
18impl MountGuard {
19    /// Mount `dev` at `path` and return a guard that will synchronously
20    /// unmount it on drop.
21    pub fn mount(
22        dev: &str,
23        path: std::path::PathBuf,
24        fstype: &str,
25        flags: MountFlags,
26        data: Option<&std::ffi::CStr>,
27    ) -> Result<Self> {
28        rustix::mount::mount(dev, &path, fstype, flags, data)
29            .with_context(|| format!("Mounting {} at {}", dev, path.display()))?;
30        Ok(Self(path))
31    }
32}
33
34impl std::ops::Deref for MountGuard {
35    type Target = Path;
36    fn deref(&self) -> &Path {
37        &self.0
38    }
39}
40
41impl Drop for MountGuard {
42    fn drop(&mut self) {
43        if let Err(e) = unmount(&self.0, UnmountFlags::empty()) {
44            // Synchronous unmount failure may mean buffered writes were not
45            // flushed to the underlying device (e.g. FAT ESP).  Treat this as
46            // an error rather than a warning.
47            tracing::error!("Failed to unmount {}: {e:?}", self.0.display());
48        }
49    }
50}
51
52/// Holds a tempdir with custom Drop impl
53#[derive(Debug)]
54pub struct MountpointTempdir(tempfile::TempDir);
55
56impl std::ops::Deref for MountpointTempdir {
57    type Target = tempfile::TempDir;
58    fn deref(&self) -> &tempfile::TempDir {
59        &self.0
60    }
61}
62
63impl MountpointTempdir {
64    fn new() -> Result<Self> {
65        let mut tmpdir = tempfile::TempDir::new()?;
66        tmpdir.disable_cleanup(true); // We will clean this ourselves
67        Ok(Self(tmpdir))
68    }
69}
70
71impl Drop for MountpointTempdir {
72    fn drop(&mut self) {
73        // Intentionally not using remove_dir_all so that we don't
74        // accidentally end up deleting anything mounted at this path
75        if let Err(e) = std::fs::remove_dir(self.path()) {
76            tracing::warn!(
77                "Failed to remove tmpdir at {}: {e:?}",
78                self.path().display()
79            )
80        }
81    }
82}
83
84/// RAII wrapper for a temporary mount that is automatically unmounted on drop.
85#[derive(Debug)]
86pub struct TempMount {
87    /// The backing temporary directory.
88    pub dir: MountpointTempdir,
89    /// An open handle to the mounted directory.
90    pub fd: Dir,
91}
92
93impl TempMount {
94    /// Mount device/partition on a tempdir which will be automatically unmounted on drop
95    #[context("Mounting {dev}")]
96    pub fn mount_dev(
97        dev: &str,
98        fstype: &str,
99        flags: MountFlags,
100        data: Option<&std::ffi::CStr>,
101    ) -> Result<Self> {
102        let tempdir = MountpointTempdir::new()?;
103
104        let utf8path = Utf8Path::from_path(tempdir.path())
105            .ok_or(anyhow::anyhow!("Failed to convert path to UTF-8 Path"))?;
106
107        rustix::mount::mount(dev, utf8path.as_std_path(), fstype, flags, data)?;
108
109        let fd = Dir::open_ambient_dir(tempdir.path(), ambient_authority())
110            .with_context(|| format!("Opening {:?}", tempdir.path()));
111
112        let fd = match fd {
113            Ok(fd) => fd,
114            Err(e) => {
115                unmount(tempdir.path(), UnmountFlags::DETACH)?;
116                return Err(e)?;
117            }
118        };
119
120        Ok(Self { dir: tempdir, fd })
121    }
122
123    /// Mount and fd acquired with `open_tree` like syscall
124    #[context("Mounting fd")]
125    pub fn mount_fd(mnt_fd: impl AsFd) -> Result<Self> {
126        let tempdir = MountpointTempdir::new()?;
127
128        move_mount(
129            mnt_fd.as_fd(),
130            "",
131            rustix::fs::CWD,
132            tempdir.path(),
133            MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH,
134        )
135        .context("move_mount")?;
136
137        let fd = Dir::open_ambient_dir(tempdir.path(), ambient_authority())
138            .with_context(|| format!("Opening {:?}", tempdir.path()));
139
140        let fd = match fd {
141            Ok(fd) => fd,
142            Err(e) => {
143                unmount(tempdir.path(), UnmountFlags::DETACH)?;
144                return Err(e)?;
145            }
146        };
147
148        Ok(Self { dir: tempdir, fd })
149    }
150}
151
152impl Drop for TempMount {
153    fn drop(&mut self) {
154        match unmount(self.dir.path(), UnmountFlags::DETACH) {
155            Ok(_) => {}
156            Err(e) => tracing::warn!("Failed to unmount tempdir: {e:?}"),
157        }
158    }
159}