From 48c7a07c428ccd680c23a18f269f3b887439d6d6 Mon Sep 17 00:00:00 2001 From: Alan Somers Date: Sun, 13 Jun 2021 19:42:13 -0600 Subject: [PATCH] Add nmount for FreeBSD. --- CHANGELOG.md | 2 + Cargo.toml | 2 +- src/lib.rs | 1 + src/mount/bsd.rs | 469 +++++++++++++++++++++++++++++++ src/{mount.rs => mount/linux.rs} | 0 src/mount/mod.rs | 20 ++ src/sys/uio.rs | 10 +- test/common/mod.rs | 14 + test/test.rs | 2 + test/test_nmount.rs | 51 ++++ 10 files changed, 569 insertions(+), 2 deletions(-) create mode 100644 src/mount/bsd.rs rename src/{mount.rs => mount/linux.rs} (100%) create mode 100644 src/mount/mod.rs create mode 100644 test/test_nmount.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c8f28b129..0e04f02040 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ This project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] - ReleaseDate ### Added - Added `if_nameindex` (#[1445](https://github.com/nix-rust/nix/pull/1445)) +- Added `nmount` for FreeBSD. + (#[1447](https://github.com/nix-rust/nix/pull/1447)) ### Changed ### Fixed diff --git a/Cargo.toml b/Cargo.toml index 843c5c1391..853b99a398 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ targets = [ ] [dependencies] -libc = { version = "0.2.95", features = [ "extra_traits" ] } +libc = { git = "https://github.com/rust-lang/libc", rev = "72da7a6a464a5a2a17993b94d94d2ab99bb87db6", features = [ "extra_traits" ] } bitflags = "1.1" cfg-if = "1.0" diff --git a/src/lib.rs b/src/lib.rs index 899d3f8bd2..d8221ded19 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,6 +44,7 @@ pub mod ifaddrs; target_os = "linux"))] pub mod kmod; #[cfg(any(target_os = "android", + target_os = "freebsd", target_os = "linux"))] pub mod mount; #[cfg(any(target_os = "dragonfly", diff --git a/src/mount/bsd.rs b/src/mount/bsd.rs new file mode 100644 index 0000000000..260e541bdb --- /dev/null +++ b/src/mount/bsd.rs @@ -0,0 +1,469 @@ +use crate::{ + Errno, + NixPath, + Result, + sys::uio::IoVec +}; +use libc::{c_char, c_int, c_uint, c_void}; +use std::{ + borrow::Cow, + ffi::{CString, CStr}, + fmt, + io, + ptr +}; + + +libc_bitflags!( + /// Used with [`Nmount::nmount`]. + pub struct MntFlags: c_int { + /// ACL support enabled. + #[cfg(any(target_os = "netbsd", target_os = "freebsd"))] + MNT_ACLS; + /// All I/O to the file system should be done asynchronously. + MNT_ASYNC; + /// dir should instead be a file system ID encoded as “FSID:val0:val1”. + #[cfg(target_os = "freebsd")] + MNT_BYFSID; + /// Force a read-write mount even if the file system appears to be + /// unclean. + MNT_FORCE; + /// GEOM journal support enabled. + #[cfg(target_os = "freebsd")] + MNT_GJOURNAL; + /// MAC support for objects. + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + MNT_MULTILABEL; + /// Disable read clustering. + #[cfg(any(target_os = "dragonfly", target_os = "freebsd"))] + MNT_NOCLUSTERR; + /// Disable write clustering. + #[cfg(any(target_os = "dragonfly", target_os = "freebsd"))] + MNT_NOCLUSTERW; + /// Enable NFS version 4 ACLs. + #[cfg(target_os = "freebsd")] + MNT_NFS4ACLS; + /// Do not update access times. + MNT_NOATIME; + /// Disallow program execution. + MNT_NOEXEC; + /// Do not honor setuid or setgid bits on files when executing them. + MNT_NOSUID; + /// Do not follow symlinks. + #[cfg(any(target_os = "dragonfly", target_os = "freebsd"))] + MNT_NOSYMFOLLOW; + /// Mount read-only. + MNT_RDONLY; + /// Causes the vfs subsystem to update its data structures pertaining to + /// the specified already mounted file system. + MNT_RELOAD; + /// Create a snapshot of the file system. + /// + /// See [mksnap_ffs(8)](https://www.freebsd.org/cgi/man.cgi?query=mksnap_ffs) + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + MNT_SNAPSHOT; + /// Using soft updates. + #[cfg(any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + MNT_SOFTDEP; + /// Directories with the SUID bit set chown new files to their own + /// owner. + #[cfg(any(target_os = "dragonfly", target_os = "freebsd"))] + MNT_SUIDDIR; + /// All I/O to the file system should be done synchronously. + MNT_SYNCHRONOUS; + /// Union with underlying fs. + #[cfg(any( + target_os = "macos", + target_os = "freebsd", + target_os = "netbsd" + ))] + MNT_UNION; + /// Indicates that the mount command is being applied to an already + /// mounted file system. + MNT_UPDATE; + ///// Exported read only. + //#[cfg(any( + //target_os = "dragonfly", + //target_os = "freebsd", + //target_os = "netbsd", + //target_os = "openbsd" + //))] + //MNT_EXRDONLY; + ///// Filesystem is exported. + //MNT_EXPORTED; + ///// Exported to the world. + //#[cfg(any( + //target_os = "dragonfly", + //target_os = "freebsd", + //target_os = "netbsd", + //target_os = "openbsd" + //))] + //MNT_DEFEXPORTED; + ///// Anon uid mapping for all. + //#[cfg(any( + //target_os = "dragonfly", + //target_os = "freebsd", + //target_os = "netbsd", + //target_os = "openbsd" + //))] + //MNT_EXPORTANON; + ///// Exported with Kerberos. + //#[cfg(any( + //target_os = "dragonfly", + //target_os = "freebsd", + //target_os = "netbsd", + //))] + //MNT_EXKERB; + ///// Public export (WebNFS). + //#[cfg(any(target_os = "dragonfly", target_os = "freebsd"))] + //MNT_EXPUBLIC; + ///// Delete export host lists. + //#[cfg(any( + //target_os = "dragonfly", + //target_os = "freebsd", + //target_os = "openbsd", + //))] + //MNT_DELEXPORT; + /// Check vnode use counts. + #[cfg(target_os = "freebsd")] + MNT_NONBUSY; + } +); + + +/// The Error type of [`Nmount::nmount`]. +/// +/// It wraps an [`Errno`], but also may contain an additional message returned +/// by `nmount(2)`. +#[derive(Debug)] +pub struct NmountError { + errno: Errno, + errmsg: Option +} + +impl NmountError { + /// Returns the additional error string sometimes generated by `nmount(2)`. + pub fn errmsg(&self) -> Option<&str> { + self.errmsg.as_deref() + } + + /// Returns the inner [`Errno`] + pub fn errno(&self) -> Errno { + self.errno + } + + fn new(errno: Errno, errmsg: Option<&CStr>) -> Self { + Self { + errno, + errmsg: errmsg.map(CStr::to_string_lossy).map(Cow::into_owned) + } + } +} + +impl std::error::Error for NmountError {} + +impl fmt::Display for NmountError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(errmsg) = &self.errmsg { + write!(f, "{:?}: {}: {}", self.errno, errmsg, self.errno.desc()) + } else { + write!(f, "{:?}: {}", self.errno, self.errno.desc()) + } + } +} + +impl From for io::Error { + fn from(err: NmountError) -> Self { + io::Error::from_raw_os_error(err.errno as i32) + } +} + +/// Result type of [`Nmount::nmount`]. +pub type NmountResult = std::result::Result<(), NmountError>; + +/// Mount a FreeBSD file system. +/// +/// The `nmount(2)` system call works similarly to the `mount(8)` program; it +/// takes its options as a series of name-value pairs. Most of the values are +/// strings, as are all of the names. The `Nmount` structure builds up an +/// argument list and then executes the syscall. +/// +/// # Examples +/// +/// To mount `target` onto `mountpoint` with `nullfs`: +/// ``` +/// # use nix::unistd::Uid; +/// # use ::sysctl::CtlValue; +/// # if !Uid::current().is_root() && CtlValue::Int(0) == ::sysctl::value("vfs.usermount").unwrap() { +/// # return; +/// # }; +/// use nix::mount::{MntFlags, Nmount, unmount}; +/// use std::ffi::CString; +/// use tempfile::tempdir; +/// +/// let mountpoint = tempdir().unwrap(); +/// let target = tempdir().unwrap(); +/// +/// let fstype = CString::new("fstype").unwrap(); +/// let nullfs = CString::new("nullfs").unwrap(); +/// Nmount::new() +/// .str_opt(&fstype, &nullfs) +/// .str_opt_owned("fspath", mountpoint.path().to_str().unwrap()) +/// .str_opt_owned("target", target.path().to_str().unwrap()) +/// .nmount(MntFlags::empty()).unwrap(); +/// +/// unmount(mountpoint.path(), MntFlags::empty()).unwrap(); +/// ``` +/// +/// # See Also +/// * [`nmount(2)`](https://www.freebsd.org/cgi/man.cgi?query=nmount) +/// * [`nullfs(5)`](https://www.freebsd.org/cgi/man.cgi?query=nullfs) +#[cfg(target_os = "freebsd")] +#[derive(Debug, Default)] +pub struct Nmount<'a>{ + iov: Vec>, + is_owned: Vec, +} + +#[cfg(target_os = "freebsd")] +impl<'a> Nmount<'a> { + /// Add an opaque mount option. + /// + /// Some file systems take binary-valued mount options. They can be set + /// with this method. + /// + /// # Safety + /// + /// Unsafe because it will cause `Nmount::nmount` to dereference a raw + /// pointer. The user is responsible for ensuring that `val` is valid and + /// its lifetime outlives `self`! An easy way to do that is to give the + /// value a larger scope than `name` + /// + /// # Examples + /// ``` + /// use libc::c_void; + /// use nix::mount::Nmount; + /// use std::ffi::CString; + /// use std::mem; + /// + /// // Note that flags outlives name + /// let mut flags: u32 = 0xdeadbeef; + /// let name = CString::new("flags").unwrap(); + /// let p = &mut flags as *mut u32 as *mut c_void; + /// let len = mem::size_of_val(&flags); + /// let mut nmount = Nmount::new(); + /// unsafe { nmount.mut_ptr_opt(&name, p, len) }; + /// ``` + pub unsafe fn mut_ptr_opt( + &mut self, + name: &'a CStr, + val: *mut c_void, + len: usize + ) -> &mut Self + { + self.iov.push(IoVec::from_slice(name.to_bytes_with_nul())); + self.is_owned.push(false); + self.iov.push(IoVec::from_raw_parts(val, len)); + self.is_owned.push(false); + self + } + + /// Add a mount option that does not take a value. + /// + /// # Examples + /// ``` + /// use nix::mount::Nmount; + /// use std::ffi::CString; + /// + /// let read_only = CString::new("ro").unwrap(); + /// Nmount::new() + /// .null_opt(&read_only); + /// ``` + pub fn null_opt(&mut self, name: &'a CStr) -> &mut Self { + self.iov.push(IoVec::from_slice(name.to_bytes_with_nul())); + self.is_owned.push(false); + self.iov.push(IoVec::from_raw_parts(ptr::null_mut(), 0)); + self.is_owned.push(false); + self + } + + /// Add a mount option that does not take a value, but whose name must be + /// owned. + /// + /// + /// This has higher runtime cost than [`Nmount::null_opt`], but is useful + /// when the name's lifetime doesn't outlive the `Nmount`, or it's a + /// different string type than `CStr`. + /// + /// # Examples + /// ``` + /// use nix::mount::Nmount; + /// + /// let read_only = "ro"; + /// let mut nmount: Nmount<'static> = Nmount::new(); + /// nmount.null_opt_owned(read_only); + /// ``` + pub fn null_opt_owned(&mut self, name: &P) -> &mut Self + { + name.with_nix_path(|s| { + let len = s.to_bytes_with_nul().len(); + self.iov.push(IoVec::from_raw_parts( + // Must free it later + s.to_owned().into_raw() as *mut c_void, + len + )); + self.is_owned.push(true); + }).unwrap(); + self.iov.push(IoVec::from_raw_parts(ptr::null_mut(), 0)); + self.is_owned.push(false); + self + } + + /// Add a mount option as a [`CStr`]. + /// + /// # Examples + /// ``` + /// use nix::mount::Nmount; + /// use std::ffi::CString; + /// + /// let fstype = CString::new("fstype").unwrap(); + /// let nullfs = CString::new("nullfs").unwrap(); + /// Nmount::new() + /// .str_opt(&fstype, &nullfs); + /// ``` + pub fn str_opt( + &mut self, + name: &'a CStr, + val: &'a CStr + ) -> &mut Self + { + self.iov.push(IoVec::from_slice(name.to_bytes_with_nul())); + self.is_owned.push(false); + self.iov.push(IoVec::from_slice(val.to_bytes_with_nul())); + self.is_owned.push(false); + self + } + + /// Add a mount option as an owned string. + /// + /// This has higher runtime cost than [`Nmount::str_opt`], but is useful + /// when the value's lifetime doesn't outlive the `Nmount`, or it's a + /// different string type than `CStr`. + /// + /// # Examples + /// ``` + /// use nix::mount::Nmount; + /// use std::path::Path; + /// + /// let mountpoint = Path::new("/mnt"); + /// Nmount::new() + /// .str_opt_owned("fspath", mountpoint.to_str().unwrap()); + /// ``` + pub fn str_opt_owned( + &mut self, + name: &P, + val: &P + ) -> &mut Self + { + name.with_nix_path(|s| { + let len = s.to_bytes_with_nul().len(); + self.iov.push(IoVec::from_raw_parts( + // Must free it later + s.to_owned().into_raw() as *mut c_void, + len + )); + self.is_owned.push(true); + }).unwrap(); + val.with_nix_path(|s| { + let len = s.to_bytes_with_nul().len(); + self.iov.push(IoVec::from_raw_parts( + // Must free it later + s.to_owned().into_raw() as *mut c_void, + len + )); + self.is_owned.push(true); + }).unwrap(); + self + } + + pub fn new() -> Self { + Self::default() + } + + /// Actually mount the file system. + pub fn nmount(&mut self, flags: MntFlags) -> NmountResult { + // nmount can return extra error information via a "errmsg" return + // argument. + const ERRMSG_NAME: &[u8] = b"errmsg\0"; + let mut errmsg = vec![0u8; 255]; + self.iov.push(IoVec::from_raw_parts( + ERRMSG_NAME.as_ptr() as *mut c_void, + ERRMSG_NAME.len() + )); + self.iov.push(IoVec::from_raw_parts( + errmsg.as_mut_ptr() as *mut c_void, + errmsg.len() + )); + + let niov = self.iov.len() as c_uint; + let iovp = self.iov.as_mut_ptr() as *mut libc::iovec; + let res = unsafe { + libc::nmount(iovp, niov, flags.bits) + }; + match Errno::result(res) { + Ok(_) => Ok(()), + Err(error) => { + let errmsg = match errmsg.iter().position(|&x| x == 0) { + None => None, + Some(0) => None, + Some(n) => { + let sl = &errmsg[0..n + 1]; + Some(CStr::from_bytes_with_nul(sl).unwrap()) + } + }; + Err(NmountError::new(error.as_errno().unwrap(), errmsg)) + } + } + } +} + +#[cfg(target_os = "freebsd")] +impl<'a> Drop for Nmount<'a> { + fn drop(&mut self) { + for (iov, is_owned) in self.iov.iter().zip(self.is_owned.iter()) { + if *is_owned { + // Free the owned string. Safe because we recorded ownership, + // and Nmount does not implement Clone. + unsafe { + CString::from_raw(iov.0.iov_base as *mut c_char); + } + } + } + } +} + +/// Unmount the file system mounted at `mountpoint`. +/// +/// Useful flags include +/// * `MNT_FORCE` - Unmount even if still in use. +/// * `MNT_BYFSID` - `mountpoint` is not a path, but a file system ID +/// encoded as `FSID:val0:val1`, where `val0` and `val1` +/// are the contents of the `fsid_t val[]` array in decimal. +/// The file system that has the specified file system ID +/// will be unmounted. See +/// [`statfs`](crate::sys::statfs::statfs) to determine the +/// `fsid`. +pub fn unmount

