diff --git a/crates/fuse-backend-core/src/api/filesystem/mod.rs b/crates/fuse-backend-core/src/api/filesystem/mod.rs index 88d1fe02..b75df3f3 100644 --- a/crates/fuse-backend-core/src/api/filesystem/mod.rs +++ b/crates/fuse-backend-core/src/api/filesystem/mod.rs @@ -559,7 +559,7 @@ impl ZeroCopyWriter for File { } /// Additional context associated with requests. -#[derive(Default, Clone, Copy, Debug)] +#[derive(Default, Clone, Debug)] pub struct Context { /// The user ID of the calling process. pub uid: libc::uid_t, @@ -579,6 +579,17 @@ pub struct Context { /// this group in their supplementary group list so that objects created /// in setgid directories get the correct group ownership. pub supp_gid: Option, + + /// Supplementary groups of the calling process. + /// + /// When set, these groups are used directly instead of reading from + /// /proc//status. This is essential for remote filesystems where + /// the PID doesn't exist on the server. The ordinary request path + /// leaves it `None`: the FUSE device only reports uid, primary gid and + /// -- for create-family requests -- `supp_gid` above. A VFS with an id + /// mapping translates the list to internal (host) ids together with + /// uid/gid, dropping groups outside the mapped range. + pub supplementary_groups: Option>, } impl Context { @@ -595,6 +606,7 @@ impl From<&fuse::InHeader> for Context { gid: source.gid, pid: source.pid as i32, supp_gid: None, + supplementary_groups: None, } } } diff --git a/crates/fuse-backend-passthrough/src/async_io.rs b/crates/fuse-backend-passthrough/src/async_io.rs index 2f1daff7..1815301d 100644 --- a/crates/fuse-backend-passthrough/src/async_io.rs +++ b/crates/fuse-backend-passthrough/src/async_io.rs @@ -327,7 +327,7 @@ macro_rules! async_relay { ($self:expr, $ctx:expr, [$($capture:tt)*], $inline_call:expr, $pool_fn:expr) => { match $self.shared_ref.upgrade() { Some(fs) if $self.async_thread_pool_enabled.load(Ordering::Relaxed) => { - let ctx = *$ctx; + let ctx = $ctx.clone(); $($capture)* join_blocking(Runtime::spawn_blocking(move || ($pool_fn)(fs, ctx))).await } diff --git a/crates/fuse-backend-passthrough/src/lib.rs b/crates/fuse-backend-passthrough/src/lib.rs index 55ddbab5..4c1a1dc9 100644 --- a/crates/fuse-backend-passthrough/src/lib.rs +++ b/crates/fuse-backend-passthrough/src/lib.rs @@ -1786,6 +1786,7 @@ mod tests { gid: uid, pid: 1, supp_gid: Some(100000 + gid), + ..Default::default() }; let parent = vfs .lookup( @@ -1800,7 +1801,7 @@ mod tests { // switches to the caller's credentials, which lack `gid`. let ctx_no_group = Context { supp_gid: None, - ..ctx + ..ctx.clone() }; let err = vfs .mkdir( diff --git a/crates/fuse-backend-vfs/src/async_io.rs b/crates/fuse-backend-vfs/src/async_io.rs index e4e6b499..9f43890b 100644 --- a/crates/fuse-backend-vfs/src/async_io.rs +++ b/crates/fuse-backend-vfs/src/async_io.rs @@ -96,7 +96,7 @@ impl AsyncFileSystem for Vfs { // The supp gid is parsed after the request-wide id remap, so // translate it here, where the target mount is known. - let mut ctx = *ctx; + let mut ctx = ctx.clone(); self.remap_ctx_supp_gid(&mut ctx, parent.fs_idx()); match self.get_real_rootfs(parent)? { @@ -232,6 +232,7 @@ mod tests { gid: 0, pid: 0, supp_gid: None, + ..Default::default() }; assert!(vfs.mount(Box::new(fs), "/x/y").is_ok()); diff --git a/crates/fuse-backend-vfs/src/lib.rs b/crates/fuse-backend-vfs/src/lib.rs index dd0da1e5..b4a3f0bf 100644 --- a/crates/fuse-backend-vfs/src/lib.rs +++ b/crates/fuse-backend-vfs/src/lib.rs @@ -599,6 +599,20 @@ impl Vfs { if let Some((internal_id, external_id, range)) = mapping { ctx.uid = remap_id(ctx.uid, external_id, internal_id, range); ctx.gid = remap_id(ctx.gid, external_id, internal_id, range); + // Translate the supplementary group list with the same security + // policy as remap_ctx_supp_gid(): groups outside the mapped range + // have no host counterpart and are dropped rather than adopted + // untranslated. + if let Some(groups) = ctx.supplementary_groups.as_mut() { + groups.retain_mut(|gid| { + if *gid >= external_id && *gid - external_id < range { + *gid = *gid - external_id + internal_id; + true + } else { + false + } + }); + } } Ok(()) } diff --git a/crates/fuse-backend-vfs/src/sync_io.rs b/crates/fuse-backend-vfs/src/sync_io.rs index ad8c7f70..6f47dddd 100644 --- a/crates/fuse-backend-vfs/src/sync_io.rs +++ b/crates/fuse-backend-vfs/src/sync_io.rs @@ -160,7 +160,7 @@ impl FileSystem for Vfs { // The supp gid is parsed after the request-wide id remap, so // translate it here, where the target mount is known. - let mut ctx = *ctx; + let mut ctx = ctx.clone(); self.remap_ctx_supp_gid(&mut ctx, parent.fs_idx()); match self.get_real_rootfs(parent)? { @@ -184,7 +184,7 @@ impl FileSystem for Vfs { // The supp gid is parsed after the request-wide id remap, so // translate it here, where the target mount is known. - let mut ctx = *ctx; + let mut ctx = ctx.clone(); self.remap_ctx_supp_gid(&mut ctx, inode.fs_idx()); match self.get_real_rootfs(inode)? { @@ -207,7 +207,7 @@ impl FileSystem for Vfs { // The supp gid is parsed after the request-wide id remap, so // translate it here, where the target mount is known. - let mut ctx = *ctx; + let mut ctx = ctx.clone(); self.remap_ctx_supp_gid(&mut ctx, parent.fs_idx()); match self.get_real_rootfs(parent)? { @@ -327,7 +327,7 @@ impl FileSystem for Vfs { // The supp gid is parsed after the request-wide id remap, so // translate it here, where the target mount is known. - let mut ctx = *ctx; + let mut ctx = ctx.clone(); self.remap_ctx_supp_gid(&mut ctx, parent.fs_idx()); match self.get_real_rootfs(parent)? { @@ -721,6 +721,7 @@ mod tests { gid: 100123, pid: 1, supp_gid: None, + ..Default::default() }; vfs.id_remap(&mut ctx).unwrap(); @@ -741,6 +742,7 @@ mod tests { gid: 100123, pid: 1, supp_gid: None, + ..Default::default() }; // fs_idx == 0 (pseudo fs) falls back to global mapping @@ -766,6 +768,7 @@ mod tests { gid: 100123, pid: 1, supp_gid: Some(100123), + ..Default::default() }; vfs.remap_ctx_supp_gid(&mut ctx, 0); assert_eq!(ctx.supp_gid, Some(123)); @@ -777,6 +780,7 @@ mod tests { gid: 100123, pid: 1, supp_gid: Some(5), + ..Default::default() }; vfs.remap_ctx_supp_gid(&mut ctx, 0); assert_eq!(ctx.supp_gid, None); @@ -788,8 +792,70 @@ mod tests { gid: 0, pid: 1, supp_gid: Some(123), + ..Default::default() }; vfs.remap_ctx_supp_gid(&mut ctx, 0); assert_eq!(ctx.supp_gid, Some(123)); } + + #[test] + fn test_remap_ctx_supplementary_groups() { + let vfs = Vfs::new(VfsOptions { + id_mapping: (0, 100000, 65536), + ..Default::default() + }); + + // Groups inside the mapped range are translated to internal (host) + // ids in order; groups outside the range have no host counterpart + // and are dropped instead of being adopted untranslated. + let mut ctx = Context { + uid: 100000, + gid: 100123, + pid: 1, + supplementary_groups: Some(vec![100123, 5, 165535, 100124]), + ..Default::default() + }; + vfs.id_remap(&mut ctx).unwrap(); + assert_eq!(ctx.supplementary_groups, Some(vec![123, 65535, 124]),); + assert_eq!(ctx.uid, 0); + assert_eq!(ctx.gid, 123); + + // An empty list stays empty, and a reversed mapping direction + // (internal base above the external base) translates without + // underflow. + let vfs = Vfs::new(VfsOptions { + id_mapping: (200000, 100000, 65536), + ..Default::default() + }); + let mut ctx = Context { + uid: 100000, + gid: 100500, + pid: 1, + supplementary_groups: Some(Vec::new()), + ..Default::default() + }; + vfs.id_remap(&mut ctx).unwrap(); + assert_eq!(ctx.supplementary_groups, Some(Vec::new())); + + ctx.supplementary_groups = Some(vec![100500, 99999]); + vfs.id_remap(&mut ctx).unwrap(); + assert_eq!(ctx.supplementary_groups, Some(vec![200500])); + + // Without an id mapping the groups pass through unchanged, and + // `None` never allocates a list. + let vfs = Vfs::new(VfsOptions::default()); + let mut ctx = Context { + uid: 0, + gid: 0, + pid: 1, + supplementary_groups: Some(vec![123]), + ..Default::default() + }; + vfs.id_remap(&mut ctx).unwrap(); + assert_eq!(ctx.supplementary_groups, Some(vec![123])); + + ctx.supplementary_groups = None; + vfs.id_remap(&mut ctx).unwrap(); + assert_eq!(ctx.supplementary_groups, None); + } }