initial commit

This commit is contained in:
2025-04-03 12:27:50 +00:00
commit 936bc7c43d
13 changed files with 1316 additions and 0 deletions

9
fanotify/Cargo.toml Normal file
View File

@ -0,0 +1,9 @@
[package]
name = "fanotify"
version = "0.1.0"
edition = "2024"
[dependencies]
libc = "0.2"
thiserror = "2"
bitflags = "2"

5
fanotify/README Normal file
View File

@ -0,0 +1,5 @@
rust style fanotify wrapper
------------------------------------------
this crate wraps around calls and consts of fanotify(7) from libc crate.

116
fanotify/src/consts.rs Normal file
View File

@ -0,0 +1,116 @@
pub use libc::{
FAN_ACCESS, FAN_ACCESS_PERM, FAN_ALLOW, FAN_ATTRIB, FAN_AUDIT, FAN_CLASS_CONTENT,
FAN_CLASS_NOTIF, FAN_CLASS_PRE_CONTENT, FAN_CLOEXEC, FAN_CLOSE, FAN_CLOSE_NOWRITE,
FAN_CLOSE_WRITE, FAN_CREATE, FAN_DELETE, FAN_DELETE_SELF, FAN_DENY, FAN_ENABLE_AUDIT,
FAN_EPIDFD, FAN_EVENT_INFO_TYPE_DFID, FAN_EVENT_INFO_TYPE_DFID_NAME, FAN_EVENT_INFO_TYPE_ERROR,
FAN_EVENT_INFO_TYPE_FID, FAN_EVENT_INFO_TYPE_NEW_DFID_NAME, FAN_EVENT_INFO_TYPE_OLD_DFID_NAME,
FAN_EVENT_INFO_TYPE_PIDFD, FAN_EVENT_ON_CHILD, FAN_FS_ERROR, FAN_INFO, FAN_MARK_ADD,
FAN_MARK_DONT_FOLLOW, FAN_MARK_EVICTABLE, FAN_MARK_FILESYSTEM, FAN_MARK_FLUSH, FAN_MARK_IGNORE,
FAN_MARK_IGNORE_SURV, FAN_MARK_IGNORED_MASK, FAN_MARK_IGNORED_SURV_MODIFY, FAN_MARK_INODE,
FAN_MARK_MOUNT, FAN_MARK_ONLYDIR, FAN_MARK_REMOVE, FAN_MODIFY, FAN_MOVE, FAN_MOVE_SELF,
FAN_MOVED_FROM, FAN_MOVED_TO, FAN_NOFD, FAN_NONBLOCK, FAN_NOPIDFD, FAN_ONDIR, FAN_OPEN,
FAN_OPEN_EXEC, FAN_OPEN_EXEC_PERM, FAN_OPEN_PERM, FAN_Q_OVERFLOW, FAN_RENAME,
FAN_REPORT_DFID_NAME, FAN_REPORT_DFID_NAME_TARGET, FAN_REPORT_DIR_FID, FAN_REPORT_FID,
FAN_REPORT_NAME, FAN_REPORT_PIDFD, FAN_REPORT_TARGET_FID, FAN_REPORT_TID,
FAN_RESPONSE_INFO_AUDIT_RULE, FAN_RESPONSE_INFO_NONE, FAN_UNLIMITED_MARKS, FAN_UNLIMITED_QUEUE,
FANOTIFY_METADATA_VERSION,
};
pub use libc::{
O_APPEND, O_CLOEXEC, O_DSYNC, O_LARGEFILE, O_NOATIME, O_NONBLOCK, O_RDONLY, O_RDWR, O_SYNC,
O_WRONLY,
};
// NOTE: the definitions is handwritten in 2025-04-03, on Debian trixie(testing) 6.12.20-1 x86_64
// it may need updating for future kernel updates, and pls update this comment for maintainence
fa_bitflags! {
pub struct InitFlags: u32 {
/*
one of three classes:
- notification-only: get notified when file or directory is accessed
- content-access: get notified and also check permission when content is ready. usually used by security softwares.
- pre-content-access: get notified and also check permission BEFORE content is ready. usually used by storage managers.
*/
FAN_CLASS_NOTIF;
FAN_CLASS_CONTENT;
FAN_CLASS_PRE_CONTENT;
// additional flags
FAN_CLOEXEC;
FAN_NONBLOCK;
FAN_UNLIMITED_QUEUE;
FAN_UNLIMITED_MARKS;
FAN_ENABLE_AUDIT; // Linux 4.15
FAN_REPORT_TID; // Linux 4.20
FAN_REPORT_FID; // Linux 5.1
FAN_REPORT_DIR_FID; // Linux 5.9
FAN_REPORT_NAME; // Linux 5.9
FAN_REPORT_DFID_NAME; // Linux 5.9, FAN_REPORT_DIR_FID|FAN_REPORT_NAME
FAN_REPORT_TARGET_FID; // Linux 5.17 / 5.15.154 / 5.10.220
FAN_REPORT_DFID_NAME_TARGET; // Linux 5.17 / 5.15.154 / 5.10.220, FAN_REPORT_DFID_NAME|FAN_REPORT_FID|FAN_REPORT_TARGET_FID
FAN_REPORT_PIDFD; // Linux 5.15 / 5.10.220
}
pub struct EventFFlags: ~u32 {
O_RDONLY;
O_WRONLY;
O_RDWR;
O_LARGEFILE; // file size limit 2G+
O_CLOEXEC; // Linux 3.18
O_APPEND;
O_DSYNC;
O_NOATIME;
O_NONBLOCK;
O_SYNC;
}
pub struct MarkFlags: u32 {
// operation, choose exactly one
FAN_MARK_ADD;
FAN_MARK_REMOVE;
FAN_MARK_FLUSH;
// additional flags
FAN_MARK_DONT_FOLLOW;
FAN_MARK_ONLYDIR;
FAN_MARK_MOUNT;
FAN_MARK_FILESYSTEM; // Linux 4.20
FAN_MARK_IGNORED_MASK;
FAN_MARK_IGNORE; // Linux 6.0 / 5.15.154 / 5.10.220
FAN_MARK_IGNORED_SURV_MODIFY;
FAN_MARK_IGNORE_SURV; // Linux 6.0 / 5.15.154 / 5.10.220, FAN_MARK_IGNORE|FAN_MARK_IGNORED_SURV_MODIFY
FAN_MARK_EVICTABLE; // Linux 5.19 / 5.15.154 / 5.10.220
}
pub struct MaskFlags: u64 {
FAN_ACCESS;
FAN_MODIFY;
FAN_CLOSE_WRITE;
FAN_CLOSE_NOWRITE;
FAN_CLOSE; // FAN_CLOSE_WRITE|FAN_CLOSE_NOWRITE
FAN_OPEN;
FAN_OPEN_EXEC; // Linux 5.0
FAN_ATTRIB; // Linux 5.1
FAN_CREATE; // Linux 5.1
FAN_DELETE; // Linux 5.1
FAN_DELETE_SELF; // Linux 5.1
FAN_FS_ERROR; // Linux 5.16 / 5.15.154 / 5.10.220
FAN_MOVED_FROM; // Linux 5.1
FAN_MOVED_TO; // Linux 5.1
FAN_MOVE; // Linux 5.1, FAN_MOVED_FROM|FAN_MOVED_TO
FAN_RENAME; // Linux 5.17 / 5.15.154 / 5.10.220
FAN_MOVE_SELF; // Linux 5.1
// Permissions, need FAN_CLASS_CONTENT or FAN_CLASS_PRE_CONTENT on init
FAN_OPEN_PERM;
FAN_ACCESS_PERM;
FAN_OPEN_EXEC_PERM; // Linux 5.0
// Flags
FAN_ONDIR; // enable events on directories
FAN_EVENT_ON_CHILD; // enable events on direct
}
}

