fuse_backend_rs/passthrough/mount_fd.rs
1// Use of this source code is governed by a BSD-style license that can be
2// found in the LICENSE-BSD-3-Clause file.
3
4use std::collections::{HashMap, HashSet};
5use std::ffi::CString;
6use std::fs::File;
7use std::io::{self, Read, Seek};
8use std::os::fd::{AsFd, BorrowedFd};
9use std::os::unix::io::{AsRawFd, RawFd};
10use std::sync::{Arc, Mutex, RwLock, Weak};
11
12use super::statx::statx;
13use super::util::{einval, is_safe_inode};
14
15const MOUNT_INFO_FILE: &str = "/proc/self/mountinfo";
16
17/// Type alias for mount id.
18pub type MountId = u64;
19
20pub struct MountFd {
21 file: File,
22 mount_id: MountId,
23 map: Weak<RwLock<HashMap<MountId, Weak<MountFd>>>>,
24}
25
26impl AsFd for MountFd {
27 fn as_fd(&self) -> BorrowedFd {
28 self.file.as_fd()
29 }
30}
31
32impl Drop for MountFd {
33 fn drop(&mut self) {
34 debug!(
35 "Dropping MountFd: mount_id={}, mount_fd={}",
36 self.mount_id,
37 self.file.as_raw_fd(),
38 );
39
40 // If `self.map.upgrade()` fails, then the `MountFds` structure was dropped while there was
41 // still an `Arc<MountFd>` alive. In this case, we don't need to remove it from the map,
42 // because the map doesn't exist anymore.
43 if let Some(map) = self.map.upgrade() {
44 let mut map = map.write().unwrap();
45 // After the refcount reaches zero and before we lock the map, there's a window where
46 // the value can be concurrently replaced by a `Weak` pointer to a new `MountFd`.
47 // Therefore, only remove the value if the refcount in the map is zero, too.
48 if let Some(0) = map.get(&self.mount_id).map(Weak::strong_count) {
49 map.remove(&self.mount_id);
50 }
51 }
52 }
53}
54
55/// This type maintains a map where each entry maps a mount ID to an open FD on that mount. Other
56/// code can request an `Arc<MountFd>` for any mount ID. A key gets added to the map, when the
57/// first `Arc<MountFd>` for that mount ID is requested. A key gets removed from the map, when the
58/// last `Arc<MountFd>` for that mount ID is dropped. That is, map entries are reference-counted
59/// and other code can keep an entry in the map by holding on to an `Arc<MountFd>`.
60///
61/// We currently have one use case for `MountFds`:
62///
63/// 1. Creating a file handle only returns a mount ID, but opening a file handle requires an open FD
64/// on the respective mount. So we look that up in the map.
65pub struct MountFds {
66 map: Arc<RwLock<HashMap<MountId, Weak<MountFd>>>>,
67
68 /// /proc/self/mountinfo
69 mount_info: Mutex<File>,
70
71 /// An optional prefix to strip from all mount points in mountinfo
72 mount_prefix: Option<String>,
73
74 /// Set of filesystems for which we have already logged file handle errors
75 error_logged: Arc<RwLock<HashSet<MountId>>>,
76}
77
78impl MountFds {
79 pub fn new(mount_prefix: Option<String>) -> io::Result<Self> {
80 let mount_info_file = File::open(MOUNT_INFO_FILE)?;
81
82 Ok(Self::with_mount_info_file(mount_info_file, mount_prefix))
83 }
84
85 pub fn with_mount_info_file(mount_info: File, mount_prefix: Option<String>) -> Self {
86 MountFds {
87 map: Default::default(),
88 mount_info: Mutex::new(mount_info),
89 mount_prefix,
90 error_logged: Default::default(),
91 }
92 }
93
94 pub fn get<F>(&self, mount_id: MountId, reopen_fd: F) -> MPRResult<Arc<MountFd>>
95 where
96 F: FnOnce(RawFd, libc::c_int, u32) -> io::Result<File>,
97 {
98 let existing_mount_fd = self
99 .map
100 // The `else` branch below (where `existing_mount_fd` matches `None`) takes a write lock
101 // to insert a new mount FD into the hash map. This doesn't deadlock, because the read
102 // lock taken here doesn't have its lifetime extended beyond the statement, because
103 // `Weak::upgrade` returns a new pointer and not a reference into the read lock.
104 .read()
105 .unwrap()
106 .get(&mount_id)
107 // We treat a failed upgrade just like a non-existent key, because it means that all
108 // strong references to the `MountFd` have disappeared, so it's in the process of being
109 // dropped, but `MountFd::drop()` just did not yet get to remove it from the map.
110 .and_then(Weak::upgrade);
111
112 let mount_fd = if let Some(mount_fd) = existing_mount_fd {
113 mount_fd
114 } else {
115 // `open_by_handle_at()` needs a non-`O_PATH` fd, which we will need to open here. We
116 // are going to open the filesystem's mount point, but we do not know whether that is a
117 // special file[1], and we must not open special files with anything but `O_PATH`, so
118 // we have to get some `O_PATH` fd first that we can stat to find out whether it is
119 // safe to open.
120 // [1] While mount points are commonly directories, it is entirely possible for a
121 // filesystem's root inode to be a regular or even special file.
122 let mount_point = self.get_mount_root(mount_id)?;
123
124 // Clone `mount_point` so we can still use it in error messages
125 let c_mount_point = CString::new(mount_point.clone()).map_err(|e| {
126 self.error_for(mount_id, e)
127 .prefix(format!("Failed to convert \"{mount_point}\" to a CString"))
128 })?;
129
130 let mount_point_fd = unsafe { libc::open(c_mount_point.as_ptr(), libc::O_PATH) };
131 if mount_point_fd < 0 {
132 return Err(self
133 .error_for(mount_id, io::Error::last_os_error())
134 .prefix(format!("Failed to open mount point \"{mount_point}\"")));
135 }
136
137 // Check the mount point has the expected `mount_id`.
138 let st_mode = self.validate_mount_id(mount_id, &mount_point_fd, &mount_point)?;
139
140 // Ensure that we can safely reopen `mount_point_path` with `O_RDONLY`
141 let file_type = st_mode & libc::S_IFMT;
142 if !is_safe_inode(file_type) {
143 return Err(self
144 .error_for(mount_id, io::Error::from_raw_os_error(libc::EIO))
145 .set_desc(format!(
146 "Mount point \"{mount_point}\" is not a regular file or directory"
147 )));
148 }
149
150 // Now that we know that this is a regular file or directory, really open it
151 let file = reopen_fd(
152 mount_point_fd.as_raw_fd(),
153 libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
154 st_mode,
155 )
156 .map_err(|e| {
157 self.error_for(mount_id, e).prefix(format!(
158 "Failed to reopen mount point \"{mount_point}\" for reading"
159 ))
160 })?;
161
162 let mut mount_fds_locked = self.map.write().unwrap();
163
164 // As above: by calling `and_then(Weak::upgrade)`, we treat a failed upgrade just like a
165 // non-existent key. If the key exists but upgrade fails, then `HashMap::insert()`
166 // below will update the value. `MountFd::drop()` takes care to only remove a `MountFd`
167 // without strong references from the map, and hence will not touch the updated one.
168 if let Some(mount_fd) = mount_fds_locked.get(&mount_id).and_then(Weak::upgrade) {
169 // A mount FD was added concurrently while we did not hold a lock on
170 // `mount_fds.map` -- use that entry (`file` will be dropped).
171 mount_fd
172 } else {
173 debug!(
174 "Creating MountFd: mount_id={}, mount_fd={}",
175 mount_id,
176 file.as_raw_fd(),
177 );
178 let mount_fd = Arc::new(MountFd {
179 file,
180 mount_id,
181 map: Arc::downgrade(&self.map),
182 });
183 mount_fds_locked.insert(mount_id, Arc::downgrade(&mount_fd));
184 mount_fd
185 }
186 };
187
188 Ok(mount_fd)
189 }
190
191 // Ensure that `mount_point_path` refers to an inode with the mount ID we need
192 fn validate_mount_id(
193 &self,
194 mount_id: MountId,
195 mount_point_fd: &impl AsRawFd,
196 mount_point: &str,
197 ) -> MPRResult<libc::mode_t> {
198 let stx = statx(mount_point_fd, None).map_err(|e| {
199 self.error_for(mount_id, e)
200 .prefix(format!("Failed to stat mount point \"{mount_point}\""))
201 })?;
202
203 if stx.mnt_id != mount_id {
204 return Err(self
205 .error_for(mount_id, io::Error::from_raw_os_error(libc::EIO))
206 .set_desc(format!(
207 "Mount point's ({}) mount ID ({}) does not match expected value ({})",
208 mount_point, stx.mnt_id, mount_id
209 )));
210 }
211
212 Ok(stx.st.st_mode)
213 }
214
215 /// Given a mount ID, return the mount root path (by reading `/proc/self/mountinfo`)
216 fn get_mount_root(&self, mount_id: MountId) -> MPRResult<String> {
217 let mountinfo = {
218 let mountinfo_file = &mut *self.mount_info.lock().unwrap();
219
220 mountinfo_file.rewind().map_err(|e| {
221 self.error_for_nolookup(mount_id, e)
222 .prefix("Failed to access /proc/self/mountinfo".into())
223 })?;
224
225 let mut mountinfo = String::new();
226 mountinfo_file.read_to_string(&mut mountinfo).map_err(|e| {
227 self.error_for_nolookup(mount_id, e)
228 .prefix("Failed to read /proc/self/mountinfo".into())
229 })?;
230
231 mountinfo
232 };
233
234 let path = mountinfo.split('\n').find_map(|line| {
235 let mut columns = line.split(char::is_whitespace);
236
237 if columns.next()?.parse::<MountId>().ok()? != mount_id {
238 return None;
239 }
240
241 // Skip parent mount ID, major:minor device ID, and the root within the filesystem
242 // (to get to the mount path)
243 columns.nth(3)
244 });
245
246 match path {
247 Some(p) => {
248 let p = String::from(p);
249 if let Some(prefix) = self.mount_prefix.as_ref() {
250 if let Some(suffix) = p.strip_prefix(prefix).filter(|s| !s.is_empty()) {
251 Ok(suffix.into())
252 } else {
253 // The shared directory is the mount point (strip_prefix() returned "") or
254 // mount is outside the shared directory, so it must be the mount the root
255 // directory is on
256 Ok("/".into())
257 }
258 } else {
259 Ok(p)
260 }
261 }
262
263 None => Err(self
264 .error_for_nolookup(mount_id, einval())
265 .set_desc(format!("Failed to find mount root for mount ID {mount_id}"))),
266 }
267 }
268
269 /// Generate an `MPRError` object for the given `mount_id`, and silence it if we have already
270 /// generated such an object for that `mount_id`.
271 /// (Called `..._nolookup`, because in contrast to `MountFds::error_for()`, this method will
272 /// not try to look up the respective mount root path, and so is safe to call when such a
273 /// lookup would be unwise.)
274 fn error_for_nolookup<E: ToString + Into<io::Error>>(
275 &self,
276 mount_id: MountId,
277 err: E,
278 ) -> MPRError {
279 let err = MPRError::from(err).set_mount_id(mount_id);
280
281 if self.error_logged.read().unwrap().contains(&mount_id) {
282 err.silence()
283 } else {
284 self.error_logged.write().unwrap().insert(mount_id);
285 err
286 }
287 }
288
289 /// Call `self.error_for_nolookup()`, and if the `MPRError` object is not silenced, try to
290 /// obtain the mount root path for the given `mount_id` and add it to the error object.
291 /// (Note: DO NOT call this method from `MountFds::get_mount_root()`, because that may lead to
292 /// an infinite loop.)
293 pub fn error_for<E: ToString + Into<io::Error>>(&self, mount_id: MountId, err: E) -> MPRError {
294 let err = self.error_for_nolookup(mount_id, err);
295
296 if err.silent() {
297 // No need to add more information
298 err
299 } else {
300 // This just adds some information, so ignore errors
301 if let Ok(mount_root) = self.get_mount_root(mount_id) {
302 err.set_mount_root(mount_root)
303 } else {
304 err
305 }
306 }
307 }
308}
309
310/**
311 * Error object (to be used as `Result<T, MPRError>`) for mount-point-related errors (hence MPR).
312 * Includes a description (that is auto-generated from the `io::Error` at first), which can be
313 * overridden with `MPRError::set_desc()`, or given a prefix with `MPRError::prefix()`.
314 *
315 * The full description can be retrieved through the `Display` trait implementation (or the
316 * auto-derived `ToString`).
317 *
318 * `MPRError` objects should generally be logged at some point, because they may indicate an error
319 * in the user's configuration or a bug in virtiofsd. However, we only want to log them once per
320 * filesystem, and so they can be silenced (setting `silent` to true if we know that we have
321 * already logged an error for the respective filesystem) and then should not be logged.
322 *
323 * Naturally, a "mount-point-related" error should be associated with some mount point, which is
324 * reflected in `fs_mount_id` and `fs_mount_root`. Setting these values will improve the error
325 * description, because the `Display` implementation will prepend these values to the returned
326 * string.
327 *
328 * To achieve this association, `MPRError` objects should be created through
329 * `MountFds::error_for()`, which obtains the mount root path for the given mount ID, and will thus
330 * try to not only set `fs_mount_id`, but `fs_mount_root` also. `MountFds::error_for()` will also
331 * take care to set `silent` as appropriate.
332 *
333 * (Sometimes, though, we know an error is associated with a mount point, but we do not know with
334 * which one. That is why the `fs_mount_id` field is optional.)
335 */
336#[derive(Debug)]
337pub struct MPRError {
338 io: io::Error,
339 description: String,
340 silent: bool,
341
342 fs_mount_id: Option<MountId>,
343 fs_mount_root: Option<String>,
344}
345
346/// Type alias for convenience
347pub type MPRResult<T> = Result<T, MPRError>;
348
349impl<E: ToString + Into<io::Error>> From<E> for MPRError {
350 /// Convert any stringifyable error object that can be converted to an `io::Error` to an
351 /// `MPRError`. Note that `fs_mount_id` and `fs_mount_root` are not set, so this `MPRError`
352 /// object is not associated with any mount point.
353 /// The initial description is taken from the original error object.
354 fn from(err: E) -> Self {
355 let description = err.to_string();
356 MPRError {
357 io: err.into(),
358 description,
359 silent: false,
360
361 fs_mount_id: None,
362 fs_mount_root: None,
363 }
364 }
365}
366
367impl MPRError {
368 /// Override the current description
369 #[must_use]
370 pub fn set_desc(mut self, s: String) -> Self {
371 self.description = s;
372 self
373 }
374
375 /// Add a prefix to the description
376 #[must_use]
377 pub fn prefix(self, s: String) -> Self {
378 let new_desc = format!("{}: {}", s, self.description);
379 self.set_desc(new_desc)
380 }
381
382 /// To give additional information to the user (when this error is logged), add the mount ID of
383 /// the filesystem associated with this error
384 #[must_use]
385 fn set_mount_id(mut self, mount_id: MountId) -> Self {
386 self.fs_mount_id = Some(mount_id);
387 self
388 }
389
390 /// To give additional information to the user (when this error is logged), add the mount root
391 /// path for the filesystem associated with this error
392 #[must_use]
393 fn set_mount_root(mut self, mount_root: String) -> Self {
394 self.fs_mount_root = Some(mount_root);
395 self
396 }
397
398 /// Mark this error as silent (i.e. not to be logged)
399 #[must_use]
400 fn silence(mut self) -> Self {
401 self.silent = true;
402 self
403 }
404
405 /// Return whether this error is silent (i.e. should not be logged)
406 pub fn silent(&self) -> bool {
407 self.silent
408 }
409
410 /// Return the `io::Error` from an `MPRError` and drop the rest
411 pub fn into_inner(self) -> io::Error {
412 self.io
413 }
414}
415
416impl std::fmt::Display for MPRError {
417 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
418 match (self.fs_mount_id, &self.fs_mount_root) {
419 (None, None) => write!(f, "{}", self.description),
420
421 (Some(id), None) => write!(f, "Filesystem with mount ID {}: {}", id, self.description),
422
423 (None, Some(root)) => write!(
424 f,
425 "Filesystem mounted on \"{}\": {}",
426 root, self.description
427 ),
428
429 (Some(id), Some(root)) => write!(
430 f,
431 "Filesystem mounted on \"{}\" (mount ID: {}): {}",
432 root, id, self.description
433 ),
434 }
435 }
436}
437
438impl std::error::Error for MPRError {}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443 use crate::passthrough::file_handle::FileHandle;
444
445 #[test]
446 fn test_mount_fd_get() {
447 let topdir = env!("CARGO_MANIFEST_DIR");
448 let dir = File::open(topdir).unwrap();
449 let filename = CString::new("build.rs").unwrap();
450 let mount_fds = MountFds::new(None).unwrap();
451 let handle = FileHandle::from_name_at(&dir, &filename).unwrap().unwrap();
452
453 // Ensure that `MountFds::get()` works for new entry.
454 let fd1 = mount_fds
455 .get(handle.mnt_id, |_fd, _flags, _mode| File::open(topdir))
456 .unwrap();
457 assert_eq!(Arc::strong_count(&fd1), 1);
458 assert_eq!(mount_fds.map.read().unwrap().len(), 1);
459
460 // Ensure that `MountFds::get()` works for existing entry.
461 let fd2 = mount_fds
462 .get(handle.mnt_id, |_fd, _flags, _mode| File::open(topdir))
463 .unwrap();
464 assert_eq!(Arc::strong_count(&fd2), 2);
465 assert_eq!(mount_fds.map.read().unwrap().len(), 1);
466
467 // Ensure fd1 and fd2 are the same object.
468 assert_eq!(fd1.as_fd().as_raw_fd(), fd2.as_fd().as_raw_fd());
469
470 drop(fd1);
471 assert_eq!(Arc::strong_count(&fd2), 1);
472 assert_eq!(mount_fds.map.read().unwrap().len(), 1);
473
474 // Ensure that `MountFd::drop()` works as expected.
475 drop(fd2);
476 assert_eq!(mount_fds.map.read().unwrap().len(), 0);
477 }
478
479 #[test]
480 fn test_mpr_error() {
481 let io_error = io::Error::new(io::ErrorKind::Other, "test");
482 let mpr_error = MPRError::from(io_error);
483
484 assert!(!mpr_error.silent);
485 assert!(mpr_error.fs_mount_id.is_none());
486 assert!(mpr_error.fs_mount_root.is_none());
487 let mpr_error = mpr_error.silence();
488 let msg = format!("{}", mpr_error);
489 assert!(msg.len() > 0);
490 assert!(mpr_error.silent());
491 }
492}