Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add missing Fanotify APIs from the libc crate into Nix. #2552

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/2552.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Adds support for nix to receive additional Fanotify information records (such as libc::fanotify_event_info_fid, libc::fanotify_event_info_error and libc::fanotify_event_info_pidfd)
Adds abstractions over the new fanotify structs.
Adds new InitFlags to allow receiving these new information records.
309 changes: 308 additions & 1 deletion src/sys/fanotify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,46 @@ libc_bitflags! {
FAN_REPORT_PIDFD;
/// Make `FanotifyEvent::pid` return thread id. Since Linux 4.20.
FAN_REPORT_TID;

/// Allows the receipt of events which contain additional information
/// about the underlying filesystem object correlated to an event.
///
/// This will make `FanotifyEvent::fd` return `FAN_NOFD`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// This will make `FanotifyEvent::fd` return `FAN_NOFD`.
/// This will make [`FanotifyEvent::fd()`] return `None`.

/// This should be used with `Fanotify::read_events_with_info_records` to
/// recieve `FanotifyInfoRecord::Fid` info records.
/// Since Linux 5.1
FAN_REPORT_FID;

/// Allows the receipt of events which contain additional information
/// about the underlying filesystem object correlated to an event.
///
/// This will make `FanotifyEvent::fd` return `FAN_NOFD`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here:

Suggested change
/// This will make `FanotifyEvent::fd` return `FAN_NOFD`.
/// This will make [`FanotifyEvent::fd()`] return `None`.

/// This should be used with `Fanotify::read_events_with_info_records` to
/// recieve `FanotifyInfoRecord::Fid` info records.
///
/// An additional event of `FAN_EVENT_INFO_TYPE_DFID` will also be received,
/// encapsulating information about the target directory (or parent directory of a file)
/// Since Linux 5.9
FAN_REPORT_DIR_FID;

/// Events for fanotify groups initialized with this flag will contain additional
/// information about the child correlated with directory entry modification events.
/// This flag must be provided in conjunction with the flags `FAN_REPORT_FID`,
/// `FAN_REPORT_DIR_FID` and `FAN_REPORT_NAME`.
/// Since Linux 5.17
FAN_REPORT_TARGET_FID;

/// Events for fanotify groups initialized with this flag will contain additional
/// information about the name of the directory entry correlated to an event. This
/// flag must be provided in conjunction with the flag `FAN_REPORT_DIR_FID`.
/// Since Linux 5.9
FAN_REPORT_NAME;

/// This is a synonym for `FAN_REPORT_DIR_FD | FAN_REPORT_NAME`.
FAN_REPORT_DFID_NAME;

/// This is a synonym for `FAN_REPORT_DIR_FD | FAN_REPORT_NAME | FAN_REPORT_TARGET_FID`.
FAN_REPORT_DFID_NAME_TARGET;
}
}

Expand Down Expand Up @@ -198,6 +238,141 @@ libc_bitflags! {
/// Compile version number of fanotify API.
pub const FANOTIFY_METADATA_VERSION: u8 = libc::FANOTIFY_METADATA_VERSION;

/// Abstract over [`libc::fanotify_event_info_fid`], which represents an
/// information record received via [`Fanotify::read_events_with_info_records`].
#[derive(Debug, Eq, Hash, PartialEq)]
#[repr(transparent)]
#[allow(missing_copy_implementations)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this not Copy?

pub struct LibcFanotifyFidRecord(libc::fanotify_event_info_fid);

/// Extends LibcFanotifyFidRecord to include file_handle bytes.
/// This allows Rust to move the record around in memory and not lose the file_handle
/// as the libc::fanotify_event_info_fid does not include any of the file_handle bytes.
// Is not Clone due to fd field, to avoid use-after-close scenarios.
#[derive(Debug, Eq, Hash, PartialEq)]
#[repr(C)]
#[allow(missing_copy_implementations)]
pub struct FanotifyFidRecord {
record: LibcFanotifyFidRecord,
handle_bytes: *const u8,
}

impl FanotifyFidRecord {
/// The filesystem id where this event occurred. The value this method returns
/// differs depending on the host system. Please read the statfs(2) documentation
/// for more information:
/// <https://man7.org/linux/man-pages/man2/statfs.2.html#VERSIONS>
pub fn filesystem_id(&self) -> libc::__kernel_fsid_t {
self.record.0.fsid
}

/// The file handle for the filesystem object where the event occurred. The handle is
/// represented as a 0-length u8 array, but it actually points to variable-length
/// file_handle struct.For more information:
/// <https://man7.org/linux/man-pages/man2/open_by_handle_at.2.html>
pub fn handle(&self) -> *const u8 {
self.handle_bytes
}

/// The specific info_type for this Fid Record. Fanotify can return an Fid Record
/// with many different possible info_types. The info_type is not always necessary
/// but can be useful for connecting similar events together (like a FAN_RENAME)
pub fn info_type(&self) -> u8 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be nice to have an enum for the info type:

libc_enum! {
    #[repr(u8)]
    #[non_exhaustive]
    enum FanEventInfoType {
        FAN_EVENT_INFO_TYPE_FID,
        FAN_EVENT_INFO_TYPE_DFID,
        FAN_EVENT_INFO_TYPE_DFID_NAME,
        FAN_EVENT_INFO_TYPE_OLD_DFID_NAME,
        FAN_EVENT_INFO_TYPE_NEW_DFID_NAME,
    }
}

self.record.0.hdr.info_type
}
}