72
fanotify/src/error.rs Normal file
View File

@ -0,0 +1,72 @@
use std::{
error::Error,
fmt::{Debug, Display},
};
pub struct Errno {
raw_errno: i32,
}
impl Errno {
pub fn new(errno: i32) -> Self {
Self { raw_errno: errno }
}
pub fn errno() -> Self {
Self::new(unsafe { *libc::__errno_location() } )
}
}
impl<E: Into<i32>> From<E> for Errno {
fn from(value: E) -> Self {
Self {
raw_errno: value.into(),
}
}
}
impl Display for Errno {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let str = unsafe {
let cstr = libc::strerror(self.raw_errno);
let len = libc::strlen(cstr);
let mut buffer = Vec::with_capacity(len);
buffer.set_len(len);
libc::strncpy(buffer.as_mut_ptr() as *mut i8, cstr, len);
String::from_utf8(buffer)
};
let result = if let Ok(str) = str {
f.write_str(&str)
} else {
f.write_str(&format!("Unknown error {}", self.raw_errno))
};
result
}
}
impl Debug for Errno {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Errno")
.field("errno", &self.raw_errno)
.finish()
}
}
impl Error for Errno {}
#[cfg(test)]
mod test {
use super::Errno;
#[test]
fn test_errno() {
assert_eq!(
Errno::new(libc::ENOENT).to_string(),
"No such file or directory"
);
assert_eq!(Errno::new(0).to_string(), "Success");
}
}