(mountpoint: &P, flags: MntFlags) -> Result<()> + where P: ?Sized + NixPath +{ + let res = mountpoint.with_nix_path(|cstr| { + unsafe { libc::unmount(cstr.as_ptr(), flags.bits) } + })?; + + Errno::result(res).map(drop) +} diff --git a/src/mount.rs b/src/mount/linux.rs similarity index 100% rename from src/mount.rs rename to src/mount/linux.rs diff --git a/src/mount/mod.rs b/src/mount/mod.rs new file mode 100644 index 0000000000..8538bf3d9a --- /dev/null +++ b/src/mount/mod.rs @@ -0,0 +1,20 @@ +#[cfg(any(target_os = "android", target_os = "linux"))] +mod linux; + +#[cfg(any(target_os = "android", target_os = "linux"))] +pub use self::linux::*; + +#[cfg(any(target_os = "dragonfly", + target_os = "freebsd", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd"))] +mod bsd; + +#[cfg(any(target_os = "dragonfly", + target_os = "freebsd", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" + ))] +pub use self::bsd::*; diff --git a/src/sys/uio.rs b/src/sys/uio.rs index b8ae86056e..48a0efd8da 100644 --- a/src/sys/uio.rs +++ b/src/sys/uio.rs @@ -168,7 +168,7 @@ pub fn process_vm_readv( #[repr(transparent)] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub struct IoVec(libc::iovec, PhantomData); +pub struct IoVec(pub(crate) libc::iovec, PhantomData); impl IoVec { #[inline] @@ -184,6 +184,14 @@ impl IoVec { } impl<'a> IoVec<&'a [u8]> { + #[cfg(target_os = "freebsd")] + pub(crate) fn from_raw_parts(base: *mut c_void, len: usize) -> Self { + IoVec(libc::iovec { + iov_base: base, + iov_len: len + }, PhantomData) + } + pub fn from_slice(buf: &'a [u8]) -> IoVec<&'a [u8]> { IoVec(libc::iovec { iov_base: buf.as_ptr() as *mut c_void, diff --git a/test/common/mod.rs b/test/common/mod.rs index 8a79d6a8e9..cdc325825d 100644 --- a/test/common/mod.rs +++ b/test/common/mod.rs @@ -31,6 +31,20 @@ cfg_if! { } } +/// Skip the test if we don't have the ability to mount file systems. +#[cfg(target_os = "freebsd")] +#[macro_export] macro_rules! require_mount { + ($name:expr) => { + use ::sysctl::CtlValue; + use nix::unistd::Uid; + + if !Uid::current().is_root() && CtlValue::Int(0) == ::sysctl::value("vfs.usermount").unwrap() + { + skip!("{} requires the ability to mount file systems. Skipping test.", $name); + } + } +} + #[cfg(any(target_os = "linux", target_os= "android"))] #[macro_export] macro_rules! skip_if_cirrus { ($reason:expr) => { diff --git a/test/test.rs b/test/test.rs index 5a5330b7e4..94f8e220ea 100644 --- a/test/test.rs +++ b/test/test.rs @@ -13,6 +13,8 @@ mod test_fcntl; #[cfg(any(target_os = "android", target_os = "linux"))] mod test_kmod; +#[cfg(target_os = "freebsd")] +mod test_nmount; #[cfg(any(target_os = "dragonfly", target_os = "freebsd", target_os = "fushsia", diff --git a/test/test_nmount.rs b/test/test_nmount.rs new file mode 100644 index 0000000000..92f459d6b8 --- /dev/null +++ b/test/test_nmount.rs @@ -0,0 +1,51 @@ +use crate::*; +use nix::{ + errno::Errno, + mount::{MntFlags, Nmount, unmount} +}; +use std::{ + ffi::CString, + fs::File, + path::Path +}; +use tempfile::tempdir; + +#[test] +fn ok() { + require_mount!("nullfs"); + + let mountpoint = tempdir().unwrap(); + let target = tempdir().unwrap(); + let _sentry = File::create(target.path().join("sentry")).unwrap(); + + let fstype = CString::new("fstype").unwrap(); + let nullfs = CString::new("nullfs").unwrap(); + Nmount::new() + .str_opt(&fstype, &nullfs) + .str_opt_owned("fspath", mountpoint.path().to_str().unwrap()) + .str_opt_owned("target", target.path().to_str().unwrap()) + .nmount(MntFlags::empty()).unwrap(); + + // Now check that the sentry is visible through the mountpoint + let exists = Path::exists(&mountpoint.path().join("sentry")); + + // Cleanup the mountpoint before asserting + unmount(mountpoint.path(), MntFlags::empty()).unwrap(); + + assert!(exists); +} + +#[test] +fn bad_fstype() { + let mountpoint = tempdir().unwrap(); + let target = tempdir().unwrap(); + let _sentry = File::create(target.path().join("sentry")).unwrap(); + + let e = Nmount::new() + .str_opt_owned("fspath", mountpoint.path().to_str().unwrap()) + .str_opt_owned("target", target.path().to_str().unwrap()) + .nmount(MntFlags::empty()).unwrap_err(); + + assert_eq!(e.errno(), Errno::EINVAL); + assert_eq!(e.errmsg(), Some("Invalid fstype")); +}