diff --git a/Cargo.toml b/Cargo.toml index dbcb99e277..aa365449e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ aio = ["pin-utils"] dir = ["fs"] env = [] event = [] +fanotify = [] feature = [] fs = [] hostname = [] diff --git a/changelog/2194.added.md b/changelog/2194.added.md new file mode 100644 index 0000000000..18e39b1ccd --- /dev/null +++ b/changelog/2194.added.md @@ -0,0 +1 @@ +Added new fanotify API: wrappers for `fanotify_init` and `fanotify_mark` diff --git a/src/fcntl.rs b/src/fcntl.rs index 7910cc9b7d..d327c4b22a 100644 --- a/src/fcntl.rs +++ b/src/fcntl.rs @@ -64,10 +64,14 @@ libc_bitflags! { } } -#[cfg(any(feature = "fs", feature = "term"))] +#[cfg(any( + feature = "fs", + feature = "term", + all(feature = "fanotify", target_os = "linux") +))] libc_bitflags!( /// Configuration options for opened files. - #[cfg_attr(docsrs, doc(cfg(any(feature = "fs", feature = "term"))))] + #[cfg_attr(docsrs, doc(cfg(any(feature = "fs", feature = "term", all(feature = "fanotify", target_os = "linux")))))] pub struct OFlag: c_int { /// Mask for the access mode of the file. O_ACCMODE; @@ -194,7 +198,8 @@ libc_bitflags!( /// Computes the raw fd consumed by a function of the form `*at`. #[cfg(any( all(feature = "fs", not(target_os = "redox")), - all(feature = "process", any(target_os = "android", target_os = "linux")) + all(feature = "process", any(target_os = "android", target_os = "linux")), + all(feature = "fanotify", target_os = "linux") ))] pub(crate) fn at_rawfd(fd: Option) -> raw::c_int { fd.unwrap_or(libc::AT_FDCWD) diff --git a/src/lib.rs b/src/lib.rs index 467b839d56..62220a822f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ //! * `dir` - Stuff relating to directory iteration //! * `env` - Manipulate environment variables //! * `event` - Event-driven APIs, like `kqueue` and `epoll` +//! * `fanotify` - Linux's `fanotify` filesystem events monitoring API //! * `feature` - Query characteristics of the OS at runtime //! * `fs` - File system functionality //! * `hostname` - Get and set the system's hostname @@ -53,6 +54,7 @@ feature = "dir", feature = "env", feature = "event", + feature = "fanotify", feature = "feature", feature = "fs", feature = "hostname", diff --git a/src/sys/fanotify.rs b/src/sys/fanotify.rs new file mode 100644 index 0000000000..a089d78df8 --- /dev/null +++ b/src/sys/fanotify.rs @@ -0,0 +1,412 @@ +//! Monitoring API for filesystem events. +//! +//! Fanotify is a Linux-only API to monitor filesystems events. +//! +//! Additional capabilities compared to the `inotify` API include the ability to +//! monitor all of the objects in a mounted filesystem, the ability to make +//! access permission decisions, and the possibility to read or modify files +//! before access by other applications. +//! +//! For more documentation, please read +//! [fanotify(7)](https://man7.org/linux/man-pages/man7/fanotify.7.html). + +use crate::{NixPath, Result}; +use crate::errno::Errno; +use crate::fcntl::{OFlag, at_rawfd}; +use crate::unistd::{close, read, write}; +use std::marker::PhantomData; +use std::mem::{MaybeUninit, size_of}; +use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd}; +use std::ptr; + +libc_bitflags! { + /// Mask for defining which events shall be listened with + /// [`fanotify_mark`](fn.fanotify_mark.html) and for querying notifications. + pub struct MaskFlags: u64 { + /// File was accessed. + FAN_ACCESS; + /// File was modified. + FAN_MODIFY; + /// Metadata has changed. Since Linux 5.1. + FAN_ATTRIB; + /// Writtable file was closed. + FAN_CLOSE_WRITE; + /// Unwrittable file was closed. + FAN_CLOSE_NOWRITE; + /// File was opened. + FAN_OPEN; + /// File was moved from X. Since Linux 5.1. + FAN_MOVED_FROM; + /// File was moved to Y. Since Linux 5.1. + FAN_MOVED_TO; + /// Subfile was created. Since Linux 5.1. + FAN_CREATE; + /// Subfile was deleted. Since Linux 5.1. + FAN_DELETE; + /// Self was deleted. Since Linux 5.1. + FAN_DELETE_SELF; + /// Self was moved. Since Linux 5.1. + FAN_MOVE_SELF; + /// File was opened for execution. Since Linux 5.0. + FAN_OPEN_EXEC; + + /// Event queue overflowed. + FAN_Q_OVERFLOW; + /// Filesystem error. Since Linux 5.16. + FAN_FS_ERROR; + + /// Permission to open file was requested. + FAN_OPEN_PERM; + /// Permission to access file was requested. + FAN_ACCESS_PERM; + /// Permission to open file for execution was requested. Since Linux + /// 5.0. + FAN_OPEN_EXEC_PERM; + + /// Interested in child events. + FAN_EVENT_ON_CHILD; + + /// File was renamed. Since Linux 5.17. + FAN_RENAME; + + /// Event occurred against dir. + FAN_ONDIR; + + /// Combination of `FAN_CLOSE_WRITE` and `FAN_CLOSE_NOWRITE`. + FAN_CLOSE; + /// Combination of `FAN_MOVED_FROM` and `FAN_MOVED_TO`. + FAN_MOVE; + } +} + +libc_bitflags! { + /// Configuration options for [`fanotify_init`](fn.fanotify_init.html). + pub struct InitFlags: libc::c_uint { + /// Close-on-exec flag set on the file descriptor. + FAN_CLOEXEC; + /// Nonblocking flag set on the file descriptor. + FAN_NONBLOCK; + + /// Receipt of events notifications. + FAN_CLASS_NOTIF; + /// Receipt of events for permission decisions, after they contain final + /// data. + FAN_CLASS_CONTENT; + /// Receipt of events for permission decisions, before they contain + /// final data. + FAN_CLASS_PRE_CONTENT; + + /// Remove the limit of 16384 events for the event queue. + FAN_UNLIMITED_QUEUE; + /// Remove the limit of 8192 marks. + FAN_UNLIMITED_MARKS; + + /// Make `FanotifyEvent::pid` return pidfd. Since Linux 5.15. + FAN_REPORT_PIDFD; + /// Make `FanotifyEvent::pid` return thread id. Since Linux 4.20. + FAN_REPORT_TID; + } +} + +libc_bitflags! { + /// File status flags for fanotify events file descriptors. + pub struct EventFFlags: libc::c_uint { + /// Read only access. + O_RDONLY as libc::c_uint; + /// Write only access. + O_WRONLY as libc::c_uint; + /// Read and write access. + O_RDWR as libc::c_uint; + /// Support for files exceeded 2 GB. + O_LARGEFILE as libc::c_uint; + /// Close-on-exec flag for the file descriptor. Since Linux 3.18. + O_CLOEXEC as libc::c_uint; + /// Append mode for the file descriptor. + O_APPEND as libc::c_uint; + /// Synchronized I/O data integrity completion. + O_DSYNC as libc::c_uint; + /// No file last access time update. + O_NOATIME as libc::c_uint; + /// Nonblocking mode for the file descriptor. + O_NONBLOCK as libc::c_uint; + /// Synchronized I/O file integrity completion. + O_SYNC as libc::c_uint; + } +} + +impl TryFrom for EventFFlags { + type Error = Errno; + + fn try_from(o_flag: OFlag) -> Result { + EventFFlags::from_bits(o_flag.bits() as u32).ok_or(Errno::EINVAL) + } +} + +impl From for OFlag { + fn from(event_f_flags: EventFFlags) -> Self { + OFlag::from_bits_retain(event_f_flags.bits() as i32) + } +} + +libc_bitflags! { + /// Configuration options for [`fanotify_mark`](fn.fanotify_mark.html). + pub struct MarkFlags: libc::c_uint { + /// Add the events to the marks. + FAN_MARK_ADD; + /// Remove the events to the marks. + FAN_MARK_REMOVE; + /// Don't follow symlinks, mark them. + FAN_MARK_DONT_FOLLOW; + /// Raise an error if filesystem to be marked is not a directory. + FAN_MARK_ONLYDIR; + /// Events added to or removed from the marks. + FAN_MARK_IGNORED_MASK; + /// Ignore mask shall survive modify events. + FAN_MARK_IGNORED_SURV_MODIFY; + /// Remove all marks. + FAN_MARK_FLUSH; + /// Do not pin inode object in the inode cache. Since Linux 5.19. + FAN_MARK_EVICTABLE; + /// Events added to or removed from the marks. Since Linux 6.0. + FAN_MARK_IGNORE; + + /// Default flag. + FAN_MARK_INODE; + /// Mark the mount specified by pathname. + FAN_MARK_MOUNT; + /// Mark the filesystem specified by pathname. Since Linux 4.20. + FAN_MARK_FILESYSTEM; + + /// Combination of `FAN_MARK_IGNORE` and `FAN_MARK_IGNORED_SURV_MODIFY`. + FAN_MARK_IGNORE_SURV; + } +} + +/// Compile version number of fanotify API. +pub const FANOTIFY_METADATA_VERSION: u8 = libc::FANOTIFY_METADATA_VERSION; + +/// Abstract over `libc::fanotify_event_metadata`, which represents an event +/// received via `Fanotify::read_events`. +// Is not Clone due to fd field, to avoid use-after-close scenarios. +#[derive(Debug, Eq, Hash, PartialEq)] +#[repr(transparent)] +#[allow(missing_copy_implementations)] +pub struct FanotifyEvent(libc::fanotify_event_metadata); + +impl FanotifyEvent { + /// Version number for the structure. It must be compared to + /// `FANOTIFY_METADATA_VERSION` to verify compile version and runtime + /// version does match. It can be done with the + /// `FanotifyEvent::check_version` method. + pub fn version(&self) -> u8 { + self.0.vers + } + + /// Checks that compile fanotify API version is equal to the version of the + /// event. + pub fn check_version(&self) -> bool { + self.version() == FANOTIFY_METADATA_VERSION + } + + /// Mask flags of the events. + pub fn mask(&self) -> MaskFlags { + MaskFlags::from_bits_truncate(self.0.mask) + } + + /// The file descriptor of the event. If the value is `None` when reading + /// from the fanotify group, this event is to notify that a group queue + /// overflow occured. + pub fn fd(&self) -> Option { + if self.0.fd == libc::FAN_NOFD { + None + } else { + // SAFETY: self.0.fd will be opened for the lifetime of `Self`, + // which is longer than the lifetime of the returned BorrowedFd, so + // it is safe. + Some(unsafe { BorrowedFd::borrow_raw(self.0.fd) }) + } + } + + /// PID of the process that caused the event. TID in case flag + /// `FAN_REPORT_TID` was set at group initialization. + pub fn pid(&self) -> i32 { + self.0.pid + } +} + +impl Drop for FanotifyEvent { + fn drop(&mut self) { + let e = close(self.0.fd); + if !std::thread::panicking() && e == Err(Errno::EBADF) { + panic!("Closing an invalid file descriptor!"); + }; + } +} + +/// Abstraction over the structure to be sent to allow or deny a given event. +#[derive(Debug)] +#[repr(transparent)] +pub struct FanotifyResponse<'a> { + inner: libc::fanotify_response, + _borrowed_fd: PhantomData>, +} + +impl<'a> FanotifyResponse<'a> { + /// Create a new response. + pub fn new(fd: BorrowedFd<'a>, response: Response) -> Self { + Self { + inner: libc::fanotify_response { + fd: fd.as_raw_fd(), + response: response.bits(), + }, + _borrowed_fd: PhantomData, + } + } +} + +libc_bitflags! { + /// Response to be wrapped in `FanotifyResponse` and sent to the `Fanotify` + /// group to allow or deny an event. + pub struct Response: u32 { + /// Allow the event. + FAN_ALLOW; + /// Deny the event. + FAN_DENY; + } +} + +/// A fanotify group. This is also a file descriptor that can feed to other +/// interfaces consuming file descriptors. +#[derive(Debug)] +pub struct Fanotify { + fd: OwnedFd, +} + +impl Fanotify { + /// Initialize a new fanotify group. + /// + /// Returns a Result containing a Fanotify instance. + /// + /// For more information, see [fanotify_init(2)](https://man7.org/linux/man-pages/man7/fanotify_init.2.html). + pub fn init(flags: InitFlags, event_f_flags: EventFFlags) -> Result { + let res = Errno::result(unsafe { + libc::fanotify_init(flags.bits(), event_f_flags.bits()) + }); + res.map(|fd| Fanotify { fd: unsafe { OwnedFd::from_raw_fd(fd) }}) + } + + /// Add, remove, or modify an fanotify mark on a filesystem object. + /// If `dirfd` is `None`, `AT_FDCWD` is used. + /// + /// Returns a Result containing either `()` on success or errno otherwise. + /// + /// For more information, see [fanotify_mark(2)](https://man7.org/linux/man-pages/man7/fanotify_mark.2.html). + pub fn mark( + &self, + flags: MarkFlags, + mask: MaskFlags, + dirfd: Option, + path: Option<&P>, + ) -> Result<()> { + fn with_opt_nix_path(p: Option<&P>, f: F) -> Result + where + P: ?Sized + NixPath, + F: FnOnce(*const libc::c_char) -> T, + { + match p { + Some(path) => path.with_nix_path(|p_str| f(p_str.as_ptr())), + None => Ok(f(std::ptr::null())), + } + } + + let res = with_opt_nix_path(path, |p| unsafe { + libc::fanotify_mark( + self.fd.as_raw_fd(), + flags.bits(), + mask.bits(), + at_rawfd(dirfd), + p, + ) + })?; + + Errno::result(res).map(|_| ()) + } + + /// Read incoming events from the fanotify group. + /// + /// Returns a Result containing either a `Vec` of events on success or errno + /// otherwise. + /// + /// # Errors + /// + /// Possible errors can be those that are explicitly listed in + /// [fanotify(2)](https://man7.org/linux/man-pages/man7/fanotify.2.html) in + /// addition to the possible errors caused by `read` call. + /// In particular, `EAGAIN` is returned when no event is available on a + /// group that has been initialized with the flag `InitFlags::FAN_NONBLOCK`, + /// thus making this method nonblocking. + pub fn read_events(&self) -> Result> { + let metadata_size = size_of::(); + const BUFSIZ: usize = 4096; + let mut buffer = [0u8; BUFSIZ]; + let mut events = Vec::new(); + let mut offset = 0; + + let nread = read(self.fd.as_raw_fd(), &mut buffer)?; + + while (nread - offset) >= metadata_size { + let metadata = unsafe { + let mut metadata = + MaybeUninit::::uninit(); + ptr::copy_nonoverlapping( + buffer.as_ptr().add(offset), + metadata.as_mut_ptr().cast(), + (BUFSIZ - offset).min(metadata_size), + ); + metadata.assume_init() + }; + + events.push(FanotifyEvent(metadata)); + offset += metadata.event_len as usize; + } + + Ok(events) + } + + /// Write an event response on the fanotify group. + /// + /// Returns a Result containing either `()` on success or errno otherwise. + /// + /// # Errors + /// + /// Possible errors can be those that are explicitly listed in + /// [fanotify(2)](https://man7.org/linux/man-pages/man7/fanotify.2.html) in + /// addition to the possible errors caused by `write` call. + /// In particular, `EAGAIN` or `EWOULDBLOCK` is returned when no event is + /// available on a group that has been initialized with the flag + /// `InitFlags::FAN_NONBLOCK`, thus making this method nonblocking. + pub fn write_response(&self, response: FanotifyResponse) -> Result<()> { + write( + self.fd.as_fd(), + unsafe { + std::slice::from_raw_parts( + (&response.inner as *const libc::fanotify_response).cast(), + size_of::(), + ) + }, + )?; + Ok(()) + } +} + +impl FromRawFd for Fanotify { + unsafe fn from_raw_fd(fd: RawFd) -> Self { + Fanotify { fd: unsafe { OwnedFd::from_raw_fd(fd) }} + } +} + +impl AsFd for Fanotify { + fn as_fd(&'_ self) -> BorrowedFd<'_> { + self.fd.as_fd() + } +} diff --git a/src/sys/mod.rs b/src/sys/mod.rs index 276d4760fd..7c5aa0692a 100644 --- a/src/sys/mod.rs +++ b/src/sys/mod.rs @@ -30,6 +30,12 @@ feature! { pub mod eventfd; } +#[cfg(target_os = "linux")] +feature! { + #![feature = "fanotify"] + pub mod fanotify; +} + #[cfg(any( target_os = "android", target_os = "dragonfly", diff --git a/test/sys/mod.rs b/test/sys/mod.rs index ae4ff953fe..43bb0bfaae 100644 --- a/test/sys/mod.rs +++ b/test/sys/mod.rs @@ -43,6 +43,8 @@ mod test_wait; #[cfg(any(target_os = "android", target_os = "linux"))] mod test_epoll; #[cfg(target_os = "linux")] +mod test_fanotify; +#[cfg(target_os = "linux")] mod test_inotify; mod test_pthread; #[cfg(any( diff --git a/test/sys/test_fanotify.rs b/test/sys/test_fanotify.rs new file mode 100644 index 0000000000..9fd23395ae --- /dev/null +++ b/test/sys/test_fanotify.rs @@ -0,0 +1,154 @@ +use crate::*; +use nix::sys::fanotify::{ + EventFFlags, Fanotify, FanotifyResponse, InitFlags, MarkFlags, MaskFlags, + Response, +}; +use std::fs::{read_link, File, OpenOptions}; +use std::io::ErrorKind; +use std::io::{Read, Write}; +use std::os::fd::AsRawFd; +use std::thread; + +#[test] +/// Run fanotify tests sequentially to avoid tmp files races +pub fn test_fanotify() { + require_capability!("test_fanotify", CAP_SYS_ADMIN); + + test_fanotify_notifications(); + test_fanotify_responses(); +} + +fn test_fanotify_notifications() { + let group = + Fanotify::init(InitFlags::FAN_CLASS_NOTIF, EventFFlags::O_RDONLY) + .unwrap(); + let tempdir = tempfile::tempdir().unwrap(); + + group + .mark( + MarkFlags::FAN_MARK_ADD | MarkFlags::FAN_MARK_MOUNT, + MaskFlags::FAN_OPEN | MaskFlags::FAN_MODIFY | MaskFlags::FAN_CLOSE, + None, + Some(tempdir.path()), + ) + .unwrap(); + + let tempfile = tempdir.path().join("test"); + + // create test file + File::create(&tempfile).unwrap(); + + let [event] = &group.read_events().unwrap()[..] else { + panic!("should have read exactly one event"); + }; + assert!(event.check_version()); + assert_eq!( + event.mask(), + MaskFlags::FAN_OPEN | MaskFlags::FAN_CLOSE_WRITE + ); + let fd_opt = event.fd(); + let fd = fd_opt.as_ref().unwrap(); + let path = read_link(format!("/proc/self/fd/{}", fd.as_raw_fd())).unwrap(); + assert_eq!(path, tempfile); + + // modify test file + { + let mut f = OpenOptions::new().write(true).open(&tempfile).unwrap(); + f.write_all(b"hello").unwrap(); + } + + let [event] = &group.read_events().unwrap()[..] else { + panic!("should have read exactly one event"); + }; + assert!(event.check_version()); + assert_eq!( + event.mask(), + MaskFlags::FAN_OPEN + | MaskFlags::FAN_MODIFY + | MaskFlags::FAN_CLOSE_WRITE + ); + let fd_opt = event.fd(); + let fd = fd_opt.as_ref().unwrap(); + let path = read_link(format!("/proc/self/fd/{}", fd.as_raw_fd())).unwrap(); + assert_eq!(path, tempfile); + + // read test file + { + let mut f = File::open(&tempfile).unwrap(); + let mut s = String::new(); + f.read_to_string(&mut s).unwrap(); + } + + let [event] = &group.read_events().unwrap()[..] else { + panic!("should have read exactly one event"); + }; + assert!(event.check_version()); + assert_eq!( + event.mask(), + MaskFlags::FAN_OPEN | MaskFlags::FAN_CLOSE_NOWRITE + ); + let fd_opt = event.fd(); + let fd = fd_opt.as_ref().unwrap(); + let path = read_link(format!("/proc/self/fd/{}", fd.as_raw_fd())).unwrap(); + assert_eq!(path, tempfile); +} + +fn test_fanotify_responses() { + let group = + Fanotify::init(InitFlags::FAN_CLASS_CONTENT, EventFFlags::O_RDONLY) + .unwrap(); + let tempdir = tempfile::tempdir().unwrap(); + + group + .mark( + MarkFlags::FAN_MARK_ADD | MarkFlags::FAN_MARK_MOUNT, + MaskFlags::FAN_OPEN_PERM, + None, + Some(tempdir.path()), + ) + .unwrap(); + + let tempfile = tempdir.path().join("test"); + let tempfname = tempfile.clone(); + + let file_thread = thread::spawn(move || { + // first try, should fail + let Err(err) = File::create(&tempfile) else { + panic!("first open is denied, should return error"); + }; + assert_eq!(err.kind(), ErrorKind::PermissionDenied); + + // second try, should succeed + File::create(&tempfile).unwrap(); + }); + + // Deny the first open try + let [event] = &group.read_events().unwrap()[..] else { + panic!("should have read exactly one event"); + }; + assert!(event.check_version()); + assert_eq!(event.mask(), MaskFlags::FAN_OPEN_PERM); + let fd_opt = event.fd(); + let fd = fd_opt.as_ref().unwrap(); + let path = read_link(format!("/proc/self/fd/{}", fd.as_raw_fd())).unwrap(); + assert_eq!(path, tempfname); + group + .write_response(FanotifyResponse::new(*fd, Response::FAN_DENY)) + .unwrap(); + + //// Allow the second open try + let [event] = &group.read_events().unwrap()[..] else { + panic!("should have read exactly one event"); + }; + assert!(event.check_version()); + assert_eq!(event.mask(), MaskFlags::FAN_OPEN_PERM); + let fd_opt = event.fd(); + let fd = fd_opt.as_ref().unwrap(); + let path = read_link(format!("/proc/self/fd/{}", fd.as_raw_fd())).unwrap(); + assert_eq!(path, tempfname); + group + .write_response(FanotifyResponse::new(*fd, Response::FAN_ALLOW)) + .unwrap(); + + file_thread.join().unwrap(); +}