188
fanotify/src/fanotify.rs Normal file
View File

@ -0,0 +1,188 @@
use std::{
ffi::CString,
mem::MaybeUninit,
os::fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd},
ptr::null,
};
use crate::{
consts::{EventFFlags, InitFlags, MarkFlags, MaskFlags},
error::Errno,
};
pub struct Fanotify {
fd: OwnedFd,
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("cannot convert path into c string: {0}")]
PathError(#[from] std::ffi::NulError),
#[error("fanotify returned errno: {0}")]
FanotifyError(#[from] Errno),
}
impl Fanotify {
pub fn init(init_flags: InitFlags, event_fd_flags: EventFFlags) -> Result<Self, Error> {
Self::try_init(init_flags, event_fd_flags)
}
pub fn try_init(init_flags: InitFlags, event_fd_flags: EventFFlags) -> Result<Self, Error> {
let fd = unsafe {
let ret = libc::fanotify_init(init_flags.bits(), event_fd_flags.bits());
if ret == -1 {
return Err(Error::FanotifyError(Errno::errno()));
}
OwnedFd::from_raw_fd(ret)
};
Ok(Self { fd })
}
pub fn mark<P: Into<String>>(
&self,
operation: MarkFlags,
mask: MaskFlags,
dirfd: Option<BorrowedFd>,
path: Option<P>,
) -> Result<(), Error> {
let dirfd = match dirfd {
Some(fd) => fd.as_raw_fd(),
None => libc::AT_FDCWD,
};
let result = unsafe {
// hold it here to prevent drop. don't merge two matches
if let Some(path) = path {
let path: String = path.into();
let cstr = CString::new(path)?;
libc::fanotify_mark(
self.fd.as_raw_fd(),
operation.bits(),
mask.bits(),
dirfd,
cstr.as_ptr(),
)
} else {
libc::fanotify_mark(
self.fd.as_raw_fd(),
operation.bits(),
mask.bits(),
dirfd,
null(),
)
}
};
if result != 0 {
return Err(Error::FanotifyError(Errno::errno()));
}
Ok(())
}
pub fn read_events(&self) -> Result<Vec<Event>, Error> {
const BUFFER_SIZE: usize = 4096;
const EVENT_SIZE: usize = size_of::<libc::fanotify_event_metadata>();
let mut buffer = [0u8; BUFFER_SIZE];
let mut result = Vec::new();
unsafe {
let nread = libc::read(self.fd.as_raw_fd(), buffer.as_mut_ptr().cast(), BUFFER_SIZE);
if nread < 0 {
return Err(Error::FanotifyError(Errno::new(nread as i32)));
}
let nread = nread as usize;
let mut offset = 0;
while offset + EVENT_SIZE <= nread {
let mut event: MaybeUninit<libc::fanotify_event_metadata> = MaybeUninit::uninit();
std::ptr::copy(
buffer.as_ptr().add(offset),
event.as_mut_ptr().cast(),
EVENT_SIZE,
);
result.push(Event(event.assume_init()));
offset += EVENT_SIZE;
}
}
Ok(result)
}
pub fn write_response(&self, response: Response) -> Result<(), Errno> {
let n = unsafe {
libc::write(
self.fd.as_raw_fd(),
(&response.inner as *const libc::fanotify_response).cast(),
size_of::<libc::fanotify_response>(),
)
};
if n == -1 {
return Err(Errno::errno());
}
Ok(())
}
}
pub struct Event(pub libc::fanotify_event_metadata);
impl Event {
// compatible to nix::sys::fanotify::FanotifyEvent
pub fn metadata_version(&self) -> u8 {
self.0.vers
}
pub fn check_metadata_version(&self) -> bool {
self.0.vers == libc::FANOTIFY_METADATA_VERSION
}
pub fn fd(&self) -> Option<BorrowedFd> {
if self.0.fd == libc::FAN_NOFD {
None
} else {
Some(unsafe { BorrowedFd::borrow_raw(self.0.fd) })
}
}
pub fn pid(&self) -> i32 {
self.0.pid
}
pub fn mask(&self) -> MaskFlags {
MaskFlags::from_bits_truncate(self.0.mask)
}
// sometimes we don't want to close the fd immediately, so we forget about it, store it somewhere, and drop it later
// it is safe to just call this method without store it in variable, it will be dropped immediately due to the nature of rust
pub fn forget_fd(&mut self) -> OwnedFd {
let fd = self.0.fd;
self.0.fd = libc::FAN_NOFD;
unsafe { OwnedFd::from_raw_fd(fd) }
}
}
impl Drop for Event {
fn drop(&mut self) {
if self.0.fd == libc::FAN_NOFD {
return;
}
let e = unsafe { libc::close(self.0.fd) };
if !std::thread::panicking() && e == libc::EBADF {
panic!("Closing an invalid file descriptor!");
};
}
}
pub struct Response {
inner: libc::fanotify_response,
}
impl Response {
pub const FAN_ALLOW: u32 = libc::FAN_ALLOW;
pub const FAN_DENY: u32 = libc::FAN_DENY;
pub fn new(fd: BorrowedFd, response: u32) -> Self {
Self {
inner: libc::fanotify_response {
fd: fd.as_raw_fd(),
response,
},
}
}
}

10
fanotify/src/lib.rs Normal file
View File

@ -0,0 +1,10 @@
#[macro_use]
mod macros;
pub mod error;
pub mod fanotify;
pub mod consts;
pub use bitflags;
pub use fanotify::{Fanotify, Error, Response, Response as FanotifyResponse, Event};

75
fanotify/src/macros.rs Normal file
View File

@ -0,0 +1,75 @@
/*
Copied from nix crate and modified to allow multiple patterns in a single block.
This simplifies flag groups definition.
*/
macro_rules! fa_bitflags {
// modified: accept a list of pub struct, force cast to T
(
// first
$(#[$outer:meta])*
pub struct $BitFlags:ident: ~$T:ty {
$(
$(#[$inner:ident $($args:tt)*])*
$Flag:ident;
)+
}
// modified part: match rest
$($t:tt)*
) => {
::bitflags::bitflags! {
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[repr(transparent)]
$(#[$outer])*
pub struct $BitFlags: $T {
$(
$(#[$inner $($args)*])*
// always cast to $T
const $Flag = libc::$Flag as $T;
)+
}
}
// modified part: recursively handle rest structs
fa_bitflags! {
$($t)*
}
};
// from nix: input: accept a list of pub struct
(
// first
$(#[$outer:meta])*
pub struct $BitFlags:ident: $T:ty {
$(
$(#[$inner:ident $($args:tt)*])*
$Flag:ident $(as $cast:ty)*;
)+
}
// modified part: match rest
$($t:tt)*
) => {
// generate bitflags struct
::bitflags::bitflags! {
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[repr(transparent)]
$(#[$outer])*
pub struct $BitFlags: $T {
$(
$(#[$inner $($args)*])*
const $Flag = libc::$Flag $(as $cast)*;
)+
}
}
// modified part: recursively handle rest structs
fa_bitflags! {
$($t)*
}
};
// modified part: empty block don't produce anything.
() => {}
}