use crate::*; use nix::errno::Errno; use nix::sys::fanotify::{ EventFFlags, Fanotify, FanotifyResponse, InitFlags, MarkFlags, MaskFlags, Response, }; use std::fs::{read_link, read_to_string, 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(); test_fanotify_overflow(); } fn test_fanotify_notifications() { let group = Fanotify::init(InitFlags::FAN_CLASS_NOTIF, EventFFlags::O_RDONLY) .unwrap(); let tempdir = tempfile::tempdir().unwrap(); let tempfile = tempdir.path().join("test"); OpenOptions::new() .write(true) .create_new(true) .open(&tempfile) .unwrap(); group .mark( MarkFlags::FAN_MARK_ADD, MaskFlags::FAN_OPEN | MaskFlags::FAN_MODIFY | MaskFlags::FAN_CLOSE, None, Some(&tempfile), ) .unwrap(); // modify test file { let mut f = OpenOptions::new().write(true).open(&tempfile).unwrap(); f.write_all(b"hello").unwrap(); } let mut events = group.read_events().unwrap(); assert_eq!(events.len(), 1, "should have read exactly one event"); let event = events.pop().unwrap(); 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 mut events = group.read_events().unwrap(); assert_eq!(events.len(), 1, "should have read exactly one event"); let event = events.pop().unwrap(); 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(); let tempfile = tempdir.path().join("test"); OpenOptions::new() .write(true) .create_new(true) .open(&tempfile) .unwrap(); group .mark( MarkFlags::FAN_MARK_ADD, MaskFlags::FAN_OPEN_PERM, None, Some(&tempfile), ) .unwrap(); let file_thread = thread::spawn({ let tempfile = tempfile.clone(); move || { // first open, should fail let Err(e) = File::open(&tempfile) else { panic!("The first open should fail"); }; assert_eq!(e.kind(), ErrorKind::PermissionDenied); // second open, should succeed File::open(&tempfile).unwrap(); } }); // Deny the first open try let mut events = group.read_events().unwrap(); assert_eq!(events.len(), 1, "should have read exactly one event"); let event = events.pop().unwrap(); 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, tempfile); group .write_response(FanotifyResponse::new(*fd, Response::FAN_DENY)) .unwrap(); // Allow the second open try let mut events = group.read_events().unwrap(); assert_eq!(events.len(), 1, "should have read exactly one event"); let event = events.pop().unwrap(); 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, tempfile); group .write_response(FanotifyResponse::new(*fd, Response::FAN_ALLOW)) .unwrap(); file_thread.join().unwrap(); } fn test_fanotify_overflow() { let max_events: usize = read_to_string("/proc/sys/fs/fanotify/max_queued_events") .unwrap() .trim() .parse() .unwrap(); // make sure the kernel is configured with the default value, // just so this test doesn't run forever assert_eq!(max_events, 16384); let group = Fanotify::init( InitFlags::FAN_CLASS_NOTIF | InitFlags::FAN_REPORT_TID | InitFlags::FAN_NONBLOCK, EventFFlags::O_RDONLY, ) .unwrap(); let tempdir = tempfile::tempdir().unwrap(); let tempfile = tempdir.path().join("test"); OpenOptions::new() .write(true) .create_new(true) .open(&tempfile) .unwrap(); group .mark( MarkFlags::FAN_MARK_ADD, MaskFlags::FAN_OPEN, None, Some(&tempfile), ) .unwrap(); thread::scope(|s| { // perform 10 more events to demonstrate some will be dropped for _ in 0..(max_events + 10) { s.spawn(|| { File::open(&tempfile).unwrap(); }); } }); // flush the queue until it's empty let mut n = 0; let mut last_event = None; loop { match group.read_events() { Ok(events) => { n += events.len(); if let Some(event) = events.last() { last_event = Some(event.mask()); } } Err(e) if e == Errno::EWOULDBLOCK => break, Err(e) => panic!("{e:?}"), } } // make sure we read all we expected. // the +1 is for the overflow event. assert_eq!(n, max_events + 1); assert_eq!(last_event, Some(MaskFlags::FAN_Q_OVERFLOW)); }