reverie-process/src/mount.rs (365 lines of code) (raw):

/* * Copyright (c) Facebook, Inc. and its affiliates. * * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ use super::fd::{create_dir_all, touch_path, FileType}; use super::util; use core::convert::Infallible; use core::fmt; use core::ptr; use core::str::FromStr; use std::collections::HashMap; use std::ffi::{CString, OsStr}; use std::os::unix::ffi::OsStrExt; use std::path::Path; use syscalls::Errno; pub use nix::mount::MsFlags as MountFlags; /// A mount. #[derive(Clone, Debug, Eq, PartialEq)] pub struct Mount { source: Option<CString>, target: CString, fstype: Option<CString>, flags: MountFlags, data: Option<CString>, touch_target: bool, } /// Represents a bind mount. Can be converted into a [`Mount`]. #[derive(Clone, Debug, Eq, PartialEq)] pub struct Bind { /// The source path of the bind mount. This path must exist. It can be either /// a file or directory. source: CString, /// The target of the bind mount. This does not need to exist and can be /// created when performing the bind mount. target: CString, } impl Mount { /// Creates a new mount at the path `target`. pub fn new<S: AsRef<OsStr>>(target: S) -> Self { Self { source: None, target: util::to_cstring(target), fstype: None, flags: MountFlags::empty(), data: None, touch_target: false, } } /// Creates a bind mount. This effectively creates hardlink of a directory, /// making the contents accessible at both places. /// /// By default, none of the mounts in the `source` directory are visible in /// `destination`. To make all mounts recursively visible, combine this with /// [`Mount::recursive`]. Can also be used with [`Mount::readonly`] to make /// the contents of `destination` read-only. pub fn bind<S: AsRef<OsStr>, D: AsRef<OsStr>>(source: S, destination: D) -> Self { Self::new(destination) .source(source) .flags(MountFlags::MS_BIND) } /// Move/rename a mount. pub fn rename<S: AsRef<OsStr>, D: AsRef<OsStr>>(source: S, destination: D) -> Self { Self::new(destination) .source(source) .flags(MountFlags::MS_MOVE) } /// Mount a fresh devpts file system. The target is usually `/dev/pts`. /// /// In order for this devpts to be private and independent of other devpts /// (i.e., for containers), use: /// ```no_compile /// Mount::devpts("/dev/pts").data("newinstance,ptmxmode=0666") /// ``` /// And either make `/dev/ptmx` a symlink pointing to `/dev/pts/ptmx` or /// bind-mount it. /// /// See also: <https://www.kernel.org/doc/Documentation/filesystems/devpts.txt> pub fn devpts<S: AsRef<OsStr>>(target: S) -> Self { Self::new(target).fstype("devpts") } /// Mount a fresh proc file system at `/proc`. pub fn proc() -> Self { Self::new("/proc").fstype("proc") } /// Mount an overlay file system. /// /// NOTE: This only works in Linux 5.11 or newer when mounted from a user /// namespace. Otherwise, you need real root privileges to mount an /// overlayfs. /// /// An overlay filesystem combines two filesystems - an upper filesystem and /// a lower filesystem. When a name exists in both filesystems, the object /// in the upper filesystem is visible while the object in the lower /// filesystem is either hidden or, in the case of directories, merged with /// the upper object. /// /// In other words, the `lowerdir` and `upperdir` are combined into a /// directory `merged` using `workdir` as a temporary work area. /// /// The lower filesystem can be any filesystem supported by Linux and does /// not need to be writable. The lower filesystem can even be another /// overlayfs. The upper filesystem should be writable. /// /// See <https://www.kernel.org/doc/html/latest/filesystems/overlayfs.html> for /// more information. /// /// # Arguments /// /// * `lowerdir` - The lower directory of the overlay. Can be any filesystem /// and does not need to be writable. This directory is never /// modified by writes to `merged`. /// * `upperdir` - The upper directory of the overlay. This is where all /// changes to `merged` are collected. Does not need to be /// empty, but should be when starting a new overlay from /// scratch. /// * `workdir` - The work directory. This should always be empty. /// * `merged` - The combination of `lowerdir` and `upperdir`. pub fn overlay(lowerdir: &Path, upperdir: &Path, workdir: &Path, merged: &Path) -> Self { // TODO: Since there can actually be multiple lowerdirs, it might be // more ergonomic to return an `OverlayBuilder` instead. let options = format!( "lowerdir={},upperdir={},workdir={}", lowerdir.display(), upperdir.display(), workdir.display() ); Self::new(merged) .fstype("overlay") .source("overlay") .data(options) } /// Creates a temporary file system at the location specified. pub fn tmpfs<S: AsRef<OsStr>>(target: S) -> Self { Self::new(target).fstype("tmpfs") } /// Creates a sys file system at the location specified. The target directory /// is usually `/sys`. This is useful when creating a network namespace. pub fn sysfs<S: AsRef<OsStr>>(target: S) -> Self { Self::new(target).fstype("sysfs") } /// Sets the mount point target. pub fn target<S: AsRef<OsStr>>(mut self, target: S) -> Self { self.target = util::to_cstring(target); self } /// Returns the mount point target path. pub fn get_target(&self) -> &Path { Path::new(OsStr::from_bytes(self.target.to_bytes())) } /// Sets the source of the mount. pub fn source<S: AsRef<OsStr>>(mut self, path: S) -> Self { self.source = Some(util::to_cstring(path)); self } /// Returns the mount point source path (if any). pub fn get_source(&self) -> Option<&Path> { self.source .as_ref() .map(|s| Path::new(OsStr::from_bytes(s.to_bytes()))) } /// Indicates that the target of a bind mount should be created /// automatically. pub fn touch_target(mut self) -> Self { self.touch_target = true; self } /// Adds mount flags. pub fn flags(mut self, flags: MountFlags) -> Self { self.flags |= flags; self } /// Make the file system read-only. pub fn readonly(mut self) -> Self { self.flags |= MountFlags::MS_RDONLY; self } /// Makes a bind mount recursive. pub fn recursive(mut self) -> Self { self.flags |= MountFlags::MS_REC; self } /// Makes this mount point private. Mount and unmount events do not propagate /// into or out of this mount point. pub fn private(mut self) -> Self { self.flags |= MountFlags::MS_PRIVATE; self } /// Make this mount point shared. Mount and unmount events immediately under /// this mount point will propagate to the other mount points that are /// members of this mount's peer group. Propagation here means that the same /// mount or unmount will automatically occur under all of the other mount /// points in the peer group. Conversely, mount and unmount events that take /// place under peer mount points will propagate to this mount point. pub fn shared(mut self) -> Self { self.flags |= MountFlags::MS_SHARED; self } /// Same as specifying both [`Mount::recursive`] and [`Mount::private`]. pub fn rprivate(mut self) -> Self { self.flags |= MountFlags::MS_REC | MountFlags::MS_PRIVATE; self } /// Same as specifying both [`Mount::recursive`] and [`Mount::shared`]. pub fn rshared(mut self) -> Self { self.flags |= MountFlags::MS_REC | MountFlags::MS_SHARED; self } /// Sets the filesystem type. pub fn fstype<S: AsRef<OsStr>>(mut self, fstype: S) -> Self { self.fstype = Some(util::to_cstring(fstype)); self } /// Sets any additional data required by the mount. pub fn data<S: AsRef<OsStr>>(mut self, data: S) -> Self { self.data = Some(util::to_cstring(data)); self } fn source_ptr(&self) -> *const libc::c_char { self.source.as_ref().map_or(ptr::null(), |s| s.as_ptr()) } fn target_ptr(&self) -> *const libc::c_char { self.target.as_ptr() } fn fstype_ptr(&self) -> *const libc::c_char { self.fstype.as_ref().map_or(ptr::null(), |s| s.as_ptr()) } fn data_ptr(&self) -> *const libc::c_void { self.data .as_ref() .map_or(ptr::null(), |s| s.as_ptr() as *const libc::c_void) } /// Performs the mount. For bind-mount operations, the target directory or /// file is created if [`touch_target`] was used. /// /// NOTE: This function *must* not allocate since it is called after `fork` /// (or `clone`) and before `execve`. Any allocations could cause deadlocks /// (which are hard to track down). pub(super) fn mount(&mut self) -> Result<(), Errno> { // NOTE: Although we can't allocate here, we can safely *modify* `self`. // When this function is called, we have forked virtual memory and any // modifications we make are copy-on-write and lost when `execve` is // called. Thus, this function takes `self` by mutable reference. if self.flags.contains(MountFlags::MS_BIND) && self.touch_target { // Bind mounts will fail unless the destination path exists, so it // is convenient to create it automatically. // // One reason for doing this here instead of the parent process is // because the target may not yet exist until we mount it. For // example, if we want to create a `/tmp` (tmpfs) folder and then // bind-mount some files or directories into it, pre-creating the // destination directories won't work because they'll get created in // a different tmpfs. if let Some(src) = &self.source { if FileType::new(src.as_ptr())?.is_dir() { create_dir_all(&mut self.target, 0o777)?; } else { touch_path(&mut self.target, 0o666, 0o777)?; } } } Errno::result(unsafe { libc::mount( self.source_ptr(), self.target_ptr(), self.fstype_ptr(), self.flags.bits(), self.data_ptr(), ) })?; Ok(()) } } impl Bind { /// Creates a new bind mount. The `target` is optional because it is often /// convenient to use an identical `source` and `target` directory. If /// `target` is `None`, then it is interpretted as being the same as /// `source`. pub fn new<S, T>(source: S, target: T) -> Self where S: AsRef<OsStr>, T: AsRef<OsStr>, { Self { source: util::to_cstring(source), target: util::to_cstring(target), } } } impl From<Bind> for Mount { fn from(b: Bind) -> Self { Self { source: Some(b.source), target: b.target, fstype: None, flags: MountFlags::MS_BIND, data: None, touch_target: false, } } } impl From<&str> for Bind { fn from(s: &str) -> Self { if let Some((source, target)) = s.split_once(':') { Self { source: util::to_cstring(source), target: util::to_cstring(target), } } else { let source = util::to_cstring(s); let target = source.clone(); Self { source, target } } } } impl FromStr for Bind { type Err = Infallible; /// Parses bind mounts of the following forms: /// 1. "path/to/source" /// 2. "path/to/source:path/to/dest" fn from_str(s: &str) -> Result<Self, Self::Err> { Ok(Self::from(s)) } } /// An error from parsing a mount. #[derive(thiserror::Error, Debug, Eq, PartialEq)] pub enum MountParseError { /// The `target` key is missing. This is always required. MissingTarget, /// An invalid mount option was specified. Invalid(String, Option<String>), } impl fmt::Display for MountParseError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::MissingTarget => write!(f, "missing mount target"), Self::Invalid(k, v) => match v { Some(v) => write!(f, "invalid mount option '{}={}'", k, v), None => write!(f, "invalid mount option '{}'", k), }, } } } impl FromStr for Mount { type Err = MountParseError; /// Parses a [`Mount`]. This accepts the same syntax as Docker mounts where /// each mount consists of a comma-separated key-value list. /// /// See <https://docs.docker.com/storage/bind-mounts/> for more information. fn from_str(s: &str) -> Result<Self, Self::Err> { let mut map: HashMap<&str, Option<&str>> = HashMap::new(); for item in s.split(',') { let item = item.trim(); if item.is_empty() { continue; } let (key, value) = match item.split_once('=') { Some((key, value)) => (key, Some(value)), None => (item, None), }; map.insert(key, value); } // The mount target is always required. let mut mount = match map .remove("target") .or_else(|| map.remove("destination")) .or_else(|| map.remove("dest")) .or_else(|| map.remove("dst")) .flatten() { Some(target) => Mount::new(target), None => { return Err(MountParseError::MissingTarget); } }; if let Some(source) = map.remove("source").or_else(|| map.remove("src")).flatten() { mount = mount.source(source); } let is_bind_mount = if let Some(fstype) = map.remove("type").flatten() { if fstype == "bind" { true } else { mount = mount.fstype(fstype); false } } else { true }; if is_bind_mount { mount = mount.flags(MountFlags::MS_BIND); } if let Some((key, value)) = map.remove_entry("readonly") { if let Some(value) = value { // No value should have been specified. return Err(MountParseError::Invalid(key.into(), Some(value.to_owned()))); } mount = mount.readonly(); } if let Some(propagation) = map.remove("bind-propagation").flatten() { let flags = match propagation { "shared" => MountFlags::MS_SHARED, "slave" => MountFlags::MS_SLAVE, "private" => MountFlags::MS_PRIVATE, "rshared" => MountFlags::MS_REC | MountFlags::MS_SHARED, "rslave" => MountFlags::MS_REC | MountFlags::MS_SLAVE, "rprivate" => MountFlags::MS_REC | MountFlags::MS_PRIVATE, _ => { return Err(MountParseError::Invalid( "bind-propagation".into(), Some(propagation.into()), )); } }; mount = mount.flags(flags); } else { // All mounts get these flags by default. mount = mount.flags(MountFlags::MS_REC | MountFlags::MS_PRIVATE); } // Any left over keys are invalid. if let Some((k, v)) = map.into_iter().next() { return Err(MountParseError::Invalid(k.into(), v.map(ToOwned::to_owned))); } Ok(mount) } } #[cfg(test)] mod tests { use super::*; #[test] fn getters_and_setters() { let m = Mount::bind("/foo", "/bar"); assert_eq!(m.get_target(), Path::new("/bar")); assert_eq!(m.get_source(), Some(Path::new("/foo"))); let m = m.target("/baz"); assert_eq!(m.get_target(), Path::new("/baz")); } #[test] fn parse_mount() { assert_eq!( Mount::from_str("type=bind,source=/foo,target=/bar,readonly"), Ok(Mount::bind("/foo", "/bar").readonly().rprivate()) ); assert_eq!( Mount::from_str("src=/foo,target=/bar,readonly"), Ok(Mount::bind("/foo", "/bar").readonly().rprivate()) ); assert_eq!( Mount::from_str("src=/foo,target=/bar,bind-propagation=rshared"), Ok(Mount::bind("/foo", "/bar").rshared()) ); assert_eq!( Mount::from_str("type=tmpfs,target=/tmp"), Ok(Mount::tmpfs("/tmp").rprivate()) ); assert_eq!( Mount::from_str("target=foo, ,,,"), Ok(Mount::new("foo").flags(MountFlags::MS_BIND).rprivate()) ); assert_eq!(Mount::from_str(""), Err(MountParseError::MissingTarget)); assert_eq!( Mount::from_str("type=bind,source=/foo,readonly"), Err(MountParseError::MissingTarget) ); assert_eq!( Mount::from_str("type=tmpfs,target=/foo,wat"), Err(MountParseError::Invalid("wat".into(), None)) ); assert_eq!( Mount::from_str("type=tmpfs,target=/foo,readonly=wat"), Err(MountParseError::Invalid( "readonly".into(), Some("wat".into()) )) ); } #[test] fn parse_bind() { assert_eq!(Bind::from("source:target"), Bind::new("source", "target")); assert_eq!(Bind::from("source"), Bind::new("source", "source")); assert_eq!( Mount::from(Bind::from("source:target")), Mount::bind("source", "target") ); } }