From 936bc7c43de698c687f3711e1b06509e517eb4de Mon Sep 17 00:00:00 2001 From: guochao Date: Thu, 3 Apr 2025 12:27:50 +0000 Subject: [PATCH] initial commit --- .gitignore | 5 + Cargo.lock | 446 ++++++++++++++++++++++++++++++ Cargo.toml | 6 + fanotify-demo/Cargo.toml | 14 + fanotify-demo/src/main.rs | 265 ++++++++++++++++++ fanotify/Cargo.toml | 9 + fanotify/README | 5 + fanotify/src/consts.rs | 116 ++++++++ fanotify/src/error.rs | 72 +++++ fanotify/src/fanotify.rs | 188 +++++++++++++ fanotify/src/lib.rs | 10 + fanotify/src/macros.rs | 75 +++++ scripts/privileged-lldb-server.sh | 105 +++++++ 13 files changed, 1316 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 fanotify-demo/Cargo.toml create mode 100644 fanotify-demo/src/main.rs create mode 100644 fanotify/Cargo.toml create mode 100644 fanotify/README create mode 100644 fanotify/src/consts.rs create mode 100644 fanotify/src/error.rs create mode 100644 fanotify/src/fanotify.rs create mode 100644 fanotify/src/lib.rs create mode 100644 fanotify/src/macros.rs create mode 100755 scripts/privileged-lldb-server.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8ff5e0e --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/target +/data + +.idea +.vscode \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..2c9fc0b --- /dev/null +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9018143 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[workspace] +resolver = "3" +members = [ + "fanotify", + "fanotify-demo", +] \ No newline at end of file diff --git a/fanotify-demo/Cargo.toml b/fanotify-demo/Cargo.toml new file mode 100644 index 0000000..025d41d --- /dev/null +++ b/fanotify-demo/Cargo.toml @@ -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" diff --git a/fanotify-demo/src/main.rs b/fanotify-demo/src/main.rs new file mode 100644 index 0000000..405004f --- /dev/null +++ b/fanotify-demo/src/main.rs @@ -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, + + #[clap(long, short, default_values_t=default_whitelist())] + whitelist: Vec, + #[clap(long, short, default_values_t=default_providers())] + providers: Vec, + + #[clap(long, short)] + init_flags: Vec, + #[clap(long, short, default_values_t=default_event_f_flags())] + event_f_flags: Vec, + #[clap(long, short, default_values_t=default_mask_flags())] + mask_flags: Vec, +} + +fn default_whitelist() -> Vec { + vec![] +} + +fn default_providers() -> Vec { + vec!["tee".to_string()] +} + +fn default_event_f_flags() -> Vec { + vec!["O_RDWR", "O_LARGEFILE"] + .iter() + .map(|s| s.to_string()) + .collect() +} + +fn default_mask_flags() -> Vec { + 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, + I: IntoIterator, + F: bitflags::Flags + BitOr + 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> = 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::>>(), + ) + } 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()); + } + } + } + } + _ => {} + } + } + } +} diff --git a/fanotify/Cargo.toml b/fanotify/Cargo.toml new file mode 100644 index 0000000..fc068e8 --- /dev/null +++ b/fanotify/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "fanotify" +version = "0.1.0" +edition = "2024" + +[dependencies] +libc = "0.2" +thiserror = "2" +bitflags = "2" \ No newline at end of file diff --git a/fanotify/README b/fanotify/README new file mode 100644 index 0000000..912a910 --- /dev/null +++ b/fanotify/README @@ -0,0 +1,5 @@ +rust style fanotify wrapper + +------------------------------------------ + +this crate wraps around calls and consts of fanotify(7) from libc crate. diff --git a/fanotify/src/consts.rs b/fanotify/src/consts.rs new file mode 100644 index 0000000..4ca6062 --- /dev/null +++ b/fanotify/src/consts.rs @@ -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 + } +} diff --git a/fanotify/src/error.rs b/fanotify/src/error.rs new file mode 100644 index 0000000..984a885 --- /dev/null +++ b/fanotify/src/error.rs @@ -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> From 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"); + } +} diff --git a/fanotify/src/fanotify.rs b/fanotify/src/fanotify.rs new file mode 100644 index 0000000..537f69c --- /dev/null +++ b/fanotify/src/fanotify.rs @@ -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::try_init(init_flags, event_fd_flags) + } + pub fn try_init(init_flags: InitFlags, event_fd_flags: EventFFlags) -> Result { + 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>( + &self, + operation: MarkFlags, + mask: MaskFlags, + dirfd: Option, + path: Option

, + ) -> 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, Error> { + const BUFFER_SIZE: usize = 4096; + const EVENT_SIZE: usize = size_of::(); + 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 = 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::(), + ) + }; + 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 { + 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, + }, + } + } +} diff --git a/fanotify/src/lib.rs b/fanotify/src/lib.rs new file mode 100644 index 0000000..4b0bb6a --- /dev/null +++ b/fanotify/src/lib.rs @@ -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}; \ No newline at end of file diff --git a/fanotify/src/macros.rs b/fanotify/src/macros.rs new file mode 100644 index 0000000..7ef1a29 --- /dev/null +++ b/fanotify/src/macros.rs @@ -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. + () => {} +} \ No newline at end of file diff --git a/scripts/privileged-lldb-server.sh b/scripts/privileged-lldb-server.sh new file mode 100755 index 0000000..90cf335 --- /dev/null +++ b/scripts/privileged-lldb-server.sh @@ -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 \ No newline at end of file