diff --git a/CHANGELOG.md b/CHANGELOG.md index d4d08d6..0748e11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ This file records each release. The format follows [Keep a Changelog](https://ke ### Added +- fanotify has `FAN_REPORT_MNT`. A group with this flag marks a mount namespace with `FAN_MARK_MNTNS` and gets `FAN_MNT_ATTACH` and `FAN_MNT_DETACH` when a mount of the namespace is attached, moved or unmounted. Each event has a `FAN_EVENT_INFO_TYPE_MNT` record with the unique mount ID, and the errors and the record layout are the same as in Linux. The `init` boot test checks the errors, the fdinfo line and the events of mount, move and umount. (#539) - The `sdbpf` boot test runs the unit tests test-bpf-devices, test-bpf-firewall and test-bpf-foreign-programs of systemd. They attach cgroup programs with `BPF_PROG_ATTACH`, `BPF_PROG_DETACH`, `BPF_PROG_QUERY` and `BPF_LINK_CREATE`, and all three pass. `RK_SYSTEMD_PACKAGES` of tools/systemd/build.sh adds packages to the systemd tree, and this test needs `systemd-tests` and `iputils-ping` (#537). ## [0.5.23] - 2026-11-12 diff --git a/core/rk-syscall/src/fanotify.rs b/core/rk-syscall/src/fanotify.rs index 5f67373..643b98e 100644 --- a/core/rk-syscall/src/fanotify.rs +++ b/core/rk-syscall/src/fanotify.rs @@ -1,8 +1,9 @@ //! fanotify: `fanotify_init` and `fanotify_mark`, as fs/notify/fanotify/fanotify_user.c of //! Linux. The group, its queue and its marks are in `rk_vfs::notify::fanotify`. //! -//! The groups of mount events (`FAN_REPORT_MNT`) are not supported: `fanotify_init` gives -//! `EINVAL` for them. +//! A group with `FAN_REPORT_MNT` gets the mount events of the mount namespaces that it marks +//! with `FAN_MARK_MNTNS`. The path of such a mark is a file of a mount namespace, as +//! `/proc/self/ns/mnt`. use crate::cx::{Cx, SysResult}; use alloc::sync::Arc; @@ -25,6 +26,7 @@ use rk_vfs::notify::Object; use rk_vfs::notify::fanotify::{ EVENT_FLAGS, FD_EVENTS, FID_BITS, Fanotify, FanotifyFile, Kind, PATH_EVENTS, PERM_EVENTS, }; +use rk_vfs::nsfs::NsKind; use rk_vfs::user_ns::UcountKind; /// The bits of the classes that need a privilege, `FANOTIFY_PERM_CLASSES` of Linux. @@ -100,7 +102,12 @@ pub(crate) fn fanotify_init(cx: &Cx<'_>, flags: u64, event_f_flags: u64) -> SysR if flags & !(ADMIN_INIT_FLAGS | USER_INIT_FLAGS | FAN_ENABLE_AUDIT) != 0 { return Err(Errno::EINVAL); } - if flags & FAN_REPORT_MNT != 0 { + // A group of mount events is of the notification class and reports no file handle and no + // error of a descriptor. + if flags & FAN_REPORT_MNT != 0 + && (flags & FAN_ALL_CLASS_BITS != FAN_CLASS_NOTIF + || flags & (FID_BITS | FAN_REPORT_FD_ERROR) != 0) + { return Err(Errno::EINVAL); } if f_flags & !EVENT_F_BITS != 0 || f_flags & o::O_ACCMODE == o::O_ACCMODE { @@ -137,7 +144,15 @@ pub(crate) fn fanotify_init(cx: &Cx<'_>, flags: u64, event_f_flags: u64) -> SysR let charge = rk_vfs::user_ns::charge(&cred.user_ns, cred.euid, UcountKind::FanotifyGroups) .ok_or(Errno::EMFILE)?; let unpriv = !cred.capable(cap::SYS_ADMIN); - let ops = FanotifyFile::new(flags, f_flags, priority, unlimited, unpriv, Some(charge)); + let ops = FanotifyFile::new( + flags, + f_flags, + priority, + unlimited, + unpriv, + cred.user_ns.clone(), + Some(charge), + ); let nonblock = if flags & FAN_NONBLOCK != 0 { o::O_NONBLOCK } else { 0 }; let file = File::with_ops(Arc::new(ops), None, o::O_RDWR | nonblock); file.set_nonotify(); @@ -195,8 +210,7 @@ pub(crate) fn fanotify_mark( FAN_MARK_INODE => Kind::Inode, FAN_MARK_MOUNT => Kind::Mount, FAN_MARK_FILESYSTEM => Kind::Fs, - // A mark on a mount name space needs `FAN_REPORT_MNT`, which is not supported. - FAN_MARK_MNTNS => Kind::Mount, + FAN_MARK_MNTNS => Kind::MntNs, _ => return Err(Errno::EINVAL), }; let cmd = flags & MARK_CMD_BITS; @@ -223,13 +237,25 @@ pub(crate) fn fanotify_mark( umask = EVENT_FLAGS; } let g = group(cx, fd)?; - if mask & MOUNT_EVENTS != 0 || mark_type == FAN_MARK_MNTNS { + // A group of mount events has marks on mount namespaces only, with the mount events only. + // The other groups cannot have them. + if g.init_flags() & FAN_REPORT_MNT != 0 { + if mask & !MOUNT_EVENTS != 0 || mark_type != FAN_MARK_MNTNS { + return Err(Errno::EINVAL); + } + } else if mask & MOUNT_EVENTS != 0 || mark_type == FAN_MARK_MNTNS { return Err(Errno::EINVAL); } - // A user without `CAP_SYS_ADMIN` and a group that such a user made can mark inodes only. - if mark_type != FAN_MARK_INODE - && (g.unprivileged() || !cx.process().cred().capable(cap::SYS_ADMIN)) - { + // A user without `CAP_SYS_ADMIN` and a group that such a user made can mark inodes only. A + // mark on a mount namespace needs `CAP_SYS_ADMIN` in the user namespace of the group, as + // Linux, so a user can watch the mount namespaces of its own user namespace. + let cred = cx.process().cred(); + let allowed = if mark_type == FAN_MARK_MNTNS { + cred.ns_capable(g.user_ns(), cap::SYS_ADMIN) + } else { + mark_type == FAN_MARK_INODE || (!g.unprivileged() && cred.capable(cap::SYS_ADMIN)) + }; + if !allowed { return Err(Errno::EPERM); } // The permission events need a class above notification, and the pre-content events the @@ -307,6 +333,20 @@ pub(crate) fn fanotify_mark( Kind::Inode => Object::Inode(at.inode().clone()), Kind::Mount => Object::Mount(at.mount.clone()), Kind::Fs => Object::Fs(at.inode().fs_id()), + // The path is the file of a mount namespace, and the mark needs `CAP_SYS_ADMIN` in the + // user namespace that owns the namespace, as `mnt_ns_from_dentry` in + // `do_fanotify_mark` of Linux. + Kind::MntNs => { + let id = match rk_vfs::nsfs::ns_of(at.inode()) { + Some((NsKind::Mnt, id)) => id, + _ => return Err(Errno::EINVAL), + }; + let ns = rk_proc::ns::mnt_ns(id).ok_or(Errno::EINVAL)?; + if !cred.ns_capable(ns.owner(), cap::SYS_ADMIN) { + return Err(Errno::EPERM); + } + Object::MntNs(Arc::downgrade(&ns), id) + } }; // `FAN_MARK_IGNORE` needs `FAN_MARK_IGNORED_SURV_MODIFY` on a mount, a file system and a // directory. diff --git a/fs/rk-vfs/src/dentry.rs b/fs/rk-vfs/src/dentry.rs index ca24f45..497bfcd 100644 --- a/fs/rk-vfs/src/dentry.rs +++ b/fs/rk-vfs/src/dentry.rs @@ -883,6 +883,13 @@ pub struct MountNamespace { owner: Arc, } +impl Drop for MountNamespace { + /// The marks on the name space go away, as `fsnotify_mntns_delete` of Linux. + fn drop(&mut self) { + crate::notify::mntns_delete(self); + } +} + /// The waits of `poll` on `/proc/PID/mounts` wait here for a change of the mounts of a name /// space, as `poll` of `struct mnt_namespace` of Linux. One queue serves each name space. static NS_POLL: WaitQueue = WaitQueue::new(); @@ -1107,6 +1114,7 @@ impl MountNamespace { self.mounts.lock().push(mount.clone()); ATTACHED.lock().push(Arc::downgrade(mount)); self.touch(); + crate::notify::mnt_event(self, crate::notify::mask::MNT_ATTACH, mount.unique_id()); } /// Counts a change of the mounts and wakes the waits of `poll`, as `touch_mnt_namespace` of @@ -1213,6 +1221,7 @@ impl MountNamespace { mounts.push(mount.clone()); drop(mounts); self.touch(); + crate::notify::mnt_event(self, crate::notify::mask::MNT_MOVE, mount.unique_id()); Ok(()) } @@ -1250,6 +1259,9 @@ impl MountNamespace { mounts.insert(index, mount.clone()); drop(mounts); self.touch(); + // Only the mount of `from` moves for the mount events, as `attach_recursive_mnt` of + // Linux. + crate::notify::mnt_event(self, crate::notify::mask::MNT_MOVE, mount.unique_id()); Ok(()) } @@ -1298,6 +1310,7 @@ impl MountNamespace { mount.change_propagation(PropagationType::Private); *mount.ns.lock() = Weak::new(); ATTACHED.lock().retain(|m| !core::ptr::eq(m.as_ptr(), Arc::as_ptr(mount))); + crate::notify::mnt_event(self, crate::notify::mask::MNT_DETACH, mount.unique_id()); true } @@ -1348,6 +1361,9 @@ impl MountNamespace { mounts.push(root_mnt.clone()); drop(mounts); self.touch(); + // The old root moves first, then the new root, as `path_pivot_root` of Linux. + crate::notify::mnt_event(self, crate::notify::mask::MNT_MOVE, root_mnt.unique_id()); + crate::notify::mnt_event(self, crate::notify::mask::MNT_MOVE, new_mnt.unique_id()); Ok(()) } diff --git a/fs/rk-vfs/src/notify/fanotify.rs b/fs/rk-vfs/src/notify/fanotify.rs index 091d6aa..c9f340a 100644 --- a/fs/rk-vfs/src/notify/fanotify.rs +++ b/fs/rk-vfs/src/notify/fanotify.rs @@ -13,13 +13,17 @@ //! metadata: the handle of the directory and the name of the entry, the new directory and the //! new name of a rename, and the handle of the file. //! -//! The mount events and the pre-content events are not supported. +//! A group with `FAN_REPORT_MNT` has marks on mount namespaces only. It gets the mount events of +//! the namespaces: `FAN_MNT_ATTACH` and `FAN_MNT_DETACH`, or both for a move. Each event has no +//! descriptor and has a record with the unique ID of the mount. +//! +//! The pre-content events are not supported. use super::{Found, FsEvent, Group, Mark, Object, add_object_mark, destroy_mark, mask}; use crate::dentry::Location; use crate::file::{File, FileOps, UserMemory, events}; use crate::inode::Result; -use crate::user_ns::{Charge, UcountKind}; +use crate::user_ns::{Charge, UcountKind, UserNs}; use crate::wait::{PollTable, WaitQueue}; use alloc::collections::VecDeque; use alloc::format; @@ -30,12 +34,12 @@ use rk_abi::Errno; use rk_abi::native::fanotify::{ FAN_ALLOW, FAN_AUDIT, FAN_DENY, FAN_ENABLE_AUDIT, FAN_EPIDFD, FAN_ERRNO_MASK, FAN_ERRNO_SHIFT, FAN_EVENT_INFO_TYPE_DFID, FAN_EVENT_INFO_TYPE_DFID_NAME, FAN_EVENT_INFO_TYPE_FID, - FAN_EVENT_INFO_TYPE_NEW_DFID_NAME, FAN_EVENT_INFO_TYPE_OLD_DFID_NAME, + FAN_EVENT_INFO_TYPE_MNT, FAN_EVENT_INFO_TYPE_NEW_DFID_NAME, FAN_EVENT_INFO_TYPE_OLD_DFID_NAME, FAN_EVENT_INFO_TYPE_PIDFD, FAN_EVENT_METADATA_LEN, FAN_INFO, FAN_MARK_EVICTABLE, FAN_MARK_IGNORE, FAN_MARK_IGNORED_MASK, FAN_MARK_IGNORED_SURV_MODIFY, FAN_NOFD, FAN_NOPIDFD, FAN_ONDIR, FAN_PRE_ACCESS, FAN_REPORT_DFID_NAME_TARGET, FAN_REPORT_DIR_FID, - FAN_REPORT_FD_ERROR, FAN_REPORT_FID, FAN_REPORT_NAME, FAN_REPORT_PIDFD, FAN_REPORT_TARGET_FID, - FAN_REPORT_TID, FAN_UNLIMITED_MARKS, FANOTIFY_METADATA_VERSION, + FAN_REPORT_FD_ERROR, FAN_REPORT_FID, FAN_REPORT_MNT, FAN_REPORT_NAME, FAN_REPORT_PIDFD, + FAN_REPORT_TARGET_FID, FAN_REPORT_TID, FAN_UNLIMITED_MARKS, FANOTIFY_METADATA_VERSION, }; use rk_abi::native::open as o; use rk_abi::native::types::{FanotifyEventMetadata, FanotifyResponse}; @@ -59,7 +63,7 @@ pub const FD_EVENTS: u32 = PATH_EVENTS | PERM_EVENTS; pub const EVENT_FLAGS: u32 = mask::EVENT_ON_CHILD | FAN_ONDIR; /// The bits of `ALL_FSNOTIFY_EVENTS` of Linux: each event, without the flags. -const ALL_FS_EVENTS: u32 = 0x00ff_ffff | mask::RENAME; +const ALL_FS_EVENTS: u32 = 0x00ff_ffff | mask::MNT_EVENTS | mask::RENAME; /// The events that a read gives in a group that reports a descriptor, `FANOTIFY_OUTGOING_EVENTS` /// of Linux without the event flags. @@ -82,6 +86,9 @@ const FID_INFO_HDR_LEN: usize = 20; /// The size of `struct fanotify_event_info_pidfd`, `FANOTIFY_PIDFD_INFO_LEN` of Linux. const PIDFD_INFO_LEN: usize = 8; +/// The size of `struct fanotify_event_info_mnt`, `FANOTIFY_MNT_INFO_LEN` of Linux. +const MNT_INFO_LEN: usize = 16; + /// `fs.fanotify.max_queued_events`: the most events in the queue of a new group without /// `FAN_UNLIMITED_QUEUE`. pub static MAX_QUEUED_EVENTS: AtomicU32 = AtomicU32::new(16384); @@ -134,6 +141,8 @@ pub enum Kind { Mount, /// A file system. Fs, + /// A mount namespace, `FAN_MARK_MNTNS`. + MntNs, } impl Kind { @@ -142,6 +151,7 @@ impl Kind { Object::Inode(_) => Self::Inode, Object::Mount(_) => Self::Mount, Object::Fs(_) => Self::Fs, + Object::MntNs(..) => Self::MntNs, } } } @@ -206,6 +216,8 @@ enum Info { Path(Location), /// The handles, for a group that reports a file handle. Fid(Fid), + /// The unique ID of the mount of a mount event, for a group with `FAN_REPORT_MNT`. + Mnt(u64), } /// An event in the queue, as `struct fanotify_event` of Linux. @@ -250,6 +262,8 @@ pub struct Fanotify { priority: u32, /// A user without `CAP_SYS_ADMIN` made the group, `FANOTIFY_UNPRIV` of Linux. unpriv: bool, + /// The user namespace of the task that made the group, `user_ns` of Linux. + user_ns: Arc, max_events: usize, state: SpinLock, /// The readers wait here. @@ -315,6 +329,12 @@ impl Fanotify { self.priority } + /// The user namespace of the task that made the group. A mark on a mount namespace needs + /// `CAP_SYS_ADMIN` in it. + pub fn user_ns(&self) -> &Arc { + &self.user_ns + } + fn wake(&self) { self.events.fetch_add(1, Ordering::Relaxed); self.wait.wake_all(); @@ -416,6 +436,9 @@ impl Fanotify { /// The size of the event in a read, as `fanotify_event_len` of Linux. fn event_len(&self, item: &Item) -> usize { let pidfd = if self.flags & FAN_REPORT_PIDFD != 0 { PIDFD_INFO_LEN } else { 0 }; + if let Item::Event(Event { info: Info::Mnt(_), .. }) = item { + return META_LEN + pidfd + MNT_INFO_LEN; + } let Item::Event(Event { info: Info::Fid(fid), mask: m, .. }) = item else { return META_LEN + pidfd; }; @@ -704,6 +727,10 @@ impl Fanotify { } Object::Mount(mount) => format!("fanotify mnt_id:{:x} ", mount.id()), Object::Fs(id) => format!("fanotify sdev:{:x} ", kernel_dev(*id)), + Object::MntNs(_, id) => { + let ino = crate::nsfs::NsInode::ino(crate::nsfs::NsKind::Mnt, *id); + format!("fanotify mnt_ns:{ino} ") + } }; out.extend_from_slice(line.as_bytes()); out.extend_from_slice( @@ -787,6 +814,16 @@ fn put_fid(out: &mut Vec, ty: u32, fsid: [i32; 2], fh: &Fh, name: &[u8]) { out.resize(start + len, 0); } +/// Puts the record of a mount event in `out`, as `struct fanotify_event_info_mnt` of Linux: the +/// header, 4 bytes of padding, and the unique ID of the mount. +fn put_mnt(out: &mut Vec, mnt_id: u64) { + out.push(FAN_EVENT_INFO_TYPE_MNT as u8); + out.push(0); + out.extend_from_slice(&(MNT_INFO_LEN as u16).to_ne_bytes()); + out.extend_from_slice(&[0; 4]); + out.extend_from_slice(&mnt_id.to_ne_bytes()); +} + impl Group for Fanotify { fn handle_event(&self, _mark: &Arc, _mask: u32, _name: Option<&[u8]>, _cookie: u32) {} @@ -908,6 +945,44 @@ impl Group for Fanotify { None => Ok(()), } } + + fn handle_mnt(&self, m: u32, mnt_id: u64, marks: &[Arc]) { + // As `fanotify_group_event_mask` of Linux: only a group with `FAN_REPORT_MNT` gets the + // mount events. + if self.flags & FAN_REPORT_MNT == 0 { + return; + } + let mut want = 0; + let mut ignore = 0; + for mark in marks.iter().filter(|mark| mark.alive()) { + ignore |= effective_ignore(mark, false, false); + want |= mark.mask(); + } + let test = m & want & !ignore & mask::MNT_EVENTS; + if test == 0 { + return; + } + let (tgid, tid) = HOOKS.get().map_or((0, 0), |h| (h.current)()); + let pid = if self.flags & FAN_REPORT_TID != 0 { tid } else { tgid }; + // A mount event never joins another event, as `fanotify_should_merge` of Linux. + { + let mut st = self.state.lock(); + if st.closed { + return; + } + if st.queue.len() >= self.max_events { + if st.overflow { + return; + } + st.overflow = true; + st.queue.push_back(Item::Overflow); + } else { + let event = Event { mask: test, info: Info::Mnt(mnt_id), pid, perm: None }; + st.queue.push_back(Item::Event(event)); + } + } + self.wake(); + } } /// The operations of the file of a fanotify group. @@ -919,14 +994,16 @@ impl FanotifyFile { /// Makes a new group, as `fanotify_init` of Linux after the checks of the flags. `flags` are /// the flags of the group, `f_flags` the flags of the files of the events, and `priority` the /// class: 0 for notification, 1 for content and 2 for pre-content. - /// `unpriv` tells that a user without `CAP_SYS_ADMIN` makes the group, and `charge` is the - /// count of the group for its user. The marks of the group then count for the same user. + /// `unpriv` tells that a user without `CAP_SYS_ADMIN` makes the group, `user_ns` is the user + /// namespace of the user, and `charge` is the count of the group for its user. The marks of + /// the group then count for the same user. pub fn new( flags: u32, f_flags: u32, priority: u32, unlimited: bool, unpriv: bool, + user_ns: Arc, charge: Option, ) -> Self { let group = Arc::new_cyclic(|me| Fanotify { @@ -935,6 +1012,7 @@ impl FanotifyFile { f_flags, priority, unpriv, + user_ns, max_events: if unlimited { usize::MAX } else { @@ -1037,6 +1115,7 @@ impl FileOps for FanotifyFile { Err(_) => break, }; let mut fid = None; + let mut mnt = None; let (m, fd, mut pid, perm) = match item { // An event without a file has `-EBADF` with `FAN_REPORT_FD_ERROR`, as Linux. Item::Overflow if g.flags & FAN_REPORT_FD_ERROR != 0 => { @@ -1047,6 +1126,10 @@ impl FileOps for FanotifyFile { fid = Some(f); (m, FAN_NOFD as i32, pid, perm) } + Item::Event(Event { info: Info::Mnt(id), mask: m, pid, perm }) => { + mnt = Some(id); + (m, FAN_NOFD as i32, pid, perm) + } Item::Event(Event { info: Info::Path(at), mask: m, pid, perm }) => { let fd = match g.open_fd(&at) { Ok(fd) => fd, @@ -1086,6 +1169,10 @@ impl FileOps for FanotifyFile { records.extend_from_slice(&(PIDFD_INFO_LEN as u16).to_ne_bytes()); records.extend_from_slice(&pidfd.to_ne_bytes()); } + // The record of the mount is the last, as `copy_mnt_info_to_user` of Linux. + if let Some(id) = mnt { + put_mnt(&mut records, id); + } let meta = FanotifyEventMetadata { event_len: FAN_EVENT_METADATA_LEN + records.len() as u32, vers: FANOTIFY_METADATA_VERSION as u8, @@ -1192,6 +1279,17 @@ mod tests { assert_eq!(i32::from_ne_bytes([out[16], out[17], out[18], out[19]]), 0xff); } + #[test] + fn a_mount_record_has_the_id_after_the_padding() { + let mut out = Vec::new(); + put_mnt(&mut out, 0x1_0000_0005); + assert_eq!(out.len(), MNT_INFO_LEN); + assert_eq!(out[0], FAN_EVENT_INFO_TYPE_MNT as u8); + assert_eq!(u16::from_ne_bytes([out[2], out[3]]), 16); + assert_eq!(&out[4..8], &[0; 4]); + assert_eq!(u64::from_ne_bytes(out[8..16].try_into().unwrap()), 0x1_0000_0005); + } + #[test] fn a_deny_without_errno_gives_eperm() { assert_eq!(answer(FAN_ALLOW), Ok(())); diff --git a/fs/rk-vfs/src/notify/mod.rs b/fs/rk-vfs/src/notify/mod.rs index c0aabf0..1cc0f43 100644 --- a/fs/rk-vfs/src/notify/mod.rs +++ b/fs/rk-vfs/src/notify/mod.rs @@ -7,7 +7,8 @@ //! the inode, and to each mark of the parent directory that wants the events of its children. //! //! The marks do not depend on inotify: dnotify and fanotify are other groups. An inotify mark and -//! a dnotify mark are on an inode. A fanotify mark can also be on a mount or on a file system. +//! a dnotify mark are on an inode. A fanotify mark can also be on a mount, on a file system or +//! on a mount namespace. A mark on a mount namespace gets the mount events of the namespace. //! fanotify gets each event with its objects in an [`FsEvent`], as the arguments of `fsnotify` of //! Linux: the path of an open file, the file, its directory and its name, and the new directory //! and the new name of a rename. A global count of the marks lets each hook return at once when @@ -21,10 +22,10 @@ pub mod fanotify; pub mod inotify; pub mod queue; -use crate::dentry::{Dentry, Location, Mount}; +use crate::dentry::{Dentry, Location, Mount, MountNamespace}; use crate::inode::{InodeRef, Result, SetAttr}; use alloc::collections::BTreeMap; -use alloc::sync::Arc; +use alloc::sync::{Arc, Weak}; use alloc::vec::Vec; use core::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; use rk_frame::sync::SpinLock; @@ -71,6 +72,14 @@ pub mod mask { pub const OPEN_EXEC_PERM: u32 = 0x4_0000; /// The permission events. pub const PERM_EVENTS: u32 = OPEN_PERM | ACCESS_PERM | OPEN_EXEC_PERM; + /// A mount was attached to the mount namespace, `FS_MNT_ATTACH`. + pub const MNT_ATTACH: u32 = 0x0100_0000; + /// A mount was detached from the mount namespace, `FS_MNT_DETACH`. + pub const MNT_DETACH: u32 = 0x0200_0000; + /// A mount moved in the mount namespace, `FS_MNT_MOVE`. + pub const MNT_MOVE: u32 = MNT_ATTACH | MNT_DETACH; + /// The mount events, `FSNOTIFY_MNT_EVENTS`. + pub const MNT_EVENTS: u32 = MNT_ATTACH | MNT_DETACH; /// Only the events after the child is unlinked, `IN_EXCL_UNLINK`: a mark with it does not /// get the events of a file that has no name. pub const EXCL_UNLINK: u32 = 0x0400_0000; @@ -152,6 +161,13 @@ pub trait Group: Send + Sync { let _ = (ev, marks); Ok(()) } + + /// Takes the mount event `mask` of the mount with the unique ID `mnt_id`. `marks` are the + /// marks of the group on the mount namespace of the event, as `fsnotify` of Linux with + /// `FSNOTIFY_EVENT_MNT`. + fn handle_mnt(&self, mask: u32, mnt_id: u64, marks: &[Arc]) { + let _ = (mask, mnt_id, marks); + } } /// The object of a mark, as the connector types of Linux. @@ -163,6 +179,9 @@ pub enum Object { Mount(Arc), /// A file system, by its ID. Fs(u64), + /// A mount namespace, with the ID of its nsfs file. The mark does not hold the namespace. + /// The marks go away when the namespace stops. + MntNs(Weak, u32), } impl Object { @@ -171,6 +190,7 @@ impl Object { Self::Inode(inode) => (0, key(inode)), Self::Mount(mount) => (1, Arc::as_ptr(mount) as usize), Self::Fs(id) => (2, *id as usize), + Self::MntNs(ns, _) => (3, ns.as_ptr() as usize), } } @@ -746,7 +766,7 @@ pub fn unmount(fs_id: u64) { for mark in marks { let on_fs = match &mark.object { Object::Inode(inode) => inode.fs_id() == fs_id, - Object::Mount(_) => false, + Object::Mount(_) | Object::MntNs(..) => false, Object::Fs(id) => *id == fs_id, }; if on_fs { @@ -763,6 +783,38 @@ pub fn unmount(fs_id: u64) { } } +/// The key of the marks on the mount namespace `ns`. +fn ns_key(ns: &MountNamespace) -> (u8, usize) { + (3, core::ptr::from_ref(ns) as usize) +} + +/// The mount event `mask` of the mount with the unique ID `mnt_id` in the namespace `ns`, as +/// `fsnotify_mnt` of Linux. Only the marks on `ns` get it. +pub fn mnt_event(ns: &MountNamespace, mask: u32, mnt_id: u64) { + if PATH_COUNT.load(Ordering::Acquire) == 0 { + return; + } + let mut found = MARKS.lock().get(&ns_key(ns)).cloned().unwrap_or_default(); + while let Some(first) = found.first() { + let group = first.group.clone(); + let (mine, rest): (Vec<_>, Vec<_>) = + found.into_iter().partition(|m| Arc::ptr_eq(&m.group, &group)); + found = rest; + group.handle_mnt(mask, mnt_id, &mine); + } +} + +/// The mount namespace `ns` stops: its marks go away, as `fsnotify_mntns_delete` of Linux. +pub fn mntns_delete(ns: &MountNamespace) { + if none() { + return; + } + let marks = MARKS.lock().get(&ns_key(ns)).cloned().unwrap_or_default(); + for mark in &marks { + destroy_mark(mark); + } +} + /// The number of marks on all inodes. pub fn mark_count() -> usize { COUNT.load(Ordering::Relaxed) diff --git a/tests/user/init.c b/tests/user/init.c index 3bef742..7ebc071 100644 --- a/tests/user/init.c +++ b/tests/user/init.c @@ -2341,6 +2341,158 @@ static void test_pidfd_setns_mnt(void) close(fds[1]); } +/* fanotify with FAN_REPORT_MNT: a mark on a mount namespace gets FAN_MNT_ATTACH for a mount, + * FAN_MNT_DETACH for an unmount and both for a move. Each event has no descriptor, and has the + * record of the unique mount ID after the record of the pidfd, as fanotify_user.c of Linux + * (#539). */ +#define RK_FAN_NONBLOCK 0x2u +#define RK_FAN_CLASS_CONTENT 0x4u +#define RK_FAN_REPORT_PIDFD 0x80u +#define RK_FAN_REPORT_FID 0x200u +#define RK_FAN_REPORT_MNT 0x4000u +#define RK_FAN_MARK_ADD 0x1u +#define RK_FAN_MARK_MNTNS 0x110u +#define RK_FAN_OPEN 0x20ul +#define RK_FAN_MNT_ATTACH 0x01000000ul +#define RK_FAN_MNT_DETACH 0x02000000ul + +static int fan_mark(int fd, unsigned int flags, unsigned long mask, const char *path) +{ + return syscall(SYS_fanotify_mark, fd, flags, mask, AT_FDCWD, path); +} + +/* The unique mount ID of statx with STATX_MNT_ID_UNIQUE, at offset 144 of struct statx. */ +static unsigned long long unique_mnt_id(const char *path) +{ + unsigned char buf[256] = {0}; + unsigned long long id = 0; + if (syscall(SYS_statx, AT_FDCWD, path, 0, 0x4000, buf) == 0) + memcpy(&id, buf + 144, 8); + return id; +} + +/* Reads one event of a group with FAN_REPORT_MNT and FAN_REPORT_PIDFD. Gives the mask and the + * mount ID, or 0 if there is no event or if the layout is not the layout of Linux. */ +static unsigned long long fan_mnt_event(int fd, unsigned long long *id) +{ + unsigned char buf[256]; + unsigned long long mask; + unsigned int len; + unsigned short hlen; + int efd, pid, pidfd; + + if (read(fd, buf, sizeof(buf)) != 48) + return 0; + memcpy(&len, buf, 4); + memcpy(&mask, buf + 8, 8); + memcpy(&efd, buf + 16, 4); + memcpy(&pid, buf + 20, 4); + if (len != 48 || buf[4] != 3 || efd != -1 || pid != getpid()) + return 0; + /* The pidfd record: type 4, length 8. */ + memcpy(&hlen, buf + 26, 2); + memcpy(&pidfd, buf + 28, 4); + if (buf[24] != 4 || hlen != 8 || pidfd < 0) + return 0; + close(pidfd); + /* The mount record: type 7, length 16, 4 bytes of padding and the ID. */ + memcpy(&hlen, buf + 34, 2); + if (buf[32] != 7 || hlen != 16 || buf[36] || buf[37] || buf[38] || buf[39]) + return 0; + memcpy(id, buf + 40, 8); + return mask; +} + +static void test_fanotify_mnt(void) +{ + unsigned long long id, got = 0; + char line[128], want[128], buf[64]; + struct stat st; + int status = -1; + + if (access("/proc/self/ns", F_OK) != 0) { + mkdir("/proc", 0555); + mount("proc", "/proc", "proc", 0, NULL); + } + errno = 0; + check("fanotify_init with FAN_REPORT_MNT and FAN_CLASS_CONTENT gives EINVAL", + syscall(SYS_fanotify_init, RK_FAN_REPORT_MNT | RK_FAN_CLASS_CONTENT, 0) == -1 && errno == EINVAL); + errno = 0; + check("fanotify_init with FAN_REPORT_MNT and FAN_REPORT_FID gives EINVAL", + syscall(SYS_fanotify_init, RK_FAN_REPORT_MNT | RK_FAN_REPORT_FID, 0) == -1 && errno == EINVAL); + int plain = syscall(SYS_fanotify_init, 0, 0); + errno = 0; + check("a group without FAN_REPORT_MNT cannot mark a mount namespace", + plain >= 0 && fan_mark(plain, RK_FAN_MARK_ADD | RK_FAN_MARK_MNTNS, RK_FAN_MNT_ATTACH, + "/proc/self/ns/mnt") == -1 && errno == EINVAL); + close(plain); + int fd = syscall(SYS_fanotify_init, RK_FAN_REPORT_MNT | RK_FAN_REPORT_PIDFD | RK_FAN_NONBLOCK, 0); + check("fanotify_init with FAN_REPORT_MNT", fd >= 0); + errno = 0; + check("a group with FAN_REPORT_MNT cannot mark an inode", + fan_mark(fd, RK_FAN_MARK_ADD, RK_FAN_MNT_ATTACH, "/") == -1 && errno == EINVAL); + errno = 0; + check("a mark on a mount namespace with FAN_OPEN gives EINVAL", + fan_mark(fd, RK_FAN_MARK_ADD | RK_FAN_MARK_MNTNS, RK_FAN_OPEN, "/proc/self/ns/mnt") == -1 && + errno == EINVAL); + errno = 0; + check("FAN_MARK_MNTNS on a file that is not a mount namespace gives EINVAL", + fan_mark(fd, RK_FAN_MARK_ADD | RK_FAN_MARK_MNTNS, RK_FAN_MNT_ATTACH, "/proc/self/ns/uts") == -1 && + errno == EINVAL); + check("FAN_MARK_MNTNS on /proc/self/ns/mnt", + fan_mark(fd, RK_FAN_MARK_ADD | RK_FAN_MARK_MNTNS, RK_FAN_MNT_ATTACH | RK_FAN_MNT_DETACH, + "/proc/self/ns/mnt") == 0); + snprintf(want, sizeof(want), "fanotify mnt_ns:%llu mflags:0 mask:3000000 ignored_mask:0", + stat("/proc/self/ns/mnt", &st) == 0 ? (unsigned long long)st.st_ino : 0ULL); + check("fdinfo of the group shows the mark on the mount namespace", + fdinfo_line(fd, "fanotify mnt_ns:", line, sizeof(line)) == 0 && strcmp(line, want) == 0); + mkdir("/fanm", 0755); + mkdir("/fanm/a", 0755); + mkdir("/fanm/b", 0755); + check("mount a tmpfs for the mount events", mount("none", "/fanm/a", "tmpfs", 0, NULL) == 0); + id = unique_mnt_id("/fanm/a"); + check("a mount gives FAN_MNT_ATTACH with the unique mount ID", + fan_mnt_event(fd, &got) == RK_FAN_MNT_ATTACH && id != 0 && got == id); + check("move the tmpfs", mount("/fanm/a", "/fanm/b", NULL, MS_MOVE, NULL) == 0); + check("a move gives FAN_MNT_ATTACH and FAN_MNT_DETACH in one event", + fan_mnt_event(fd, &got) == (RK_FAN_MNT_ATTACH | RK_FAN_MNT_DETACH) && got == id); + check("umount the tmpfs", umount("/fanm/b") == 0); + check("an unmount gives FAN_MNT_DETACH", fan_mnt_event(fd, &got) == RK_FAN_MNT_DETACH && got == id); + errno = 0; + check("a group of mount events has no other event", read(fd, buf, sizeof(buf)) == -1 && errno == EAGAIN); + close(fd); + rmdir("/fanm/a"); + rmdir("/fanm/b"); + rmdir("/fanm"); + + /* A user without CAP_SYS_ADMIN can make a group of mount events, and can mark only a mount + * namespace that its user namespace owns. */ + pid_t pid = fork(); + if (pid == 0) { + if (setresgid(65534, 65534, 65534) != 0 || setresuid(65534, 65534, 65534) != 0) + _exit(10); + int g = syscall(SYS_fanotify_init, RK_FAN_REPORT_MNT, 0); + if (g < 0) + _exit(11); + errno = 0; + if (fan_mark(g, RK_FAN_MARK_ADD | RK_FAN_MARK_MNTNS, RK_FAN_MNT_ATTACH, "/proc/self/ns/mnt") != -1 || + errno != EPERM) + _exit(12); + close(g); + if (unshare(CLONE_NEWUSER) != 0 || unshare(CLONE_NEWNS) != 0) + _exit(13); + g = syscall(SYS_fanotify_init, RK_FAN_REPORT_MNT, 0); + if (g < 0 || fan_mark(g, RK_FAN_MARK_ADD | RK_FAN_MARK_MNTNS, RK_FAN_MNT_ATTACH, "/proc/self/ns/mnt") != 0) + _exit(14); + _exit(0); + } + waitpid(pid, &status, 0); + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) + printf("init: the unprivileged child of the mount events exited with %d\n", status); + check("a user marks only a mount namespace that its own user namespace owns", + WIFEXITED(status) && WEXITSTATUS(status) == 0); +} + /* A thread that ends with SYS_exit a second after its start. */ static void *pidfd_late_thread(void *arg) { @@ -15347,6 +15499,7 @@ static void test_all(void) test_pidfd_leader_exit(); test_process_mrelease(); test_scm_pidfd(); + test_fanotify_mnt(); test_signalfd_poll(); test_seccomp(); test_ptrace(); @@ -16110,6 +16263,8 @@ int main(int argc, char **argv) test_pidfd_leader_exit(); test_process_mrelease(); } + if (argc > 1 && strcmp(argv[1], "fanotify") == 0) + test_fanotify_mnt(); if (argc > 1 && strcmp(argv[1], "bpf") == 0) test_bpf(); if (argc > 1 && strcmp(argv[1], "userns") == 0)