From 676cba8c9117981723cbdf2a0a5bb0e52217af63 Mon Sep 17 00:00:00 2001 From: zephyrcf Date: Fri, 3 Jul 2026 22:37:56 +0800 Subject: [PATCH] support id_mapping for nydusd VFS mounts Signed-off-by: zephyrcf --- Cargo.lock | 3 +- Cargo.toml | 3 + api/src/config.rs | 64 ++++++++++ service/src/fs_service.rs | 255 +++++++++++++++++++++++++++++++------- src/bin/nydusd/main.rs | 62 ++++----- 5 files changed, 310 insertions(+), 77 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0da4d19b0cf..8e36b97762e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1380,8 +1380,7 @@ dependencies = [ [[package]] name = "fuse-backend-rs" version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d8148185e731fa601a078e4ae8c742bf4b69e86e19136848780deaef08fd7d" +source = "git+https://github.com/Zephyrcf/fuse-backend-rs.git?tag=v0.0.1#f5ebf63af6406f089f080f91ac52b282cb134ba6" dependencies = [ "arc-swap", "bitflags 1.3.2", diff --git a/Cargo.toml b/Cargo.toml index 226e4855a6d..82bf7bd95e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -153,3 +153,6 @@ vhost-user-backend = "0.21.0" # fuse-backend-rs which depends on rust-vmm fuse-backend-rs = "0.14.0" + +[patch.crates-io] +fuse-backend-rs = { git = "https://github.com/Zephyrcf/fuse-backend-rs.git", tag = "v0.0.1" } \ No newline at end of file diff --git a/api/src/config.rs b/api/src/config.rs index 706634873c5..8f41c49cc78 100644 --- a/api/src/config.rs +++ b/api/src/config.rs @@ -161,6 +161,17 @@ impl ConfigV2 { )) } + /// Get the UID/GID mapping `(internal, external, range)` if configured. + /// + /// Returns `None` when no mapping is configured or the range is zero, in which + /// case the FUSE VFS layer performs no ownership remapping. + pub fn get_id_mapping(&self) -> Option<(u32, u32, u32)> { + self.rafs + .as_ref() + .and_then(|rafs| rafs.id_mapping) + .filter(|(_, _, range)| *range != 0) + } + /// Get configuration information for RAFS filesystem. pub fn get_rafs_config(&self) -> Result<&RafsConfigV2> { self.rafs.as_ref().ok_or_else(|| { @@ -849,6 +860,9 @@ pub struct RafsConfigV2 { /// Filesystem prefetching configuration. #[serde(default)] pub prefetch: PrefetchConfigV2, + /// UID/GID mapping for user namespace support, `(internal, external, range)`. + #[serde(default)] + pub id_mapping: Option<(u32, u32, u32)>, } impl RafsConfigV2 { @@ -1381,6 +1395,9 @@ struct RafsConfig { // ZERO value means, amplifying user io is not enabled. #[serde(rename = "amplify_io", default = "default_user_io_batch_size")] pub user_io_batch_size: usize, + /// UID/GID mapping for user namespace support, `(internal, external, range)`. + #[serde(default)] + pub id_mapping: Option<(u32, u32, u32)>, } impl TryFrom for ConfigV2 { @@ -1398,6 +1415,7 @@ impl TryFrom for ConfigV2 { access_pattern: v.access_pattern, latest_read_files: v.latest_read_files, prefetch: v.fs_prefetch.into(), + id_mapping: v.id_mapping, }; if !cache.prefetch.enable && rafs.prefetch.enable { cache.prefetch = rafs.prefetch.clone(); @@ -2683,6 +2701,52 @@ mod tests { assert!(!cfg.is_fs_cache()); } + #[test] + fn test_rafs_id_mapping_parse_and_get() { + // v2 TOML: id_mapping is a 3-element array under [rafs]. + let content = r#"version=2 + [rafs] + mode = "direct" + id_mapping = [0, 100000, 65536] + "#; + let cfg: ConfigV2 = toml::from_str(content).unwrap(); + assert!(cfg.validate()); + assert_eq!(cfg.get_id_mapping(), Some((0, 100000, 65536))); + + // Absent id_mapping -> None. + let content = r#"version=2 + [rafs] + mode = "direct" + "#; + let cfg: ConfigV2 = toml::from_str(content).unwrap(); + assert_eq!(cfg.get_id_mapping(), None); + + // range == 0 is treated as disabled. + let content = r#"version=2 + [rafs] + mode = "direct" + id_mapping = [0, 100000, 0] + "#; + let cfg: ConfigV2 = toml::from_str(content).unwrap(); + assert!(cfg.validate()); + assert_eq!(cfg.get_id_mapping(), None); + } + + #[test] + fn test_rafs_id_mapping_v1_json() { + // Legacy v1 RafsConfig JSON (as emitted by nydus-snapshotter) carries + // id_mapping at top level and must survive the TryFrom conversion. + let content = r#"{ + "device": { + "backend": { "type": "localfs", "config": { "dir": "/tmp" } } + }, + "mode": "direct", + "id_mapping": [0, 100000, 65536] + }"#; + let cfg = ConfigV2::from_str(content).unwrap(); + assert_eq!(cfg.get_id_mapping(), Some((0, 100000, 65536))); + } + #[test] fn test_get_cache_working_directory_missing_sub_config() { // filecache type but no [cache.filecache] section → error. diff --git a/service/src/fs_service.rs b/service/src/fs_service.rs index 29eb3593cca..e65480a50fe 100644 --- a/service/src/fs_service.rs +++ b/service/src/fs_service.rs @@ -13,6 +13,8 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::{Arc, MutexGuard}; +use crate::upgrade::UpgradeManager; +use crate::{Error, FsBackendDescriptor, FsBackendType, Result}; use fuse_backend_rs::abi::fuse_abi::ROOT_ID; #[cfg(target_os = "linux")] use fuse_backend_rs::api::filesystem::{FileSystem, FsOptions, Layer}; @@ -31,8 +33,8 @@ use serde::{Deserialize, Serialize}; use versionize::{VersionMap, Versionize, VersionizeResult}; use versionize_derive::Versionize; -use crate::upgrade::UpgradeManager; -use crate::{Error, FsBackendDescriptor, FsBackendType, Result}; +type IdMapping = (u32, u32, u32); +type BackendMountResult = (BackFileSystem, Option); /// Request structure to mount a filesystem instance. #[derive(Clone, Versionize, Debug)] @@ -115,8 +117,8 @@ pub trait FsService: Send + Sync { if self.backend_from_mountpoint(&cmd.mountpoint)?.is_some() { return Err(Error::AlreadyExists); } - let backend = fs_backend_factory(&cmd)?; - let index = self.get_vfs().mount(backend, &cmd.mountpoint)?; + let (backend, id_mapping) = fs_backend_factory(&cmd)?; + let index = self.get_vfs().mount(backend, &cmd.mountpoint, id_mapping)?; info!("{} filesystem mounted at {}", &cmd.fs_type, &cmd.mountpoint); if let Err(e) = self.backend_collection().add(&cmd.mountpoint, &cmd) { @@ -169,9 +171,9 @@ pub trait FsService: Send + Sync { /// Restore a filesystem instance. fn restore_mount(&self, cmd: &FsBackendMountCmd, vfs_index: u8) -> Result<()> { - let backend = fs_backend_factory(cmd)?; + let (backend, id_mapping) = fs_backend_factory(cmd)?; self.get_vfs() - .restore_mount(backend, vfs_index, &cmd.mountpoint) + .restore_mount(backend, vfs_index, &cmd.mountpoint, id_mapping) .map_err(VfsError::RestoreMount)?; self.backend_collection().add(&cmd.mountpoint, cmd)?; info!("backend fs restored at {}", cmd.mountpoint); @@ -293,12 +295,13 @@ fn validate_prefetch_file_list(input: &Option>) -> Result Result { +fn fs_backend_factory(cmd: &FsBackendMountCmd) -> Result { let prefetch_files = validate_prefetch_file_list(&cmd.prefetch_files)?; match cmd.fs_type { FsBackendType::Rafs => { let config = ConfigV2::from_str(cmd.config.as_str()).map_err(RafsError::LoadConfig)?; + let id_mapping = config.get_id_mapping(); let config = Arc::new(config); let (mut rafs, reader) = Rafs::new(&config, &cmd.mountpoint, Path::new(&cmd.source))?; rafs.import(reader, prefetch_files)?; @@ -365,12 +368,12 @@ fn fs_backend_factory(cmd: &FsBackendMountCmd) -> Result { .import() .map_err(|e| Error::InvalidConfig(format!("{}", e)))?; info!("Overlay filesystem imported"); - Ok(Box::new(overlayfs)) + Ok((Box::new(overlayfs), id_mapping)) } } None => { info!("RAFS filesystem imported"); - Ok(Box::new(rafs)) + Ok((Box::new(rafs), id_mapping)) } } } @@ -396,7 +399,7 @@ fn fs_backend_factory(cmd: &FsBackendMountCmd) -> Result { PassthroughFs::<()>::new(fs_cfg).map_err(Error::PassthroughFs)?; passthrough_fs.import().map_err(Error::PassthroughFs)?; info!("PassthroughFs imported"); - Ok(Box::new(passthrough_fs)) + Ok((Box::new(passthrough_fs), None)) } } } @@ -405,6 +408,153 @@ fn fs_backend_factory(cmd: &FsBackendMountCmd) -> Result { #[cfg(test)] mod tests { use super::*; + use std::ffi::CString; + use std::sync::Mutex; + + use fuse_backend_rs::api::filesystem::{Context, Entry, FileSystem}; + use vmm_sys_util::tempdir::TempDir; + + struct TestFsService { + vfs: Vfs, + backends: Mutex, + } + + impl TestFsService { + fn new() -> Self { + Self { + vfs: Vfs::default(), + backends: Mutex::new(FsBackendCollection::default()), + } + } + } + + impl FsService for TestFsService { + fn get_vfs(&self) -> &Vfs { + &self.vfs + } + + fn upgrade_mgr(&self) -> Option> { + None + } + + fn backend_collection(&self) -> MutexGuard<'_, FsBackendCollection> { + self.backends.lock().unwrap() + } + + fn export_inflight_ops(&self) -> Result> { + Ok(None) + } + + fn as_any(&self) -> &dyn Any { + self + } + } + + fn prepare_rafs_fixture() -> (TempDir, PathBuf) { + let tmp_dir = TempDir::new().unwrap(); + let root_dir = std::env::var("CARGO_MANIFEST_DIR").expect("$CARGO_MANIFEST_DIR"); + + let mut blob_src = PathBuf::from(&root_dir); + blob_src.push("../tests/texture/blobs/be7d77eeb719f70884758d1aa800ed0fb09d701aaec469964e9d54325f0d5fef"); + let mut blob_dst = tmp_dir.as_path().to_path_buf(); + blob_dst.push("be7d77eeb719f70884758d1aa800ed0fb09d701aaec469964e9d54325f0d5fef"); + std::fs::copy(&blob_src, &blob_dst).unwrap(); + + let mut bootstrap = PathBuf::from(&root_dir); + bootstrap.push("../tests/texture/bootstrap/rafs-v6-2.2.boot"); + + (tmp_dir, bootstrap) + } + + fn rafs_test_config(work_dir: &Path, id_mapping: Option<(u32, u32, u32)>) -> String { + let mut config = format!( + r#" + version = 2 + id = "factory1" + + [backend] + type = "localfs" + + [backend.localfs] + dir = "{}" + + [cache] + type = "filecache" + + [cache.filecache] + work_dir = "{}" + + [rafs] + mode = "direct" + enable_xattr = true + "#, + work_dir.display(), + work_dir.display() + ); + + if let Some((internal, external, range)) = id_mapping { + config.push_str(&format!( + "\n id_mapping = [{internal}, {external}, {range}]\n" + )); + } + + config + } + + fn lookup_mount_root(vfs: &Vfs, mount_name: &str) -> Entry { + let ctx = Context::new(); + vfs.lookup( + &ctx, + ROOT_ID.into(), + CString::new(mount_name).unwrap().as_c_str(), + ) + .unwrap() + } + + fn mount_rafs( + service: &TestFsService, + mountpoint: &str, + bootstrap: &Path, + work_dir: &Path, + id_mapping: Option<(u32, u32, u32)>, + ) { + service + .mount(FsBackendMountCmd { + fs_type: FsBackendType::Rafs, + config: rafs_test_config(work_dir, id_mapping), + mountpoint: mountpoint.to_string(), + source: bootstrap.display().to_string(), + prefetch_files: None, + }) + .unwrap(); + } + + fn assert_vfs_root_ids(service: &TestFsService, mount_name: &str, uid: u32, gid: u32) { + let entry = lookup_mount_root(service.get_vfs(), mount_name); + assert_eq!(entry.attr.st_uid, uid); + assert_eq!(entry.attr.st_gid, gid); + + let (attr, _) = service + .get_vfs() + .getattr(&Context::new(), entry.inode.into(), None) + .unwrap(); + assert_eq!(attr.st_uid, uid); + assert_eq!(attr.st_gid, gid); + } + + fn assert_backend_root_ids(service: &TestFsService, mountpoint: &str, uid: u32, gid: u32) { + let (backend, _) = service + .backend_from_mountpoint(mountpoint) + .unwrap() + .unwrap(); + let rafs = backend.deref().as_any().downcast_ref::().unwrap(); + + let root_ino = rafs.get_root_inode().unwrap().ino(); + let (attr, _) = rafs.getattr(&Context::new(), root_ino, None).unwrap(); + + assert_eq!(attr.st_uid, uid); + assert_eq!(attr.st_gid, gid); + } #[test] fn it_should_add_new_backend() { @@ -539,45 +689,62 @@ mod tests { #[test] fn it_should_create_rafs_backend() { - let config = r#" - { - "device": { - "backend": { - "type": "oss", - "config": { - "endpoint": "test", - "access_key_id": "test", - "access_key_secret": "test", - "bucket_name": "antsys-nydus", - "object_prefix":"nydus_v2/", - "scheme": "http" - } - } - }, - "mode": "direct", - "digest_validate": false, - "enable_xattr": true, - "fs_prefetch": { - "enable": true, - "threads_count": 10, - "merging_size": 131072, - "bandwidth_rate": 10485760 - } - }"#; - let bootstrap = "../tests/texture/bootstrap/nydusd_daemon_test_bootstrap"; - if fs_backend_factory(&FsBackendMountCmd { + let (tmp_dir, bootstrap) = prepare_rafs_fixture(); + let config = rafs_test_config(tmp_dir.as_path(), None); + let (backend, id_mapping) = fs_backend_factory(&FsBackendMountCmd { fs_type: FsBackendType::Rafs, - config: config.to_string(), + config, mountpoint: "testmountpoint".to_string(), - source: bootstrap.to_string(), + source: bootstrap.display().to_string(), prefetch_files: Some(vec!["/testfile".to_string()]), }) - .unwrap() - .as_any() - .downcast_ref::() - .is_none() - { + .unwrap(); + + assert!(id_mapping.is_none()); + if backend.as_any().downcast_ref::().is_none() { panic!("failed to create rafs backend") } } + + #[test] + fn it_should_apply_id_mapping_after_mount() { + let service = TestFsService::new(); + let (tmp_dir, bootstrap) = prepare_rafs_fixture(); + + mount_rafs( + &service, + "/mapped", + &bootstrap, + tmp_dir.as_path(), + Some((0, 100000, 65536)), + ); + assert_vfs_root_ids(&service, "mapped", 100000, 100000); + assert_backend_root_ids(&service, "/mapped", 0, 0); + } + + #[test] + fn it_should_keep_per_mount_id_mapping_isolated() { + let service = TestFsService::new(); + let (tmp_dir, bootstrap) = prepare_rafs_fixture(); + + mount_rafs( + &service, + "/mapped-a", + &bootstrap, + tmp_dir.as_path(), + Some((0, 100000, 65536)), + ); + mount_rafs( + &service, + "/mapped-b", + &bootstrap, + tmp_dir.as_path(), + Some((0, 200000, 65536)), + ); + + assert_vfs_root_ids(&service, "mapped-a", 100000, 100000); + assert_vfs_root_ids(&service, "mapped-b", 200000, 200000); + assert_backend_root_ids(&service, "/mapped-a", 0, 0); + assert_backend_root_ids(&service, "/mapped-b", 0, 0); + } } diff --git a/src/bin/nydusd/main.rs b/src/bin/nydusd/main.rs index f14b5c62ced..6449d2c9326 100644 --- a/src/bin/nydusd/main.rs +++ b/src/bin/nydusd/main.rs @@ -58,37 +58,37 @@ fn append_fs_options(app: Command) -> Command { .help("Path to the RAFS filesystem metadata file") .conflicts_with("shared-dir"), ) - .arg( - Arg::new("localfs-dir") - .long("localfs-dir") - .short('D') - .help( - "Path to the `localfs` working directory, which also enables the `localfs` storage backend" - ) - .conflicts_with("config"), - ) - .arg( - Arg::new("shared-dir") - .long("shared-dir") - .short('s') - .help("Path to the directory to be shared via the `passthroughfs` FUSE driver") - ) - .arg( - Arg::new("prefetch-files") - .long("prefetch-files") - .help("Path to the prefetch configuration file containing a list of directories/files separated by newlines") - .required(false) - .requires("bootstrap") - .num_args(1), - ) - .arg( - Arg::new("virtual-mountpoint") - .long("virtual-mountpoint") - .short('m') - .help("Mountpoint within the FUSE/virtiofs device to mount the RAFS/passthroughfs filesystem") - .default_value("/") - .required(false), - ); + .arg( + Arg::new("localfs-dir") + .long("localfs-dir") + .short('D') + .help( + "Path to the `localfs` working directory, which also enables the `localfs` storage backend" + ) + .conflicts_with("config"), + ) + .arg( + Arg::new("shared-dir") + .long("shared-dir") + .short('s') + .help("Path to the directory to be shared via the `passthroughfs` FUSE driver") + ) + .arg( + Arg::new("prefetch-files") + .long("prefetch-files") + .help("Path to the prefetch configuration file containing a list of directories/files separated by newlines") + .required(false) + .requires("bootstrap") + .num_args(1), + ) + .arg( + Arg::new("virtual-mountpoint") + .long("virtual-mountpoint") + .short('m') + .help("Mountpoint within the FUSE/virtiofs device to mount the RAFS/passthroughfs filesystem") + .default_value("/") + .required(false), + ); #[cfg(feature = "dedup")] {