initial commit

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

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/target
/data
.idea
.vscode

446
Cargo.lock generated Normal file
View File

@ -0,0 +1,446 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "anstream"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anstyle-parse"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
dependencies = [
"anstyle",
"once_cell",
"windows-sys",
]
[[package]]
name = "bitflags"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "clap"
version = "4.5.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "colorchoice"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "env_filter"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"jiff",
"log",
]
[[package]]
name = "fanotify"
version = "0.1.0"
dependencies = [
"bitflags",
"libc",
"thiserror",
]
[[package]]
name = "fanotify-demo"
version = "0.1.0"
dependencies = [
"clap",
"env_logger",
"fanotify",
"log",
"nix",
"thiserror",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "jiff"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c102670231191d07d37a35af3eb77f1f0dbf7a71be51a962dcd57ea607be7260"
dependencies = [
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde",
]
[[package]]
name = "jiff-static"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cdde31a9d349f1b1f51a0b3714a5940ac022976f4b49485fc04be052b183b4c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "libc"
version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "nix"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "portable-atomic"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
[[package]]
name = "portable-atomic-util"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
dependencies = [
"portable-atomic",
]
[[package]]
name = "proc-macro2"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"

6
Cargo.toml Normal file
View File

@ -0,0 +1,6 @@
[workspace]
resolver = "3"
members = [
"fanotify",
"fanotify-demo",
]

14
fanotify-demo/Cargo.toml Normal file
View File

@ -0,0 +1,14 @@
[package]
name = "fanotify-demo"
version = "0.1.0"
edition = "2024"
[dependencies]
nix = { version = "0.29", features = ["signal", "user"] }
fanotify = { path = "../fanotify" }
thiserror = "2"
clap = { version = "4", features = ["derive"] }
log = "0.4"
env_logger = "0.11"

265
fanotify-demo/src/main.rs Normal file
View File

@ -0,0 +1,265 @@
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
use std::ops::BitOr;
use std::os::fd::{AsFd, AsRawFd, OwnedFd};
use ::fanotify::{consts::*, *};
use clap::Parser;
use log::*;
use nix::sys::signal::{SaFlags, SigAction, SigSet, Signal};
#[derive(thiserror::Error, Debug)]
enum Error {
#[error("fanotify failed: {0}")]
FanotifyError(#[from] ::fanotify::Error),
#[error("nix failed: {0}")]
Errno(#[from] nix::Error),
}
extern "C" fn interrupt_handler(_: i32) {
info!("exiting");
std::process::exit(0);
}
#[derive(Debug, clap::Parser)]
#[clap(about="fanotify demo", long_about="
monitor filesystem changes demo
to use as storage provider:
--providers PROVIDER_COMMAND specify what command can write to file, like you can simulate it with tee
--init-flags FAN_CLASS_PRE_CONTENT this is needed to instruct fanotify send permission events and wait for response
--mask-flags FAN_CLOSE_WRITE to know that storage provider has done writing into the file
--mask-flags FAN_OPEN_PERM setup permission notification
--mask-flags FAN_ACCESS_PERM setup permission notification
--mask-flags FAN_ON_DIR create events for directories itself
--mask-flags FAN_EVENT_ON_CHILD create events for direct children
")]
struct Args {
#[clap(required(true))]
path: Vec<String>,
#[clap(long, short, default_values_t=default_whitelist())]
whitelist: Vec<String>,
#[clap(long, short, default_values_t=default_providers())]
providers: Vec<String>,
#[clap(long, short)]
init_flags: Vec<String>,
#[clap(long, short, default_values_t=default_event_f_flags())]
event_f_flags: Vec<String>,
#[clap(long, short, default_values_t=default_mask_flags())]
mask_flags: Vec<String>,
}
fn default_whitelist() -> Vec<String> {
vec![]
}
fn default_providers() -> Vec<String> {
vec!["tee".to_string()]
}
fn default_event_f_flags() -> Vec<String> {
vec!["O_RDWR", "O_LARGEFILE"]
.iter()
.map(|s| s.to_string())
.collect()
}
fn default_mask_flags() -> Vec<String> {
vec![
"FAN_ACCESS",
"FAN_OPEN",
"FAN_CLOSE",
"FAN_ONDIR",
"FAN_EVENT_ON_CHILD",
]
.iter()
.map(|s| s.to_string())
.collect()
}
fn reduce_flags<
S: Into<String>,
I: IntoIterator<Item = S>,
F: bitflags::Flags + BitOr<Output = F> + Debug,
>(
iter: I,
) -> F {
iter.into_iter()
.map(|s| {
let s: String = s.into();
let Some(flag) = F::from_name(&s.as_str()) else {
panic!("invalid value for flag: {}", s)
};
trace!("flag {} is parted into {:?}", s, flag);
flag
})
.reduce(|a, b| a | b)
.unwrap_or(F::empty())
}
fn main() -> Result<(), Error> {
env_logger::Builder::from_default_env().init();
let args = Args::parse();
let init_flags: InitFlags = reduce_flags(&args.init_flags);
let event_f_flags: EventFFlags = reduce_flags(&args.event_f_flags);
let mask_flags: MaskFlags = reduce_flags(&args.mask_flags);
info!("init flag: {:x} {:?}", init_flags.bits(), init_flags);
info!(
"event fd flag: {:x} {:?}",
event_f_flags.bits(),
event_f_flags
);
info!("mask flag: {:x} {:?}", mask_flags.bits(), mask_flags);
let fan = Fanotify::init(init_flags, event_f_flags)?;
for path in args.path {
debug!("marking path: {path}");
fan.mark(MarkFlags::FAN_MARK_ADD, mask_flags, None, Some(&path))?;
info!("path marked: {path}");
}
unsafe {
nix::sys::signal::sigaction(
nix::sys::signal::SIGINT,
&SigAction::new(
nix::sys::signal::SigHandler::Handler(interrupt_handler),
SaFlags::SA_RESETHAND,
SigSet::from(Signal::SIGINT),
),
)?
};
info!("interrupt handler is set");
let whitelist = args.whitelist;
let storage_provider = args.providers;
let mut ready = HashSet::new();
let mut bufferdfds: HashMap<std::path::PathBuf, Vec<OwnedFd>> = HashMap::new();
let mut arg0map = HashMap::new();
loop {
let mut events = fan.read_events()?;
if events.len() == 0 {
assert!(init_flags & InitFlags::FAN_NONBLOCK == InitFlags::FAN_NONBLOCK);
std::thread::sleep(std::time::Duration::from_millis(100));
continue;
}
for event in events.iter_mut() {
let Some(fd) = event.fd() else {
warn!("queue full");
continue;
};
let path = match std::fs::read_link(format!("/proc/self/fd/{}", fd.as_raw_fd())) {
Ok(p) => p,
Err(err) => {
warn!(
"failed to read fd link for fd {}: {:?}",
fd.as_raw_fd(),
err
);
continue;
}
};
let cmdline_raw = match std::fs::read(format!("/proc/{}/cmdline", event.pid())) {
Ok(raw) => raw,
Err(err) => {
warn!(
"failed to read pid cmdline for fd {}: {:?}",
event.pid(),
err
);
continue;
}
};
let cmdline = if cmdline_raw.len() > 0 {
Some(
cmdline_raw
.split(|&b| b == 0)
.map(|v| String::from_utf8_lossy(v))
.collect::<Vec<Cow<str>>>(),
)
} else {
None
};
trace!(
"++++++++= {:?} {} {:?} {:?}",
fd,
event.pid(),
event.mask(),
path
);
let arg0 = if let Some(cmdline) = cmdline.as_ref() {
for (idx, arg) in cmdline.iter().enumerate() {
trace!(" - {}: {}", idx, arg);
}
let arg0 = cmdline[0].to_string();
arg0map.insert(event.pid(), arg0.clone());
arg0
} else if arg0map.contains_key(&event.pid()) {
arg0map[&event.pid()].clone()
} else {
"".to_string()
};
match event.mask() {
MaskFlags::FAN_ACCESS_PERM
| MaskFlags::FAN_OPEN_PERM
| MaskFlags::FAN_OPEN_EXEC_PERM => {
let allowed = match std::fs::metadata(&path) {
Ok(metadata) => {
// is a directory or filled with content
metadata.is_dir() || ready.contains(&path)
}
Err(error) => {
error.kind() == std::io::ErrorKind::NotFound
}
};
if allowed || whitelist.contains(&arg0) || storage_provider.contains(&arg0) {
info!("<<<<< {} allowed", fd.as_raw_fd());
if let Err(err) =
fan.write_response(FanotifyResponse::new(fd, Response::FAN_ALLOW))
{
warn!("write response for {} failed: {}", fd.as_raw_fd(), err);
}
} else {
let fd = event.forget_fd();
info!("<<<<< {} defered", fd.as_raw_fd());
if let Some(fds) = bufferdfds.get_mut(&path) {
fds.push(fd);
} else {
bufferdfds.insert(path, vec![fd]);
}
}
}
MaskFlags::FAN_CLOSE_WRITE => {
if storage_provider.contains(&arg0) {
ready.insert(path.clone());
if let Some(fds) = bufferdfds.remove(&path) {
for fd in fds {
if let Err(err) = fan.write_response(FanotifyResponse::new(
fd.as_fd(),
Response::FAN_ALLOW,
)) {
warn!("write response for {} failed: {}", fd.as_raw_fd(), err);
}
info!(">>>>> {} allowed(defer)", fd.as_raw_fd());
}
}
}
}
_ => {}
}
}
}
}

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.
() => {}
}

105
scripts/privileged-lldb-server.sh Executable file
View File

@ -0,0 +1,105 @@
#!/usr/bin/env bash
cat > /dev/null << .vscode/launch.json
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "run with remote lldb-server",
"type": "lldb",
"request": "launch",
"program": "${workspaceFolder}/target/debug/fanotify-demo",
"args": [
"/home/guochao/fanotify-demo/data",
],
"env": {
"RUST_LOG": "trace"
},
"initCommands": [
"platform select remote-linux", // For example: 'remote-linux', 'remote-macosx', 'remote-android', etc.
"platform connect connect://127.0.0.1:11213",
// "settings set target.inherit-env false", // See note below.
],
"preLaunchTask": "prepare debug"
}
]
}
.vscode/launch.json
cat > /dev/null << .vscode/tasks.json
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "run remote server",
"type": "shell",
"command": "${workspaceFolder}/scripts/privileged-lldb-server.sh",
"isBackground": true,
"hide": true,
"runOptions": {
"instanceLimit": 1,
"runOn": "folderOpen"
},
"problemMatcher": {
"background": {
"activeOnStart": true,
"beginsPattern": "starting",
"endsPattern": "server started"
},
"pattern": {
"regexp": ""
}
},
"presentation": {
"echo": true,
"reveal": "never",
"focus": false,
"panel": "dedicated",
"showReuseMessage": true,
"clear": false
},
},
{
"label": "prepare debug",
"type": "shell",
"command": "true",
"dependsOn": [
"rust: cargo build",
"run remote server"
],
"presentation": {
"echo": false,
"reveal": "never",
"focus": false,
"panel": "shared",
"showReuseMessage": false,
"clear": false
}
}
]
}
.vscode/tasks.json
set -euo pipefail
HOST=${HOST:-127.0.0.1}
PORT=${PORT:-11213}
if [ "$(whoami)" != "root" ]; then
set -x
exec sudo -E bash "$0" "$@"
fi
echo starting
TEMPDIR="$(mktemp -d)"
pushd $TEMPDIR # lldb-server will receive binary and save to current directory
lldb-server platform --server --listen "$HOST:$PORT" &
PID=$!
trap 'kill $PID; rm -rfv "$TEMPDIR"' EXIT
echo server started
wait