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;
27
28pub 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 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 "--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
73fn 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}")]
83pub fn inspect_filesystem(path: &Utf8Path) -> Result<Filesystem> {
86 findmnt_filesystem(&["--mountpoint"], None, path.as_str())
87}
88
89#[context("Inspecting filesystem")]
90pub fn inspect_filesystem_of_dir(d: &Dir) -> Result<Filesystem> {
93 findmnt_filesystem(&["--mountpoint"], Some(d), ".")
94}
95
96#[context("Inspecting filesystem by UUID {uuid}")]
97pub fn inspect_filesystem_by_uuid(uuid: &str) -> Result<Filesystem> {
99 findmnt_filesystem(&["--source"], None, &(format!("UUID={uuid}")))
100}
101
102pub 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
112pub 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
129pub 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#[context("Comparing filesystems at {path} and /proc/1/root/{path}")]
141pub fn is_same_as_host(path: &Utf8Path) -> Result<bool> {
142 let path = Utf8Path::new("/").join(path);
144
145 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#[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 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 drop(sock_parent);
182
183 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 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 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 std::process::exit(0)
215 }
216 -1 => {
217 let e = std::io::Error::last_os_error();
219 anyhow::bail!("failed to fork: {e}");
220 }
221 n => {
222 let pid = rustix::process::Pid::from_raw(n).unwrap();
224 drop(sock_child);
225 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 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 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
264pub 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
284pub fn ensure_mirrored_host_mount(path: impl AsRef<Utf8Path>) -> Result<()> {
287 let path = path.as_ref();
288 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}