/// Abstract over [`libc::fanotify_event_info_error`], which represents an
/// information record received via [`Fanotify::read_events_with_info_records`].
// Is not Clone due to fd field, to avoid use-after-close scenarios.
#[derive(Debug, Eq, Hash, PartialEq)]
#[repr(transparent)]
#[allow(missing_copy_implementations)]
#[cfg(target_env = "gnu")]
pub struct FanotifyErrorRecord(libc::fanotify_event_info_error);

#[cfg(target_env = "gnu")]
impl FanotifyErrorRecord {
/// Errno of the FAN_FS_ERROR that occurred.
pub fn err(&self) -> Errno {
Errno::from_raw(self.0.error)
}

/// Number of errors that occurred in the filesystem Fanotify in watching.
/// Only a single FAN_FS_ERROR is stored per filesystem at once. As such, Fanotify
/// suppresses subsequent error messages and only increments the `err_count` value.
pub fn err_count(&self) -> u32 {
self.0.error_count
}
}

/// Abstract over [`libc::fanotify_event_info_pidfd`], which represents an
/// information record received via [`Fanotify::read_events_with_info_records`].
// Is not Clone due to fd field, to avoid use-after-close scenarios.
#[derive(Debug, Eq, Hash, PartialEq)]
#[repr(transparent)]
#[allow(missing_copy_implementations)]
#[cfg(target_env = "gnu")]
pub struct FanotifyPidfdRecord(libc::fanotify_event_info_pidfd);

#[cfg(target_env = "gnu")]
impl FanotifyPidfdRecord {
/// The process file descriptor that refers to the process responsible for
/// generating this event. If the underlying pidfd_create fails, `None` is returned.
pub fn pidfd(&self) -> Option<BorrowedFd> {
if self.0.pidfd == libc::FAN_NOPIDFD || self.0.pidfd == libc::FAN_EPIDFD
{
None
} else {
// SAFETY: self.0.pidfd 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.pidfd) })
}
}
}

#[cfg(target_env = "gnu")]
impl Drop for FanotifyPidfdRecord {
fn drop(&mut self) {
if self.0.pidfd == libc::FAN_NOFD {
return;
}
let e = close(self.0.pidfd);
if !std::thread::panicking() && e == Err(Errno::EBADF) {
panic!("Closing an invalid file descriptor!");
};
}
}

/// After a [`libc::fanotify_event_metadata`], there can be 0 or more event_info
/// structs depending on which InitFlags were used in [`Fanotify::init`].
// Is not Clone due to pidfd in `libc::fanotify_event_info_pidfd`
// Other fanotify_event_info records are not implemented as they don't exist in
// the libc crate yet.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Emm, do we have any other types of information records? All the information record types mentioned in the man page are listed here, though the Linux kernel could add more in the future, which means this enum should be #[non_exhaustive]

#[derive(Debug, Eq, Hash, PartialEq)]
#[allow(missing_copy_implementations)]
pub enum FanotifyInfoRecord {
/// A [`libc::fanotify_event_info_fid`] event was recieved, usually as
/// a result of passing [`InitFlags::FAN_REPORT_FID`] or [`InitFlags::FAN_REPORT_DIR_FID`]
/// into [`Fanotify::init`]. The containing struct includes a `file_handle` for
/// use with `open_by_handle_at(2)`.
Fid(FanotifyFidRecord),

/// A [`libc::fanotify_event_info_error`] event was recieved.
/// This occurs when a FAN_FS_ERROR occurs, indicating an error with
/// the watch filesystem object. (such as a bad file or bad link lookup)
#[cfg(target_env = "gnu")]
Error(FanotifyErrorRecord),

/// A [`libc::fanotify_event_info_pidfd`] event was recieved, usually as
/// a result of passing [`InitFlags::FAN_REPORT_PIDFD`] into [`Fanotify::init`].
/// The containing struct includes a `pidfd` for reliably determining
/// whether the process responsible for generating an event has been recycled or terminated
#[cfg(target_env = "gnu")]
Pidfd(FanotifyPidfdRecord),
}

