1use 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;
28
29pub const PID1: Pid = const {
31 match Pid::from_raw(1) {
32 Some(v) => v,
33 None => panic!("Expected to parse pid1"),
34 }
35};
36
37#[derive(Deserialize, Debug)]
39#[serde(rename_all = "kebab-case")]
40#[allow(dead_code)]
41pub struct Filesystem {
42 pub source: String,
45 pub target: String,
47 #[serde(rename = "maj:min")]
49 pub maj_min: String,
50 pub fstype: String,
52 pub options: String,
54 pub uuid: Option<String>,
56 pub children: Option<Vec<Filesystem>>,
58}
59
60#[derive(Deserialize, Debug, Default)]
62pub struct Findmnt {
63 pub filesystems: Vec<Filesystem>,
65}
66
67pub fn run_findmnt(args: &[&str], cwd: Option<&Dir>, path: Option<&str>) -> Result<Findmnt> {
69 let mut cmd = Command::new("findmnt");
70 if let Some(cwd) = cwd {
71 cmd.cwd_dir(cwd.try_clone()?);
72 }
73 cmd.args([
74 "-J",
75 "-v",
76 "--output=SOURCE,TARGET,MAJ:MIN,FSTYPE,OPTIONS,UUID",
78 ])
79 .args(args)
80 .args(path);
81 let o: Findmnt = cmd.log_debug().run_and_parse_json()?;
82 Ok(o)
83}
84
85fn findmnt_filesystem(args: &[&str], cwd: Option<&Dir>, path: &str) -> Result<Filesystem> {
87 let o = run_findmnt(args, cwd, Some(path))?;
88 o.filesystems
89 .into_iter()
90 .next()
91 .ok_or_else(|| anyhow!("findmnt returned no data for {path}"))
92}
93
94#[context("Inspecting filesystem {path}")]
95pub fn inspect_filesystem(path: &Utf8Path) -> Result<Filesystem> {
98 findmnt_filesystem(&["--mountpoint"], None, path.as_str())
99}
100
101#[context("Inspecting filesystem")]
102pub fn inspect_filesystem_of_dir(d: &Dir) -> Result<Filesystem> {
105 findmnt_filesystem(&["--mountpoint"], Some(d), ".")
106}
107
108#[context("Inspecting filesystem by UUID {uuid}")]
109pub fn inspect_filesystem_by_uuid(uuid: &str) -> Result<Filesystem> {
111 findmnt_filesystem(&["--source"], None, &(format!("UUID={uuid}")))
112}
113
114fn pid1_mounts() -> Result<Findmnt> {
116 run_findmnt(&["-N"], None, Some("1"))
117}
118
119pub fn is_mounted_in_pid1_mountns(path: &str) -> Result<bool> {
122 let o = pid1_mounts()?;
123
124 let mounted = o.filesystems.iter().any(|fs| is_source_mounted(path, fs));
125
126 Ok(mounted)
127}
128
129pub fn find_mount_target_by_source(dev: &str) -> Result<Option<camino::Utf8PathBuf>> {
143 let o = run_findmnt(&[], None, None)?;
144 let found = find_source_mount_target(dev, &o.filesystems);
145 if found.is_none() {
146 tracing::debug!(
147 "find_mount_target_by_source: no mount found for source {dev}; \
148 note that findmnt may report by-uuid or by-partuuid paths instead \
149 of the block device path in some environments"
150 );
151 }
152 Ok(found)
153}
154
155fn find_source_mount_target(dev: &str, mounts: &[Filesystem]) -> Option<camino::Utf8PathBuf> {
156 for m in mounts {
157 if m.source == dev {
158 return Some(camino::Utf8PathBuf::from(&m.target));
159 }
160 if let Some(children) = &m.children {
161 if let Some(t) = find_source_mount_target(dev, children) {
162 return Some(t);
163 }
164 }
165 }
166 None
167}
168
169pub fn is_source_mounted(path: &str, mounted_fs: &Filesystem) -> bool {
171 if mounted_fs.source.contains(path) {
172 return true;
173 }
174
175 if let Some(ref children) = mounted_fs.children {
176 for child in children {
177 if is_source_mounted(path, child) {
178 return true;
179 }
180 }
181 }
182
183 false
184}
185
186pub fn mount(dev: &str, target: &Utf8Path) -> Result<()> {
188 Command::new("mount")
189 .args([dev, target.as_str()])
190 .run_inherited_with_cmd_context()
191}
192
193pub fn mount_typed(dev: &str, fstype: &str, target: &Utf8Path) -> Result<()> {
201 Command::new("mount")
202 .args(["-t", fstype, dev, target.as_str()])
203 .run_inherited_with_cmd_context()
204}
205
206pub fn unmount_recursive(target: &Utf8Path) -> Result<()> {
208 Command::new("umount")
209 .args(["--recursive", target.as_str()])
210 .run_inherited_with_cmd_context()
211}
212
213#[context("Comparing filesystems at {path} and /proc/1/root/{path}")]
218pub fn is_same_as_host(path: &Utf8Path) -> Result<bool> {
219 let path = Utf8Path::new("/").join(path);
221
222 let devstat = rustix::fs::statvfs(path.as_std_path())?;
225 let hostpath = Utf8Path::new("/proc/1/root").join(path.strip_prefix("/")?);
226 let hostdevstat = rustix::fs::statvfs(hostpath.as_std_path())?;
227 tracing::trace!(
228 "base mount id {:?}, host mount id {:?}",
229 devstat.f_fsid,
230 hostdevstat.f_fsid
231 );
232 Ok(devstat.f_fsid == hostdevstat.f_fsid)
233}
234
235#[allow(unsafe_code)]
238#[context("Opening mount tree from pid")]
239pub fn open_tree_from_pidns(
240 pid: rustix::process::Pid,
241 path: &Utf8Path,
242 recursive: bool,
243) -> Result<OwnedFd> {
244 let (sock_parent, sock_child) = rustix::net::socketpair(
246 AddressFamily::UNIX,
247 SocketType::STREAM,
248 SocketFlags::CLOEXEC,
249 None,
250 )
251 .context("socketpair")?;
252 const DUMMY_DATA: &[u8] = b"!";
253 match unsafe { libc::fork() } {
254 0 => {
255 drop(sock_parent);
259
260 let pidlink = fs::File::open(format!("/proc/{}/ns/mnt", pid.as_raw_nonzero()))?;
262 rustix::thread::move_into_link_name_space(
263 pidlink.as_fd(),
264 Some(rustix::thread::LinkNameSpaceType::Mount),
265 )
266 .context("setns")?;
267
268 let recursive = if recursive {
270 OpenTreeFlags::AT_RECURSIVE
271 } else {
272 OpenTreeFlags::empty()
273 };
274 let fd = rustix::mount::open_tree(
275 rustix::fs::CWD,
276 path.as_std_path(),
277 OpenTreeFlags::OPEN_TREE_CLOEXEC | OpenTreeFlags::OPEN_TREE_CLONE | recursive,
278 )
279 .context("open_tree")?;
280
281 let fd = fd.as_fd();
283 let fds = [fd];
284 let mut buffer = [MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(1))];
285 let mut control = SendAncillaryBuffer::new(&mut buffer);
286 let pushed = control.push(SendAncillaryMessage::ScmRights(&fds));
287 assert!(pushed);
288 let ios = std::io::IoSlice::new(DUMMY_DATA);
289 rustix::net::sendmsg(sock_child, &[ios], &mut control, SendFlags::empty())?;
290 std::process::exit(0)
292 }
293 -1 => {
294 let e = std::io::Error::last_os_error();
296 anyhow::bail!("failed to fork: {e}");
297 }
298 n => {
299 let pid = rustix::process::Pid::from_raw(n).unwrap();
301 drop(sock_child);
302 let mut cmsg_space = vec![MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(1))];
304 let mut cmsg_buffer = rustix::net::RecvAncillaryBuffer::new(&mut cmsg_space);
305 let mut buf = [0u8; DUMMY_DATA.len()];
306 let iov = std::io::IoSliceMut::new(buf.as_mut());
307 let mut iov = [iov];
308 let nread = rustix::net::recvmsg(
309 sock_parent,
310 &mut iov,
311 &mut cmsg_buffer,
312 RecvFlags::CMSG_CLOEXEC,
313 )
314 .context("recvmsg")?
315 .bytes;
316 anyhow::ensure!(nread == DUMMY_DATA.len());
317 assert_eq!(buf, DUMMY_DATA);
318 let r = cmsg_buffer
320 .drain()
321 .filter_map(|m| match m {
322 rustix::net::RecvAncillaryMessage::ScmRights(f) => Some(f),
323 _ => None,
324 })
325 .flatten()
326 .next()
327 .ok_or_else(|| anyhow::anyhow!("Did not receive a file descriptor"))?;
328 let st = rustix::process::waitpid(Some(pid), WaitOptions::empty())?
330 .expect("Wait status")
331 .1;
332 if let Some(0) = st.exit_status() {
333 Ok(r)
334 } else {
335 anyhow::bail!("forked helper failed: {st:?}");
336 }
337 }
338 }
339}
340
341pub fn bind_mount_from_pidns(
344 pid: Pid,
345 src: &Utf8Path,
346 target: &Utf8Path,
347 recursive: bool,
348) -> Result<()> {
349 let src = open_tree_from_pidns(pid, src, recursive)?;
350 rustix::mount::move_mount(
351 src.as_fd(),
352 "",
353 rustix::fs::CWD,
354 target.as_std_path(),
355 MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH,
356 )
357 .context("Moving mount")?;
358 Ok(())
359}
360
361pub fn ensure_mirrored_host_mount(path: impl AsRef<Utf8Path>) -> Result<()> {
364 let path = path.as_ref();
365 std::fs::create_dir_all(path)?;
368 if is_same_as_host(path)? {
369 tracing::debug!("Already mounted from host: {path}");
370 return Ok(());
371 }
372 tracing::debug!("Propagating host mount: {path}");
373 bind_mount_from_pidns(PID1, path, path, true)
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379 use camino::Utf8PathBuf;
380
381 fn mk_fs(source: &str, target: &str, children: Vec<Filesystem>) -> Filesystem {
382 Filesystem {
383 source: source.into(),
384 target: target.into(),
385 maj_min: "0:0".into(),
386 fstype: "ext4".into(),
387 options: "rw".into(),
388 uuid: None,
389 children: if children.is_empty() {
390 None
391 } else {
392 Some(children)
393 },
394 }
395 }
396
397 #[test]
398 fn find_source_mount_target_cases() {
399 let top_level = vec![
400 mk_fs("/dev/vda2", "/sysroot", vec![]),
401 mk_fs("/dev/vda1", "/boot", vec![]),
402 ];
403 let nested = vec![mk_fs(
406 "/dev/vda2",
407 "/sysroot",
408 vec![mk_fs("/dev/vda1", "/sysroot/boot", vec![])],
409 )];
410 let deep = vec![mk_fs(
415 "/dev/vda2",
416 "/sysroot",
417 vec![mk_fs(
418 "/dev/vda2",
419 "/var",
420 vec![mk_fs("/dev/mapper/var", "/var", vec![])],
421 )],
422 )];
423
424 let cases: &[(&str, &str, &[Filesystem], Option<&str>)] = &[
425 ("top-level match", "/dev/vda1", &top_level, Some("/boot")),
426 ("nested match", "/dev/vda1", &nested, Some("/sysroot/boot")),
427 ("not found", "/dev/vda1", &top_level[..1], None),
428 ("empty tree", "/dev/vda1", &[], None),
429 ("deep nesting", "/dev/mapper/var", &deep, Some("/var")),
430 ];
431
432 for (name, dev, tree, expected) in cases {
433 let got = find_source_mount_target(dev, tree);
434 let want = expected.map(Utf8PathBuf::from);
435 assert_eq!(got, want, "case: {name}");
436 }
437 }
438}