From 82b9c514623e5ee46a8007eed02b52a9004b67b7 Mon Sep 17 00:00:00 2001 From: Qi Wang Date: Sat, 2 Dec 2023 21:46:47 +0800 Subject: [PATCH 1/2] api: add support for multiple uid/gid mappings Currently, only single mapping for uid/gid is supported. However, for userns, mutiple uid/gid mappings can be setted. This commit follows how fuse-overlayfs support this feature. https://github.com/containers/fuse-overlayfs/commit/33788697ad590543fd9c5a5c325d516cd7904492 Signed-off-by: Qi Wang --- src/api/filesystem/sync_io.rs | 8 +- src/api/server/sync_io.rs | 4 +- src/api/vfs/mod.rs | 166 +++++++++++++++++++++------------ src/api/vfs/sync_io.rs | 18 +--- src/lib.rs | 6 -- src/passthrough/file_handle.rs | 2 - 6 files changed, 113 insertions(+), 91 deletions(-) diff --git a/src/api/filesystem/sync_io.rs b/src/api/filesystem/sync_io.rs index c3b7df43c..44f6dbc41 100644 --- a/src/api/filesystem/sync_io.rs +++ b/src/api/filesystem/sync_io.rs @@ -899,9 +899,7 @@ pub trait FileSystem { } /// Remap the external IDs in context to internal IDs. - fn id_remap(&self, ctx: &mut Context) -> io::Result<()> { - Ok(()) - } + fn id_remap(&self, ctx: &mut Context) {} } impl FileSystem for Arc { @@ -1349,7 +1347,7 @@ impl FileSystem for Arc { } #[inline] - fn id_remap(&self, ctx: &mut Context) -> io::Result<()> { - self.deref().id_remap(ctx) + fn id_remap(&self, ctx: &mut Context) { + self.deref().id_remap(ctx); } } diff --git a/src/api/server/sync_io.rs b/src/api/server/sync_io.rs index d9925cca7..86e68a946 100644 --- a/src/api/server/sync_io.rs +++ b/src/api/server/sync_io.rs @@ -72,9 +72,7 @@ impl Server { ) -> Result { let in_header: InHeader = r.read_obj().map_err(Error::DecodeMessage)?; let mut ctx = SrvContext::::new(in_header, r, w); - self.fs - .id_remap(&mut ctx.context) - .map_err(|e| Error::FailedToRemapID((ctx.context.uid, ctx.context.gid)))?; + self.fs.id_remap(&mut ctx.context); if ctx.in_header.len > (MAX_BUFFER_SIZE + BUFFER_HEADER_SIZE) { if in_header.opcode == Opcode::Forget as u32 || in_header.opcode == Opcode::BatchForget as u32 diff --git a/src/api/vfs/mod.rs b/src/api/vfs/mod.rs index 3e0f4a740..0af8b77ac 100644 --- a/src/api/vfs/mod.rs +++ b/src/api/vfs/mod.rs @@ -19,8 +19,8 @@ use std::any::Any; use std::collections::HashMap; use std::ffi::CStr; use std::fmt; -use std::io; -use std::io::{Error, ErrorKind, Result}; +use std::fs::File; +use std::io::{self, Error, ErrorKind, Result, Read}; use std::ops::Deref; use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use std::sync::{Arc, Mutex}; @@ -28,6 +28,9 @@ use std::time::Duration; use arc_swap::ArcSwap; +#[cfg(feature = "persist")] +use {versionize::{VersionMap, Versionize, VersionizeResult}, versionize_derive::Versionize}; + use crate::abi::fuse_abi::*; use crate::api::filesystem::*; use crate::api::pseudo_fs::PseudoFs; @@ -67,6 +70,8 @@ pub type VfsIndex = u8; // VfsIndex is type of 'u8', so maximum 256 entries. const MAX_VFS_INDEX: usize = 256; +// The default overflow uid/gid +const DEFAULT_OVERFLOWID: u32 = 65534; /// Data struct to store inode number for the VFS filesystem. #[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)] pub struct VfsInode(u64); @@ -223,7 +228,7 @@ struct MountPointData { _path: String, } -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Clone)] /// vfs init options pub struct VfsOptions { /// Make readdir/readdirplus request return zero dirent even if dir has children. @@ -235,11 +240,14 @@ pub struct VfsOptions { pub in_opts: FsOptions, /// File system options returned to client pub out_opts: FsOptions, - /// Declaration of ID mapping, in the format (internal ID, external ID, range). - /// For example, (0, 1, 65536) represents mapping the external UID/GID range of `1~65536` + /// Declaration of UID mappings, each mapping is consists of internal UID, external UID, and size. + /// For example, (0, 1, 65536) represents mapping the external UID range of `1~65536` /// to the range of `0~65535` within the filesystem. - pub id_mapping: (u32, u32, u32), - + pub uid_mappings: Vec, + /// Declaration of GID mappings, each mapping is consists of internal GID, external GID, and size. + /// For example, (0, 1, 65536) represents mapping the external GID range of `1~65536` + /// to the range of `0~65535` within the filesystem. + pub gid_mappings: Vec, /// Disable fuse open request handling. When enabled, fuse open /// requests are always replied with ENOSYS. #[cfg(target_os = "linux")] @@ -294,7 +302,8 @@ impl Default for VfsOptions { killpriv_v2: false, in_opts: FsOptions::empty(), out_opts, - id_mapping: (0, 0, 0), + uid_mappings: vec![], + gid_mappings: vec![], } } @@ -306,7 +315,31 @@ impl Default for VfsOptions { seal_size: false, in_opts: FsOptions::empty(), out_opts, - id_mapping: (0, 0, 0), + uid_mappings: vec![], + gid_mappings: vec![], + } + } +} + +#[derive(Debug, Copy, Clone)] +#[cfg_attr(feature = "persist", derive(Versionize))] +/// vfs id mapping +pub struct VfsIdMapping { + /// internal id used by the server + pub internal_id: u32, + /// external id used by the client + pub external_id: u32, + /// the length of this mapping + pub size: u32, +} + +impl VfsIdMapping { + /// Create a new vfs id mapping + pub fn new(internal_id: u32, external_id: u32, size: u32) -> Self { + Self { + internal_id, + external_id, + size, } } } @@ -323,7 +356,10 @@ pub struct Vfs { initialized: AtomicBool, lock: Mutex<()>, remove_pseudo_root: bool, - id_mapping: Option<(u32, u32, u32)>, + uid_mappings: Vec, + gid_mappings: Vec, + overflow_uid: u32, + overflow_gid: u32, } impl Default for Vfs { @@ -335,19 +371,34 @@ impl Default for Vfs { impl Vfs { /// Create a new vfs instance pub fn new(opts: VfsOptions) -> Self { + let mut overflow_uid = DEFAULT_OVERFLOWID; + let mut overflow_gid = DEFAULT_OVERFLOWID; + if let Ok(mut overflowuid) = File::open("/proc/sys/kernel/overflowuid") { + let mut ouid = String::new(); + if overflowuid.read_to_string(&mut ouid).is_ok() { + overflow_uid = ouid.trim().parse().unwrap_or(DEFAULT_OVERFLOWID); + } + } + if let Ok(mut overflowgid) = File::open("/proc/sys/kernel/overflowgid") { + let mut ogid = String::new(); + if overflowgid.read_to_string(&mut ogid).is_ok() { + overflow_gid = ogid.trim().parse().unwrap_or(DEFAULT_OVERFLOWID); + } + } + Vfs { next_super: AtomicU8::new(VFS_PSEUDO_FS_IDX + 1), mountpoints: ArcSwap::new(Arc::new(HashMap::new())), superblocks: ArcSwap::new(Arc::new(vec![None; MAX_VFS_INDEX])), root: PseudoFs::new(), - opts: ArcSwap::new(Arc::new(opts)), + opts: ArcSwap::new(Arc::new(opts.clone())), lock: Mutex::new(()), initialized: AtomicBool::new(false), remove_pseudo_root: false, - id_mapping: match opts.id_mapping.2 { - 0 => None, - _ => Some(opts.id_mapping), - }, + uid_mappings: opts.uid_mappings.clone(), + gid_mappings: opts.gid_mappings.clone(), + overflow_uid, + overflow_gid, } } @@ -364,7 +415,7 @@ impl Vfs { /// Get a snapshot of the current vfs options. pub fn options(&self) -> VfsOptions { - *self.opts.load_full() + self.opts.load_full().deref().clone() } fn insert_mount_locked( @@ -555,18 +606,38 @@ impl Vfs { entry.inode = ino; entry.attr.st_ino = ino; // If id_mapping is enabled, map the internal ID to the external ID. - if let Some((internal_id, external_id, range)) = self.id_mapping { - if entry.attr.st_uid >= internal_id && entry.attr.st_uid < internal_id + range { - entry.attr.st_uid += external_id - internal_id; - } - if entry.attr.st_gid >= internal_id && entry.attr.st_gid < internal_id + range { - entry.attr.st_gid += external_id - internal_id; - } - } + entry.attr.st_uid = self.do_mapping(entry.attr.st_uid, true, true); + entry.attr.st_gid = self.do_mapping(entry.attr.st_gid, true, false); *entry }) } + fn do_mapping(&self, id: u32, map_internal_to_external: bool, is_uid: bool) -> u32 { + let (mut mapped_id, id_mappings) = if is_uid { + (self.overflow_uid, &self.uid_mappings) + } else { + (self.overflow_gid, &self.gid_mappings) + }; + + if id_mappings.is_empty() { + return id; + } + + for id_mapping in id_mappings { + let (src_id, dst_id, size) = if map_internal_to_external { + (id_mapping.internal_id, id_mapping.external_id, id_mapping.size) + } else { + (id_mapping.external_id, id_mapping.internal_id, id_mapping.size) + }; + + if id >= src_id && id < src_id + size { + mapped_id = dst_id + id - src_id; + break; + } + } + + mapped_id + } /// If id_mapping is enabled, remap the uid/gid in attributes. /// /// If `map_internal_to_external` is true, the IDs inside VFS will be mapped @@ -574,32 +645,8 @@ impl Vfs { /// If `map_internal_to_external` is false, the external IDs will be mapped /// to VFS internal IDs. fn remap_attr_id(&self, map_internal_to_external: bool, attr: &mut stat64) { - if let Some((internal_id, external_id, range)) = self.id_mapping { - if map_internal_to_external - && attr.st_uid >= internal_id - && attr.st_uid < internal_id + range - { - attr.st_uid += external_id - internal_id; - } - if map_internal_to_external - && attr.st_gid >= internal_id - && attr.st_gid < internal_id + range - { - attr.st_gid += external_id - internal_id; - } - if !map_internal_to_external - && attr.st_uid >= external_id - && attr.st_uid < external_id + range - { - attr.st_uid += internal_id - external_id; - } - if !map_internal_to_external - && attr.st_gid >= external_id - && attr.st_gid < external_id + range - { - attr.st_gid += internal_id - external_id; - } - } + attr.st_uid = self.do_mapping(attr.st_uid, map_internal_to_external, true); + attr.st_gid = self.do_mapping(attr.st_gid, map_internal_to_external, false); } fn allocate_fs_idx(&self) -> Result { @@ -706,7 +753,7 @@ pub mod persist { use crate::api::{ filesystem::FsOptions, pseudo_fs::persist::PseudoFsState, - vfs::{VfsError, VfsResult}, + vfs::{VfsError, VfsResult, VfsIdMapping}, Vfs, VfsOptions, }; @@ -727,9 +774,8 @@ pub mod persist { out_opts: u64, no_readdir: bool, seal_size: bool, - id_mapping_internal: u32, - id_mapping_external: u32, - id_mapping_range: u32, + uid_mappings: Vec, + gid_mappings: Vec, #[cfg(target_os = "linux")] no_open: bool, @@ -748,9 +794,8 @@ pub mod persist { out_opts: self.out_opts.bits(), no_readdir: self.no_readdir, seal_size: self.seal_size, - id_mapping_internal: self.id_mapping.0, - id_mapping_external: self.id_mapping.1, - id_mapping_range: self.id_mapping.2, + uid_mappings: self.uid_mappings.clone(), + gid_mappings: self.gid_mappings.clone(), #[cfg(target_os = "linux")] no_open: self.no_open, @@ -773,11 +818,8 @@ pub mod persist { ))?, no_readdir: state.no_readdir, seal_size: state.seal_size, - id_mapping: ( - state.id_mapping_internal, - state.id_mapping_external, - state.id_mapping_range, - ), + uid_mappings: state.uid_mappings.clone(), + gid_mappings: state.gid_mappings.clone(), #[cfg(target_os = "linux")] no_open: state.no_open, diff --git a/src/api/vfs/sync_io.rs b/src/api/vfs/sync_io.rs index 3418fbe4a..a4190ce2c 100644 --- a/src/api/vfs/sync_io.rs +++ b/src/api/vfs/sync_io.rs @@ -19,7 +19,7 @@ impl FileSystem for Vfs { error!("vfs is already initialized"); return Err(Error::from_raw_os_error(libc::EINVAL)); } - let mut n_opts = *self.opts.load().deref().deref(); + let mut n_opts = self.opts.load().deref().deref().clone(); #[cfg(target_os = "linux")] { if n_opts.no_open { @@ -44,7 +44,7 @@ impl FileSystem for Vfs { n_opts.in_opts = opts; n_opts.out_opts &= opts; - self.opts.store(Arc::new(n_opts)); + self.opts.store(Arc::new(n_opts.clone())); { // Serialize mount operations. Do not expect poisoned lock here. // Ensure that every backend fs only get init()ed once. @@ -648,18 +648,10 @@ impl FileSystem for Vfs { } #[inline] - fn id_remap(&self, ctx: &mut Context) -> Result<()> { + fn id_remap(&self, ctx: &mut Context) { // If id_mapping is enabled, map the external ID to the internal ID. - if let Some((internal_id, external_id, range)) = self.id_mapping { - if ctx.uid >= external_id && ctx.uid < external_id + range { - ctx.uid += internal_id - external_id; - } - if ctx.gid >= external_id && ctx.gid < external_id + range { - ctx.gid += internal_id - external_id; - } - } - - Ok(()) + ctx.uid = self.do_mapping(ctx.uid, false, true); + ctx.gid = self.do_mapping(ctx.gid, false, false); } #[cfg(any(feature = "vhost-user-fs", feature = "virtiofs"))] diff --git a/src/lib.rs b/src/lib.rs index b8756920a..2eaaa9ffd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,8 +80,6 @@ pub enum Error { FailedToWrite(io::Error), /// Failed to split a writer. FailedToSplitWriter(transport::Error), - /// Failed to remap uid/gid. - FailedToRemapID((u32, u32)), } impl error::Error for Error {} @@ -103,10 +101,6 @@ impl fmt::Display for Error { InvalidMessage(err) => write!(f, "cannot process fuse message: {err}"), FailedToWrite(err) => write!(f, "cannot write to buffer: {err}"), FailedToSplitWriter(err) => write!(f, "cannot split a writer: {err}"), - FailedToRemapID((uid, gid)) => write!( - f, - "failed to remap the context of user (uid={uid}, gid={gid})." - ), } } } diff --git a/src/passthrough/file_handle.rs b/src/passthrough/file_handle.rs index 5eb00c282..44c779dc2 100644 --- a/src/passthrough/file_handle.rs +++ b/src/passthrough/file_handle.rs @@ -320,9 +320,7 @@ impl OpenableFileHandle { #[cfg(test)] mod tests { use super::*; - use nix::unistd::getuid; use std::ffi::CString; - use std::io::Read; fn generate_c_file_handle( handle_bytes: usize, From ce73f426d0ecd1705a5558b58d2a635003e7f662 Mon Sep 17 00:00:00 2001 From: Qi Wang Date: Sat, 2 Dec 2023 22:01:16 +0800 Subject: [PATCH 2/2] test: skip test_clone_fuse_file when not running as root When running test_clone_fuse_file as non-root, the test will fail. ``` ---- transport::fusedev::linux_session::tests::test_clone_fuse_file stdout ---- thread 'transport::fusedev::linux_session::tests::test_clone_fuse_file' panicked at src/transport/fusedev/linux_session.rs:680:20: called `Result::unwrap()` on an `Err` value: SessionFailure("Unexpected exit code when running fusermount: Some(1)") note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ``` Signed-off-by: Qi Wang --- src/transport/fusedev/linux_session.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/transport/fusedev/linux_session.rs b/src/transport/fusedev/linux_session.rs index 81e284ce2..a755e902b 100644 --- a/src/transport/fusedev/linux_session.rs +++ b/src/transport/fusedev/linux_session.rs @@ -644,6 +644,14 @@ mod tests { use std::path::Path; use vmm_sys_util::tempdir::TempDir; + fn is_root() -> bool { + if unsafe { libc::getuid() } == 0 { + true + } else { + false + } + } + #[test] fn test_new_session() { let se = FuseSession::new(Path::new("haha"), "foo", "bar", true); @@ -675,6 +683,10 @@ mod tests { #[test] fn test_clone_fuse_file() { + // skip test if not root user + if !is_root() { + return; + } let dir = TempDir::new().unwrap(); let mut se = FuseSession::new(dir.as_path(), "foo", "bar", true).unwrap(); se.mount().unwrap();