/// 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.
Expand Down Expand Up @@ -341,6 +516,19 @@ impl Fanotify {
Errno::result(res).map(|_| ())
}

fn get_struct<T>(&self, buffer: &[u8; 4096], offset: usize) -> T {
let struct_size = size_of::<T>();
unsafe {
let mut struct_obj = MaybeUninit::<T>::uninit();
std::ptr::copy_nonoverlapping(
buffer.as_ptr().add(offset),
struct_obj.as_mut_ptr().cast(),
(4096 - offset).min(struct_size),
);
struct_obj.assume_init()
}
}

/// Read incoming events from the fanotify group.
///
/// Returns a Result containing either a `Vec` of events on success or errno
Expand Down Expand Up @@ -382,6 +570,125 @@ impl Fanotify {
Ok(events)
}

/// Read incoming events and information records from the fanotify group.
///
/// Returns a Result containing either a `Vec` of events and information records 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.
#[allow(clippy::cast_ptr_alignment)] // False positive
pub fn read_events_with_info_records(
&self,
) -> Result<Vec<(FanotifyEvent, Vec<FanotifyInfoRecord>)>> {
let metadata_size = size_of::<libc::fanotify_event_metadata>();
const BUFSIZ: usize = 4096;
let mut buffer = [0u8; BUFSIZ];
let mut events = Vec::new();
let mut offset = 0;

let nread = read(&self.fd, &mut buffer)?;

while (nread - offset) >= metadata_size {
let metadata = unsafe {
let mut metadata =
MaybeUninit::<libc::fanotify_event_metadata>::uninit();
std::ptr::copy_nonoverlapping(
buffer.as_ptr().add(offset),
metadata.as_mut_ptr().cast(),
(BUFSIZ - offset).min(metadata_size),
);
metadata.assume_init()
};

let mut remaining_len = metadata.event_len - metadata_size as u32;
let mut info_records = Vec::new();
let mut current_event_offset = offset + metadata_size;

while remaining_len > 0 {
let header = self
.get_struct::<libc::fanotify_event_info_header>(
&buffer,
current_event_offset,
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If possible, I hope we can inspect the header without doing any copy


let info_record = match header.info_type {
// FanotifyFidRecord can be returned for any of the following info_type.
// This isn't found in the fanotify(7) documentation, but the fanotify_init(2) documentation
// https://man7.org/linux/man-pages/man2/fanotify_init.2.html
Comment on lines +660 to +661
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean types FAN_EVENT_INFO_TYPE_NEW_DFID_NAME and FAN_EVENT_INFO_TYPE_OLD_DFID_NAME?

libc::FAN_EVENT_INFO_TYPE_FID
| libc::FAN_EVENT_INFO_TYPE_DFID
| libc::FAN_EVENT_INFO_TYPE_DFID_NAME
| libc::FAN_EVENT_INFO_TYPE_NEW_DFID_NAME
| libc::FAN_EVENT_INFO_TYPE_OLD_DFID_NAME => {
let record = self
.get_struct::<libc::fanotify_event_info_fid>(
&buffer,
current_event_offset,
);

let record_ptr: *const libc::fanotify_event_info_fid = unsafe {
buffer.as_ptr().add(current_event_offset)
as *const libc::fanotify_event_info_fid
};

let file_handle_ptr = unsafe { record_ptr.add(1) as *const u8 };

Some(FanotifyInfoRecord::Fid(FanotifyFidRecord {
record: LibcFanotifyFidRecord(record),
handle_bytes: file_handle_ptr,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a dangling pointer as it comes from buffer, which belongs to the current function frame

}))
}
#[cfg(target_env = "gnu")]
libc::FAN_EVENT_INFO_TYPE_ERROR => {
let record = self
.get_struct::<libc::fanotify_event_info_error>(
&buffer,
current_event_offset,
);

Some(FanotifyInfoRecord::Error(FanotifyErrorRecord(
record,
)))
}
#[cfg(target_env = "gnu")]
libc::FAN_EVENT_INFO_TYPE_PIDFD => {
let record = self
.get_struct::<libc::fanotify_event_info_pidfd>(
&buffer,
current_event_offset,
);
Some(FanotifyInfoRecord::Pidfd(FanotifyPidfdRecord(
record,
)))
}
// Ignore unsupported events
_ => None,
};

if let Some(record) = info_record {
info_records.push(record);
}

remaining_len -= header.len as u32;
current_event_offset += header.len as usize;
}

// libc::fanotify_event_info_header
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An unused comment


events.push((FanotifyEvent(metadata), info_records));
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.
Expand Down Expand Up @@ -443,4 +750,4 @@ impl Fanotify {
fd
}
}
}
}
Loading
Loading