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::{
10    MountFlags, MoveMountFlags, OpenTreeFlags, UnmountFlags, move_mount, open_tree, unmount,
11};
12
13/// RAII guard that synchronously unmounts a path on drop, flushing all writes.
14///
15/// Prefer this over `MNT_DETACH` when the mounted filesystem has received
16/// writes (e.g. FAT ESP) and you need them flushed before the guard drops.
17#[derive(Debug)]
18pub struct MountGuard(std::path::PathBuf);
19
20impl MountGuard {
21    /// Mount `dev` at `path` and return a guard that will synchronously
22    /// unmount it on drop.
23    pub fn mount(
24        dev: &str,
25        path: std::path::PathBuf,
26        fstype: &str,
27        flags: MountFlags,
28        data: Option<&std::ffi::CStr>,
29    ) -> Result<Self> {
30        rustix::mount::mount(dev, &path, fstype, flags, data)
31            .with_context(|| format!("Mounting {} at {}", dev, path.display()))?;
32        Ok(Self(path))
33    }
34}
35
36impl std::ops::Deref for MountGuard {
37    type Target = Path;
38    fn deref(&self) -> &Path {
39        &self.0
40    }
41}
42
43impl Drop for MountGuard {
44    fn drop(&mut self) {
45        if let Err(e) = unmount(&self.0, UnmountFlags::empty()) {
46            // Synchronous unmount failure may mean buffered writes were not
47            // flushed to the underlying device (e.g. FAT ESP).  Treat this as
48            // an error rather than a warning.
49            tracing::error!("Failed to unmount {}: {e:?}", self.0.display());
50        }
51    }
52}
53
54/// Holds a tempdir with custom Drop impl
55#[derive(Debug)]
56pub struct MountpointTempdir(tempfile::TempDir);
57
58impl std::ops::Deref for MountpointTempdir {
59    type Target = tempfile::TempDir;
60    fn deref(&self) -> &tempfile::TempDir {
61        &self.0
62    }
63}
64
65impl MountpointTempdir {
66    fn new() -> Result<Self> {
67        let mut tmpdir = tempfile::TempDir::new()?;
68        tmpdir.disable_cleanup(true); // We will clean this ourselves
69        Ok(Self(tmpdir))
70    }
71}
72
73impl Drop for MountpointTempdir {
74    fn drop(&mut self) {
75        // Intentionally not using remove_dir_all so that we don't
76        // accidentally end up deleting anything mounted at this path
77        if let Err(e) = std::fs::remove_dir(self.path()) {
78            tracing::warn!(
79                "Failed to remove tmpdir at {}: {e:?}",
80                self.path().display()
81            )
82        }
83    }
84}
85
86/// RAII wrapper for a temporary mount that is automatically unmounted on drop.
87#[derive(Debug)]
88pub struct TempMount {
89    /// The backing temporary directory.
90    pub dir: MountpointTempdir,
91    /// An open handle to the mounted directory.
92    pub fd: Dir,
93}
94
95impl TempMount {
96    /// Mount device/partition on a tempdir which will be automatically unmounted on drop
97    #[context("Mounting {dev}")]
98    pub fn mount_dev(
99        dev: &str,
100        fstype: &str,
101        flags: MountFlags,
102        data: Option<&std::ffi::CStr>,
103    ) -> Result<Self> {
104        let tempdir = MountpointTempdir::new()?;
105
106        let utf8path = Utf8Path::from_path(tempdir.path())
107            .ok_or(anyhow::anyhow!("Failed to convert path to UTF-8 Path"))?;
108
109        rustix::mount::mount(dev, utf8path.as_std_path(), fstype, flags, data)?;
110
111        let fd = Dir::open_ambient_dir(tempdir.path(), ambient_authority())
112            .with_context(|| format!("Opening {:?}", tempdir.path()));
113
114        let fd = match fd {
115            Ok(fd) => fd,
116            Err(e) => {
117                unmount(tempdir.path(), UnmountFlags::DETACH)?;
118                return Err(e)?;
119            }
120        };
121
122        Ok(Self { dir: tempdir, fd })
123    }
124
125    /// Clone an existing mount into a tempdir via `open_tree(OPEN_TREE_CLONE)`
126    /// + `move_mount(MOVE_MOUNT_F_EMPTY_PATH)`. The returned `TempMount`
127    /// is a private view of the same filesystem — the source mount at
128    /// `source_target` is untouched and remains visible to other processes.
129    ///
130    /// The clone inherits the source mount's attributes (rw/ro, nosuid,
131    /// nodev, fmask/dmask, etc.). Callers that require specific mount
132    /// options should use `mount_dev` and treat `EBUSY` as unrecoverable
133    /// — this function is intended for read-only callers that can accept
134    /// whatever the current mount happens to expose.
135    #[context("Cloning existing mount at {source_target}")]
136    pub fn clone_existing_mount(source_target: &Utf8Path) -> Result<Self> {
137        let tempdir = MountpointTempdir::new()?;
138
139        let cloned = open_tree(
140            rustix::fs::CWD,
141            source_target.as_std_path(),
142            OpenTreeFlags::OPEN_TREE_CLOEXEC | OpenTreeFlags::OPEN_TREE_CLONE,
143        )
144        .with_context(|| format!("open_tree({source_target})"))?;
145        move_mount(
146            cloned.as_fd(),
147            "",
148            rustix::fs::CWD,
149            tempdir.path(),
150            MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH,
151        )
152        .context("move_mount")?;
153
154        let fd = Dir::open_ambient_dir(tempdir.path(), ambient_authority())
155            .with_context(|| format!("Opening {:?}", tempdir.path()));
156
157        let fd = match fd {
158            Ok(fd) => fd,
159            Err(e) => {
160                unmount(tempdir.path(), UnmountFlags::DETACH)?;
161                return Err(e)?;
162            }
163        };
164
165        Ok(Self { dir: tempdir, fd })
166    }
167
168    /// Mount and fd acquired with `open_tree` like syscall
169    #[context("Mounting fd")]
170    pub fn mount_fd(mnt_fd: impl AsFd) -> Result<Self> {
171        let tempdir = MountpointTempdir::new()?;
172
173        move_mount(
174            mnt_fd.as_fd(),
175            "",
176            rustix::fs::CWD,
177            tempdir.path(),
178            MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH,
179        )
180        .context("move_mount")?;
181
182        let fd = Dir::open_ambient_dir(tempdir.path(), ambient_authority())
183            .with_context(|| format!("Opening {:?}", tempdir.path()));
184
185        let fd = match fd {
186            Ok(fd) => fd,
187            Err(e) => {
188                unmount(tempdir.path(), UnmountFlags::DETACH)?;
189                return Err(e)?;
190            }
191        };
192
193        Ok(Self { dir: tempdir, fd })
194    }
195}
196
197impl Drop for TempMount {
198    fn drop(&mut self) {
199        match unmount(self.dir.path(), UnmountFlags::DETACH) {
200            Ok(_) => {}
201            Err(e) => tracing::warn!("Failed to unmount tempdir: {e:?}"),
202        }
203    }
204}