diff --git a/CHANGELOG.md b/CHANGELOG.md index 3824e2c8..276d5c2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,24 @@ # FUSE for Rust - Changelog ## Unreleased +* **Breaking:** `Request::uid()` and `Request::gid()` return `Option`. An idmapped mount + has no caller ids to report, so there is not always a uid. A filesystem that does not request + `InitFlags::FUSE_ALLOW_IDMAP` is always given ids and can unwrap +* **Breaking:** `mknod()`, `mkdir()`, `symlink()`, `create()` and `tmpfile()` take an `Owner` + argument, which is who the inode they create belongs to, and `rename()` takes an + `Option` for the inode a `RENAME_WHITEOUT` leaves behind. Those are exactly the requests the + kernel sends ids for, and on an idmapped mount the owner is the caller's ids mapped through + the mount's idmapping rather than the ids themselves, so reading it off the request would + have been wrong +* `KernelConfig::add_capabilities()` now accepts `InitFlags::FUSE_ALLOW_IDMAP` (ABI 7.41), + which lets the mount be idmapped. It is accepted only where it can be honored - the session + must allow other users, and `default_permissions` must be in force, whether from + `MountOption::DefaultPermissions` or from negotiating `InitFlags::FUSE_POSIX_ACL` - and + refused otherwise: the kernel refuses the connection outright without `default_permissions`, + and without allow_other fuser would be offering an owner-only ACL it can no longer enforce. Once negotiated the kernel withholds the + caller's ids from every request that does not create an inode, so `Request::uid()` and + `Request::gid()` report the new `FUSE_INVALID_UIDGID` there; the requests that do create an + inode still carry ids, and they are the owner the new inode should get, already mapped * Add `Filesystem::statx()`, which the kernel calls for `statx(2)` (ABI 7.38), along with `StatxAttr` and `ReplyStatx`. This exists to report a creation time, which no other request can carry: `fuse_attr` has a field for it on macOS alone, so on Linux `statx(2)` otherwise diff --git a/examples/simple.rs b/examples/simple.rs index 5a0d2411..deac0c49 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -43,6 +43,7 @@ use fuser::LockOwner; use fuser::MountOption; use fuser::OpenAccMode; use fuser::OpenFlags; +use fuser::Owner; use fuser::RenameFlags; use fuser::ReplyAttr; use fuser::ReplyCreate; @@ -304,7 +305,7 @@ fn clear_suid_sgid(attr: &mut InodeAttributes, req: &Request, privilege: Privile let sgid_exec = attr.mode & (libc::S_ISGID | libc::S_IXGRP) as u16 == (libc::S_ISGID | libc::S_IXGRP) as u16; let outside_group = privilege.lacks_fsetid(req) - && req.gid() != attr.gid + && caller_gid(req) != attr.gid && !get_groups(req.pid()).contains(&attr.gid); if sgid_exec || outside_group { attr.mode &= !libc::S_ISGID as u16; @@ -327,7 +328,7 @@ impl Privilege { fn lacks_fsetid(&self, req: &Request) -> bool { match self { Privilege::KnownLacking => true, - Privilege::Unknown => !has_fsetid(req.pid(), req.uid()), + Privilege::Unknown => !has_fsetid(req.pid(), caller_uid(req)), } } } @@ -352,7 +353,7 @@ fn clear_file_capabilities(attr: &mut InodeAttributes) { /// Call this only when a killpriv capability was negotiated: without one the kernel still /// removes privileges itself, and clearing here as well would strip bits it had decided to keep fn clear_privileges_unprompted(attr: &mut InodeAttributes, req: &Request) { - if !has_fsetid(req.pid(), req.uid()) { + if !has_fsetid(req.pid(), caller_uid(req)) { // The capability set has just answered the question, so do not read it a second time clear_suid_sgid(attr, req, Privilege::KnownLacking); } @@ -381,12 +382,12 @@ fn xattr_access_check( match parse_xattr_namespace(key)? { XattrNamespace::Security => { - if access_mask != libc::R_OK && request.uid() != 0 { + if access_mask != libc::R_OK && caller_uid(request) != 0 { return Err(Errno::EPERM); } } XattrNamespace::Trusted => { - if request.uid() != 0 { + if caller_uid(request) != 0 { return Err(Errno::EPERM); } } @@ -396,13 +397,13 @@ fn xattr_access_check( inode_attrs.uid, inode_attrs.gid, inode_attrs.mode, - request.uid(), - request.gid(), + caller_uid(request), + caller_gid(request), AccessFlags::from_bits_retain(access_mask), ) { return Err(Errno::EPERM); } - } else if request.uid() != 0 { + } else if caller_uid(request) != 0 { return Err(Errno::EPERM); } } @@ -411,8 +412,8 @@ fn xattr_access_check( inode_attrs.uid, inode_attrs.gid, inode_attrs.mode, - request.uid(), - request.gid(), + caller_uid(request), + caller_gid(request), AccessFlags::from_bits_retain(access_mask), ) { return Err(Errno::EPERM); @@ -423,6 +424,19 @@ fn xattr_access_check( Ok(()) } +/// The caller's uid, which this filesystem is always given. The kernel withholds ids only on +/// an idmapped mount, which needs `FUSE_ALLOW_IDMAP`, and this example does not request it +fn caller_uid(req: &Request) -> u32 { + req.uid() + .expect("ids are withheld only on an idmapped mount") +} + +/// The caller's gid, always present for the same reason as [`caller_uid`] +fn caller_gid(req: &Request) -> u32 { + req.gid() + .expect("ids are withheld only on an idmapped mount") +} + fn time_now() -> (i64, u32) { time_from_system_time(&SystemTime::now()) } @@ -844,8 +858,8 @@ impl SimpleFS { parent_attrs.uid, parent_attrs.gid, parent_attrs.mode, - req.uid(), - req.gid(), + caller_uid(req), + caller_gid(req), AccessFlags::W_OK, ) { return Err(Errno::EACCES); @@ -927,8 +941,8 @@ impl Filesystem for SimpleFS { parent_attrs.uid, parent_attrs.gid, parent_attrs.mode, - _req.uid(), - _req.gid(), + caller_uid(_req), + caller_gid(_req), AccessFlags::X_OK, ) { reply.error(Errno::EACCES); @@ -1001,7 +1015,7 @@ impl Filesystem for SimpleFS { #[cfg(target_os = "freebsd")] { // FreeBSD: sticky bit only valid on directories; otherwise EFTYPE - if _req.uid() != 0 + if caller_uid(_req) != 0 && (mode as u16 & libc::S_ISVTX as u16) != 0 && attrs.kind != FileKind::Directory { @@ -1009,12 +1023,12 @@ impl Filesystem for SimpleFS { return; } } - if _req.uid() != 0 && _req.uid() != attrs.uid { + if caller_uid(_req) != 0 && caller_uid(_req) != attrs.uid { reply.error(Errno::EPERM); return; } - if _req.uid() != 0 - && _req.gid() != attrs.gid + if caller_uid(_req) != 0 + && caller_gid(_req) != attrs.gid && !get_groups(_req.pid()).contains(&attrs.gid) { // If SGID is set and the file belongs to a group that the caller is not part of @@ -1033,22 +1047,22 @@ impl Filesystem for SimpleFS { debug!("chown() called with {ino:?} {uid:?} {gid:?}"); if let Some(gid) = gid { // Non-root users can only change gid to a group they're in - if _req.uid() != 0 && !get_groups(_req.pid()).contains(&gid) { + if caller_uid(_req) != 0 && !get_groups(_req.pid()).contains(&gid) { reply.error(Errno::EPERM); return; } } if let Some(uid) = uid { - if _req.uid() != 0 + if caller_uid(_req) != 0 // but no-op changes by the owner are not an error - && !(uid == attrs.uid && _req.uid() == attrs.uid) + && !(uid == attrs.uid && caller_uid(_req) == attrs.uid) { reply.error(Errno::EPERM); return; } } // Only owner may change the group - if gid.is_some() && _req.uid() != 0 && _req.uid() != attrs.uid { + if gid.is_some() && caller_uid(_req) != 0 && caller_uid(_req) != attrs.uid { reply.error(Errno::EPERM); return; } @@ -1088,9 +1102,14 @@ impl Filesystem for SimpleFS { reply.error(Errno::EACCES); return; } - } else if let Err(error_code) = - self.truncate(_req, ino, size, _req.uid(), _req.gid(), kill_suid_gid) - { + } else if let Err(error_code) = self.truncate( + _req, + ino, + size, + caller_uid(_req), + caller_gid(_req), + kill_suid_gid, + ) { reply.error(error_code); return; } @@ -1100,18 +1119,18 @@ impl Filesystem for SimpleFS { if let Some(atime) = _atime { debug!("utimens() called with {ino:?}, atime={atime:?}"); - if attrs.uid != _req.uid() && _req.uid() != 0 && atime != Now { + if attrs.uid != caller_uid(_req) && caller_uid(_req) != 0 && atime != Now { reply.error(Errno::EPERM); return; } - if attrs.uid != _req.uid() + if attrs.uid != caller_uid(_req) && !check_access( attrs.uid, attrs.gid, attrs.mode, - _req.uid(), - _req.gid(), + caller_uid(_req), + caller_gid(_req), AccessFlags::W_OK, ) { @@ -1129,18 +1148,18 @@ impl Filesystem for SimpleFS { if let Some(mtime) = _mtime { debug!("utimens() called with {ino:?}, mtime={mtime:?}"); - if attrs.uid != _req.uid() && _req.uid() != 0 && mtime != Now { + if attrs.uid != caller_uid(_req) && caller_uid(_req) != 0 && mtime != Now { reply.error(Errno::EPERM); return; } - if attrs.uid != _req.uid() + if attrs.uid != caller_uid(_req) && !check_access( attrs.uid, attrs.gid, attrs.mode, - _req.uid(), - _req.gid(), + caller_uid(_req), + caller_gid(_req), AccessFlags::W_OK, ) { @@ -1188,6 +1207,7 @@ impl Filesystem for SimpleFS { mut mode: u32, _umask: u32, rdev: u32, + owner: Owner, reply: ReplyEntry, ) { let file_type = mode & libc::S_IFMT as u32; @@ -1227,8 +1247,8 @@ impl Filesystem for SimpleFS { parent_attrs.uid, parent_attrs.gid, parent_attrs.mode, - _req.uid(), - _req.gid(), + caller_uid(_req), + caller_gid(_req), AccessFlags::W_OK, ) { reply.error(Errno::EACCES); @@ -1239,7 +1259,7 @@ impl Filesystem for SimpleFS { parent_attrs.last_metadata_changed = now; self.write_inode(&parent_attrs); - if _req.uid() != 0 { + if caller_uid(_req) != 0 { mode &= !(libc::S_ISUID | libc::S_ISGID) as u32; } @@ -1247,7 +1267,7 @@ impl Filesystem for SimpleFS { { let kind = as_file_kind(mode); // FreeBSD: sticky bit only valid on directories; otherwise EFTYPE - if _req.uid() != 0 + if caller_uid(_req) != 0 && (mode as u16 & libc::S_ISVTX as u16) != 0 && kind != FileKind::Directory { @@ -1268,8 +1288,8 @@ impl Filesystem for SimpleFS { kind: as_file_kind(mode), mode: self.creation_mode(mode), hardlinks: 1, - uid: _req.uid(), - gid: creation_gid(&parent_attrs, _req.gid()), + uid: owner.uid, + gid: creation_gid(&parent_attrs, owner.gid), rdev: match as_file_kind(mode) { FileKind::CharDevice | FileKind::BlockDevice => rdev, // Every other type reports 0, as a local filesystem does @@ -1310,6 +1330,7 @@ impl Filesystem for SimpleFS { name: &OsStr, mut mode: u32, _umask: u32, + owner: Owner, reply: ReplyEntry, ) { debug!("mkdir() called with {parent:?} {name:?} {mode:o}"); @@ -1335,8 +1356,8 @@ impl Filesystem for SimpleFS { parent_attrs.uid, parent_attrs.gid, parent_attrs.mode, - _req.uid(), - _req.gid(), + caller_uid(_req), + caller_gid(_req), AccessFlags::W_OK, ) { reply.error(Errno::EACCES); @@ -1347,7 +1368,7 @@ impl Filesystem for SimpleFS { parent_attrs.last_metadata_changed = now; self.write_inode(&parent_attrs); - if _req.uid() != 0 { + if caller_uid(_req) != 0 { mode &= !(libc::S_ISUID | libc::S_ISGID) as u32; } if parent_attrs.mode & libc::S_ISGID as u16 != 0 { @@ -1366,8 +1387,8 @@ impl Filesystem for SimpleFS { kind: FileKind::Directory, mode: self.creation_mode(mode), hardlinks: 2, // Directories start with link count of 2, since they have a self link - uid: _req.uid(), - gid: creation_gid(&parent_attrs, _req.gid()), + uid: owner.uid, + gid: creation_gid(&parent_attrs, owner.gid), rdev: 0, flags: 0, xattrs: BTreeMap::default(), @@ -1420,15 +1441,15 @@ impl Filesystem for SimpleFS { parent_attrs.uid, parent_attrs.gid, parent_attrs.mode, - _req.uid(), - _req.gid(), + caller_uid(_req), + caller_gid(_req), AccessFlags::W_OK, ) { reply.error(Errno::EACCES); return; } - let uid = _req.uid(); + let uid = caller_uid(_req); // "Sticky bit" handling if parent_attrs.mode & libc::S_ISVTX as u16 != 0 && uid != 0 @@ -1499,8 +1520,8 @@ impl Filesystem for SimpleFS { parent_attrs.uid, parent_attrs.gid, parent_attrs.mode, - _req.uid(), - _req.gid(), + caller_uid(_req), + caller_gid(_req), AccessFlags::W_OK, ) { reply.error(Errno::EACCES); @@ -1509,9 +1530,9 @@ impl Filesystem for SimpleFS { // "Sticky bit" handling if parent_attrs.mode & libc::S_ISVTX as u16 != 0 - && _req.uid() != 0 - && _req.uid() != parent_attrs.uid - && _req.uid() != attrs.uid + && caller_uid(_req) != 0 + && caller_uid(_req) != parent_attrs.uid + && caller_uid(_req) != attrs.uid { reply.error(Errno::EACCES); return; @@ -1540,6 +1561,7 @@ impl Filesystem for SimpleFS { parent: INodeNo, link_name: &OsStr, target: &Path, + owner: Owner, reply: ReplyEntry, ) { debug!("symlink() called with {parent:?} {link_name:?} {target:?}"); @@ -1560,8 +1582,8 @@ impl Filesystem for SimpleFS { parent_attrs.uid, parent_attrs.gid, parent_attrs.mode, - _req.uid(), - _req.gid(), + caller_uid(_req), + caller_gid(_req), AccessFlags::W_OK, ) { reply.error(Errno::EACCES); @@ -1584,8 +1606,8 @@ impl Filesystem for SimpleFS { kind: FileKind::Symlink, mode: 0o777, hardlinks: 1, - uid: _req.uid(), - gid: creation_gid(&parent_attrs, _req.gid()), + uid: owner.uid, + gid: creation_gid(&parent_attrs, owner.gid), rdev: 0, flags: 0, xattrs: BTreeMap::default(), @@ -1622,6 +1644,7 @@ impl Filesystem for SimpleFS { newparent: INodeNo, newname: &OsStr, flags: RenameFlags, + owner: Option, reply: ReplyEmpty, ) { debug!( @@ -1648,8 +1671,8 @@ impl Filesystem for SimpleFS { parent_attrs.uid, parent_attrs.gid, parent_attrs.mode, - _req.uid(), - _req.gid(), + caller_uid(_req), + caller_gid(_req), AccessFlags::W_OK, ) { reply.error(Errno::EACCES); @@ -1658,9 +1681,9 @@ impl Filesystem for SimpleFS { // "Sticky bit" handling if parent_attrs.mode & libc::S_ISVTX as u16 != 0 - && _req.uid() != 0 - && _req.uid() != parent_attrs.uid - && _req.uid() != inode_attrs.uid + && caller_uid(_req) != 0 + && caller_uid(_req) != parent_attrs.uid + && caller_uid(_req) != inode_attrs.uid { reply.error(Errno::EACCES); return; @@ -1678,8 +1701,8 @@ impl Filesystem for SimpleFS { new_parent_attrs.uid, new_parent_attrs.gid, new_parent_attrs.mode, - _req.uid(), - _req.gid(), + caller_uid(_req), + caller_gid(_req), AccessFlags::W_OK, ) { reply.error(Errno::EACCES); @@ -1689,9 +1712,9 @@ impl Filesystem for SimpleFS { // "Sticky bit" handling in new_parent if new_parent_attrs.mode & libc::S_ISVTX as u16 != 0 { if let Ok(existing_attrs) = self.lookup_name(newparent, newname) { - if _req.uid() != 0 - && _req.uid() != new_parent_attrs.uid - && _req.uid() != existing_attrs.uid + if caller_uid(_req) != 0 + && caller_uid(_req) != new_parent_attrs.uid + && caller_uid(_req) != existing_attrs.uid { reply.error(Errno::EACCES); return; @@ -1832,8 +1855,8 @@ impl Filesystem for SimpleFS { inode_attrs.uid, inode_attrs.gid, inode_attrs.mode, - _req.uid(), - _req.gid(), + caller_uid(_req), + caller_gid(_req), AccessFlags::W_OK, ) { @@ -1866,6 +1889,12 @@ impl Filesystem for SimpleFS { // is a character device with device number 0 and no permission bits, which is not a // node anything can be read from or written to if whiteout { + // The kernel names the owner for exactly this case: a rename creates an inode + // only here, and this is the only rename it sends ids for + let Some(owner) = owner else { + reply.error(Errno::EIO); + return; + }; let whiteout_inode = self.allocate_next_inode(); let whiteout_attrs = InodeAttributes { inode: whiteout_inode.0, @@ -1878,8 +1907,8 @@ impl Filesystem for SimpleFS { kind: FileKind::CharDevice, mode: 0, hardlinks: 1, - uid: _req.uid(), - gid: creation_gid(&parent_attrs, _req.gid()), + uid: owner.uid, + gid: creation_gid(&parent_attrs, owner.gid), rdev: 0, flags: 0, xattrs: BTreeMap::default(), @@ -1999,8 +2028,8 @@ impl Filesystem for SimpleFS { attr.uid, attr.gid, attr.mode, - _req.uid(), - _req.gid(), + caller_uid(_req), + caller_gid(_req), AccessFlags::from_bits_retain(access_mask), ) { attr.open_file_handles += 1; @@ -2176,8 +2205,8 @@ impl Filesystem for SimpleFS { attr.uid, attr.gid, attr.mode, - _req.uid(), - _req.gid(), + caller_uid(_req), + caller_gid(_req), AccessFlags::from_bits_retain(access_mask), ) { attr.open_file_handles += 1; @@ -2397,7 +2426,14 @@ impl Filesystem for SimpleFS { debug!("access() called with {ino:?} {mask:?}"); match self.get_inode(ino) { Ok(attr) => { - if check_access(attr.uid, attr.gid, attr.mode, _req.uid(), _req.gid(), mask) { + if check_access( + attr.uid, + attr.gid, + attr.mode, + caller_uid(_req), + caller_gid(_req), + mask, + ) { reply.ok(); } else { reply.error(Errno::EACCES); @@ -2416,6 +2452,7 @@ impl Filesystem for SimpleFS { _umask: u32, flags: i32, _kill_suid_gid: bool, + owner: Owner, reply: ReplyCreate, ) { debug!("create() called with {parent:?} {name:?}"); @@ -2452,8 +2489,8 @@ impl Filesystem for SimpleFS { parent_attrs.uid, parent_attrs.gid, parent_attrs.mode, - req.uid(), - req.gid(), + caller_uid(req), + caller_gid(req), AccessFlags::W_OK, ) { reply.error(Errno::EACCES); @@ -2464,7 +2501,7 @@ impl Filesystem for SimpleFS { parent_attrs.last_metadata_changed = now; self.write_inode(&parent_attrs); - if req.uid() != 0 { + if caller_uid(req) != 0 { mode &= !(libc::S_ISUID | libc::S_ISGID) as u32; } @@ -2472,7 +2509,7 @@ impl Filesystem for SimpleFS { { let kind = as_file_kind(mode); // FreeBSD: sticky bit only valid on directories; otherwise EFTYPE - if req.uid() != 0 + if caller_uid(req) != 0 && (mode as u16 & libc::S_ISVTX as u16) != 0 && kind != FileKind::Directory { @@ -2493,8 +2530,8 @@ impl Filesystem for SimpleFS { kind: as_file_kind(mode), mode: self.creation_mode(mode), hardlinks: 1, - uid: req.uid(), - gid: creation_gid(&parent_attrs, req.gid()), + uid: owner.uid, + gid: creation_gid(&parent_attrs, owner.gid), rdev: 0, flags: 0, xattrs: BTreeMap::default(), @@ -2546,10 +2583,9 @@ impl Filesystem for SimpleFS { }; // The inode flags this filesystem honors, in the encoding statx uses. The kernel - // discards these today - fuse_do_statx() takes the creation time and the basic stats - // out of the reply and nothing else - so `chattr +i` stays invisible to statx(2) - // whatever is reported here. Filled in anyway, since it costs nothing and is what the - // field is for if the kernel starts reading it + // discards them, so `chattr +i` stays invisible to statx(2) whatever is reported here + // - see StatxAttr::attributes. Filled in because that is what the field is for, and + // so nothing has to change should the kernel start reading it let mut attributes = fuser::StatxAttributes::empty(); attributes.set(fuser::StatxAttributes::IMMUTABLE, attrs.is_immutable()); attributes.set(fuser::StatxAttributes::APPEND, attrs.is_append_only()); @@ -2660,6 +2696,7 @@ impl Filesystem for SimpleFS { _umask: u32, flags: i32, _kill_suid_gid: bool, + owner: Owner, reply: ReplyCreate, ) { debug!("tmpfile() called with {parent:?} {mode:o}"); @@ -2689,9 +2726,9 @@ impl Filesystem for SimpleFS { // The directory has to be writable even though nothing is written to it, and // searchable even though nothing is looked up in it: `vfs_tmpfile` asks its own - // filesystems for `MAY_WRITE | MAY_EXEC` here. Unlike create(), neither is implied by - // anything the kernel has already checked - the directory is the operand rather than - // a path component, so no lookup has passed through it + // filesystems for `MAY_WRITE | MAY_EXEC`. The directory is this request's operand + // rather than a component of a path, so no lookup has passed through it and nothing + // the kernel has already checked implies either if let Err(error_code) = parent_attrs.check_writable() { reply.error(error_code); return; @@ -2701,17 +2738,17 @@ impl Filesystem for SimpleFS { parent_attrs.uid, parent_attrs.gid, parent_attrs.mode, - req.uid(), - req.gid(), + caller_uid(req), + caller_gid(req), AccessFlags::W_OK | AccessFlags::X_OK, ) { reply.error(Errno::EACCES); return; } - // Unlike create(), the parent's timestamps are left alone: no entry is added to it + // The parent's timestamps are left alone, since no entry is added to it - if req.uid() != 0 { + if caller_uid(req) != 0 { mode &= !(libc::S_ISUID | libc::S_ISGID) as u32; } @@ -2731,8 +2768,8 @@ impl Filesystem for SimpleFS { // What makes it anonymous. Closing the last handle without linking it collects it, // and link() takes it from here to 1 hardlinks: 0, - uid: req.uid(), - gid: creation_gid(&parent_attrs, req.gid()), + uid: owner.uid, + gid: creation_gid(&parent_attrs, owner.gid), rdev: 0, flags: 0, xattrs: BTreeMap::default(), @@ -2856,8 +2893,8 @@ impl Filesystem for SimpleFS { reply.error(Errno::EACCES); return; } - // Unlike a plain write, this is checked against the destination's flags every time - // rather than only at open, matching what Linux does for the copy_file_range syscall + // Checked on every call rather than at open, which is where the flags are otherwise + // enforced: `generic_copy_file_checks` rejects an immutable destination each time match self.get_inode(dest_inode) { Ok(attrs) => { if let Err(error_code) = attrs.check_writable() { diff --git a/src/experimental.rs b/src/experimental.rs index fad85739..5fc282bd 100644 --- a/src/experimental.rs +++ b/src/experimental.rs @@ -24,14 +24,14 @@ pub type Result = std::result::Result; /// Standard request context for all filesystem operations pub struct RequestContext { - uid: u32, - gid: u32, + uid: Option, + gid: Option, pid: u32, request_id: RequestId, } impl RequestContext { - fn new(uid: u32, gid: u32, pid: u32, request_id: RequestId) -> Self { + fn new(uid: Option, gid: Option, pid: u32, request_id: RequestId) -> Self { Self { uid, gid, @@ -40,13 +40,15 @@ impl RequestContext { } } - /// The user making the request - pub fn user_id(&self) -> u32 { + /// The user making the request, or `None` where the kernel withheld it. See + /// [`Request::uid`] for when that is + pub fn user_id(&self) -> Option { self.uid } - /// The group the user belongs to - pub fn group_id(&self) -> u32 { + /// The group the user belongs to, or `None` where the kernel withheld it. See + /// [`Request::uid`] for when that is + pub fn group_id(&self) -> Option { self.gid } diff --git a/src/lib.rs b/src/lib.rs index 60b0023a..2e6b3a31 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -134,6 +134,9 @@ fn default_init_flags(capabilities: InitFlags) -> InitFlags { flags } +/// What the kernel puts in a request header in place of ids it is not sending. +pub(crate) const FUSE_INVALID_UIDGID: u32 = u32::MAX; + /// Capabilities fuser has no implementation behind, whatever the kernel advertises. /// /// Negotiating one of these makes the kernel change the protocol in a way fuser gets wrong, or @@ -151,10 +154,6 @@ const UNSUPPORTED_CAPABILITIES: InitFlags = ALIASED_UNSUPPORTED_CAPABILITIES // Likewise for the fuse_supp_groups extension. Dropping it leaves the filesystem with the // caller's fsgid in exactly the case the extension exists to correct .union(InitFlags::FUSE_CREATE_SUPP_GROUP) - // Without MountOption::DefaultPermissions the kernel refuses the connection. With it, uid - // and gid arrive as FUSE_INVALID_UIDGID on every request that does not create an inode, - // breaking Request::uid()/gid() and the SessionACL check built on them - .union(InitFlags::FUSE_ALLOW_IDMAP) // Selecting DAX per inode requires FUSE_ATTR_DAX in the flags field of fuse_attr, which // fuser sends as padding. Only a DAX-capable transport (virtiofs) advertises this .union(InitFlags::FUSE_HAS_INODE_DAX) @@ -181,6 +180,21 @@ const ALIASED_UNSUPPORTED_CAPABILITIES: InitFlags = InitFlags::empty() #[cfg(target_os = "macos")] const ALIASED_UNSUPPORTED_CAPABILITIES: InitFlags = InitFlags::empty(); +/// Who a newly created inode belongs to. +/// +/// The kernel decides this rather than the filesystem, and on an idmapped mount it is not the +/// same as the caller's ids: it is those ids mapped through the mount's idmapping. It is given +/// to the requests that create an inode, and to no others, which is why it is an argument to +/// those rather than something [`Request`] carries. A rename is one of them only with +/// `RENAME_WHITEOUT`, so it takes an `Option`. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct Owner { + /// Owning user + pub uid: u32, + /// Owning group, before any setgid inheritance the filesystem applies + pub gid: u32, +} + /// File types #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[cfg_attr(feature = "serializable", derive(Serialize, Deserialize))] @@ -274,12 +288,11 @@ pub struct StatxAttr { pub btime: Option, /// Properties of the file that are set, such as immutable or append-only. /// - /// **The kernel currently discards this.** `fuse_do_statx()` takes the mask, the creation - /// time and the basic stats out of the reply and ignores the attributes, so nothing set - /// here reaches `statx(2)` - checked against mainline as of 6.18. The field is part of - /// the wire format and is sent, so a filesystem that fills it is correct today and needs - /// no change if the kernel starts reading it, but do not expect `chattr +i` to become - /// visible to `statx(2)` this way. + /// **The kernel discards this.** `fuse_do_statx()` takes the mask, the creation time and + /// the basic stats out of the reply and ignores the attributes, so nothing set here + /// reaches `statx(2)`, and `chattr +i` cannot be made visible to it this way. The field + /// is part of the wire format and is sent regardless, so a filesystem that fills it needs + /// no change should the kernel start reading it. pub attributes: StatxAttributes, /// Properties this filesystem knows about at all, set or not, distinguishing "not set" /// from "cannot say". @@ -383,10 +396,21 @@ pub struct KernelConfig { time_gran: Duration, max_stack_depth: u32, kernel_abi: Version, + /// Whether the mount carries `MountOption::DefaultPermissions`, which + /// `InitFlags::FUSE_ALLOW_IDMAP` cannot be negotiated without + default_permissions: bool, + /// The session's ACL, for the same reason + acl: SessionACL, } impl KernelConfig { - fn new(capabilities: InitFlags, max_readahead: u32, kernel_abi: Version) -> Self { + fn new( + capabilities: InitFlags, + max_readahead: u32, + kernel_abi: Version, + default_permissions: bool, + acl: SessionACL, + ) -> Self { Self { capabilities, requested: default_init_flags(capabilities), @@ -400,9 +424,34 @@ impl KernelConfig { time_gran: Duration::new(0, 1), max_stack_depth: 0, kernel_abi, + default_permissions, + acl, } } + /// Whether `InitFlags::FUSE_ALLOW_IDMAP` can be negotiated on this mount, given the + /// capabilities being added alongside it. + /// + /// Two things have to hold, and neither is fuser's choice. The kernel needs something to + /// check access against, since it withholds the caller's ids from the filesystem: it + /// refuses the connection outright - every request answered `ECONNREFUSED` - unless + /// `default_permissions` is in force. The mount option is one way to put it in force and + /// `InitFlags::FUSE_POSIX_ACL` is the other, the kernel setting it from that flag before + /// it looks at this one. + /// + /// And once the ids are withheld fuser cannot tell the mounting user's requests from + /// anyone else's, so `SessionACL::Owner` and `SessionACL::RootAndOwner` could not be + /// enforced; refusing is better than continuing to offer a restriction that has quietly + /// stopped applying. + fn idmap_available(&self, also_adding: InitFlags) -> bool { + // Requesting POSIX ACLs only counts if the kernel offers them, since a capability it + // does not offer is dropped from the negotiated set and sets nothing + let acl_forces_default_permissions = (self.requested | also_adding) + .intersection(self.capabilities) + .contains(InitFlags::FUSE_POSIX_ACL); + (self.default_permissions || acl_forces_default_permissions) && self.acl == SessionACL::All + } + /// Set the maximum stacking depth of the filesystem /// /// This has to be at least 1 to support passthrough to backing files. Setting this to 0 (the @@ -506,12 +555,24 @@ impl KernelConfig { /// Add a set of capabilities. /// + /// [`InitFlags::FUSE_ALLOW_IDMAP`] is accepted only where it can be honored: the session + /// must allow other users, and `default_permissions` must be in force, whether from + /// [`MountOption::DefaultPermissions`] or from negotiating [`InitFlags::FUSE_POSIX_ACL`]. + /// Relying on the latter means adding it in this call or an earlier one, since what has + /// not been asked for yet cannot be counted on. + /// /// # Errors /// When the argument includes capabilities the kernel does not support, or ones fuser cannot /// honor, returns the bits of the capabilities that were refused. Nothing is added in that /// case, not even the capabilities that would have been accepted on their own. pub fn add_capabilities(&mut self, capabilities_to_add: InitFlags) -> Result<(), InitFlags> { - let refused = capabilities_to_add & (!self.capabilities | UNSUPPORTED_CAPABILITIES); + let conditional = if self.idmap_available(capabilities_to_add) { + InitFlags::empty() + } else { + InitFlags::FUSE_ALLOW_IDMAP + }; + let refused = + capabilities_to_add & (!self.capabilities | UNSUPPORTED_CAPABILITIES | conditional); if !refused.is_empty() { return Err(refused); } @@ -623,8 +684,7 @@ pub trait Filesystem: Send + Sync + 'static { /// the reply itself. /// /// Answering `ENOSYS`, which is the default, is permanent: the kernel stops sending - /// `FUSE_STATX` on this connection and serves `statx(2)` out of [`Filesystem::getattr`], - /// which is what it did before this existed. + /// `FUSE_STATX` on this connection and serves `statx(2)` out of [`Filesystem::getattr`]. fn statx( &self, _req: &Request, @@ -694,6 +754,7 @@ pub trait Filesystem: Send + Sync + 'static { mode: u32, umask: u32, rdev: u32, + _owner: Owner, reply: ReplyEntry, ) { warn!( @@ -711,6 +772,7 @@ pub trait Filesystem: Send + Sync + 'static { name: &OsStr, mode: u32, umask: u32, + _owner: Owner, reply: ReplyEntry, ) { warn!( @@ -738,6 +800,7 @@ pub trait Filesystem: Send + Sync + 'static { parent: INodeNo, link_name: &OsStr, target: &Path, + _owner: Owner, reply: ReplyEntry, ) { warn!( @@ -758,6 +821,7 @@ pub trait Filesystem: Send + Sync + 'static { newparent: INodeNo, newname: &OsStr, flags: RenameFlags, + _owner: Option, reply: ReplyEmpty, ) { warn!( @@ -1089,6 +1153,7 @@ pub trait Filesystem: Send + Sync + 'static { umask: u32, flags: i32, kill_suid_gid: bool, + _owner: Owner, reply: ReplyCreate, ) { warn!( @@ -1266,6 +1331,7 @@ pub trait Filesystem: Send + Sync + 'static { umask: u32, flags: i32, kill_suid_gid: bool, + _owner: Owner, reply: ReplyCreate, ) { warn!( @@ -1448,7 +1514,13 @@ mod tests { #[test] fn kernel_config_set_max_write_bounds() { - let mut config = KernelConfig::new(InitFlags::empty(), 65536, Version(7, 31)); + let mut config = KernelConfig::new( + InitFlags::empty(), + 65536, + Version(7, 31), + true, + SessionACL::All, + ); // Values the kernel would not honor (it clamps max_write below 4096 up // to 4096) are rejected with the nearest value that will take effect assert_eq!(config.set_max_write(0), Err(MIN_WRITE_SIZE as u32)); @@ -1474,10 +1546,126 @@ mod tests { ); } + /// FUSE_ALLOW_IDMAP is refused unless the mount can carry it, since the kernel refuses + /// the connection without default_permissions and fuser cannot enforce an owner-only ACL + /// once the caller's ids are withheld + #[test] + fn add_capabilities_idmap_needs_default_permissions_and_allow_other() { + let cases = [ + (true, SessionACL::All, true), + (false, SessionACL::All, false), + (true, SessionACL::Owner, false), + (true, SessionACL::RootAndOwner, false), + (false, SessionACL::Owner, false), + ]; + for (default_permissions, acl, accepted) in cases { + let mut config = KernelConfig::new( + InitFlags::all(), + 65536, + Version(7, 43), + default_permissions, + acl, + ); + let result = config.add_capabilities(InitFlags::FUSE_ALLOW_IDMAP); + assert_eq!( + result.is_ok(), + accepted, + "default_permissions={default_permissions} acl={acl:?} should {} the capability", + if accepted { "accept" } else { "refuse" } + ); + if !accepted { + assert_eq!(result, Err(InitFlags::FUSE_ALLOW_IDMAP)); + assert!(!config.requested.contains(InitFlags::FUSE_ALLOW_IDMAP)); + } else { + assert!(config.requested.contains(InitFlags::FUSE_ALLOW_IDMAP)); + } + } + } + + /// Negotiating POSIX ACLs puts default_permissions in force just as the mount option does, + /// the kernel setting it from that flag before it validates this one, so it satisfies the + /// requirement on its own - but only where the kernel offers it, since a capability it + /// does not offer is dropped from the negotiated set and sets nothing + #[test] + fn add_capabilities_idmap_accepts_posix_acl_for_default_permissions() { + // Both in one call + let mut config = KernelConfig::new( + InitFlags::all(), + 65536, + Version(7, 43), + false, + SessionACL::All, + ); + assert_eq!( + config.add_capabilities(InitFlags::FUSE_POSIX_ACL | InitFlags::FUSE_ALLOW_IDMAP), + Ok(()) + ); + + // ACLs asked for first, in an earlier call + let mut config = KernelConfig::new( + InitFlags::all(), + 65536, + Version(7, 43), + false, + SessionACL::All, + ); + assert_eq!(config.add_capabilities(InitFlags::FUSE_POSIX_ACL), Ok(())); + assert_eq!(config.add_capabilities(InitFlags::FUSE_ALLOW_IDMAP), Ok(())); + + // A kernel without POSIX ACLs would drop them, leaving nothing to force + // default_permissions and a connection the kernel would refuse + let mut config = KernelConfig::new( + InitFlags::all() & !InitFlags::FUSE_POSIX_ACL, + 65536, + Version(7, 43), + false, + SessionACL::All, + ); + assert_eq!( + config.add_capabilities(InitFlags::FUSE_POSIX_ACL | InitFlags::FUSE_ALLOW_IDMAP), + Err(InitFlags::FUSE_POSIX_ACL | InitFlags::FUSE_ALLOW_IDMAP) + ); + + // Neither one on its own is enough without allow_other + let mut config = KernelConfig::new( + InitFlags::all(), + 65536, + Version(7, 43), + false, + SessionACL::Owner, + ); + assert_eq!( + config.add_capabilities(InitFlags::FUSE_POSIX_ACL | InitFlags::FUSE_ALLOW_IDMAP), + Err(InitFlags::FUSE_ALLOW_IDMAP) + ); + } + + /// A kernel that does not offer it cannot have it forced on, whatever the mount looks like + #[test] + fn add_capabilities_idmap_needs_the_kernel_to_offer_it() { + let mut config = KernelConfig::new( + InitFlags::all() & !InitFlags::FUSE_ALLOW_IDMAP, + 65536, + Version(7, 43), + true, + SessionACL::All, + ); + assert_eq!( + config.add_capabilities(InitFlags::FUSE_ALLOW_IDMAP), + Err(InitFlags::FUSE_ALLOW_IDMAP) + ); + } + #[test] fn add_capabilities_refuses_unimplemented() { // A kernel advertising everything, so only fuser's own limits can refuse anything - let mut config = KernelConfig::new(InitFlags::all(), 65536, Version(7, 43)); + let mut config = KernelConfig::new( + InitFlags::all(), + 65536, + Version(7, 43), + true, + SessionACL::All, + ); let before = config.requested; for capability in UNSUPPORTED_CAPABILITIES { assert_eq!(config.add_capabilities(capability), Err(capability)); @@ -1508,7 +1696,8 @@ mod tests { fn add_capabilities_refuses_all_or_nothing() { // Advertise everything except one capability fuser does implement let capabilities = InitFlags::all() - InitFlags::FUSE_POSIX_ACL; - let mut config = KernelConfig::new(capabilities, 65536, Version(7, 43)); + let mut config = + KernelConfig::new(capabilities, 65536, Version(7, 43), true, SessionACL::All); let before = config.requested; // Both reasons for refusing are reported together, and a capability that would have // been accepted on its own is not added diff --git a/src/ll/flags/statx_flags.rs b/src/ll/flags/statx_flags.rs index 69629faf..4e2a3e99 100644 --- a/src/ll/flags/statx_flags.rs +++ b/src/ll/flags/statx_flags.rs @@ -40,12 +40,10 @@ bitflags! { bitflags! { /// Properties of a file that `statx(2)` reports beyond `stat(2)`, as `stx_attributes`. /// - /// The kernel has no way to learn these from a FUSE filesystem other than being told: it - /// fills `stx_attributes` from its own inode flags, which a FUSE filesystem's flags are - /// not. Answering `FUSE_STATX` is what makes `chattr +i` and friends visible to - /// `statx(2)`. + /// The kernel discards these from a FUSE reply - see [`crate::StatxAttr::attributes`], + /// which is where they are set and where that is documented. /// - /// Whatever is reported here counts only where the matching bit is also set in + /// Whatever is reported counts only where the matching bit is also set in /// [`crate::StatxAttr::attributes_mask`], which is how a caller tells "not set" from /// "not supported". #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] diff --git a/src/ll/fuse_abi.rs b/src/ll/fuse_abi.rs index 992a80e0..2babc74e 100644 --- a/src/ll/fuse_abi.rs +++ b/src/ll/fuse_abi.rs @@ -730,7 +730,7 @@ pub(crate) struct fuse_fallocate_in { } #[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] +#[derive(Debug, Clone, FromBytes, KnownLayout, Immutable)] pub(crate) struct fuse_in_header { pub(crate) len: u32, pub(crate) opcode: u32, diff --git a/src/request.rs b/src/request.rs index fe3990f9..755303ac 100644 --- a/src/request.rs +++ b/src/request.rs @@ -12,6 +12,7 @@ use log::debug; use log::error; use crate::Filesystem; +use crate::Owner; use crate::PollNotifier; use crate::Request; use crate::channel::ChannelSender; @@ -21,6 +22,7 @@ use crate::ll::Errno; use crate::ll::ResponseData; use crate::ll::ResponseErrno; use crate::ll::flags::init_flags::InitFlags; +use crate::ll::fuse_abi::fuse_in_header; use crate::reply::Reply; use crate::reply::ReplyDirectory; use crate::reply::ReplyDirectoryPlus; @@ -36,6 +38,15 @@ pub(crate) struct RequestWithSender<'a> { ch: ChannelSender, /// Parsed request pub(crate) request: ll::AnyRequest<'a>, + /// The header as the filesystem sees it, where that differs from the one on the wire. + /// + /// An idmapped mount has no caller ids to report. The kernel already leaves them out of + /// most requests; the ones it does send ids for are sending the owner for an inode they + /// create, which is those ids mapped through the mount and so not the caller's. Reporting + /// them as the caller's would invite a filesystem to decide policy against a mapped id - + /// letting an idmapping that lands on uid 0 pass a check for root. They reach the + /// filesystem as an `Owner` argument instead, and the caller reads as unknown here + masked_header: Option, } impl<'a> RequestWithSender<'a> { @@ -54,7 +65,20 @@ impl<'a> RequestWithSender<'a> { }; request.set_negotiated(negotiated); - Some(Self { ch, request }) + let masked_header = + negotiated + .contains(InitFlags::FUSE_ALLOW_IDMAP) + .then(|| fuse_in_header { + uid: crate::FUSE_INVALID_UIDGID, + gid: crate::FUSE_INVALID_UIDGID, + ..request.header().clone() + }); + + Some(Self { + ch, + request, + masked_header, + }) } /// Dispatch request to the given filesystem. @@ -174,6 +198,7 @@ impl<'a> RequestWithSender<'a> { x.mode(), x.umask(), x.rdev(), + self.owner(), self.reply(), ); } @@ -184,6 +209,7 @@ impl<'a> RequestWithSender<'a> { x.name().as_ref(), x.mode(), x.umask(), + self.owner(), self.reply(), ); } @@ -209,6 +235,7 @@ impl<'a> RequestWithSender<'a> { self.request.nodeid(), x.link_name().as_ref(), Path::new(x.target()), + self.owner(), self.reply(), ); } @@ -226,6 +253,7 @@ impl<'a> RequestWithSender<'a> { x.dest().dir, x.dest().name.as_ref(), flags, + self.optional_owner(), self.reply(), ); } @@ -404,6 +432,7 @@ impl<'a> RequestWithSender<'a> { x.umask(), x.flags(), x.kill_suid_gid(), + self.owner(), self.reply(), ); } @@ -519,6 +548,7 @@ impl<'a> RequestWithSender<'a> { x.to().dir, x.to().name.as_ref(), x.flags(), + self.optional_owner(), self.reply(), ); } @@ -568,6 +598,7 @@ impl<'a> RequestWithSender<'a> { x.umask(), x.flags(), x.kill_suid_gid(), + self.owner(), self.reply(), ); } @@ -602,6 +633,29 @@ impl<'a> RequestWithSender<'a> { /// Create a reply object for this request that can be passed to the filesystem /// implementation and makes sure that a request is replied exactly once + /// The owner for an inode this request creates. + /// + /// On an idmapped mount these are the caller's ids mapped through the mount's idmapping, + /// which is why they are not the caller's ids; otherwise they are the caller's unchanged. + fn owner(&self) -> Owner { + Owner { + uid: self.request.header().uid, + gid: self.request.header().gid, + } + } + + /// The owner for an inode this request may create, where the kernel sent one. + /// + /// A rename creates an inode only with `RENAME_WHITEOUT`, and that is the only rename an + /// idmapped mount sends ids for. Off such a mount every request carries them, so this is + /// `Some` whatever the flags say, and it names the whiteout's owner only when the flags + /// call for one. + fn optional_owner(&self) -> Option { + let owner = self.owner(); + (owner.uid != crate::FUSE_INVALID_UIDGID && owner.gid != crate::FUSE_INVALID_UIDGID) + .then_some(owner) + } + pub(crate) fn reply(&self) -> T { Reply::new(self.request.unique(), ReplySender::Channel(self.ch.clone())) } @@ -609,6 +663,6 @@ impl<'a> RequestWithSender<'a> { /// Returns a Request reference for this request #[inline] fn request_header(&self) -> &Request { - Request::ref_cast(self.request.header()) + Request::ref_cast(self.masked_header.as_ref().unwrap_or(self.request.header())) } } diff --git a/src/request_param.rs b/src/request_param.rs index e69bb855..0ed9be8c 100644 --- a/src/request_param.rs +++ b/src/request_param.rs @@ -21,16 +21,29 @@ impl Request { ll::RequestId(self.header.unique) } - /// Returns the uid of this request + /// Returns the uid of the process that triggered this request, or `None` when the kernel + /// did not send one. + /// + /// An idmapped mount has none to send: which ids a caller would have depends on the mount + /// it came through. Nothing is left needing them. The capability that allows such a mount, + /// [`crate::InitFlags::FUSE_ALLOW_IDMAP`], can only be negotiated where + /// `default_permissions` is in force, so the kernel makes the access checks these ids + /// would otherwise serve, and the requests that create an inode are given an + /// [`crate::Owner`] naming who it belongs to. Those ids reach the filesystem only that + /// way: they are the owner mapped through the mount rather than the caller's, so + /// reporting them here would invite a check against the wrong identity. + /// + /// A filesystem that does not request that capability is always given ids. #[inline] - pub fn uid(&self) -> u32 { - self.header.uid + pub fn uid(&self) -> Option { + (self.header.uid != crate::FUSE_INVALID_UIDGID).then_some(self.header.uid) } - /// Returns the gid of this request + /// Returns the gid of the process that triggered this request, or `None` when the kernel + /// did not send one. See [`Request::uid`] for when that is. #[inline] - pub fn gid(&self) -> u32 { - self.header.gid + pub fn gid(&self) -> Option { + (self.header.gid != crate::FUSE_INVALID_UIDGID).then_some(self.header.gid) } /// Returns the pid of this request diff --git a/src/session.rs b/src/session.rs index 4c98cf81..3e1dc359 100644 --- a/src/session.rs +++ b/src/session.rs @@ -441,7 +441,15 @@ impl Session { )); } - let mut config = KernelConfig::new(init.capabilities(), init.max_readahead(), v); + let mut config = KernelConfig::new( + init.capabilities(), + init.max_readahead(), + v, + self.config + .mount_options + .contains(&MountOption::DefaultPermissions), + self.allowed, + ); // Call filesystem init method and give it a chance to return an error let Some(filesystem) = &mut self.filesystem.fs else { @@ -825,6 +833,173 @@ mod test { ManuallyDrop::into_inner(tmp); } + /// An idmapped mount has no caller ids to report. Pinned against a real kernel, since + /// what it sends is its rule rather than fuser's: nothing reports a caller, and the + /// requests that create an inode name its owner instead - the caller's ids mapped through + /// the mount, which are not the caller's. + #[test] + #[cfg(target_os = "linux")] + fn idmap_withholds_caller_ids_and_names_the_owner() { + use std::sync::atomic::AtomicBool; + use std::sync::atomic::Ordering; + use std::time::Duration; + use std::time::SystemTime; + + use crate::FileAttr; + use crate::FileType; + use crate::INodeNo; + use crate::MountOption; + + struct IdmapFs { + negotiated: Arc, + /// Whether `getattr` was given the caller's uid at all + getattr_had_uid: Arc>>, + mkdir_owner: Arc>>, + /// Whether the request that names an owner also reported a caller + mkdir_had_uid: Arc>>, + } + + impl Filesystem for IdmapFs { + fn init(&mut self, _req: &Request, config: &mut KernelConfig) -> io::Result<()> { + let accepted = config + .add_capabilities(crate::InitFlags::FUSE_ALLOW_IDMAP) + .is_ok(); + self.negotiated.store(accepted, Ordering::SeqCst); + Ok(()) + } + /// Answered so that creating a name gets as far as mkdir: the kernel looks it up + /// first, and a default ENOSYS there would end the operation before it + fn lookup( + &self, + _req: &Request, + _parent: INodeNo, + _name: &std::ffi::OsStr, + reply: crate::ReplyEntry, + ) { + reply.error(crate::Errno::ENOENT); + } + fn getattr( + &self, + req: &Request, + ino: INodeNo, + _fh: Option, + reply: crate::ReplyAttr, + ) { + *self.getattr_had_uid.lock() = Some(req.uid().is_some()); + reply.attr( + &Duration::from_secs(0), + &FileAttr { + ino, + size: 0, + blocks: 0, + atime: SystemTime::UNIX_EPOCH, + mtime: SystemTime::UNIX_EPOCH, + ctime: SystemTime::UNIX_EPOCH, + crtime: SystemTime::UNIX_EPOCH, + kind: FileType::Directory, + perm: 0o777, + nlink: 2, + uid: 0, + gid: 0, + rdev: 0, + blksize: 512, + flags: 0, + }, + ); + } + fn mkdir( + &self, + _req: &Request, + _parent: INodeNo, + _name: &std::ffi::OsStr, + _mode: u32, + _umask: u32, + owner: crate::Owner, + reply: crate::ReplyEntry, + ) { + *self.mkdir_had_uid.lock() = Some(_req.uid().is_some()); + *self.mkdir_owner.lock() = Some(owner); + // Nothing is created; the owner is the whole point here + reply.error(crate::Errno::ENOSPC); + } + } + + let tmp = ManuallyDrop::new(tempfile::tempdir().unwrap()); + let mountpoint = tmp.path().canonicalize().unwrap(); + let negotiated = Arc::new(AtomicBool::new(false)); + let getattr_had_uid = Arc::new(Mutex::new(None)); + let mkdir_owner = Arc::new(Mutex::new(None)); + let mkdir_had_uid = Arc::new(Mutex::new(None)); + + // Both are what the capability requires: without default_permissions the kernel + // refuses the connection, and without allow_other fuser refuses the capability + let mut config = Config::default(); + config.mount_options.push(MountOption::DefaultPermissions); + config.acl = crate::SessionACL::All; + + let session = match Session::new( + IdmapFs { + negotiated: negotiated.clone(), + getattr_had_uid: getattr_had_uid.clone(), + mkdir_owner: mkdir_owner.clone(), + mkdir_had_uid: mkdir_had_uid.clone(), + }, + &mountpoint, + &config, + ) { + Ok(session) => session, + Err(error) => { + // allow_other needs either root or user_allow_other in /etc/fuse.conf + eprintln!("skipping idmap: cannot mount with allow_other: {error}"); + ManuallyDrop::into_inner(tmp); + return; + } + }; + // FUSE_ALLOW_IDMAP arrived in ABI 7.41; an older kernel never offers it + let supported = session.proto_version.is_some_and(|v| v >= Version(7, 41)); + let bg = session.spawn().unwrap(); + + let _ = std::fs::metadata(&mountpoint); + let _ = std::fs::create_dir(mountpoint.join("newdir")); + + let getattr_had_uid = getattr_had_uid.lock().take(); + let mkdir_owner = mkdir_owner.lock().take(); + let mkdir_had_uid = mkdir_had_uid.lock().take(); + let negotiated = negotiated.load(Ordering::SeqCst); + drop(bg); + ManuallyDrop::into_inner(tmp); + + if !supported { + eprintln!("skipping idmap: the kernel's FUSE protocol predates 7.41"); + return; + } + assert!( + negotiated, + "a mount with default_permissions and allow_other must be allowed the capability" + ); + assert_eq!( + getattr_had_uid, + Some(false), + "a request that creates nothing must arrive with the caller's ids withheld" + ); + assert_eq!( + mkdir_owner, + Some(crate::Owner { + uid: nix::unistd::geteuid().as_raw(), + gid: nix::unistd::getegid().as_raw(), + }), + "a request that creates an inode must name the owner it should get" + ); + // The header carries ids on this request, but they are the owner mapped through the + // mount rather than the caller's. Reporting them as the caller would let an idmapping + // that lands on uid 0 pass for root + assert_eq!( + mkdir_had_uid, + Some(false), + "the mapped owner must not be reported as the caller's id" + ); + } + /// statx(2) must reach Filesystem::statx(), and the creation time it carries - which no /// other request can express on Linux - must arrive intact. Also pins the one field the /// wire format carries but the kernel drops, so that a kernel change shows up here. @@ -1104,6 +1279,7 @@ mod test { _umask: u32, flags: i32, _kill_suid_gid: bool, + _owner: crate::Owner, reply: ReplyCreate, ) { *self.seen.lock() = Some((mode, flags)); @@ -1191,8 +1367,11 @@ mod test { /// from there would carry uid 0 and pid 0. #[test] fn requests_carry_the_calling_task() { + /// The caller's uid, absent on an idmapped mount, and the calling thread's id + type Caller = (Option, u32); + struct CallerFs { - seen: Arc>>, + seen: Arc>>, } impl Filesystem for CallerFs { fn getattr( @@ -1228,8 +1407,8 @@ mod test { let (uid, pid) = seen.expect("the filesystem must have been asked for the root inode"); assert_eq!( uid, - geteuid().as_raw(), - "the request must carry the caller's uid" + Some(geteuid().as_raw()), + "the request must carry the caller's uid, this mount not being idmapped" ); // Discriminating even when the test runs as root, where the uid above is 0 either // way. The kernel takes this from task_pid(current), so it is the id of the thread