bootc_mount/
mount.rs

1//! Helpers for interacting with mountpoints
2
3use std::{
4    fs,
5    mem::MaybeUninit,
6    os::fd::{AsFd, OwnedFd},
7    process::Command,
8};
9
10use anyhow::{Context, Result, anyhow};
11use bootc_utils::CommandRunExt;
12use camino::Utf8Path;
13use cap_std_ext::{cap_std::fs::Dir, cmdext::CapStdExtCommandExt};
14use fn_error_context::context;
15use rustix::{
16    mount::{MoveMountFlags, OpenTreeFlags},
17    net::{
18        AddressFamily, RecvFlags, SendAncillaryBuffer, SendAncillaryMessage, SendFlags,
19        SocketFlags, SocketType,
20    },
21    process::WaitOptions,
22    thread::Pid,
23};
24use serde::Deserialize;
25
26pub mod tempmount;
27
28/// Well known identifier for pid 1
29pub const PID1: Pid = const {
30    match Pid::from_raw(1) {
31        Some(v) => v,
32        None => panic!("Expected to parse pid1"),
33    }
34};
35
36#[derive(Deserialize, Debug)]
37#[serde(rename_all = "kebab-case")]
38#[allow(dead_code)]
39pub struct Filesystem {
40    // Note if you add an entry to this list, you need to change the --output invocation below too
41    pub source: String,
42    pub target: String,
43    #[serde(rename = "maj:min")]
44    pub maj_min: String,
45    pub fstype: String,
46    pub options: String,
47    pub uuid: Option<String>,
48    pub children: Option<Vec<Filesystem>>,
49}
50
51#[derive(Deserialize, Debug, Default)]
52pub struct Findmnt {
53    pub filesystems: Vec<Filesystem>,
54}
55
56pub fn run_findmnt(args: &[&str], cwd: Option<&Dir>, path: Option<&str>) -> Result<Findmnt> {
57    let mut cmd = Command::new("findmnt");
58    if let Some(cwd) = cwd {
59        cmd.cwd_dir(cwd.try_clone()?);
60    }
61    cmd.args([
62        "-J",
63        "-v",
64        // If you change this you probably also want to change the Filesystem struct above
65        "--output=SOURCE,TARGET,MAJ:MIN,FSTYPE,OPTIONS,UUID",
66    ])
67    .args(args)
68    .args(path);
69    let o: Findmnt = cmd.log_debug().run_and_parse_json()?;
70    Ok(o)
71}
72
73// Retrieve a mounted filesystem from a device given a matching path
74fn findmnt_filesystem(args: &[&str], cwd: Option<&Dir>, path: &str) -> Result<Filesystem> {
75    let o = run_findmnt(args, cwd, Some(path))?;
76    o.filesystems
77        .into_iter()
78        .next()
79        .ok_or_else(|| anyhow!("findmnt returned no data for {path}"))
80}
81
82#[context("Inspecting filesystem {path}")]
83/// Inspect a target which must be a mountpoint root - it is an error
84/// if the target is not the mount root.
85pub fn inspect_filesystem(path: &Utf8Path) -> Result<Filesystem> {
86    findmnt_filesystem(&["--mountpoint"], None, path.as_str())
87}
88
89#[context("Inspecting filesystem")]
90/// Inspect a target which must be a mountpoint root - it is an error
91/// if the target is not the mount root.
92pub fn inspect_filesystem_of_dir(d: &Dir) -> Result<Filesystem> {
93    findmnt_filesystem(&["--mountpoint"], Some(d), ".")
94}
95
96#[context("Inspecting filesystem by UUID {uuid}")]
97/// Inspect a filesystem by partition UUID
98pub fn inspect_filesystem_by_uuid(uuid: &str) -> Result<Filesystem> {
99    findmnt_filesystem(&["--source"], None, &(format!("UUID={uuid}")))
100}
101
102// Check if a specified device contains an already mounted filesystem
103// in the root mount namespace
104pub fn is_mounted_in_pid1_mountns(path: &str) -> Result<bool> {
105    let o = run_findmnt(&["-N"], None, Some("1"))?;
106
107    let mounted = o.filesystems.iter().any(|fs| is_source_mounted(path, fs));
108
109    Ok(mounted)
110}
111
112// Recursively check a given filesystem to see if it contains an already mounted source
113pub fn is_source_mounted(path: &str, mounted_fs: &Filesystem) -> bool {
114    if mounted_fs.source.contains(path) {
115        return true;
116    }
117
118    if let Some(ref children) = mounted_fs.children {
119        for child in children {
120            if is_source_mounted(path, child) {
121                return true;
122            }
123        }
124    }
125
126    false
127}
128
129/// Mount a device to the target path.
130pub fn mount(dev: &str, target: &Utf8Path) -> Result<()> {
131    Command::new("mount")
132        .args([dev, target.as_str()])
133        .run_inherited_with_cmd_context()
134}
135
136/// If the fsid of the passed path matches the fsid of the same path rooted
137/// at /proc/1/root, it is assumed that these are indeed the same mounted
138/// filesystem between container and host.
139/// Path should be absolute.
140#[context("Comparing filesystems at {path} and /proc/1/root/{path}")]
141pub fn is_same_as_host(path: &Utf8Path) -> Result<bool> {
142    // Add a leading '/' in case a relative path is passed
143    let path = Utf8Path::new("/").join(path);
144
145    // Using statvfs instead of fs, since rustix will translate the fsid field
146    // for us.
147    let devstat = rustix::fs::statvfs(path.as_std_path())?;
148    let hostpath = Utf8Path::new("/proc/1/root").join(path.strip_prefix("/")?);
149    let hostdevstat = rustix::fs::statvfs(hostpath.as_std_path())?;
150    tracing::trace!(
151        "base mount id {:?}, host mount id {:?}",
152        devstat.f_fsid,
153        hostdevstat.f_fsid
154    );
155    Ok(devstat.f_fsid == hostdevstat.f_fsid)
156}
157
158/// Given a pid, enter its mount namespace and acquire a file descriptor
159/// for a mount from that namespace.
160#[allow(unsafe_code)]
161#[context("Opening mount tree from pid")]
162pub fn open_tree_from_pidns(
163    pid: rustix::process::Pid,
164    path: &Utf8Path,
165    recursive: bool,
166) -> Result<OwnedFd> {
167    // Allocate a socket pair to use for sending file descriptors.
168    let (sock_parent, sock_child) = rustix::net::socketpair(
169        AddressFamily::UNIX,
170        SocketType::STREAM,
171        SocketFlags::CLOEXEC,
172        None,
173    )
174    .context("socketpair")?;
175    const DUMMY_DATA: &[u8] = b"!";
176    match unsafe { libc::fork() } {
177        0 => {
178            // We're in the child. At this point we know we don't have multiple threads, so we
179            // can safely `setns`.
180
181            drop(sock_parent);
182
183            // Open up the namespace of the target process as a file descriptor, and enter it.
184            let pidlink = fs::File::open(format!("/proc/{}/ns/mnt", pid.as_raw_nonzero()))?;
185            rustix::thread::move_into_link_name_space(
186                pidlink.as_fd(),
187                Some(rustix::thread::LinkNameSpaceType::Mount),
188            )
189            .context("setns")?;
190
191            // Open the target mount path as a file descriptor.
192            let recursive = if recursive {
193                OpenTreeFlags::AT_RECURSIVE
194            } else {
195                OpenTreeFlags::empty()
196            };
197            let fd = rustix::mount::open_tree(
198                rustix::fs::CWD,
199                path.as_std_path(),
200                OpenTreeFlags::OPEN_TREE_CLOEXEC | OpenTreeFlags::OPEN_TREE_CLONE | recursive,
201            )
202            .context("open_tree")?;
203
204            // And send that file descriptor via fd passing over the socketpair.
205            let fd = fd.as_fd();
206            let fds = [fd];
207            let mut buffer = [MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(1))];
208            let mut control = SendAncillaryBuffer::new(&mut buffer);
209            let pushed = control.push(SendAncillaryMessage::ScmRights(&fds));
210            assert!(pushed);
211            let ios = std::io::IoSlice::new(DUMMY_DATA);
212            rustix::net::sendmsg(sock_child, &[ios], &mut control, SendFlags::empty())?;
213            // Then we're done.
214            std::process::exit(0)
215        }
216        -1 => {
217            // fork failed
218            let e = std::io::Error::last_os_error();
219            anyhow::bail!("failed to fork: {e}");
220        }
221        n => {
222            // We're in the parent; create a pid (checking that n > 0).
223            let pid = rustix::process::Pid::from_raw(n).unwrap();
224            drop(sock_child);
225            // Receive the mount file descriptor from the child
226            let mut cmsg_space = vec![MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(1))];
227            let mut cmsg_buffer = rustix::net::RecvAncillaryBuffer::new(&mut cmsg_space);
228            let mut buf = [0u8; DUMMY_DATA.len()];
229            let iov = std::io::IoSliceMut::new(buf.as_mut());
230            let mut iov = [iov];
231            let nread = rustix::net::recvmsg(
232                sock_parent,
233                &mut iov,
234                &mut cmsg_buffer,
235                RecvFlags::CMSG_CLOEXEC,
236            )
237            .context("recvmsg")?
238            .bytes;
239            anyhow::ensure!(nread == DUMMY_DATA.len());
240            assert_eq!(buf, DUMMY_DATA);
241            // And extract the file descriptor
242            let r = cmsg_buffer
243                .drain()
244                .filter_map(|m| match m {
245                    rustix::net::RecvAncillaryMessage::ScmRights(f) => Some(f),
246                    _ => None,
247                })
248                .flatten()
249                .next()
250                .ok_or_else(|| anyhow::anyhow!("Did not receive a file descriptor"))?;
251            // SAFETY: Since we're not setting WNOHANG, this will always return Some().
252            let st = rustix::process::waitpid(Some(pid), WaitOptions::empty())?
253                .expect("Wait status")
254                .1;
255            if let Some(0) = st.exit_status() {
256                Ok(r)
257            } else {
258                anyhow::bail!("forked helper failed: {st:?}");
259            }
260        }
261    }
262}
263
264/// Create a bind mount from the mount namespace of the target pid
265/// into our mount namespace.
266pub fn bind_mount_from_pidns(
267    pid: Pid,
268    src: &Utf8Path,
269    target: &Utf8Path,
270    recursive: bool,
271) -> Result<()> {
272    let src = open_tree_from_pidns(pid, src, recursive)?;
273    rustix::mount::move_mount(
274        src.as_fd(),
275        "",
276        rustix::fs::CWD,
277        target.as_std_path(),
278        MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH,
279    )
280    .context("Moving mount")?;
281    Ok(())
282}
283
284// If the target path is not already mirrored from the host (e.g. via -v /dev:/dev)
285// then recursively mount it.
286pub fn ensure_mirrored_host_mount(path: impl AsRef<Utf8Path>) -> Result<()> {
287    let path = path.as_ref();
288    // If we didn't have this in our filesystem already (e.g. for /var/lib/containers)
289    // then create it now.
290    std::fs::create_dir_all(path)?;
291    if is_same_as_host(path)? {
292        tracing::debug!("Already mounted from host: {path}");
293        return Ok(());
294    }
295    tracing::debug!("Propagating host mount: {path}");
296    bind_mount_from_pidns(PID1, path, path, true)
297}